@gethmy/agent 1.9.1 → 1.10.0
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/dist/cli.js +747 -109
- package/dist/index.js +742 -108
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -219,12 +219,170 @@ var init_board_helpers = __esm(() => {
|
|
|
219
219
|
init_log();
|
|
220
220
|
});
|
|
221
221
|
|
|
222
|
+
// src/plan-phase.ts
|
|
223
|
+
function scoreComplexity(enriched) {
|
|
224
|
+
const { card, labels, subtasks } = enriched;
|
|
225
|
+
let score = 0;
|
|
226
|
+
const desc = (card.description ?? "").trim();
|
|
227
|
+
if (desc.length > 600)
|
|
228
|
+
score += 3;
|
|
229
|
+
else if (desc.length > 200)
|
|
230
|
+
score += 2;
|
|
231
|
+
else if (desc.length > 0)
|
|
232
|
+
score += 1;
|
|
233
|
+
score += Math.min(subtasks.length, 4);
|
|
234
|
+
const names = labels.map((l) => l.name.toLowerCase());
|
|
235
|
+
if (names.some((n) => /feature|epic|refactor|architecture|migration/.test(n))) {
|
|
236
|
+
score += 2;
|
|
237
|
+
}
|
|
238
|
+
if (names.some((n) => /typo|chore|trivial|docs/.test(n))) {
|
|
239
|
+
score -= 2;
|
|
240
|
+
}
|
|
241
|
+
return Math.max(0, score);
|
|
242
|
+
}
|
|
243
|
+
function shouldPlan(enriched, config) {
|
|
244
|
+
if (!config.enabled)
|
|
245
|
+
return false;
|
|
246
|
+
const { card } = enriched;
|
|
247
|
+
const hasPlan = !!card.plan_id;
|
|
248
|
+
const needsRefresh = card.needs_plan_refresh === true;
|
|
249
|
+
if (hasPlan && !needsRefresh)
|
|
250
|
+
return false;
|
|
251
|
+
return scoreComplexity(enriched) >= config.minComplexityScore;
|
|
252
|
+
}
|
|
253
|
+
function buildPlanPrompt(enriched, worktreePath) {
|
|
254
|
+
const { card, column, labels, subtasks } = enriched;
|
|
255
|
+
const labelStr = labels.length > 0 ? labels.map((l) => l.name).join(", ") : "none";
|
|
256
|
+
const subtaskStr = subtasks.length > 0 ? subtasks.map((s) => `- ${s.title}`).join(`
|
|
257
|
+
`) : "No subtasks defined.";
|
|
258
|
+
const description = card.description?.trim() || "No description provided.";
|
|
259
|
+
return `You are a senior engineer producing an IMPLEMENTATION PLAN for a task on the Harmony board. You are in PLAN MODE: explore the codebase to ground the plan, but do NOT write, edit, or commit any code in this pass.
|
|
260
|
+
|
|
261
|
+
## Card: #${card.short_id} - ${card.title}
|
|
262
|
+
**Labels**: ${labelStr}
|
|
263
|
+
**Column**: ${column.name}
|
|
264
|
+
**Priority**: ${card.priority}
|
|
265
|
+
|
|
266
|
+
## Description
|
|
267
|
+
${description}
|
|
268
|
+
|
|
269
|
+
## Subtasks
|
|
270
|
+
${subtaskStr}
|
|
271
|
+
|
|
272
|
+
## Your job
|
|
273
|
+
1. Read the parts of the codebase relevant to this task (use Read/Grep/Glob; do NOT edit).
|
|
274
|
+
2. Decide the smallest correct approach. Note the exact files you expect to touch.
|
|
275
|
+
3. Call out risks, unknowns, and anything that needs a human decision.
|
|
276
|
+
4. Break the work into ordered, independently-verifiable tasks.
|
|
277
|
+
|
|
278
|
+
You are exploring the worktree at \`${worktreePath}\`. Read-only this pass — no Write/Edit/Bash-that-mutates, no commits.
|
|
279
|
+
|
|
280
|
+
## Output contract
|
|
281
|
+
End your final message with EXACTLY ONE fenced block tagged \`plan\`, in this structure:
|
|
282
|
+
|
|
283
|
+
\`\`\`plan
|
|
284
|
+
# <one-line plan title>
|
|
285
|
+
|
|
286
|
+
## Approach
|
|
287
|
+
<2-4 sentences: the chosen approach and why>
|
|
288
|
+
|
|
289
|
+
## Files
|
|
290
|
+
- <path> — <what changes here>
|
|
291
|
+
|
|
292
|
+
## Steps
|
|
293
|
+
1. <ordered step>
|
|
294
|
+
2. <ordered step>
|
|
295
|
+
|
|
296
|
+
## Tasks
|
|
297
|
+
- [ ] <discrete, verifiable task>
|
|
298
|
+
- [ ] <discrete, verifiable task>
|
|
299
|
+
|
|
300
|
+
## Risks
|
|
301
|
+
- <risk / unknown / decision needed, or "none">
|
|
302
|
+
\`\`\`
|
|
303
|
+
|
|
304
|
+
The \`## Tasks\` checklist is parsed into trackable tasks, so keep each line a single concrete action.`;
|
|
305
|
+
}
|
|
306
|
+
function extractPlanArtifact(assistantText, fallbackTitle) {
|
|
307
|
+
const text = assistantText ?? "";
|
|
308
|
+
const fenced = text.match(PLAN_FENCE);
|
|
309
|
+
const markdown = (fenced ? fenced[1] : text).trim();
|
|
310
|
+
const titleMatch = markdown.match(H1);
|
|
311
|
+
const title = (titleMatch?.[1] ?? fallbackTitle).trim() || fallbackTitle;
|
|
312
|
+
return {
|
|
313
|
+
title,
|
|
314
|
+
markdown,
|
|
315
|
+
tasks: parseTasksSection(markdown)
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function parseTasksSection(markdown) {
|
|
319
|
+
const lines = markdown.split(`
|
|
320
|
+
`);
|
|
321
|
+
const tasks = [];
|
|
322
|
+
let inTasks = false;
|
|
323
|
+
for (const line of lines) {
|
|
324
|
+
const heading = line.match(/^#{1,6}\s+(.+?)\s*$/);
|
|
325
|
+
if (heading) {
|
|
326
|
+
inTasks = /^tasks\b/i.test(heading[1].trim());
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (!inTasks)
|
|
330
|
+
continue;
|
|
331
|
+
const item = line.match(TASK_LINE);
|
|
332
|
+
if (item) {
|
|
333
|
+
const content = item[1].trim();
|
|
334
|
+
if (content)
|
|
335
|
+
tasks.push({ content });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return tasks;
|
|
339
|
+
}
|
|
340
|
+
function buildPlanComment(artifact) {
|
|
341
|
+
const body = artifact.markdown.trim();
|
|
342
|
+
return [
|
|
343
|
+
"## \uD83E\uDDED Plan (agent, advisory)",
|
|
344
|
+
"",
|
|
345
|
+
"The daemon explored the worktree read-only and produced this plan before implementing. Implementation is starting now in the same run.",
|
|
346
|
+
"",
|
|
347
|
+
body
|
|
348
|
+
].join(`
|
|
349
|
+
`);
|
|
350
|
+
}
|
|
351
|
+
function buildGatedPlanComment(artifact, pickupColumnName) {
|
|
352
|
+
const body = artifact.markdown.trim();
|
|
353
|
+
return [
|
|
354
|
+
"## \uD83E\uDDED Plan (agent, awaiting approval)",
|
|
355
|
+
"",
|
|
356
|
+
`The daemon explored the worktree read-only and produced this plan. Implementation is **gated on your approval** — review the plan below, then move this card to **${pickupColumnName}** to start implementation with it. Edit the linked plan first if the approach needs changes.`,
|
|
357
|
+
"",
|
|
358
|
+
body
|
|
359
|
+
].join(`
|
|
360
|
+
`);
|
|
361
|
+
}
|
|
362
|
+
var DEFAULT_PLANNING_CONFIG, PLAN_FENCE, H1, TASK_LINE;
|
|
363
|
+
var init_plan_phase = __esm(() => {
|
|
364
|
+
DEFAULT_PLANNING_CONFIG = {
|
|
365
|
+
enabled: false,
|
|
366
|
+
mode: "advisory",
|
|
367
|
+
model: "sonnet",
|
|
368
|
+
maxTurns: 40,
|
|
369
|
+
postComment: true,
|
|
370
|
+
awaitingApprovalColumn: "To Do",
|
|
371
|
+
minComplexityScore: 3,
|
|
372
|
+
approvalTtlHours: 0
|
|
373
|
+
};
|
|
374
|
+
PLAN_FENCE = /```plan\s*\n([\s\S]*?)```/i;
|
|
375
|
+
H1 = /^#\s+(.+?)\s*$/m;
|
|
376
|
+
TASK_LINE = /^\s*(?:[-*]\s*\[[ xX]?\]|[-*]|\d+[.)])\s+(.+?)\s*$/;
|
|
377
|
+
});
|
|
378
|
+
|
|
222
379
|
// src/types.ts
|
|
223
380
|
function agentIdentifier(workerId) {
|
|
224
381
|
return `harmony-daemon-${workerId}`;
|
|
225
382
|
}
|
|
226
|
-
var DEFAULT_AGENT_CONFIG, NEED_REVIEW_LABEL = "Need Review", NEED_REVIEW_LABEL_COLOR = "#f59e0b", AGENT_NAME = "Harmony Agent";
|
|
383
|
+
var DEFAULT_AGENT_CONFIG, IN_PROGRESS_COLUMN = "In Progress", NEED_REVIEW_LABEL = "Need Review", NEED_REVIEW_LABEL_COLOR = "#f59e0b", AGENT_NAME = "Harmony Agent";
|
|
227
384
|
var init_types = __esm(() => {
|
|
385
|
+
init_plan_phase();
|
|
228
386
|
DEFAULT_AGENT_CONFIG = {
|
|
229
387
|
poolSize: 3,
|
|
230
388
|
maxTimeout: 1800000,
|
|
@@ -291,7 +449,8 @@ var init_types = __esm(() => {
|
|
|
291
449
|
staleHeartbeatMs: 120000,
|
|
292
450
|
reconcileIntervalMs: 60000,
|
|
293
451
|
worktreeGcIntervalMs: 5 * 60000
|
|
294
|
-
}
|
|
452
|
+
},
|
|
453
|
+
planning: DEFAULT_PLANNING_CONFIG
|
|
295
454
|
};
|
|
296
455
|
});
|
|
297
456
|
|
|
@@ -387,6 +546,10 @@ function loadDaemonConfig() {
|
|
|
387
546
|
timing: {
|
|
388
547
|
...DEFAULT_AGENT_CONFIG.timing,
|
|
389
548
|
...agentOverrides.timing ?? {}
|
|
549
|
+
},
|
|
550
|
+
planning: {
|
|
551
|
+
...DEFAULT_AGENT_CONFIG.planning,
|
|
552
|
+
...agentOverrides.planning ?? {}
|
|
390
553
|
}
|
|
391
554
|
};
|
|
392
555
|
return {
|
|
@@ -446,6 +609,20 @@ async function validateColumnReferences(client, projectId, config) {
|
|
|
446
609
|
}
|
|
447
610
|
required.push({ value: config.review.moveToColumn, where: "review.moveToColumn" }, { value: config.review.failColumn, where: "review.failColumn" });
|
|
448
611
|
}
|
|
612
|
+
if (config.planning.enabled && config.planning.mode === "gated") {
|
|
613
|
+
required.push({
|
|
614
|
+
value: config.planning.awaitingApprovalColumn,
|
|
615
|
+
where: "planning.awaitingApprovalColumn"
|
|
616
|
+
});
|
|
617
|
+
const parkCol = config.planning.awaitingApprovalColumn?.toLowerCase();
|
|
618
|
+
const allPickups = [
|
|
619
|
+
...config.pickupColumns,
|
|
620
|
+
...config.review.enabled ? config.review.pickupColumns : []
|
|
621
|
+
];
|
|
622
|
+
if (parkCol && allPickups.some((c) => c.toLowerCase() === parkCol)) {
|
|
623
|
+
issues.push(`planning.awaitingApprovalColumn: "${config.planning.awaitingApprovalColumn}" is also a pickup column (implement or review) — a gated card parked there is picked up immediately, bypassing approval. Use a column the daemon does not pick up from.`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
449
626
|
for (const { value, where } of required) {
|
|
450
627
|
if (!value)
|
|
451
628
|
continue;
|
|
@@ -840,24 +1017,63 @@ import {
|
|
|
840
1017
|
class HttpServer {
|
|
841
1018
|
opts;
|
|
842
1019
|
server = null;
|
|
1020
|
+
boundPort = null;
|
|
843
1021
|
constructor(opts) {
|
|
844
1022
|
this.opts = opts;
|
|
845
1023
|
}
|
|
1024
|
+
get port() {
|
|
1025
|
+
return this.boundPort;
|
|
1026
|
+
}
|
|
846
1027
|
async start() {
|
|
847
|
-
|
|
848
|
-
this.
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
}
|
|
855
|
-
});
|
|
1028
|
+
this.server = createServer((req, res) => {
|
|
1029
|
+
this.route(req, res).catch((err) => {
|
|
1030
|
+
log.error(TAG3, `unhandled: ${err instanceof Error ? err.message : err}`);
|
|
1031
|
+
if (!res.headersSent) {
|
|
1032
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
1033
|
+
res.end(JSON.stringify({ error: "internal_error" }));
|
|
1034
|
+
}
|
|
856
1035
|
});
|
|
857
|
-
|
|
858
|
-
|
|
1036
|
+
});
|
|
1037
|
+
const attempts = Math.max(1, this.opts.maxPortAttempts ?? 10);
|
|
1038
|
+
const startPort = this.opts.port;
|
|
1039
|
+
for (let i = 0;i < attempts; i++) {
|
|
1040
|
+
const port = startPort + i;
|
|
1041
|
+
try {
|
|
1042
|
+
await this.listenOnce(port);
|
|
1043
|
+
this.boundPort = port;
|
|
1044
|
+
if (port !== startPort) {
|
|
1045
|
+
log.info(TAG3, `port ${startPort} busy — bound to ${port} instead`);
|
|
1046
|
+
}
|
|
1047
|
+
return port;
|
|
1048
|
+
} catch (err) {
|
|
1049
|
+
const lastAttempt = i === attempts - 1;
|
|
1050
|
+
if (isAddrInUse(err) && !lastAttempt) {
|
|
1051
|
+
log.debug(TAG3, `port ${port} in use, trying ${port + 1}`);
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1054
|
+
throw err;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
throw new Error("HTTP server failed to bind");
|
|
1058
|
+
}
|
|
1059
|
+
listenOnce(port) {
|
|
1060
|
+
return new Promise((resolve, reject) => {
|
|
1061
|
+
const server = this.server;
|
|
1062
|
+
if (!server) {
|
|
1063
|
+
reject(new Error("server not created"));
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
const onError = (err) => {
|
|
1067
|
+
server.removeListener("listening", onListening);
|
|
1068
|
+
reject(err);
|
|
1069
|
+
};
|
|
1070
|
+
const onListening = () => {
|
|
1071
|
+
server.removeListener("error", onError);
|
|
859
1072
|
resolve();
|
|
860
|
-
}
|
|
1073
|
+
};
|
|
1074
|
+
server.once("error", onError);
|
|
1075
|
+
server.once("listening", onListening);
|
|
1076
|
+
server.listen(port, this.opts.bindAddr);
|
|
861
1077
|
});
|
|
862
1078
|
}
|
|
863
1079
|
async stop() {
|
|
@@ -867,6 +1083,7 @@ class HttpServer {
|
|
|
867
1083
|
this.server?.close(() => resolve());
|
|
868
1084
|
});
|
|
869
1085
|
this.server = null;
|
|
1086
|
+
this.boundPort = null;
|
|
870
1087
|
}
|
|
871
1088
|
async route(req, res) {
|
|
872
1089
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
@@ -913,6 +1130,9 @@ class HttpServer {
|
|
|
913
1130
|
}
|
|
914
1131
|
}
|
|
915
1132
|
}
|
|
1133
|
+
function isAddrInUse(err) {
|
|
1134
|
+
return typeof err === "object" && err !== null && err.code === "EADDRINUSE";
|
|
1135
|
+
}
|
|
916
1136
|
function parseCommand(path) {
|
|
917
1137
|
const match = path.match(/^\/(pause|resume|stop)\/([^/]+)$/);
|
|
918
1138
|
if (!match)
|
|
@@ -1373,6 +1593,62 @@ function formatCents(cents) {
|
|
|
1373
1593
|
return `$${(cents / 100).toFixed(2)}`;
|
|
1374
1594
|
}
|
|
1375
1595
|
|
|
1596
|
+
// src/error-classifier.ts
|
|
1597
|
+
function parseRetryAfterMs(message) {
|
|
1598
|
+
const match = message.match(/retry[- ]?after["':\s]+(\d+)/i);
|
|
1599
|
+
if (!match)
|
|
1600
|
+
return;
|
|
1601
|
+
const seconds = Number(match[1]);
|
|
1602
|
+
if (!Number.isFinite(seconds) || seconds <= 0)
|
|
1603
|
+
return;
|
|
1604
|
+
return seconds * 1000;
|
|
1605
|
+
}
|
|
1606
|
+
function classifyRunError(message) {
|
|
1607
|
+
if (!message)
|
|
1608
|
+
return { kind: null };
|
|
1609
|
+
const retryAfterMs = parseRetryAfterMs(message);
|
|
1610
|
+
if (AUTH.test(message))
|
|
1611
|
+
return { kind: "auth", retryAfterMs };
|
|
1612
|
+
if (OUT_OF_CREDITS.test(message))
|
|
1613
|
+
return { kind: "out_of_credits", retryAfterMs };
|
|
1614
|
+
if (USAGE_LIMIT.test(message))
|
|
1615
|
+
return { kind: "usage_limit", retryAfterMs };
|
|
1616
|
+
if (RATE_LIMIT.test(message))
|
|
1617
|
+
return { kind: "rate_limit", retryAfterMs };
|
|
1618
|
+
return { kind: null };
|
|
1619
|
+
}
|
|
1620
|
+
function describeApiError(kind) {
|
|
1621
|
+
switch (kind) {
|
|
1622
|
+
case "auth":
|
|
1623
|
+
return "Anthropic auth error — agent paused, check API credentials";
|
|
1624
|
+
case "out_of_credits":
|
|
1625
|
+
return "Anthropic credit balance too low — retrying after top-up";
|
|
1626
|
+
case "usage_limit":
|
|
1627
|
+
return "Anthropic usage limit reached — retrying after reset";
|
|
1628
|
+
case "rate_limit":
|
|
1629
|
+
return "Anthropic rate limit hit — retrying shortly";
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
function cooldownMsFor(kind) {
|
|
1633
|
+
switch (kind) {
|
|
1634
|
+
case "rate_limit":
|
|
1635
|
+
return 60000;
|
|
1636
|
+
case "usage_limit":
|
|
1637
|
+
return 15 * 60000;
|
|
1638
|
+
case "out_of_credits":
|
|
1639
|
+
return 30 * 60000;
|
|
1640
|
+
case "auth":
|
|
1641
|
+
return 30 * 60000;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
var AUTH, OUT_OF_CREDITS, USAGE_LIMIT, RATE_LIMIT;
|
|
1645
|
+
var init_error_classifier = __esm(() => {
|
|
1646
|
+
AUTH = /\b401\b|invalid x-api-key|authentication_error|unauthorized|oauth token (?:has )?expired|please run .*login|invalid bearer token/i;
|
|
1647
|
+
OUT_OF_CREDITS = /\b402\b|credit balance is too low|insufficient (?:funds|credit|balance)|billing|payment required|purchase more credits/i;
|
|
1648
|
+
USAGE_LIMIT = /usage limit|daily limit|monthly limit|quota (?:exceeded|reached)|reached your .{0,20}limit|usage_limit_reached|limit will reset/i;
|
|
1649
|
+
RATE_LIMIT = /\b429\b|\b529\b|rate[ _-]?limit|too many requests|overloaded_error|"type"\s*:\s*"overloaded"/i;
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1376
1652
|
// src/queue.ts
|
|
1377
1653
|
class PriorityQueue {
|
|
1378
1654
|
config;
|
|
@@ -2385,13 +2661,15 @@ class ProgressTracker {
|
|
|
2385
2661
|
sessionId = null;
|
|
2386
2662
|
lastAssistantText = "";
|
|
2387
2663
|
assistantTextBlocks = [];
|
|
2388
|
-
constructor(client, cardId, workerId, subtasks) {
|
|
2664
|
+
constructor(client, cardId, workerId, subtasks, initialPhase = "exploring") {
|
|
2389
2665
|
this.client = client;
|
|
2390
2666
|
this.cardId = cardId;
|
|
2391
2667
|
this.workerId = workerId;
|
|
2392
2668
|
this.subtaskTotal = subtasks.length;
|
|
2393
2669
|
this.subtaskCompleted = subtasks.filter((s) => s.completed).length;
|
|
2394
2670
|
this.subtaskMode = subtasks.length > 0;
|
|
2671
|
+
this.phase = initialPhase;
|
|
2672
|
+
this.progress = PHASES[initialPhase].min;
|
|
2395
2673
|
}
|
|
2396
2674
|
setSessionId(id) {
|
|
2397
2675
|
this.sessionId = id;
|
|
@@ -2719,6 +2997,7 @@ var init_progress_tracker = __esm(() => {
|
|
|
2719
2997
|
GIT_COMMIT_RE = /\bgit\s+commit\b/;
|
|
2720
2998
|
BUILD_CMD_RE = /\b(test|build|lint|check|tsc|vitest|jest|(?:bun|npm|pnpm|yarn) run (?:build|lint))\b/;
|
|
2721
2999
|
PHASES = {
|
|
3000
|
+
planning: { min: 5, max: 20, label: "Planning" },
|
|
2722
3001
|
exploring: { min: 10, max: 25, label: "Exploring" },
|
|
2723
3002
|
implementing: { min: 25, max: 55, label: "Implementing" },
|
|
2724
3003
|
testing: { min: 55, max: 65, label: "Testing" },
|
|
@@ -2726,6 +3005,7 @@ var init_progress_tracker = __esm(() => {
|
|
|
2726
3005
|
finishing: { min: 70, max: 75, label: "Finalizing" }
|
|
2727
3006
|
};
|
|
2728
3007
|
PHASE_ORDER = {
|
|
3008
|
+
planning: -1,
|
|
2729
3009
|
exploring: 0,
|
|
2730
3010
|
implementing: 1,
|
|
2731
3011
|
testing: 2,
|
|
@@ -3031,6 +3311,14 @@ ${finding.description}${locationLine}`
|
|
|
3031
3311
|
`);
|
|
3032
3312
|
await postReviewComment(client, card, "summary", body);
|
|
3033
3313
|
}
|
|
3314
|
+
if (config.planning.enabled && card.plan_id) {
|
|
3315
|
+
try {
|
|
3316
|
+
await client.updateCard(card.id, { needsPlanRefresh: true });
|
|
3317
|
+
log.info(TAG15, `#${card.short_id} flagged needs_plan_refresh after rejected review`);
|
|
3318
|
+
} catch (err) {
|
|
3319
|
+
log.warn(TAG15, `Failed to flag needs_plan_refresh for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3034
3322
|
await moveCardToColumn(client, card, config.review.failColumn);
|
|
3035
3323
|
const failureSummary = `Review rejected (cycle ${currentCycle}/${maxCycles}): ${criticalFindings.length} critical, ${majorFindings.length} major, ${minorFindings.length} minor`;
|
|
3036
3324
|
const recoveryBranch = branchName ?? undefined;
|
|
@@ -3443,6 +3731,13 @@ class StateStore {
|
|
|
3443
3731
|
await this.persist();
|
|
3444
3732
|
return rec.attempts;
|
|
3445
3733
|
}
|
|
3734
|
+
async decrementAttempt(cardId) {
|
|
3735
|
+
const rec = this.getCard(cardId);
|
|
3736
|
+
if (!rec || rec.attempts === 0)
|
|
3737
|
+
return;
|
|
3738
|
+
rec.attempts = Math.max(0, rec.attempts - 1);
|
|
3739
|
+
await this.persist();
|
|
3740
|
+
}
|
|
3446
3741
|
async recordOutcome(cardId, outcome) {
|
|
3447
3742
|
const rec = this.ensureCard(cardId);
|
|
3448
3743
|
rec.lastOutcome = outcome;
|
|
@@ -3752,7 +4047,6 @@ var init_transitions = __esm(() => {
|
|
|
3752
4047
|
|
|
3753
4048
|
// src/review-worker.ts
|
|
3754
4049
|
import { execFileSync as execFileSync8 } from "node:child_process";
|
|
3755
|
-
import { createHash } from "node:crypto";
|
|
3756
4050
|
|
|
3757
4051
|
class ReviewWorker {
|
|
3758
4052
|
config;
|
|
@@ -3944,19 +4238,11 @@ class ReviewWorker {
|
|
|
3944
4238
|
const systemPrompt = buildReviewSystemPrompt();
|
|
3945
4239
|
const userPrompt = buildReviewUserPrompt(enriched, this.branchName, cwd, previewUrl, diff, this.config.worktree.baseBranch);
|
|
3946
4240
|
try {
|
|
3947
|
-
const sessionResp = await this.client.getAgentSession(card.id);
|
|
3948
|
-
const reviewSession2 = sessionResp.session;
|
|
3949
|
-
const reviewSessionId = reviewSession2?.id ?? null;
|
|
3950
|
-
const contentHash = createHash("sha256").update(systemPrompt).digest("hex");
|
|
3951
4241
|
await this.client.recordPromptHistory({
|
|
3952
4242
|
cardId: card.id,
|
|
3953
4243
|
generatedPrompt: systemPrompt,
|
|
3954
4244
|
variant: "execute",
|
|
3955
|
-
contextIncluded: { source: "review-knowledge", mode: "review" }
|
|
3956
|
-
sessionId: reviewSessionId,
|
|
3957
|
-
contentHash,
|
|
3958
|
-
templateVersion: 1,
|
|
3959
|
-
confidence: 0.5
|
|
4245
|
+
contextIncluded: { source: "review-knowledge", mode: "review" }
|
|
3960
4246
|
});
|
|
3961
4247
|
} catch (err) {
|
|
3962
4248
|
log.warn(this.tag, `prompt_history persistence skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -4408,8 +4694,13 @@ async function promoteUnblockedSuccessors(completedCard, deps) {
|
|
|
4408
4694
|
const successorId = link.target_card.id;
|
|
4409
4695
|
try {
|
|
4410
4696
|
const { card } = await deps.client.getCard(successorId);
|
|
4411
|
-
if (card.assigned_agent_id
|
|
4412
|
-
log.
|
|
4697
|
+
if (card.assigned_agent_id === deps.agentId) {} else if (card.assigned_agent_id === null && !card.assignee_id) {
|
|
4698
|
+
log.info(TAG21, `successor #${card.short_id} unassigned — auto-assigning to continue chain`);
|
|
4699
|
+
await deps.client.updateCard(successorId, {
|
|
4700
|
+
assignedAgentId: deps.agentId
|
|
4701
|
+
});
|
|
4702
|
+
} else {
|
|
4703
|
+
log.debug(TAG21, `successor #${card.short_id} assigned to different entity — skipping`);
|
|
4413
4704
|
continue;
|
|
4414
4705
|
}
|
|
4415
4706
|
await deps.enqueue(successorId);
|
|
@@ -4691,6 +4982,7 @@ class Worker {
|
|
|
4691
4982
|
projectId;
|
|
4692
4983
|
stateStore;
|
|
4693
4984
|
onCardCompleted;
|
|
4985
|
+
onApiError;
|
|
4694
4986
|
id;
|
|
4695
4987
|
state = "idle";
|
|
4696
4988
|
cardId = null;
|
|
@@ -4707,7 +4999,7 @@ class Worker {
|
|
|
4707
4999
|
verificationFailed = false;
|
|
4708
5000
|
sessionId = null;
|
|
4709
5001
|
runId = null;
|
|
4710
|
-
constructor(id, config, client, agentId, onDone, workspaceId, projectId, stateStore, onCardCompleted) {
|
|
5002
|
+
constructor(id, config, client, agentId, onDone, workspaceId, projectId, stateStore, onCardCompleted, onApiError) {
|
|
4711
5003
|
this.config = config;
|
|
4712
5004
|
this.client = client;
|
|
4713
5005
|
this.agentId = agentId;
|
|
@@ -4716,6 +5008,7 @@ class Worker {
|
|
|
4716
5008
|
this.projectId = projectId;
|
|
4717
5009
|
this.stateStore = stateStore;
|
|
4718
5010
|
this.onCardCompleted = onCardCompleted;
|
|
5011
|
+
this.onApiError = onApiError;
|
|
4719
5012
|
this.id = id;
|
|
4720
5013
|
}
|
|
4721
5014
|
startHeartbeat() {
|
|
@@ -4756,7 +5049,7 @@ class Worker {
|
|
|
4756
5049
|
return this.state === "idle";
|
|
4757
5050
|
}
|
|
4758
5051
|
get isActive() {
|
|
4759
|
-
return this.state === "preparing" || this.state === "running" || this.state === "verifying" || this.state === "completing";
|
|
5052
|
+
return this.state === "preparing" || this.state === "planning" || this.state === "running" || this.state === "verifying" || this.state === "completing";
|
|
4760
5053
|
}
|
|
4761
5054
|
async run(card, column, labels, subtasks) {
|
|
4762
5055
|
this.aborted = false;
|
|
@@ -4802,7 +5095,7 @@ class Worker {
|
|
|
4802
5095
|
}
|
|
4803
5096
|
this.sessionId = sid;
|
|
4804
5097
|
await this.recordPhase("preparing");
|
|
4805
|
-
const moved = await moveCardAndAddLabel(this.client, card,
|
|
5098
|
+
const moved = await moveCardAndAddLabel(this.client, card, IN_PROGRESS_COLUMN, "agent");
|
|
4806
5099
|
if (!moved) {
|
|
4807
5100
|
log.warn(this.tag, `Card #${card.short_id} was NOT moved to "In Progress" — check API logs`);
|
|
4808
5101
|
}
|
|
@@ -4811,8 +5104,6 @@ class Worker {
|
|
|
4811
5104
|
this.worktreePath = createWorktree(this.config.worktree.basePath, this.config.worktree.baseBranch, this.branchName);
|
|
4812
5105
|
if (this.aborted)
|
|
4813
5106
|
return;
|
|
4814
|
-
this.state = "running";
|
|
4815
|
-
await this.recordPhase("running");
|
|
4816
5107
|
const enriched = {
|
|
4817
5108
|
card,
|
|
4818
5109
|
column,
|
|
@@ -4820,6 +5111,19 @@ class Worker {
|
|
|
4820
5111
|
subtasks,
|
|
4821
5112
|
mode: "implement"
|
|
4822
5113
|
};
|
|
5114
|
+
if (shouldPlan(enriched, this.config.planning)) {
|
|
5115
|
+
this.state = "planning";
|
|
5116
|
+
await this.recordPhase("planning");
|
|
5117
|
+
const parked = await this.runPlanningPhase(enriched);
|
|
5118
|
+
if (this.aborted)
|
|
5119
|
+
return;
|
|
5120
|
+
if (parked) {
|
|
5121
|
+
log.info(this.tag, `#${card.short_id} parked for plan approval — ending run`);
|
|
5122
|
+
return;
|
|
5123
|
+
}
|
|
5124
|
+
}
|
|
5125
|
+
this.state = "running";
|
|
5126
|
+
await this.recordPhase("running");
|
|
4823
5127
|
const prompt = await buildPrompt(enriched, this.branchName, this.worktreePath, this.client, this.workspaceId, this.projectId);
|
|
4824
5128
|
await this.client.updateAgentProgress(card.id, {
|
|
4825
5129
|
agentIdentifier: agentIdentifier(this.id),
|
|
@@ -4858,21 +5162,46 @@ class Worker {
|
|
|
4858
5162
|
this.state = "error";
|
|
4859
5163
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4860
5164
|
log.error(this.tag, `Error on #${card.short_id}: ${msg}`);
|
|
5165
|
+
const rawStderr = err?.stderr;
|
|
5166
|
+
const errClass = classifyRunError(typeof rawStderr === "string" && rawStderr ? rawStderr : msg);
|
|
5167
|
+
const apiError = errClass.kind !== null;
|
|
5168
|
+
if (apiError) {
|
|
5169
|
+
try {
|
|
5170
|
+
this.onApiError?.(errClass);
|
|
5171
|
+
} catch {}
|
|
5172
|
+
}
|
|
5173
|
+
if (this.worktreePath) {
|
|
5174
|
+
try {
|
|
5175
|
+
cleanupWorktree(this.worktreePath, this.branchName ?? undefined);
|
|
5176
|
+
} catch {
|
|
5177
|
+
log.warn(this.tag, "Failed to cleanup worktree before requeue");
|
|
5178
|
+
}
|
|
5179
|
+
this.worktreePath = null;
|
|
5180
|
+
}
|
|
5181
|
+
const failureReason = apiError ? errClass.kind : "other";
|
|
5182
|
+
const failureSummary = apiError ? describeApiError(errClass.kind) : `Run failed: ${msg.slice(0, 300)}`;
|
|
4861
5183
|
try {
|
|
4862
5184
|
await runTransition(this.client, card, {
|
|
5185
|
+
move: { columnName: this.config.pickupColumns[0] ?? "To Do" },
|
|
4863
5186
|
endSession: {
|
|
4864
|
-
status: "
|
|
5187
|
+
status: "failed",
|
|
5188
|
+
failureReason,
|
|
5189
|
+
failureSummary,
|
|
4865
5190
|
...buildTokenPayload(this.lastSessionStats)
|
|
4866
5191
|
}
|
|
4867
5192
|
});
|
|
4868
5193
|
} catch (tErr) {
|
|
4869
|
-
log.error(this.tag, `
|
|
5194
|
+
log.error(this.tag, `error transition failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
|
|
4870
5195
|
}
|
|
4871
5196
|
if (this.runId) {
|
|
4872
5197
|
try {
|
|
4873
|
-
await this.stateStore.endRun(this.runId, "
|
|
5198
|
+
await this.stateStore.endRun(this.runId, "failed", errClass.kind ?? msg);
|
|
4874
5199
|
} catch {}
|
|
4875
|
-
|
|
5200
|
+
if (apiError) {
|
|
5201
|
+
await this.stateStore.decrementAttempt(card.id);
|
|
5202
|
+
} else {
|
|
5203
|
+
await this.recordOutcome(card.id, "failure");
|
|
5204
|
+
}
|
|
4876
5205
|
}
|
|
4877
5206
|
} finally {
|
|
4878
5207
|
const succeeded = this.runId && this.state !== "error" && !this.aborted && !this.verificationFailed;
|
|
@@ -5016,7 +5345,122 @@ class Worker {
|
|
|
5016
5345
|
}
|
|
5017
5346
|
}
|
|
5018
5347
|
}
|
|
5019
|
-
async
|
|
5348
|
+
async runPlanningPhase(enriched) {
|
|
5349
|
+
const planning = this.config.planning;
|
|
5350
|
+
const { card } = enriched;
|
|
5351
|
+
log.info(this.tag, `Planning pass for #${card.short_id} (mode=${planning.mode}, model=${planning.model})`);
|
|
5352
|
+
await this.client.updateAgentProgress(card.id, {
|
|
5353
|
+
agentIdentifier: agentIdentifier(this.id),
|
|
5354
|
+
agentName: AGENT_NAME,
|
|
5355
|
+
status: "working",
|
|
5356
|
+
currentTask: "Planning approach (read-only)",
|
|
5357
|
+
progressPercent: 5,
|
|
5358
|
+
phase: "planning"
|
|
5359
|
+
}).catch(() => {});
|
|
5360
|
+
const planPrompt = buildPlanPrompt(enriched, this.worktreePath);
|
|
5361
|
+
let planTimedOut = false;
|
|
5362
|
+
const planTimeout = setTimeout(() => {
|
|
5363
|
+
planTimedOut = true;
|
|
5364
|
+
log.warn(this.tag, "Planning pass exceeded timeout — abandoning, implementing directly");
|
|
5365
|
+
if (this.process && !this.process.killed) {
|
|
5366
|
+
terminateGroup(this.process, {
|
|
5367
|
+
sigintTimeoutMs: 1e4,
|
|
5368
|
+
sigtermTimeoutMs: 5000
|
|
5369
|
+
}).catch(() => {});
|
|
5370
|
+
}
|
|
5371
|
+
}, Math.min(this.config.maxTimeout, PLAN_PHASE_TIMEOUT));
|
|
5372
|
+
try {
|
|
5373
|
+
await this.spawnClaude(planPrompt, card, [], {
|
|
5374
|
+
model: planning.model,
|
|
5375
|
+
maxTurns: planning.maxTurns,
|
|
5376
|
+
allowedTools: PLAN_ALLOWED_TOOLS,
|
|
5377
|
+
initialPhase: "planning"
|
|
5378
|
+
});
|
|
5379
|
+
} catch (err) {
|
|
5380
|
+
log.warn(this.tag, `Planning pass failed (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
5381
|
+
return false;
|
|
5382
|
+
} finally {
|
|
5383
|
+
clearTimeout(planTimeout);
|
|
5384
|
+
}
|
|
5385
|
+
if (this.aborted || planTimedOut)
|
|
5386
|
+
return false;
|
|
5387
|
+
const stats = this.lastSessionStats;
|
|
5388
|
+
if (stats?.cost) {
|
|
5389
|
+
const cents = Math.round(stats.cost.totalCostUsd * 100);
|
|
5390
|
+
if (cents > 0) {
|
|
5391
|
+
try {
|
|
5392
|
+
await this.stateStore.addCost(card.id, cents);
|
|
5393
|
+
} catch {}
|
|
5394
|
+
}
|
|
5395
|
+
}
|
|
5396
|
+
const planText = stats?.lastAssistantText ?? "";
|
|
5397
|
+
if (!planText.trim()) {
|
|
5398
|
+
log.warn(this.tag, `Planning pass for #${card.short_id} produced no text — implementing directly`);
|
|
5399
|
+
return false;
|
|
5400
|
+
}
|
|
5401
|
+
const artifact = extractPlanArtifact(planText, card.title);
|
|
5402
|
+
let planId = null;
|
|
5403
|
+
try {
|
|
5404
|
+
const result = await this.client.createPlan(this.projectId, {
|
|
5405
|
+
title: artifact.title,
|
|
5406
|
+
content: artifact.markdown,
|
|
5407
|
+
source: "agent",
|
|
5408
|
+
tasks: artifact.tasks
|
|
5409
|
+
});
|
|
5410
|
+
const plan = result?.plan;
|
|
5411
|
+
const createdId = plan && typeof plan === "object" && "id" in plan ? plan.id : null;
|
|
5412
|
+
if (createdId) {
|
|
5413
|
+
await this.client.updateCard(card.id, {
|
|
5414
|
+
planId: createdId,
|
|
5415
|
+
needsPlanRefresh: false
|
|
5416
|
+
});
|
|
5417
|
+
planId = createdId;
|
|
5418
|
+
}
|
|
5419
|
+
log.info(this.tag, `Stored plan ${planId ?? "(unlinked)"} for #${card.short_id} (${artifact.tasks.length} tasks)`);
|
|
5420
|
+
} catch (err) {
|
|
5421
|
+
log.warn(this.tag, `Failed to store/link plan (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
5422
|
+
}
|
|
5423
|
+
if (planning.mode === "gated" && planId) {
|
|
5424
|
+
try {
|
|
5425
|
+
if (planning.postComment) {
|
|
5426
|
+
await this.client.addComment(card.id, buildGatedPlanComment(artifact, planning.awaitingApprovalColumn), {
|
|
5427
|
+
commentType: "decision",
|
|
5428
|
+
agentSessionId: this.sessionId ?? undefined
|
|
5429
|
+
});
|
|
5430
|
+
}
|
|
5431
|
+
await runTransition(this.client, card, {
|
|
5432
|
+
move: { columnName: planning.awaitingApprovalColumn },
|
|
5433
|
+
removeLabels: ["agent"],
|
|
5434
|
+
endSession: {
|
|
5435
|
+
status: "completed",
|
|
5436
|
+
progressPercent: 20,
|
|
5437
|
+
...buildTokenPayload(stats)
|
|
5438
|
+
}
|
|
5439
|
+
}, { store: this.stateStore, runId: this.runId ?? undefined });
|
|
5440
|
+
log.info(this.tag, `#${card.short_id} parked in "${planning.awaitingApprovalColumn}" for plan approval`);
|
|
5441
|
+
this.lastSessionStats = undefined;
|
|
5442
|
+
return true;
|
|
5443
|
+
} catch (err) {
|
|
5444
|
+
log.warn(this.tag, `Gated park failed for #${card.short_id} (non-fatal, implementing directly): ${err instanceof TransitionError ? err.detail : err instanceof Error ? err.message : err}`);
|
|
5445
|
+
}
|
|
5446
|
+
}
|
|
5447
|
+
if (planId && planning.postComment) {
|
|
5448
|
+
try {
|
|
5449
|
+
await this.client.addComment(card.id, buildPlanComment(artifact), {
|
|
5450
|
+
commentType: "decision",
|
|
5451
|
+
agentSessionId: this.sessionId ?? undefined
|
|
5452
|
+
});
|
|
5453
|
+
} catch (err) {
|
|
5454
|
+
log.warn(this.tag, `Failed to post advisory plan comment (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
5455
|
+
}
|
|
5456
|
+
}
|
|
5457
|
+
return false;
|
|
5458
|
+
}
|
|
5459
|
+
async spawnClaude(prompt, card, subtasks, opts = {}) {
|
|
5460
|
+
const model = opts.model ?? this.config.claude.model;
|
|
5461
|
+
const maxTurns = opts.maxTurns ?? this.config.claude.maxTurns;
|
|
5462
|
+
const allowedTools = opts.allowedTools ?? IMPLEMENT_ALLOWED_TOOLS;
|
|
5463
|
+
const initialPhase = opts.initialPhase ?? "exploring";
|
|
5020
5464
|
return new Promise((resolve3, reject) => {
|
|
5021
5465
|
const args = [
|
|
5022
5466
|
"-p",
|
|
@@ -5024,11 +5468,11 @@ class Worker {
|
|
|
5024
5468
|
"--output-format",
|
|
5025
5469
|
"stream-json",
|
|
5026
5470
|
"--model",
|
|
5027
|
-
|
|
5471
|
+
model,
|
|
5028
5472
|
"--max-turns",
|
|
5029
|
-
String(
|
|
5473
|
+
String(maxTurns),
|
|
5030
5474
|
"--allowedTools",
|
|
5031
|
-
|
|
5475
|
+
allowedTools,
|
|
5032
5476
|
...this.config.claude.additionalArgs,
|
|
5033
5477
|
"--",
|
|
5034
5478
|
prompt
|
|
@@ -5047,7 +5491,7 @@ class Worker {
|
|
|
5047
5491
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5048
5492
|
});
|
|
5049
5493
|
const parser = new StreamParser;
|
|
5050
|
-
this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks);
|
|
5494
|
+
this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks, initialPhase);
|
|
5051
5495
|
if (this.sessionId) {
|
|
5052
5496
|
this.progressTracker.setSessionId(this.sessionId);
|
|
5053
5497
|
}
|
|
@@ -5092,7 +5536,9 @@ class Worker {
|
|
|
5092
5536
|
} else if (code === 0) {
|
|
5093
5537
|
resolve3();
|
|
5094
5538
|
} else {
|
|
5095
|
-
|
|
5539
|
+
const err = new Error(`claude exited with code ${code}${stderr ? `: ${stderr.slice(0, 500)}` : ""}`);
|
|
5540
|
+
err.stderr = stderr;
|
|
5541
|
+
reject(err);
|
|
5096
5542
|
}
|
|
5097
5543
|
});
|
|
5098
5544
|
});
|
|
@@ -5124,11 +5570,13 @@ class Worker {
|
|
|
5124
5570
|
this.sessionId = null;
|
|
5125
5571
|
}
|
|
5126
5572
|
}
|
|
5127
|
-
var TAG23 = "worker", CANCEL_SIGINT_TIMEOUT2 = 30000, CANCEL_SIGTERM_TIMEOUT2 = 1e4;
|
|
5573
|
+
var TAG23 = "worker", CANCEL_SIGINT_TIMEOUT2 = 30000, CANCEL_SIGTERM_TIMEOUT2 = 1e4, PLAN_ALLOWED_TOOLS = "Read,Grep,Glob,mcp__harmony__*", IMPLEMENT_ALLOWED_TOOLS = "Bash,Read,Write,Edit,Glob,Grep,Agent,mcp__harmony__*", PLAN_PHASE_TIMEOUT;
|
|
5128
5574
|
var init_worker = __esm(() => {
|
|
5129
5575
|
init_board_helpers();
|
|
5130
5576
|
init_completion();
|
|
5577
|
+
init_error_classifier();
|
|
5131
5578
|
init_log();
|
|
5579
|
+
init_plan_phase();
|
|
5132
5580
|
init_process_group();
|
|
5133
5581
|
init_progress_tracker();
|
|
5134
5582
|
init_prompt();
|
|
@@ -5138,6 +5586,7 @@ var init_worker = __esm(() => {
|
|
|
5138
5586
|
init_transitions();
|
|
5139
5587
|
init_types();
|
|
5140
5588
|
init_worktree();
|
|
5589
|
+
PLAN_PHASE_TIMEOUT = 10 * 60000;
|
|
5141
5590
|
});
|
|
5142
5591
|
|
|
5143
5592
|
// src/pool.ts
|
|
@@ -5153,6 +5602,8 @@ class Pool {
|
|
|
5153
5602
|
budget;
|
|
5154
5603
|
sleepGuard = new SleepGuard;
|
|
5155
5604
|
shuttingDown = false;
|
|
5605
|
+
apiCooldownUntil = 0;
|
|
5606
|
+
authPaused = false;
|
|
5156
5607
|
onCardCompleted = null;
|
|
5157
5608
|
constructor(config, client, _userEmail, workspaceId, projectId, stateStore, agentId) {
|
|
5158
5609
|
this.client = client;
|
|
@@ -5171,7 +5622,7 @@ class Pool {
|
|
|
5171
5622
|
}
|
|
5172
5623
|
}, workspaceId, projectId, stateStore, async (completedCard) => {
|
|
5173
5624
|
await this.onCardCompleted?.(completedCard);
|
|
5174
|
-
}));
|
|
5625
|
+
}, (err) => this.noteApiError(err)));
|
|
5175
5626
|
}
|
|
5176
5627
|
if (config.review.enabled) {
|
|
5177
5628
|
for (let i = 0;i < config.review.poolSize; i++) {
|
|
@@ -5192,6 +5643,17 @@ class Pool {
|
|
|
5192
5643
|
return;
|
|
5193
5644
|
}
|
|
5194
5645
|
if (mode === "implement") {
|
|
5646
|
+
if (this.authPaused) {
|
|
5647
|
+
log.debug(TAG24, `#${card.short_id} held — agent paused (auth error)`);
|
|
5648
|
+
await this.emitWaiting(card.id, "Agent paused — Anthropic auth error, check API credentials");
|
|
5649
|
+
return;
|
|
5650
|
+
}
|
|
5651
|
+
const cooldownMs = this.apiCooldownRemainingMs();
|
|
5652
|
+
if (cooldownMs > 0) {
|
|
5653
|
+
log.debug(TAG24, `#${card.short_id} held — API cooldown ${Math.round(cooldownMs / 1000)}s remaining`);
|
|
5654
|
+
await this.emitWaiting(card.id, `Paused — Anthropic API limit, retrying in ~${Math.round(cooldownMs / 1000)}s`);
|
|
5655
|
+
return;
|
|
5656
|
+
}
|
|
5195
5657
|
const decision = this.budget.check(card.id);
|
|
5196
5658
|
if (!decision.allow) {
|
|
5197
5659
|
if (decision.reason === "daily_budget") {
|
|
@@ -5241,6 +5703,26 @@ class Pool {
|
|
|
5241
5703
|
log.debug(TAG24, `waiting emit failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
5242
5704
|
}
|
|
5243
5705
|
}
|
|
5706
|
+
noteApiError(err) {
|
|
5707
|
+
if (!err.kind)
|
|
5708
|
+
return;
|
|
5709
|
+
if (err.kind === "auth") {
|
|
5710
|
+
if (!this.authPaused) {
|
|
5711
|
+
log.error(TAG24, "Auth error from Claude CLI — pausing implement pickups until the daemon is restarted with valid credentials");
|
|
5712
|
+
}
|
|
5713
|
+
this.authPaused = true;
|
|
5714
|
+
return;
|
|
5715
|
+
}
|
|
5716
|
+
const cooldownMs = err.retryAfterMs ?? cooldownMsFor(err.kind);
|
|
5717
|
+
const until = Date.now() + cooldownMs;
|
|
5718
|
+
if (until > this.apiCooldownUntil) {
|
|
5719
|
+
this.apiCooldownUntil = until;
|
|
5720
|
+
log.warn(TAG24, `${describeApiError(err.kind)} — pausing implement pickups for ${Math.round(cooldownMs / 1000)}s`);
|
|
5721
|
+
}
|
|
5722
|
+
}
|
|
5723
|
+
apiCooldownRemainingMs() {
|
|
5724
|
+
return Math.max(0, this.apiCooldownUntil - Date.now());
|
|
5725
|
+
}
|
|
5244
5726
|
async removeCard(cardId) {
|
|
5245
5727
|
await this.stateStore.resetAttempts(cardId);
|
|
5246
5728
|
this.lastWaitingEmit.delete(cardId);
|
|
@@ -5368,6 +5850,7 @@ class Pool {
|
|
|
5368
5850
|
}
|
|
5369
5851
|
var TAG24 = "pool";
|
|
5370
5852
|
var init_pool = __esm(() => {
|
|
5853
|
+
init_error_classifier();
|
|
5371
5854
|
init_log();
|
|
5372
5855
|
init_queue();
|
|
5373
5856
|
init_review_worker();
|
|
@@ -5377,6 +5860,78 @@ var init_pool = __esm(() => {
|
|
|
5377
5860
|
init_worker();
|
|
5378
5861
|
});
|
|
5379
5862
|
|
|
5863
|
+
// src/port-registry.ts
|
|
5864
|
+
var exports_port_registry = {};
|
|
5865
|
+
__export(exports_port_registry, {
|
|
5866
|
+
recordDaemonPort: () => recordDaemonPort,
|
|
5867
|
+
lookupDaemonPort: () => lookupDaemonPort,
|
|
5868
|
+
defaultRegistryPath: () => defaultRegistryPath,
|
|
5869
|
+
clearDaemonPort: () => clearDaemonPort
|
|
5870
|
+
});
|
|
5871
|
+
import {
|
|
5872
|
+
existsSync as existsSync5,
|
|
5873
|
+
mkdirSync as mkdirSync3,
|
|
5874
|
+
readFileSync as readFileSync4,
|
|
5875
|
+
renameSync as renameSync2,
|
|
5876
|
+
writeFileSync as writeFileSync2
|
|
5877
|
+
} from "node:fs";
|
|
5878
|
+
import { homedir as homedir4 } from "node:os";
|
|
5879
|
+
import { dirname as dirname2, join as join4 } from "node:path";
|
|
5880
|
+
function defaultRegistryPath() {
|
|
5881
|
+
return join4(homedir4(), ".harmony-mcp", "agent-ports.json");
|
|
5882
|
+
}
|
|
5883
|
+
function load(path) {
|
|
5884
|
+
if (!existsSync5(path))
|
|
5885
|
+
return {};
|
|
5886
|
+
try {
|
|
5887
|
+
const raw = readFileSync4(path, "utf-8");
|
|
5888
|
+
const parsed = JSON.parse(raw);
|
|
5889
|
+
if (parsed && typeof parsed === "object")
|
|
5890
|
+
return parsed;
|
|
5891
|
+
return {};
|
|
5892
|
+
} catch (err) {
|
|
5893
|
+
log.warn(TAG25, `failed to read ${path}: ${err instanceof Error ? err.message : err}`);
|
|
5894
|
+
return {};
|
|
5895
|
+
}
|
|
5896
|
+
}
|
|
5897
|
+
function save(path, registry) {
|
|
5898
|
+
const dir = dirname2(path);
|
|
5899
|
+
if (!existsSync5(dir))
|
|
5900
|
+
mkdirSync3(dir, { recursive: true });
|
|
5901
|
+
const tmp = `${path}.tmp`;
|
|
5902
|
+
writeFileSync2(tmp, JSON.stringify(registry, null, 2), "utf-8");
|
|
5903
|
+
renameSync2(tmp, path);
|
|
5904
|
+
}
|
|
5905
|
+
function recordDaemonPort(projectId, entry, path = defaultRegistryPath()) {
|
|
5906
|
+
try {
|
|
5907
|
+
const registry = load(path);
|
|
5908
|
+
registry[projectId] = { ...entry, updatedAt: Date.now() };
|
|
5909
|
+
save(path, registry);
|
|
5910
|
+
} catch (err) {
|
|
5911
|
+
log.warn(TAG25, `failed to record port for ${projectId}: ${err instanceof Error ? err.message : err}`);
|
|
5912
|
+
}
|
|
5913
|
+
}
|
|
5914
|
+
function lookupDaemonPort(projectId, path = defaultRegistryPath()) {
|
|
5915
|
+
const registry = load(path);
|
|
5916
|
+
return registry[projectId] ?? null;
|
|
5917
|
+
}
|
|
5918
|
+
function clearDaemonPort(projectId, pid, path = defaultRegistryPath()) {
|
|
5919
|
+
try {
|
|
5920
|
+
const registry = load(path);
|
|
5921
|
+
const existing = registry[projectId];
|
|
5922
|
+
if (!existing || existing.pid !== pid)
|
|
5923
|
+
return;
|
|
5924
|
+
delete registry[projectId];
|
|
5925
|
+
save(path, registry);
|
|
5926
|
+
} catch (err) {
|
|
5927
|
+
log.warn(TAG25, `failed to clear port for ${projectId}: ${err instanceof Error ? err.message : err}`);
|
|
5928
|
+
}
|
|
5929
|
+
}
|
|
5930
|
+
var TAG25 = "port-registry";
|
|
5931
|
+
var init_port_registry = __esm(() => {
|
|
5932
|
+
init_log();
|
|
5933
|
+
});
|
|
5934
|
+
|
|
5380
5935
|
// src/recovery.ts
|
|
5381
5936
|
function isProcessAlive(pid, currentPid) {
|
|
5382
5937
|
if (pid === currentPid)
|
|
@@ -5393,7 +5948,7 @@ async function fetchCardSafely(client, cardId) {
|
|
|
5393
5948
|
const { card } = await client.getCard(cardId);
|
|
5394
5949
|
return card;
|
|
5395
5950
|
} catch (err) {
|
|
5396
|
-
log.warn(
|
|
5951
|
+
log.warn(TAG26, `cannot fetch card ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
5397
5952
|
return null;
|
|
5398
5953
|
}
|
|
5399
5954
|
}
|
|
@@ -5403,7 +5958,7 @@ async function recoverOrphans(store, client, config) {
|
|
|
5403
5958
|
return [];
|
|
5404
5959
|
}
|
|
5405
5960
|
const outcomes = [];
|
|
5406
|
-
log.info(
|
|
5961
|
+
log.info(TAG26, `recovering ${active.length} orphan run(s) from prior daemon`);
|
|
5407
5962
|
for (const run of active) {
|
|
5408
5963
|
const outcome = {
|
|
5409
5964
|
runId: run.runId,
|
|
@@ -5415,11 +5970,11 @@ async function recoverOrphans(store, client, config) {
|
|
|
5415
5970
|
};
|
|
5416
5971
|
outcomes.push(outcome);
|
|
5417
5972
|
if (isProcessAlive(run.daemonPid, process.pid)) {
|
|
5418
|
-
log.warn(
|
|
5973
|
+
log.warn(TAG26, `run ${run.runId} claims live daemon pid ${run.daemonPid} — skipping`);
|
|
5419
5974
|
outcome.actions.push("skipped: daemon pid still alive");
|
|
5420
5975
|
continue;
|
|
5421
5976
|
}
|
|
5422
|
-
log.info(
|
|
5977
|
+
log.info(TAG26, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
|
|
5423
5978
|
await recoverRun(run, store, client, config, outcome);
|
|
5424
5979
|
}
|
|
5425
5980
|
return outcomes;
|
|
@@ -5437,7 +5992,7 @@ async function recoverRun(run, store, client, config, outcome) {
|
|
|
5437
5992
|
} catch (err) {
|
|
5438
5993
|
const msg = err instanceof Error ? err.message : String(err);
|
|
5439
5994
|
outcome.errors.push(`endAgentSession: ${msg}`);
|
|
5440
|
-
log.warn(
|
|
5995
|
+
log.warn(TAG26, `endAgentSession failed for ${run.cardId}: ${msg}`);
|
|
5441
5996
|
}
|
|
5442
5997
|
const card = await fetchCardSafely(client, run.cardId);
|
|
5443
5998
|
if (card) {
|
|
@@ -5478,9 +6033,9 @@ async function recoverRun(run, store, client, config, outcome) {
|
|
|
5478
6033
|
const msg = err instanceof Error ? err.message : String(err);
|
|
5479
6034
|
outcome.errors.push(`endRun: ${msg}`);
|
|
5480
6035
|
}
|
|
5481
|
-
log.info(
|
|
6036
|
+
log.info(TAG26, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
|
|
5482
6037
|
}
|
|
5483
|
-
var
|
|
6038
|
+
var TAG26 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
|
|
5484
6039
|
var init_recovery = __esm(() => {
|
|
5485
6040
|
init_board_helpers();
|
|
5486
6041
|
init_log();
|
|
@@ -5528,7 +6083,7 @@ class Reconciler {
|
|
|
5528
6083
|
clearInterval(this.timer);
|
|
5529
6084
|
this.timer = null;
|
|
5530
6085
|
}
|
|
5531
|
-
log.info(
|
|
6086
|
+
log.info(TAG27, "Heartbeat stopped");
|
|
5532
6087
|
}
|
|
5533
6088
|
async recoverStaleRuns() {
|
|
5534
6089
|
if (!this.stateStore || !this.agentConfig)
|
|
@@ -5545,7 +6100,7 @@ class Reconciler {
|
|
|
5545
6100
|
if (!daemonDead && !(heartbeatStale && ourZombie))
|
|
5546
6101
|
continue;
|
|
5547
6102
|
const reason = daemonDead ? `foreign daemon ${run.daemonPid} is dead` : `our worker lost card ${run.cardId} with ${Math.round((now - run.lastHeartbeatAt) / 1000)}s stale heartbeat`;
|
|
5548
|
-
log.warn(
|
|
6103
|
+
log.warn(TAG27, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
|
|
5549
6104
|
await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
|
|
5550
6105
|
runId: run.runId,
|
|
5551
6106
|
cardId: run.cardId,
|
|
@@ -5556,6 +6111,57 @@ class Reconciler {
|
|
|
5556
6111
|
});
|
|
5557
6112
|
}
|
|
5558
6113
|
}
|
|
6114
|
+
async recoverStrandedInProgress(cards, columns, knownCardIds) {
|
|
6115
|
+
const inProgressCol = columns.find((c) => c.name.toLowerCase() === IN_PROGRESS_COLUMN.toLowerCase());
|
|
6116
|
+
const pickupName = this.pickupColumns[0];
|
|
6117
|
+
const pickupCol = pickupName ? columns.find((c) => c.name.toLowerCase() === pickupName.toLowerCase()) : undefined;
|
|
6118
|
+
if (!inProgressCol || !pickupCol)
|
|
6119
|
+
return;
|
|
6120
|
+
const graceMs = this.agentConfig?.timing.staleHeartbeatMs ?? 120000;
|
|
6121
|
+
const now = Date.now();
|
|
6122
|
+
const activeCardIds = new Set((this.stateStore?.getActiveRuns() ?? []).map((r) => r.cardId));
|
|
6123
|
+
for (const card of cards) {
|
|
6124
|
+
if (card.assigned_agent_id !== this.agentId || card.archived_at || card.column_id !== inProgressCol.id || knownCardIds.has(card.id) || activeCardIds.has(card.id)) {
|
|
6125
|
+
continue;
|
|
6126
|
+
}
|
|
6127
|
+
const stalledAt = Date.parse(card.updated_at ?? "");
|
|
6128
|
+
if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
|
|
6129
|
+
continue;
|
|
6130
|
+
log.warn(TAG27, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
|
|
6131
|
+
try {
|
|
6132
|
+
await this.client.moveCard(card.id, pickupCol.id);
|
|
6133
|
+
} catch (err) {
|
|
6134
|
+
log.error(TAG27, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
6135
|
+
}
|
|
6136
|
+
}
|
|
6137
|
+
}
|
|
6138
|
+
async releaseStalledApprovals(cards, columns, knownCardIds) {
|
|
6139
|
+
const planning = this.agentConfig?.planning;
|
|
6140
|
+
if (!planning?.enabled || planning.mode !== "gated" || planning.approvalTtlHours <= 0) {
|
|
6141
|
+
return;
|
|
6142
|
+
}
|
|
6143
|
+
const parkCol = columns.find((c) => c.name.toLowerCase() === planning.awaitingApprovalColumn.toLowerCase());
|
|
6144
|
+
const pickupName = this.pickupColumns[0];
|
|
6145
|
+
const pickupCol = pickupName ? columns.find((c) => c.name.toLowerCase() === pickupName.toLowerCase()) : undefined;
|
|
6146
|
+
if (!parkCol || !pickupCol)
|
|
6147
|
+
return;
|
|
6148
|
+
const ttlMs = planning.approvalTtlHours * 3600000;
|
|
6149
|
+
const now = Date.now();
|
|
6150
|
+
for (const card of cards) {
|
|
6151
|
+
if (card.assigned_agent_id !== this.agentId || card.archived_at || card.column_id !== parkCol.id || !card.plan_id || knownCardIds.has(card.id)) {
|
|
6152
|
+
continue;
|
|
6153
|
+
}
|
|
6154
|
+
const parkedAt = Date.parse(card.updated_at ?? "");
|
|
6155
|
+
if (!Number.isFinite(parkedAt) || now - parkedAt < ttlMs)
|
|
6156
|
+
continue;
|
|
6157
|
+
log.warn(TAG27, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
|
|
6158
|
+
try {
|
|
6159
|
+
await this.client.moveCard(card.id, pickupCol.id);
|
|
6160
|
+
} catch (err) {
|
|
6161
|
+
log.error(TAG27, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
6162
|
+
}
|
|
6163
|
+
}
|
|
6164
|
+
}
|
|
5559
6165
|
async tick() {
|
|
5560
6166
|
this.lastTickAt = Date.now();
|
|
5561
6167
|
try {
|
|
@@ -5581,37 +6187,39 @@ class Reconciler {
|
|
|
5581
6187
|
const subtasks = card.subtasks ?? [];
|
|
5582
6188
|
const mode = reviewColumnIds.has(card.column_id) ? "review" : "implement";
|
|
5583
6189
|
if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
|
|
5584
|
-
log.debug(
|
|
6190
|
+
log.debug(TAG27, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
|
|
5585
6191
|
continue;
|
|
5586
6192
|
}
|
|
5587
6193
|
if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
|
|
5588
|
-
log.debug(
|
|
6194
|
+
log.debug(TAG27, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
|
|
5589
6195
|
continue;
|
|
5590
6196
|
}
|
|
5591
6197
|
if (mode === "review" && !extractBranchFromDescription(card.description)) {
|
|
5592
|
-
log.debug(
|
|
6198
|
+
log.debug(TAG27, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
|
|
5593
6199
|
continue;
|
|
5594
6200
|
}
|
|
5595
|
-
log.info(
|
|
6201
|
+
log.info(TAG27, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
|
|
5596
6202
|
await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
5597
6203
|
}
|
|
5598
6204
|
}
|
|
5599
6205
|
if (this.stateStore && this.agentConfig) {
|
|
5600
6206
|
await this.recoverStaleRuns();
|
|
5601
6207
|
}
|
|
6208
|
+
await this.recoverStrandedInProgress(cards, columns, knownCardIds);
|
|
5602
6209
|
for (const knownId of knownCardIds) {
|
|
5603
6210
|
if (!allAgentCardIds.has(knownId)) {
|
|
5604
|
-
log.info(
|
|
6211
|
+
log.info(TAG27, `Missed unassign: ${knownId} — removing`);
|
|
5605
6212
|
await this.pool.removeCard(knownId);
|
|
5606
6213
|
}
|
|
5607
6214
|
}
|
|
5608
|
-
|
|
6215
|
+
await this.releaseStalledApprovals(cards, columns, knownCardIds);
|
|
6216
|
+
log.debug(TAG27, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
|
|
5609
6217
|
} catch (err) {
|
|
5610
|
-
log.error(
|
|
6218
|
+
log.error(TAG27, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
|
|
5611
6219
|
}
|
|
5612
6220
|
}
|
|
5613
6221
|
}
|
|
5614
|
-
var
|
|
6222
|
+
var TAG27 = "reconcile";
|
|
5615
6223
|
var init_reconcile = __esm(() => {
|
|
5616
6224
|
init_board_helpers();
|
|
5617
6225
|
init_log();
|
|
@@ -5632,6 +6240,7 @@ function prettyBanner(config, version) {
|
|
|
5632
6240
|
const checks = [];
|
|
5633
6241
|
let projectName;
|
|
5634
6242
|
let gitProvider;
|
|
6243
|
+
let httpPort;
|
|
5635
6244
|
let failed = false;
|
|
5636
6245
|
let rendered = false;
|
|
5637
6246
|
return {
|
|
@@ -5641,11 +6250,14 @@ function prettyBanner(config, version) {
|
|
|
5641
6250
|
setGitProvider(provider) {
|
|
5642
6251
|
gitProvider = provider;
|
|
5643
6252
|
},
|
|
6253
|
+
setHttpPort(port) {
|
|
6254
|
+
httpPort = port;
|
|
6255
|
+
},
|
|
5644
6256
|
check(message) {
|
|
5645
6257
|
checks.push({ kind: "ok", message });
|
|
5646
6258
|
},
|
|
5647
6259
|
warn(message) {
|
|
5648
|
-
log.warn(
|
|
6260
|
+
log.warn(TAG28, message);
|
|
5649
6261
|
checks.push({ kind: "warn", message: message.split(`
|
|
5650
6262
|
`, 1)[0] });
|
|
5651
6263
|
},
|
|
@@ -5661,6 +6273,7 @@ function prettyBanner(config, version) {
|
|
|
5661
6273
|
config,
|
|
5662
6274
|
projectName,
|
|
5663
6275
|
gitProvider,
|
|
6276
|
+
httpPort,
|
|
5664
6277
|
checks,
|
|
5665
6278
|
readyMessage: message
|
|
5666
6279
|
});
|
|
@@ -5669,22 +6282,25 @@ function prettyBanner(config, version) {
|
|
|
5669
6282
|
};
|
|
5670
6283
|
}
|
|
5671
6284
|
function jsonBanner(config, version) {
|
|
5672
|
-
log.info(
|
|
5673
|
-
log.info(
|
|
6285
|
+
log.info(TAG28, `Harmony Agent Daemon v${version} starting...`);
|
|
6286
|
+
log.info(TAG28, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
|
|
5674
6287
|
if (config.agent.review.enabled) {
|
|
5675
|
-
log.info(
|
|
6288
|
+
log.info(TAG28, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
|
|
5676
6289
|
}
|
|
5677
6290
|
let failed = false;
|
|
5678
6291
|
return {
|
|
5679
6292
|
setProjectName(_name) {},
|
|
5680
6293
|
setGitProvider(provider) {
|
|
5681
|
-
log.info(
|
|
6294
|
+
log.info(TAG28, `Git provider: ${provider}`);
|
|
6295
|
+
},
|
|
6296
|
+
setHttpPort(port) {
|
|
6297
|
+
log.info(TAG28, `HTTP server on port ${port}`);
|
|
5682
6298
|
},
|
|
5683
6299
|
check(message) {
|
|
5684
|
-
log.info(
|
|
6300
|
+
log.info(TAG28, message);
|
|
5685
6301
|
},
|
|
5686
6302
|
warn(message) {
|
|
5687
|
-
log.warn(
|
|
6303
|
+
log.warn(TAG28, message);
|
|
5688
6304
|
},
|
|
5689
6305
|
fail() {
|
|
5690
6306
|
failed = true;
|
|
@@ -5692,17 +6308,25 @@ function jsonBanner(config, version) {
|
|
|
5692
6308
|
async ready(message) {
|
|
5693
6309
|
if (failed)
|
|
5694
6310
|
return;
|
|
5695
|
-
log.info(
|
|
6311
|
+
log.info(TAG28, message);
|
|
5696
6312
|
}
|
|
5697
6313
|
};
|
|
5698
6314
|
}
|
|
5699
6315
|
function renderPretty(input) {
|
|
5700
|
-
const {
|
|
6316
|
+
const {
|
|
6317
|
+
version,
|
|
6318
|
+
config,
|
|
6319
|
+
projectName,
|
|
6320
|
+
gitProvider,
|
|
6321
|
+
httpPort,
|
|
6322
|
+
checks,
|
|
6323
|
+
readyMessage
|
|
6324
|
+
} = input;
|
|
5701
6325
|
const lines = [];
|
|
5702
6326
|
lines.push("");
|
|
5703
6327
|
lines.push(titleRule(`Harmony Agent Daemon v${version}`));
|
|
5704
6328
|
lines.push("");
|
|
5705
|
-
for (const row of configRows(config, projectName, gitProvider)) {
|
|
6329
|
+
for (const row of configRows(config, projectName, gitProvider, httpPort)) {
|
|
5706
6330
|
lines.push(` ${dim(row.label.padEnd(9))} ${row.value}`);
|
|
5707
6331
|
}
|
|
5708
6332
|
lines.push("");
|
|
@@ -5716,7 +6340,7 @@ function renderPretty(input) {
|
|
|
5716
6340
|
return lines.join(`
|
|
5717
6341
|
`);
|
|
5718
6342
|
}
|
|
5719
|
-
function configRows(config, projectName, gitProvider) {
|
|
6343
|
+
function configRows(config, projectName, gitProvider, httpPort) {
|
|
5720
6344
|
const rows = [];
|
|
5721
6345
|
const projectLabel = projectName ? `${projectName} (${shortenId(config.projectId)})` : shortenId(config.projectId);
|
|
5722
6346
|
rows.push({ label: "Project", value: projectLabel });
|
|
@@ -5732,7 +6356,7 @@ function configRows(config, projectName, gitProvider) {
|
|
|
5732
6356
|
if (gitProvider)
|
|
5733
6357
|
tail.push(gitProvider);
|
|
5734
6358
|
if (config.agent.http.enabled) {
|
|
5735
|
-
tail.push(`HTTP http://${config.agent.http.bindAddr}:${config.agent.http.port}`);
|
|
6359
|
+
tail.push(`HTTP http://${config.agent.http.bindAddr}:${httpPort ?? config.agent.http.port}`);
|
|
5736
6360
|
}
|
|
5737
6361
|
if (tail.length > 0) {
|
|
5738
6362
|
rows.push({ label: "Git", value: tail.join(" · ") });
|
|
@@ -5759,7 +6383,7 @@ function cyan(s) {
|
|
|
5759
6383
|
function yellow(s) {
|
|
5760
6384
|
return `${ANSI.yellow}${s}${ANSI.reset}`;
|
|
5761
6385
|
}
|
|
5762
|
-
var
|
|
6386
|
+
var TAG28 = "daemon", RULE_WIDTH = 70, ANSI;
|
|
5763
6387
|
var init_startup_banner = __esm(() => {
|
|
5764
6388
|
init_log();
|
|
5765
6389
|
ANSI = {
|
|
@@ -5906,18 +6530,18 @@ class Watcher {
|
|
|
5906
6530
|
}
|
|
5907
6531
|
async start() {
|
|
5908
6532
|
if (!isPretty()) {
|
|
5909
|
-
log.info(
|
|
6533
|
+
log.info(TAG29, "Connecting to Supabase realtime (broadcast)...");
|
|
5910
6534
|
}
|
|
5911
6535
|
this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
|
|
5912
6536
|
const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
|
|
5913
6537
|
const channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
|
|
5914
|
-
log.debug(
|
|
6538
|
+
log.debug(TAG29, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
|
|
5915
6539
|
this.onCardBroadcast({
|
|
5916
6540
|
event: "card_update",
|
|
5917
6541
|
payload: msg.payload ?? {}
|
|
5918
6542
|
});
|
|
5919
6543
|
}).on("broadcast", { event: "card_created" }, (msg) => {
|
|
5920
|
-
log.debug(
|
|
6544
|
+
log.debug(TAG29, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
|
|
5921
6545
|
this.onCardBroadcast({
|
|
5922
6546
|
event: "card_created",
|
|
5923
6547
|
payload: msg.payload ?? {}
|
|
@@ -5927,29 +6551,29 @@ class Watcher {
|
|
|
5927
6551
|
const cardId = payload.card_id;
|
|
5928
6552
|
const command = payload.command;
|
|
5929
6553
|
if (cardId && command) {
|
|
5930
|
-
log.info(
|
|
6554
|
+
log.info(TAG29, `Broadcast: agent_command ${command} for ${cardId}`);
|
|
5931
6555
|
this.onAgentCommand?.({ cardId, command });
|
|
5932
6556
|
}
|
|
5933
6557
|
}).subscribe((status) => {
|
|
5934
6558
|
if (status === "SUBSCRIBED") {
|
|
5935
6559
|
this.connected = true;
|
|
5936
6560
|
if (!isPretty() || !this.suppressStartupLogs) {
|
|
5937
|
-
log.info(
|
|
6561
|
+
log.info(TAG29, "Broadcast subscription active");
|
|
5938
6562
|
}
|
|
5939
6563
|
this.maybeResolveReady();
|
|
5940
6564
|
} else if (status === "CHANNEL_ERROR") {
|
|
5941
6565
|
this.connected = false;
|
|
5942
|
-
log.error(
|
|
6566
|
+
log.error(TAG29, "Broadcast channel error — will rely on reconciliation");
|
|
5943
6567
|
} else if (status === "TIMED_OUT") {
|
|
5944
6568
|
this.connected = false;
|
|
5945
|
-
log.warn(
|
|
6569
|
+
log.warn(TAG29, "Broadcast subscription timed out — retrying...");
|
|
5946
6570
|
} else if (status === "CLOSED") {
|
|
5947
6571
|
this.connected = false;
|
|
5948
6572
|
}
|
|
5949
6573
|
});
|
|
5950
6574
|
this.channel = channel;
|
|
5951
6575
|
presenceChannel.on("presence", { event: "sync" }, () => {
|
|
5952
|
-
log.debug(
|
|
6576
|
+
log.debug(TAG29, "Presence sync");
|
|
5953
6577
|
}).subscribe(async (status) => {
|
|
5954
6578
|
if (status === "SUBSCRIBED") {
|
|
5955
6579
|
await presenceChannel.track({
|
|
@@ -5962,7 +6586,7 @@ class Watcher {
|
|
|
5962
6586
|
agentName: this.identity.agentName
|
|
5963
6587
|
});
|
|
5964
6588
|
if (!isPretty() || !this.suppressStartupLogs) {
|
|
5965
|
-
log.info(
|
|
6589
|
+
log.info(TAG29, "Presence tracked on board-presence channel");
|
|
5966
6590
|
}
|
|
5967
6591
|
this.presenceTracked = true;
|
|
5968
6592
|
this.maybeResolveReady();
|
|
@@ -5984,10 +6608,10 @@ class Watcher {
|
|
|
5984
6608
|
this.supabase = null;
|
|
5985
6609
|
}
|
|
5986
6610
|
this.connected = false;
|
|
5987
|
-
log.info(
|
|
6611
|
+
log.info(TAG29, "Broadcast subscription stopped");
|
|
5988
6612
|
}
|
|
5989
6613
|
}
|
|
5990
|
-
var
|
|
6614
|
+
var TAG29 = "watcher";
|
|
5991
6615
|
var init_watcher = __esm(() => {
|
|
5992
6616
|
init_log();
|
|
5993
6617
|
});
|
|
@@ -6062,10 +6686,10 @@ function runWorktreeGc(basePath, store, opts = {}) {
|
|
|
6062
6686
|
});
|
|
6063
6687
|
} catch {}
|
|
6064
6688
|
if (result.removed.length > 0) {
|
|
6065
|
-
log.info(
|
|
6689
|
+
log.info(TAG30, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
|
|
6066
6690
|
}
|
|
6067
6691
|
if (result.errors.length > 0) {
|
|
6068
|
-
log.warn(
|
|
6692
|
+
log.warn(TAG30, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
|
|
6069
6693
|
}
|
|
6070
6694
|
return result;
|
|
6071
6695
|
}
|
|
@@ -6142,10 +6766,10 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
6142
6766
|
}
|
|
6143
6767
|
}
|
|
6144
6768
|
if (result.removed.length > 0) {
|
|
6145
|
-
log.info(
|
|
6769
|
+
log.info(TAG30, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
|
|
6146
6770
|
}
|
|
6147
6771
|
if (result.errors.length > 0) {
|
|
6148
|
-
log.warn(
|
|
6772
|
+
log.warn(TAG30, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
|
|
6149
6773
|
}
|
|
6150
6774
|
return result;
|
|
6151
6775
|
}
|
|
@@ -6176,13 +6800,13 @@ class WorktreeGc {
|
|
|
6176
6800
|
try {
|
|
6177
6801
|
runWorktreeGc(this.basePath, this.store);
|
|
6178
6802
|
} catch (err) {
|
|
6179
|
-
log.warn(
|
|
6803
|
+
log.warn(TAG30, `GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
6180
6804
|
}
|
|
6181
6805
|
if (this.remoteOpts) {
|
|
6182
6806
|
try {
|
|
6183
6807
|
pruneFailedRemoteBranches(this.remoteOpts);
|
|
6184
6808
|
} catch (err) {
|
|
6185
|
-
log.warn(
|
|
6809
|
+
log.warn(TAG30, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
6186
6810
|
}
|
|
6187
6811
|
}
|
|
6188
6812
|
}
|
|
@@ -6196,7 +6820,7 @@ function getRepoRoot2() {
|
|
|
6196
6820
|
return null;
|
|
6197
6821
|
}
|
|
6198
6822
|
}
|
|
6199
|
-
var
|
|
6823
|
+
var TAG30 = "worktree-gc";
|
|
6200
6824
|
var init_worktree_gc = __esm(() => {
|
|
6201
6825
|
init_log();
|
|
6202
6826
|
init_worktree();
|
|
@@ -6278,7 +6902,7 @@ async function main() {
|
|
|
6278
6902
|
} catch (err) {
|
|
6279
6903
|
if (err instanceof ConfigValidationError) {
|
|
6280
6904
|
banner.fail();
|
|
6281
|
-
log.error(
|
|
6905
|
+
log.error(TAG31, err.message);
|
|
6282
6906
|
process.exit(1);
|
|
6283
6907
|
}
|
|
6284
6908
|
throw err;
|
|
@@ -6387,25 +7011,28 @@ async function main() {
|
|
|
6387
7011
|
if (shuttingDown)
|
|
6388
7012
|
return;
|
|
6389
7013
|
shuttingDown = true;
|
|
6390
|
-
log.info(
|
|
7014
|
+
log.info(TAG31, `Received ${signal}, shutting down gracefully...`);
|
|
6391
7015
|
reconciler.stop();
|
|
6392
7016
|
mergeMonitor?.stop();
|
|
6393
7017
|
worktreeGc.stop();
|
|
6394
|
-
|
|
7018
|
+
if (httpServer) {
|
|
7019
|
+
clearDaemonPort(config.projectId, process.pid);
|
|
7020
|
+
await httpServer.stop();
|
|
7021
|
+
}
|
|
6395
7022
|
await watcher.stop();
|
|
6396
7023
|
await pool.shutdown();
|
|
6397
|
-
log.info(
|
|
7024
|
+
log.info(TAG31, "Daemon stopped.");
|
|
6398
7025
|
process.exit(exitCode);
|
|
6399
7026
|
};
|
|
6400
7027
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
6401
7028
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
6402
7029
|
process.on("uncaughtException", (err) => {
|
|
6403
|
-
log.error(
|
|
7030
|
+
log.error(TAG31, `Uncaught exception: ${err.message}`);
|
|
6404
7031
|
exitCode = 1;
|
|
6405
7032
|
shutdown("uncaughtException");
|
|
6406
7033
|
});
|
|
6407
7034
|
process.on("unhandledRejection", (reason) => {
|
|
6408
|
-
log.error(
|
|
7035
|
+
log.error(TAG31, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
|
|
6409
7036
|
exitCode = 1;
|
|
6410
7037
|
shutdown("unhandledRejection");
|
|
6411
7038
|
});
|
|
@@ -6415,7 +7042,13 @@ async function main() {
|
|
|
6415
7042
|
worktreeGc.start();
|
|
6416
7043
|
if (httpServer) {
|
|
6417
7044
|
try {
|
|
6418
|
-
await httpServer.start();
|
|
7045
|
+
const boundPort = await httpServer.start();
|
|
7046
|
+
recordDaemonPort(config.projectId, {
|
|
7047
|
+
port: boundPort,
|
|
7048
|
+
pid: process.pid,
|
|
7049
|
+
bindAddr: config.agent.http.bindAddr
|
|
7050
|
+
});
|
|
7051
|
+
banner.setHttpPort(boundPort);
|
|
6419
7052
|
} catch (err) {
|
|
6420
7053
|
banner.warn(`HTTP server failed to bind: ${err instanceof Error ? err.message : err}`);
|
|
6421
7054
|
}
|
|
@@ -6452,34 +7085,34 @@ async function handleBroadcast(event, client, pool, config, agentId) {
|
|
|
6452
7085
|
if (assignedAgentId === undefined)
|
|
6453
7086
|
return;
|
|
6454
7087
|
if (assignedAgentId === agentId) {
|
|
6455
|
-
log.info(
|
|
7088
|
+
log.info(TAG31, `Broadcast: card ${cardId} assigned to agent`);
|
|
6456
7089
|
try {
|
|
6457
7090
|
await tryEnqueueCard(cardId, client, pool, config, agentId);
|
|
6458
7091
|
} catch (err) {
|
|
6459
|
-
log.error(
|
|
7092
|
+
log.error(TAG31, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
|
|
6460
7093
|
}
|
|
6461
7094
|
} else if (pool.isCardKnown(cardId)) {
|
|
6462
|
-
log.info(
|
|
7095
|
+
log.info(TAG31, `Broadcast: card ${cardId} unassigned from agent`);
|
|
6463
7096
|
await pool.removeCard(cardId);
|
|
6464
7097
|
}
|
|
6465
7098
|
}
|
|
6466
7099
|
async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
6467
7100
|
const { card } = await client.getCard(cardId);
|
|
6468
7101
|
if (card.assigned_agent_id !== agentId) {
|
|
6469
|
-
log.debug(
|
|
7102
|
+
log.debug(TAG31, `Card ${cardId} no longer assigned to agent — skipping`);
|
|
6470
7103
|
return;
|
|
6471
7104
|
}
|
|
6472
7105
|
const board = await client.getBoard(config.projectId, { summary: true });
|
|
6473
7106
|
const columns = board.columns;
|
|
6474
7107
|
const column = columns.find((c) => c.id === card.column_id);
|
|
6475
7108
|
if (!column) {
|
|
6476
|
-
log.warn(
|
|
7109
|
+
log.warn(TAG31, `Column not found for card ${cardId}`);
|
|
6477
7110
|
return;
|
|
6478
7111
|
}
|
|
6479
7112
|
const isPickupColumn = config.agent.pickupColumns.some((name) => name.toLowerCase() === column.name.toLowerCase());
|
|
6480
7113
|
const isReviewColumn = config.agent.review.enabled && config.agent.review.pickupColumns.some((name) => name.toLowerCase() === column.name.toLowerCase());
|
|
6481
7114
|
if (!isPickupColumn && !isReviewColumn) {
|
|
6482
|
-
log.info(
|
|
7115
|
+
log.info(TAG31, `Card #${card.short_id} is in "${column.name}", not a pickup/review column — skipping`);
|
|
6483
7116
|
return;
|
|
6484
7117
|
}
|
|
6485
7118
|
const mode = isReviewColumn ? "review" : "implement";
|
|
@@ -6487,16 +7120,16 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
|
6487
7120
|
const cardLabels = resolveCardLabels(card, labelMap);
|
|
6488
7121
|
const subtasks = card.subtasks ?? [];
|
|
6489
7122
|
if (mode === "review" && config.agent.review.approvedLabel && hasLabel(cardLabels, config.agent.review.approvedLabel)) {
|
|
6490
|
-
log.debug(
|
|
7123
|
+
log.debug(TAG31, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
|
|
6491
7124
|
return;
|
|
6492
7125
|
}
|
|
6493
7126
|
if (mode === "review" && !extractBranchFromDescription(card.description)) {
|
|
6494
|
-
log.info(
|
|
7127
|
+
log.info(TAG31, `Card #${card.short_id} has no branch reference — skipping auto-review`);
|
|
6495
7128
|
return;
|
|
6496
7129
|
}
|
|
6497
7130
|
await pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
6498
7131
|
}
|
|
6499
|
-
var
|
|
7132
|
+
var TAG31 = "daemon", PKG_VERSION;
|
|
6500
7133
|
var init_src = __esm(() => {
|
|
6501
7134
|
init_board_helpers();
|
|
6502
7135
|
init_config();
|
|
@@ -6506,6 +7139,7 @@ var init_src = __esm(() => {
|
|
|
6506
7139
|
init_log();
|
|
6507
7140
|
init_merge_monitor();
|
|
6508
7141
|
init_pool();
|
|
7142
|
+
init_port_registry();
|
|
6509
7143
|
init_reconcile();
|
|
6510
7144
|
init_recovery();
|
|
6511
7145
|
init_review_worktree();
|