@gethmy/agent 1.9.0 → 1.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +739 -97
- package/dist/index.js +734 -96
- 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;
|
|
@@ -4691,6 +4986,7 @@ class Worker {
|
|
|
4691
4986
|
projectId;
|
|
4692
4987
|
stateStore;
|
|
4693
4988
|
onCardCompleted;
|
|
4989
|
+
onApiError;
|
|
4694
4990
|
id;
|
|
4695
4991
|
state = "idle";
|
|
4696
4992
|
cardId = null;
|
|
@@ -4707,7 +5003,7 @@ class Worker {
|
|
|
4707
5003
|
verificationFailed = false;
|
|
4708
5004
|
sessionId = null;
|
|
4709
5005
|
runId = null;
|
|
4710
|
-
constructor(id, config, client, agentId, onDone, workspaceId, projectId, stateStore, onCardCompleted) {
|
|
5006
|
+
constructor(id, config, client, agentId, onDone, workspaceId, projectId, stateStore, onCardCompleted, onApiError) {
|
|
4711
5007
|
this.config = config;
|
|
4712
5008
|
this.client = client;
|
|
4713
5009
|
this.agentId = agentId;
|
|
@@ -4716,6 +5012,7 @@ class Worker {
|
|
|
4716
5012
|
this.projectId = projectId;
|
|
4717
5013
|
this.stateStore = stateStore;
|
|
4718
5014
|
this.onCardCompleted = onCardCompleted;
|
|
5015
|
+
this.onApiError = onApiError;
|
|
4719
5016
|
this.id = id;
|
|
4720
5017
|
}
|
|
4721
5018
|
startHeartbeat() {
|
|
@@ -4756,7 +5053,7 @@ class Worker {
|
|
|
4756
5053
|
return this.state === "idle";
|
|
4757
5054
|
}
|
|
4758
5055
|
get isActive() {
|
|
4759
|
-
return this.state === "preparing" || this.state === "running" || this.state === "verifying" || this.state === "completing";
|
|
5056
|
+
return this.state === "preparing" || this.state === "planning" || this.state === "running" || this.state === "verifying" || this.state === "completing";
|
|
4760
5057
|
}
|
|
4761
5058
|
async run(card, column, labels, subtasks) {
|
|
4762
5059
|
this.aborted = false;
|
|
@@ -4802,7 +5099,7 @@ class Worker {
|
|
|
4802
5099
|
}
|
|
4803
5100
|
this.sessionId = sid;
|
|
4804
5101
|
await this.recordPhase("preparing");
|
|
4805
|
-
const moved = await moveCardAndAddLabel(this.client, card,
|
|
5102
|
+
const moved = await moveCardAndAddLabel(this.client, card, IN_PROGRESS_COLUMN, "agent");
|
|
4806
5103
|
if (!moved) {
|
|
4807
5104
|
log.warn(this.tag, `Card #${card.short_id} was NOT moved to "In Progress" — check API logs`);
|
|
4808
5105
|
}
|
|
@@ -4811,8 +5108,6 @@ class Worker {
|
|
|
4811
5108
|
this.worktreePath = createWorktree(this.config.worktree.basePath, this.config.worktree.baseBranch, this.branchName);
|
|
4812
5109
|
if (this.aborted)
|
|
4813
5110
|
return;
|
|
4814
|
-
this.state = "running";
|
|
4815
|
-
await this.recordPhase("running");
|
|
4816
5111
|
const enriched = {
|
|
4817
5112
|
card,
|
|
4818
5113
|
column,
|
|
@@ -4820,6 +5115,19 @@ class Worker {
|
|
|
4820
5115
|
subtasks,
|
|
4821
5116
|
mode: "implement"
|
|
4822
5117
|
};
|
|
5118
|
+
if (shouldPlan(enriched, this.config.planning)) {
|
|
5119
|
+
this.state = "planning";
|
|
5120
|
+
await this.recordPhase("planning");
|
|
5121
|
+
const parked = await this.runPlanningPhase(enriched);
|
|
5122
|
+
if (this.aborted)
|
|
5123
|
+
return;
|
|
5124
|
+
if (parked) {
|
|
5125
|
+
log.info(this.tag, `#${card.short_id} parked for plan approval — ending run`);
|
|
5126
|
+
return;
|
|
5127
|
+
}
|
|
5128
|
+
}
|
|
5129
|
+
this.state = "running";
|
|
5130
|
+
await this.recordPhase("running");
|
|
4823
5131
|
const prompt = await buildPrompt(enriched, this.branchName, this.worktreePath, this.client, this.workspaceId, this.projectId);
|
|
4824
5132
|
await this.client.updateAgentProgress(card.id, {
|
|
4825
5133
|
agentIdentifier: agentIdentifier(this.id),
|
|
@@ -4858,21 +5166,46 @@ class Worker {
|
|
|
4858
5166
|
this.state = "error";
|
|
4859
5167
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4860
5168
|
log.error(this.tag, `Error on #${card.short_id}: ${msg}`);
|
|
5169
|
+
const rawStderr = err?.stderr;
|
|
5170
|
+
const errClass = classifyRunError(typeof rawStderr === "string" && rawStderr ? rawStderr : msg);
|
|
5171
|
+
const apiError = errClass.kind !== null;
|
|
5172
|
+
if (apiError) {
|
|
5173
|
+
try {
|
|
5174
|
+
this.onApiError?.(errClass);
|
|
5175
|
+
} catch {}
|
|
5176
|
+
}
|
|
5177
|
+
if (this.worktreePath) {
|
|
5178
|
+
try {
|
|
5179
|
+
cleanupWorktree(this.worktreePath, this.branchName ?? undefined);
|
|
5180
|
+
} catch {
|
|
5181
|
+
log.warn(this.tag, "Failed to cleanup worktree before requeue");
|
|
5182
|
+
}
|
|
5183
|
+
this.worktreePath = null;
|
|
5184
|
+
}
|
|
5185
|
+
const failureReason = apiError ? errClass.kind : "other";
|
|
5186
|
+
const failureSummary = apiError ? describeApiError(errClass.kind) : `Run failed: ${msg.slice(0, 300)}`;
|
|
4861
5187
|
try {
|
|
4862
5188
|
await runTransition(this.client, card, {
|
|
5189
|
+
move: { columnName: this.config.pickupColumns[0] ?? "To Do" },
|
|
4863
5190
|
endSession: {
|
|
4864
|
-
status: "
|
|
5191
|
+
status: "failed",
|
|
5192
|
+
failureReason,
|
|
5193
|
+
failureSummary,
|
|
4865
5194
|
...buildTokenPayload(this.lastSessionStats)
|
|
4866
5195
|
}
|
|
4867
5196
|
});
|
|
4868
5197
|
} catch (tErr) {
|
|
4869
|
-
log.error(this.tag, `
|
|
5198
|
+
log.error(this.tag, `error transition failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
|
|
4870
5199
|
}
|
|
4871
5200
|
if (this.runId) {
|
|
4872
5201
|
try {
|
|
4873
|
-
await this.stateStore.endRun(this.runId, "
|
|
5202
|
+
await this.stateStore.endRun(this.runId, "failed", errClass.kind ?? msg);
|
|
4874
5203
|
} catch {}
|
|
4875
|
-
|
|
5204
|
+
if (apiError) {
|
|
5205
|
+
await this.stateStore.decrementAttempt(card.id);
|
|
5206
|
+
} else {
|
|
5207
|
+
await this.recordOutcome(card.id, "failure");
|
|
5208
|
+
}
|
|
4876
5209
|
}
|
|
4877
5210
|
} finally {
|
|
4878
5211
|
const succeeded = this.runId && this.state !== "error" && !this.aborted && !this.verificationFailed;
|
|
@@ -5016,7 +5349,122 @@ class Worker {
|
|
|
5016
5349
|
}
|
|
5017
5350
|
}
|
|
5018
5351
|
}
|
|
5019
|
-
async
|
|
5352
|
+
async runPlanningPhase(enriched) {
|
|
5353
|
+
const planning = this.config.planning;
|
|
5354
|
+
const { card } = enriched;
|
|
5355
|
+
log.info(this.tag, `Planning pass for #${card.short_id} (mode=${planning.mode}, model=${planning.model})`);
|
|
5356
|
+
await this.client.updateAgentProgress(card.id, {
|
|
5357
|
+
agentIdentifier: agentIdentifier(this.id),
|
|
5358
|
+
agentName: AGENT_NAME,
|
|
5359
|
+
status: "working",
|
|
5360
|
+
currentTask: "Planning approach (read-only)",
|
|
5361
|
+
progressPercent: 5,
|
|
5362
|
+
phase: "planning"
|
|
5363
|
+
}).catch(() => {});
|
|
5364
|
+
const planPrompt = buildPlanPrompt(enriched, this.worktreePath);
|
|
5365
|
+
let planTimedOut = false;
|
|
5366
|
+
const planTimeout = setTimeout(() => {
|
|
5367
|
+
planTimedOut = true;
|
|
5368
|
+
log.warn(this.tag, "Planning pass exceeded timeout — abandoning, implementing directly");
|
|
5369
|
+
if (this.process && !this.process.killed) {
|
|
5370
|
+
terminateGroup(this.process, {
|
|
5371
|
+
sigintTimeoutMs: 1e4,
|
|
5372
|
+
sigtermTimeoutMs: 5000
|
|
5373
|
+
}).catch(() => {});
|
|
5374
|
+
}
|
|
5375
|
+
}, Math.min(this.config.maxTimeout, PLAN_PHASE_TIMEOUT));
|
|
5376
|
+
try {
|
|
5377
|
+
await this.spawnClaude(planPrompt, card, [], {
|
|
5378
|
+
model: planning.model,
|
|
5379
|
+
maxTurns: planning.maxTurns,
|
|
5380
|
+
allowedTools: PLAN_ALLOWED_TOOLS,
|
|
5381
|
+
initialPhase: "planning"
|
|
5382
|
+
});
|
|
5383
|
+
} catch (err) {
|
|
5384
|
+
log.warn(this.tag, `Planning pass failed (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
5385
|
+
return false;
|
|
5386
|
+
} finally {
|
|
5387
|
+
clearTimeout(planTimeout);
|
|
5388
|
+
}
|
|
5389
|
+
if (this.aborted || planTimedOut)
|
|
5390
|
+
return false;
|
|
5391
|
+
const stats = this.lastSessionStats;
|
|
5392
|
+
if (stats?.cost) {
|
|
5393
|
+
const cents = Math.round(stats.cost.totalCostUsd * 100);
|
|
5394
|
+
if (cents > 0) {
|
|
5395
|
+
try {
|
|
5396
|
+
await this.stateStore.addCost(card.id, cents);
|
|
5397
|
+
} catch {}
|
|
5398
|
+
}
|
|
5399
|
+
}
|
|
5400
|
+
const planText = stats?.lastAssistantText ?? "";
|
|
5401
|
+
if (!planText.trim()) {
|
|
5402
|
+
log.warn(this.tag, `Planning pass for #${card.short_id} produced no text — implementing directly`);
|
|
5403
|
+
return false;
|
|
5404
|
+
}
|
|
5405
|
+
const artifact = extractPlanArtifact(planText, card.title);
|
|
5406
|
+
let planId = null;
|
|
5407
|
+
try {
|
|
5408
|
+
const result = await this.client.createPlan(this.projectId, {
|
|
5409
|
+
title: artifact.title,
|
|
5410
|
+
content: artifact.markdown,
|
|
5411
|
+
source: "agent",
|
|
5412
|
+
tasks: artifact.tasks
|
|
5413
|
+
});
|
|
5414
|
+
const plan = result?.plan;
|
|
5415
|
+
const createdId = plan && typeof plan === "object" && "id" in plan ? plan.id : null;
|
|
5416
|
+
if (createdId) {
|
|
5417
|
+
await this.client.updateCard(card.id, {
|
|
5418
|
+
planId: createdId,
|
|
5419
|
+
needsPlanRefresh: false
|
|
5420
|
+
});
|
|
5421
|
+
planId = createdId;
|
|
5422
|
+
}
|
|
5423
|
+
log.info(this.tag, `Stored plan ${planId ?? "(unlinked)"} for #${card.short_id} (${artifact.tasks.length} tasks)`);
|
|
5424
|
+
} catch (err) {
|
|
5425
|
+
log.warn(this.tag, `Failed to store/link plan (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
5426
|
+
}
|
|
5427
|
+
if (planning.mode === "gated" && planId) {
|
|
5428
|
+
try {
|
|
5429
|
+
if (planning.postComment) {
|
|
5430
|
+
await this.client.addComment(card.id, buildGatedPlanComment(artifact, planning.awaitingApprovalColumn), {
|
|
5431
|
+
commentType: "decision",
|
|
5432
|
+
agentSessionId: this.sessionId ?? undefined
|
|
5433
|
+
});
|
|
5434
|
+
}
|
|
5435
|
+
await runTransition(this.client, card, {
|
|
5436
|
+
move: { columnName: planning.awaitingApprovalColumn },
|
|
5437
|
+
removeLabels: ["agent"],
|
|
5438
|
+
endSession: {
|
|
5439
|
+
status: "completed",
|
|
5440
|
+
progressPercent: 20,
|
|
5441
|
+
...buildTokenPayload(stats)
|
|
5442
|
+
}
|
|
5443
|
+
}, { store: this.stateStore, runId: this.runId ?? undefined });
|
|
5444
|
+
log.info(this.tag, `#${card.short_id} parked in "${planning.awaitingApprovalColumn}" for plan approval`);
|
|
5445
|
+
this.lastSessionStats = undefined;
|
|
5446
|
+
return true;
|
|
5447
|
+
} catch (err) {
|
|
5448
|
+
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}`);
|
|
5449
|
+
}
|
|
5450
|
+
}
|
|
5451
|
+
if (planId && planning.postComment) {
|
|
5452
|
+
try {
|
|
5453
|
+
await this.client.addComment(card.id, buildPlanComment(artifact), {
|
|
5454
|
+
commentType: "decision",
|
|
5455
|
+
agentSessionId: this.sessionId ?? undefined
|
|
5456
|
+
});
|
|
5457
|
+
} catch (err) {
|
|
5458
|
+
log.warn(this.tag, `Failed to post advisory plan comment (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
5459
|
+
}
|
|
5460
|
+
}
|
|
5461
|
+
return false;
|
|
5462
|
+
}
|
|
5463
|
+
async spawnClaude(prompt, card, subtasks, opts = {}) {
|
|
5464
|
+
const model = opts.model ?? this.config.claude.model;
|
|
5465
|
+
const maxTurns = opts.maxTurns ?? this.config.claude.maxTurns;
|
|
5466
|
+
const allowedTools = opts.allowedTools ?? IMPLEMENT_ALLOWED_TOOLS;
|
|
5467
|
+
const initialPhase = opts.initialPhase ?? "exploring";
|
|
5020
5468
|
return new Promise((resolve3, reject) => {
|
|
5021
5469
|
const args = [
|
|
5022
5470
|
"-p",
|
|
@@ -5024,11 +5472,11 @@ class Worker {
|
|
|
5024
5472
|
"--output-format",
|
|
5025
5473
|
"stream-json",
|
|
5026
5474
|
"--model",
|
|
5027
|
-
|
|
5475
|
+
model,
|
|
5028
5476
|
"--max-turns",
|
|
5029
|
-
String(
|
|
5477
|
+
String(maxTurns),
|
|
5030
5478
|
"--allowedTools",
|
|
5031
|
-
|
|
5479
|
+
allowedTools,
|
|
5032
5480
|
...this.config.claude.additionalArgs,
|
|
5033
5481
|
"--",
|
|
5034
5482
|
prompt
|
|
@@ -5047,7 +5495,7 @@ class Worker {
|
|
|
5047
5495
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5048
5496
|
});
|
|
5049
5497
|
const parser = new StreamParser;
|
|
5050
|
-
this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks);
|
|
5498
|
+
this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks, initialPhase);
|
|
5051
5499
|
if (this.sessionId) {
|
|
5052
5500
|
this.progressTracker.setSessionId(this.sessionId);
|
|
5053
5501
|
}
|
|
@@ -5092,7 +5540,9 @@ class Worker {
|
|
|
5092
5540
|
} else if (code === 0) {
|
|
5093
5541
|
resolve3();
|
|
5094
5542
|
} else {
|
|
5095
|
-
|
|
5543
|
+
const err = new Error(`claude exited with code ${code}${stderr ? `: ${stderr.slice(0, 500)}` : ""}`);
|
|
5544
|
+
err.stderr = stderr;
|
|
5545
|
+
reject(err);
|
|
5096
5546
|
}
|
|
5097
5547
|
});
|
|
5098
5548
|
});
|
|
@@ -5124,11 +5574,13 @@ class Worker {
|
|
|
5124
5574
|
this.sessionId = null;
|
|
5125
5575
|
}
|
|
5126
5576
|
}
|
|
5127
|
-
var TAG23 = "worker", CANCEL_SIGINT_TIMEOUT2 = 30000, CANCEL_SIGTERM_TIMEOUT2 = 1e4;
|
|
5577
|
+
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
5578
|
var init_worker = __esm(() => {
|
|
5129
5579
|
init_board_helpers();
|
|
5130
5580
|
init_completion();
|
|
5581
|
+
init_error_classifier();
|
|
5131
5582
|
init_log();
|
|
5583
|
+
init_plan_phase();
|
|
5132
5584
|
init_process_group();
|
|
5133
5585
|
init_progress_tracker();
|
|
5134
5586
|
init_prompt();
|
|
@@ -5138,6 +5590,7 @@ var init_worker = __esm(() => {
|
|
|
5138
5590
|
init_transitions();
|
|
5139
5591
|
init_types();
|
|
5140
5592
|
init_worktree();
|
|
5593
|
+
PLAN_PHASE_TIMEOUT = 10 * 60000;
|
|
5141
5594
|
});
|
|
5142
5595
|
|
|
5143
5596
|
// src/pool.ts
|
|
@@ -5153,6 +5606,8 @@ class Pool {
|
|
|
5153
5606
|
budget;
|
|
5154
5607
|
sleepGuard = new SleepGuard;
|
|
5155
5608
|
shuttingDown = false;
|
|
5609
|
+
apiCooldownUntil = 0;
|
|
5610
|
+
authPaused = false;
|
|
5156
5611
|
onCardCompleted = null;
|
|
5157
5612
|
constructor(config, client, _userEmail, workspaceId, projectId, stateStore, agentId) {
|
|
5158
5613
|
this.client = client;
|
|
@@ -5171,7 +5626,7 @@ class Pool {
|
|
|
5171
5626
|
}
|
|
5172
5627
|
}, workspaceId, projectId, stateStore, async (completedCard) => {
|
|
5173
5628
|
await this.onCardCompleted?.(completedCard);
|
|
5174
|
-
}));
|
|
5629
|
+
}, (err) => this.noteApiError(err)));
|
|
5175
5630
|
}
|
|
5176
5631
|
if (config.review.enabled) {
|
|
5177
5632
|
for (let i = 0;i < config.review.poolSize; i++) {
|
|
@@ -5192,6 +5647,17 @@ class Pool {
|
|
|
5192
5647
|
return;
|
|
5193
5648
|
}
|
|
5194
5649
|
if (mode === "implement") {
|
|
5650
|
+
if (this.authPaused) {
|
|
5651
|
+
log.debug(TAG24, `#${card.short_id} held — agent paused (auth error)`);
|
|
5652
|
+
await this.emitWaiting(card.id, "Agent paused — Anthropic auth error, check API credentials");
|
|
5653
|
+
return;
|
|
5654
|
+
}
|
|
5655
|
+
const cooldownMs = this.apiCooldownRemainingMs();
|
|
5656
|
+
if (cooldownMs > 0) {
|
|
5657
|
+
log.debug(TAG24, `#${card.short_id} held — API cooldown ${Math.round(cooldownMs / 1000)}s remaining`);
|
|
5658
|
+
await this.emitWaiting(card.id, `Paused — Anthropic API limit, retrying in ~${Math.round(cooldownMs / 1000)}s`);
|
|
5659
|
+
return;
|
|
5660
|
+
}
|
|
5195
5661
|
const decision = this.budget.check(card.id);
|
|
5196
5662
|
if (!decision.allow) {
|
|
5197
5663
|
if (decision.reason === "daily_budget") {
|
|
@@ -5241,6 +5707,26 @@ class Pool {
|
|
|
5241
5707
|
log.debug(TAG24, `waiting emit failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
5242
5708
|
}
|
|
5243
5709
|
}
|
|
5710
|
+
noteApiError(err) {
|
|
5711
|
+
if (!err.kind)
|
|
5712
|
+
return;
|
|
5713
|
+
if (err.kind === "auth") {
|
|
5714
|
+
if (!this.authPaused) {
|
|
5715
|
+
log.error(TAG24, "Auth error from Claude CLI — pausing implement pickups until the daemon is restarted with valid credentials");
|
|
5716
|
+
}
|
|
5717
|
+
this.authPaused = true;
|
|
5718
|
+
return;
|
|
5719
|
+
}
|
|
5720
|
+
const cooldownMs = err.retryAfterMs ?? cooldownMsFor(err.kind);
|
|
5721
|
+
const until = Date.now() + cooldownMs;
|
|
5722
|
+
if (until > this.apiCooldownUntil) {
|
|
5723
|
+
this.apiCooldownUntil = until;
|
|
5724
|
+
log.warn(TAG24, `${describeApiError(err.kind)} — pausing implement pickups for ${Math.round(cooldownMs / 1000)}s`);
|
|
5725
|
+
}
|
|
5726
|
+
}
|
|
5727
|
+
apiCooldownRemainingMs() {
|
|
5728
|
+
return Math.max(0, this.apiCooldownUntil - Date.now());
|
|
5729
|
+
}
|
|
5244
5730
|
async removeCard(cardId) {
|
|
5245
5731
|
await this.stateStore.resetAttempts(cardId);
|
|
5246
5732
|
this.lastWaitingEmit.delete(cardId);
|
|
@@ -5368,6 +5854,7 @@ class Pool {
|
|
|
5368
5854
|
}
|
|
5369
5855
|
var TAG24 = "pool";
|
|
5370
5856
|
var init_pool = __esm(() => {
|
|
5857
|
+
init_error_classifier();
|
|
5371
5858
|
init_log();
|
|
5372
5859
|
init_queue();
|
|
5373
5860
|
init_review_worker();
|
|
@@ -5377,6 +5864,78 @@ var init_pool = __esm(() => {
|
|
|
5377
5864
|
init_worker();
|
|
5378
5865
|
});
|
|
5379
5866
|
|
|
5867
|
+
// src/port-registry.ts
|
|
5868
|
+
var exports_port_registry = {};
|
|
5869
|
+
__export(exports_port_registry, {
|
|
5870
|
+
recordDaemonPort: () => recordDaemonPort,
|
|
5871
|
+
lookupDaemonPort: () => lookupDaemonPort,
|
|
5872
|
+
defaultRegistryPath: () => defaultRegistryPath,
|
|
5873
|
+
clearDaemonPort: () => clearDaemonPort
|
|
5874
|
+
});
|
|
5875
|
+
import {
|
|
5876
|
+
existsSync as existsSync5,
|
|
5877
|
+
mkdirSync as mkdirSync3,
|
|
5878
|
+
readFileSync as readFileSync4,
|
|
5879
|
+
renameSync as renameSync2,
|
|
5880
|
+
writeFileSync as writeFileSync2
|
|
5881
|
+
} from "node:fs";
|
|
5882
|
+
import { homedir as homedir4 } from "node:os";
|
|
5883
|
+
import { dirname as dirname2, join as join4 } from "node:path";
|
|
5884
|
+
function defaultRegistryPath() {
|
|
5885
|
+
return join4(homedir4(), ".harmony-mcp", "agent-ports.json");
|
|
5886
|
+
}
|
|
5887
|
+
function load(path) {
|
|
5888
|
+
if (!existsSync5(path))
|
|
5889
|
+
return {};
|
|
5890
|
+
try {
|
|
5891
|
+
const raw = readFileSync4(path, "utf-8");
|
|
5892
|
+
const parsed = JSON.parse(raw);
|
|
5893
|
+
if (parsed && typeof parsed === "object")
|
|
5894
|
+
return parsed;
|
|
5895
|
+
return {};
|
|
5896
|
+
} catch (err) {
|
|
5897
|
+
log.warn(TAG25, `failed to read ${path}: ${err instanceof Error ? err.message : err}`);
|
|
5898
|
+
return {};
|
|
5899
|
+
}
|
|
5900
|
+
}
|
|
5901
|
+
function save(path, registry) {
|
|
5902
|
+
const dir = dirname2(path);
|
|
5903
|
+
if (!existsSync5(dir))
|
|
5904
|
+
mkdirSync3(dir, { recursive: true });
|
|
5905
|
+
const tmp = `${path}.tmp`;
|
|
5906
|
+
writeFileSync2(tmp, JSON.stringify(registry, null, 2), "utf-8");
|
|
5907
|
+
renameSync2(tmp, path);
|
|
5908
|
+
}
|
|
5909
|
+
function recordDaemonPort(projectId, entry, path = defaultRegistryPath()) {
|
|
5910
|
+
try {
|
|
5911
|
+
const registry = load(path);
|
|
5912
|
+
registry[projectId] = { ...entry, updatedAt: Date.now() };
|
|
5913
|
+
save(path, registry);
|
|
5914
|
+
} catch (err) {
|
|
5915
|
+
log.warn(TAG25, `failed to record port for ${projectId}: ${err instanceof Error ? err.message : err}`);
|
|
5916
|
+
}
|
|
5917
|
+
}
|
|
5918
|
+
function lookupDaemonPort(projectId, path = defaultRegistryPath()) {
|
|
5919
|
+
const registry = load(path);
|
|
5920
|
+
return registry[projectId] ?? null;
|
|
5921
|
+
}
|
|
5922
|
+
function clearDaemonPort(projectId, pid, path = defaultRegistryPath()) {
|
|
5923
|
+
try {
|
|
5924
|
+
const registry = load(path);
|
|
5925
|
+
const existing = registry[projectId];
|
|
5926
|
+
if (!existing || existing.pid !== pid)
|
|
5927
|
+
return;
|
|
5928
|
+
delete registry[projectId];
|
|
5929
|
+
save(path, registry);
|
|
5930
|
+
} catch (err) {
|
|
5931
|
+
log.warn(TAG25, `failed to clear port for ${projectId}: ${err instanceof Error ? err.message : err}`);
|
|
5932
|
+
}
|
|
5933
|
+
}
|
|
5934
|
+
var TAG25 = "port-registry";
|
|
5935
|
+
var init_port_registry = __esm(() => {
|
|
5936
|
+
init_log();
|
|
5937
|
+
});
|
|
5938
|
+
|
|
5380
5939
|
// src/recovery.ts
|
|
5381
5940
|
function isProcessAlive(pid, currentPid) {
|
|
5382
5941
|
if (pid === currentPid)
|
|
@@ -5393,7 +5952,7 @@ async function fetchCardSafely(client, cardId) {
|
|
|
5393
5952
|
const { card } = await client.getCard(cardId);
|
|
5394
5953
|
return card;
|
|
5395
5954
|
} catch (err) {
|
|
5396
|
-
log.warn(
|
|
5955
|
+
log.warn(TAG26, `cannot fetch card ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
5397
5956
|
return null;
|
|
5398
5957
|
}
|
|
5399
5958
|
}
|
|
@@ -5403,7 +5962,7 @@ async function recoverOrphans(store, client, config) {
|
|
|
5403
5962
|
return [];
|
|
5404
5963
|
}
|
|
5405
5964
|
const outcomes = [];
|
|
5406
|
-
log.info(
|
|
5965
|
+
log.info(TAG26, `recovering ${active.length} orphan run(s) from prior daemon`);
|
|
5407
5966
|
for (const run of active) {
|
|
5408
5967
|
const outcome = {
|
|
5409
5968
|
runId: run.runId,
|
|
@@ -5415,11 +5974,11 @@ async function recoverOrphans(store, client, config) {
|
|
|
5415
5974
|
};
|
|
5416
5975
|
outcomes.push(outcome);
|
|
5417
5976
|
if (isProcessAlive(run.daemonPid, process.pid)) {
|
|
5418
|
-
log.warn(
|
|
5977
|
+
log.warn(TAG26, `run ${run.runId} claims live daemon pid ${run.daemonPid} — skipping`);
|
|
5419
5978
|
outcome.actions.push("skipped: daemon pid still alive");
|
|
5420
5979
|
continue;
|
|
5421
5980
|
}
|
|
5422
|
-
log.info(
|
|
5981
|
+
log.info(TAG26, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
|
|
5423
5982
|
await recoverRun(run, store, client, config, outcome);
|
|
5424
5983
|
}
|
|
5425
5984
|
return outcomes;
|
|
@@ -5437,7 +5996,7 @@ async function recoverRun(run, store, client, config, outcome) {
|
|
|
5437
5996
|
} catch (err) {
|
|
5438
5997
|
const msg = err instanceof Error ? err.message : String(err);
|
|
5439
5998
|
outcome.errors.push(`endAgentSession: ${msg}`);
|
|
5440
|
-
log.warn(
|
|
5999
|
+
log.warn(TAG26, `endAgentSession failed for ${run.cardId}: ${msg}`);
|
|
5441
6000
|
}
|
|
5442
6001
|
const card = await fetchCardSafely(client, run.cardId);
|
|
5443
6002
|
if (card) {
|
|
@@ -5478,9 +6037,9 @@ async function recoverRun(run, store, client, config, outcome) {
|
|
|
5478
6037
|
const msg = err instanceof Error ? err.message : String(err);
|
|
5479
6038
|
outcome.errors.push(`endRun: ${msg}`);
|
|
5480
6039
|
}
|
|
5481
|
-
log.info(
|
|
6040
|
+
log.info(TAG26, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
|
|
5482
6041
|
}
|
|
5483
|
-
var
|
|
6042
|
+
var TAG26 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
|
|
5484
6043
|
var init_recovery = __esm(() => {
|
|
5485
6044
|
init_board_helpers();
|
|
5486
6045
|
init_log();
|
|
@@ -5528,7 +6087,7 @@ class Reconciler {
|
|
|
5528
6087
|
clearInterval(this.timer);
|
|
5529
6088
|
this.timer = null;
|
|
5530
6089
|
}
|
|
5531
|
-
log.info(
|
|
6090
|
+
log.info(TAG27, "Heartbeat stopped");
|
|
5532
6091
|
}
|
|
5533
6092
|
async recoverStaleRuns() {
|
|
5534
6093
|
if (!this.stateStore || !this.agentConfig)
|
|
@@ -5545,7 +6104,7 @@ class Reconciler {
|
|
|
5545
6104
|
if (!daemonDead && !(heartbeatStale && ourZombie))
|
|
5546
6105
|
continue;
|
|
5547
6106
|
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(
|
|
6107
|
+
log.warn(TAG27, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
|
|
5549
6108
|
await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
|
|
5550
6109
|
runId: run.runId,
|
|
5551
6110
|
cardId: run.cardId,
|
|
@@ -5556,6 +6115,57 @@ class Reconciler {
|
|
|
5556
6115
|
});
|
|
5557
6116
|
}
|
|
5558
6117
|
}
|
|
6118
|
+
async recoverStrandedInProgress(cards, columns, knownCardIds) {
|
|
6119
|
+
const inProgressCol = columns.find((c) => c.name.toLowerCase() === IN_PROGRESS_COLUMN.toLowerCase());
|
|
6120
|
+
const pickupName = this.pickupColumns[0];
|
|
6121
|
+
const pickupCol = pickupName ? columns.find((c) => c.name.toLowerCase() === pickupName.toLowerCase()) : undefined;
|
|
6122
|
+
if (!inProgressCol || !pickupCol)
|
|
6123
|
+
return;
|
|
6124
|
+
const graceMs = this.agentConfig?.timing.staleHeartbeatMs ?? 120000;
|
|
6125
|
+
const now = Date.now();
|
|
6126
|
+
const activeCardIds = new Set((this.stateStore?.getActiveRuns() ?? []).map((r) => r.cardId));
|
|
6127
|
+
for (const card of cards) {
|
|
6128
|
+
if (card.assigned_agent_id !== this.agentId || card.archived_at || card.column_id !== inProgressCol.id || knownCardIds.has(card.id) || activeCardIds.has(card.id)) {
|
|
6129
|
+
continue;
|
|
6130
|
+
}
|
|
6131
|
+
const stalledAt = Date.parse(card.updated_at ?? "");
|
|
6132
|
+
if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
|
|
6133
|
+
continue;
|
|
6134
|
+
log.warn(TAG27, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
|
|
6135
|
+
try {
|
|
6136
|
+
await this.client.moveCard(card.id, pickupCol.id);
|
|
6137
|
+
} catch (err) {
|
|
6138
|
+
log.error(TAG27, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
6139
|
+
}
|
|
6140
|
+
}
|
|
6141
|
+
}
|
|
6142
|
+
async releaseStalledApprovals(cards, columns, knownCardIds) {
|
|
6143
|
+
const planning = this.agentConfig?.planning;
|
|
6144
|
+
if (!planning?.enabled || planning.mode !== "gated" || planning.approvalTtlHours <= 0) {
|
|
6145
|
+
return;
|
|
6146
|
+
}
|
|
6147
|
+
const parkCol = columns.find((c) => c.name.toLowerCase() === planning.awaitingApprovalColumn.toLowerCase());
|
|
6148
|
+
const pickupName = this.pickupColumns[0];
|
|
6149
|
+
const pickupCol = pickupName ? columns.find((c) => c.name.toLowerCase() === pickupName.toLowerCase()) : undefined;
|
|
6150
|
+
if (!parkCol || !pickupCol)
|
|
6151
|
+
return;
|
|
6152
|
+
const ttlMs = planning.approvalTtlHours * 3600000;
|
|
6153
|
+
const now = Date.now();
|
|
6154
|
+
for (const card of cards) {
|
|
6155
|
+
if (card.assigned_agent_id !== this.agentId || card.archived_at || card.column_id !== parkCol.id || !card.plan_id || knownCardIds.has(card.id)) {
|
|
6156
|
+
continue;
|
|
6157
|
+
}
|
|
6158
|
+
const parkedAt = Date.parse(card.updated_at ?? "");
|
|
6159
|
+
if (!Number.isFinite(parkedAt) || now - parkedAt < ttlMs)
|
|
6160
|
+
continue;
|
|
6161
|
+
log.warn(TAG27, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
|
|
6162
|
+
try {
|
|
6163
|
+
await this.client.moveCard(card.id, pickupCol.id);
|
|
6164
|
+
} catch (err) {
|
|
6165
|
+
log.error(TAG27, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
6166
|
+
}
|
|
6167
|
+
}
|
|
6168
|
+
}
|
|
5559
6169
|
async tick() {
|
|
5560
6170
|
this.lastTickAt = Date.now();
|
|
5561
6171
|
try {
|
|
@@ -5581,37 +6191,39 @@ class Reconciler {
|
|
|
5581
6191
|
const subtasks = card.subtasks ?? [];
|
|
5582
6192
|
const mode = reviewColumnIds.has(card.column_id) ? "review" : "implement";
|
|
5583
6193
|
if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
|
|
5584
|
-
log.debug(
|
|
6194
|
+
log.debug(TAG27, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
|
|
5585
6195
|
continue;
|
|
5586
6196
|
}
|
|
5587
6197
|
if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
|
|
5588
|
-
log.debug(
|
|
6198
|
+
log.debug(TAG27, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
|
|
5589
6199
|
continue;
|
|
5590
6200
|
}
|
|
5591
6201
|
if (mode === "review" && !extractBranchFromDescription(card.description)) {
|
|
5592
|
-
log.debug(
|
|
6202
|
+
log.debug(TAG27, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
|
|
5593
6203
|
continue;
|
|
5594
6204
|
}
|
|
5595
|
-
log.info(
|
|
6205
|
+
log.info(TAG27, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
|
|
5596
6206
|
await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
5597
6207
|
}
|
|
5598
6208
|
}
|
|
5599
6209
|
if (this.stateStore && this.agentConfig) {
|
|
5600
6210
|
await this.recoverStaleRuns();
|
|
5601
6211
|
}
|
|
6212
|
+
await this.recoverStrandedInProgress(cards, columns, knownCardIds);
|
|
5602
6213
|
for (const knownId of knownCardIds) {
|
|
5603
6214
|
if (!allAgentCardIds.has(knownId)) {
|
|
5604
|
-
log.info(
|
|
6215
|
+
log.info(TAG27, `Missed unassign: ${knownId} — removing`);
|
|
5605
6216
|
await this.pool.removeCard(knownId);
|
|
5606
6217
|
}
|
|
5607
6218
|
}
|
|
5608
|
-
|
|
6219
|
+
await this.releaseStalledApprovals(cards, columns, knownCardIds);
|
|
6220
|
+
log.debug(TAG27, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
|
|
5609
6221
|
} catch (err) {
|
|
5610
|
-
log.error(
|
|
6222
|
+
log.error(TAG27, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
|
|
5611
6223
|
}
|
|
5612
6224
|
}
|
|
5613
6225
|
}
|
|
5614
|
-
var
|
|
6226
|
+
var TAG27 = "reconcile";
|
|
5615
6227
|
var init_reconcile = __esm(() => {
|
|
5616
6228
|
init_board_helpers();
|
|
5617
6229
|
init_log();
|
|
@@ -5632,6 +6244,7 @@ function prettyBanner(config, version) {
|
|
|
5632
6244
|
const checks = [];
|
|
5633
6245
|
let projectName;
|
|
5634
6246
|
let gitProvider;
|
|
6247
|
+
let httpPort;
|
|
5635
6248
|
let failed = false;
|
|
5636
6249
|
let rendered = false;
|
|
5637
6250
|
return {
|
|
@@ -5641,11 +6254,14 @@ function prettyBanner(config, version) {
|
|
|
5641
6254
|
setGitProvider(provider) {
|
|
5642
6255
|
gitProvider = provider;
|
|
5643
6256
|
},
|
|
6257
|
+
setHttpPort(port) {
|
|
6258
|
+
httpPort = port;
|
|
6259
|
+
},
|
|
5644
6260
|
check(message) {
|
|
5645
6261
|
checks.push({ kind: "ok", message });
|
|
5646
6262
|
},
|
|
5647
6263
|
warn(message) {
|
|
5648
|
-
log.warn(
|
|
6264
|
+
log.warn(TAG28, message);
|
|
5649
6265
|
checks.push({ kind: "warn", message: message.split(`
|
|
5650
6266
|
`, 1)[0] });
|
|
5651
6267
|
},
|
|
@@ -5661,6 +6277,7 @@ function prettyBanner(config, version) {
|
|
|
5661
6277
|
config,
|
|
5662
6278
|
projectName,
|
|
5663
6279
|
gitProvider,
|
|
6280
|
+
httpPort,
|
|
5664
6281
|
checks,
|
|
5665
6282
|
readyMessage: message
|
|
5666
6283
|
});
|
|
@@ -5669,22 +6286,25 @@ function prettyBanner(config, version) {
|
|
|
5669
6286
|
};
|
|
5670
6287
|
}
|
|
5671
6288
|
function jsonBanner(config, version) {
|
|
5672
|
-
log.info(
|
|
5673
|
-
log.info(
|
|
6289
|
+
log.info(TAG28, `Harmony Agent Daemon v${version} starting...`);
|
|
6290
|
+
log.info(TAG28, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
|
|
5674
6291
|
if (config.agent.review.enabled) {
|
|
5675
|
-
log.info(
|
|
6292
|
+
log.info(TAG28, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
|
|
5676
6293
|
}
|
|
5677
6294
|
let failed = false;
|
|
5678
6295
|
return {
|
|
5679
6296
|
setProjectName(_name) {},
|
|
5680
6297
|
setGitProvider(provider) {
|
|
5681
|
-
log.info(
|
|
6298
|
+
log.info(TAG28, `Git provider: ${provider}`);
|
|
6299
|
+
},
|
|
6300
|
+
setHttpPort(port) {
|
|
6301
|
+
log.info(TAG28, `HTTP server on port ${port}`);
|
|
5682
6302
|
},
|
|
5683
6303
|
check(message) {
|
|
5684
|
-
log.info(
|
|
6304
|
+
log.info(TAG28, message);
|
|
5685
6305
|
},
|
|
5686
6306
|
warn(message) {
|
|
5687
|
-
log.warn(
|
|
6307
|
+
log.warn(TAG28, message);
|
|
5688
6308
|
},
|
|
5689
6309
|
fail() {
|
|
5690
6310
|
failed = true;
|
|
@@ -5692,17 +6312,25 @@ function jsonBanner(config, version) {
|
|
|
5692
6312
|
async ready(message) {
|
|
5693
6313
|
if (failed)
|
|
5694
6314
|
return;
|
|
5695
|
-
log.info(
|
|
6315
|
+
log.info(TAG28, message);
|
|
5696
6316
|
}
|
|
5697
6317
|
};
|
|
5698
6318
|
}
|
|
5699
6319
|
function renderPretty(input) {
|
|
5700
|
-
const {
|
|
6320
|
+
const {
|
|
6321
|
+
version,
|
|
6322
|
+
config,
|
|
6323
|
+
projectName,
|
|
6324
|
+
gitProvider,
|
|
6325
|
+
httpPort,
|
|
6326
|
+
checks,
|
|
6327
|
+
readyMessage
|
|
6328
|
+
} = input;
|
|
5701
6329
|
const lines = [];
|
|
5702
6330
|
lines.push("");
|
|
5703
6331
|
lines.push(titleRule(`Harmony Agent Daemon v${version}`));
|
|
5704
6332
|
lines.push("");
|
|
5705
|
-
for (const row of configRows(config, projectName, gitProvider)) {
|
|
6333
|
+
for (const row of configRows(config, projectName, gitProvider, httpPort)) {
|
|
5706
6334
|
lines.push(` ${dim(row.label.padEnd(9))} ${row.value}`);
|
|
5707
6335
|
}
|
|
5708
6336
|
lines.push("");
|
|
@@ -5716,7 +6344,7 @@ function renderPretty(input) {
|
|
|
5716
6344
|
return lines.join(`
|
|
5717
6345
|
`);
|
|
5718
6346
|
}
|
|
5719
|
-
function configRows(config, projectName, gitProvider) {
|
|
6347
|
+
function configRows(config, projectName, gitProvider, httpPort) {
|
|
5720
6348
|
const rows = [];
|
|
5721
6349
|
const projectLabel = projectName ? `${projectName} (${shortenId(config.projectId)})` : shortenId(config.projectId);
|
|
5722
6350
|
rows.push({ label: "Project", value: projectLabel });
|
|
@@ -5732,7 +6360,7 @@ function configRows(config, projectName, gitProvider) {
|
|
|
5732
6360
|
if (gitProvider)
|
|
5733
6361
|
tail.push(gitProvider);
|
|
5734
6362
|
if (config.agent.http.enabled) {
|
|
5735
|
-
tail.push(`HTTP http://${config.agent.http.bindAddr}:${config.agent.http.port}`);
|
|
6363
|
+
tail.push(`HTTP http://${config.agent.http.bindAddr}:${httpPort ?? config.agent.http.port}`);
|
|
5736
6364
|
}
|
|
5737
6365
|
if (tail.length > 0) {
|
|
5738
6366
|
rows.push({ label: "Git", value: tail.join(" · ") });
|
|
@@ -5759,7 +6387,7 @@ function cyan(s) {
|
|
|
5759
6387
|
function yellow(s) {
|
|
5760
6388
|
return `${ANSI.yellow}${s}${ANSI.reset}`;
|
|
5761
6389
|
}
|
|
5762
|
-
var
|
|
6390
|
+
var TAG28 = "daemon", RULE_WIDTH = 70, ANSI;
|
|
5763
6391
|
var init_startup_banner = __esm(() => {
|
|
5764
6392
|
init_log();
|
|
5765
6393
|
ANSI = {
|
|
@@ -5906,18 +6534,18 @@ class Watcher {
|
|
|
5906
6534
|
}
|
|
5907
6535
|
async start() {
|
|
5908
6536
|
if (!isPretty()) {
|
|
5909
|
-
log.info(
|
|
6537
|
+
log.info(TAG29, "Connecting to Supabase realtime (broadcast)...");
|
|
5910
6538
|
}
|
|
5911
6539
|
this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
|
|
5912
6540
|
const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
|
|
5913
6541
|
const channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
|
|
5914
|
-
log.debug(
|
|
6542
|
+
log.debug(TAG29, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
|
|
5915
6543
|
this.onCardBroadcast({
|
|
5916
6544
|
event: "card_update",
|
|
5917
6545
|
payload: msg.payload ?? {}
|
|
5918
6546
|
});
|
|
5919
6547
|
}).on("broadcast", { event: "card_created" }, (msg) => {
|
|
5920
|
-
log.debug(
|
|
6548
|
+
log.debug(TAG29, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
|
|
5921
6549
|
this.onCardBroadcast({
|
|
5922
6550
|
event: "card_created",
|
|
5923
6551
|
payload: msg.payload ?? {}
|
|
@@ -5927,29 +6555,29 @@ class Watcher {
|
|
|
5927
6555
|
const cardId = payload.card_id;
|
|
5928
6556
|
const command = payload.command;
|
|
5929
6557
|
if (cardId && command) {
|
|
5930
|
-
log.info(
|
|
6558
|
+
log.info(TAG29, `Broadcast: agent_command ${command} for ${cardId}`);
|
|
5931
6559
|
this.onAgentCommand?.({ cardId, command });
|
|
5932
6560
|
}
|
|
5933
6561
|
}).subscribe((status) => {
|
|
5934
6562
|
if (status === "SUBSCRIBED") {
|
|
5935
6563
|
this.connected = true;
|
|
5936
6564
|
if (!isPretty() || !this.suppressStartupLogs) {
|
|
5937
|
-
log.info(
|
|
6565
|
+
log.info(TAG29, "Broadcast subscription active");
|
|
5938
6566
|
}
|
|
5939
6567
|
this.maybeResolveReady();
|
|
5940
6568
|
} else if (status === "CHANNEL_ERROR") {
|
|
5941
6569
|
this.connected = false;
|
|
5942
|
-
log.error(
|
|
6570
|
+
log.error(TAG29, "Broadcast channel error — will rely on reconciliation");
|
|
5943
6571
|
} else if (status === "TIMED_OUT") {
|
|
5944
6572
|
this.connected = false;
|
|
5945
|
-
log.warn(
|
|
6573
|
+
log.warn(TAG29, "Broadcast subscription timed out — retrying...");
|
|
5946
6574
|
} else if (status === "CLOSED") {
|
|
5947
6575
|
this.connected = false;
|
|
5948
6576
|
}
|
|
5949
6577
|
});
|
|
5950
6578
|
this.channel = channel;
|
|
5951
6579
|
presenceChannel.on("presence", { event: "sync" }, () => {
|
|
5952
|
-
log.debug(
|
|
6580
|
+
log.debug(TAG29, "Presence sync");
|
|
5953
6581
|
}).subscribe(async (status) => {
|
|
5954
6582
|
if (status === "SUBSCRIBED") {
|
|
5955
6583
|
await presenceChannel.track({
|
|
@@ -5962,7 +6590,7 @@ class Watcher {
|
|
|
5962
6590
|
agentName: this.identity.agentName
|
|
5963
6591
|
});
|
|
5964
6592
|
if (!isPretty() || !this.suppressStartupLogs) {
|
|
5965
|
-
log.info(
|
|
6593
|
+
log.info(TAG29, "Presence tracked on board-presence channel");
|
|
5966
6594
|
}
|
|
5967
6595
|
this.presenceTracked = true;
|
|
5968
6596
|
this.maybeResolveReady();
|
|
@@ -5984,10 +6612,10 @@ class Watcher {
|
|
|
5984
6612
|
this.supabase = null;
|
|
5985
6613
|
}
|
|
5986
6614
|
this.connected = false;
|
|
5987
|
-
log.info(
|
|
6615
|
+
log.info(TAG29, "Broadcast subscription stopped");
|
|
5988
6616
|
}
|
|
5989
6617
|
}
|
|
5990
|
-
var
|
|
6618
|
+
var TAG29 = "watcher";
|
|
5991
6619
|
var init_watcher = __esm(() => {
|
|
5992
6620
|
init_log();
|
|
5993
6621
|
});
|
|
@@ -6062,10 +6690,10 @@ function runWorktreeGc(basePath, store, opts = {}) {
|
|
|
6062
6690
|
});
|
|
6063
6691
|
} catch {}
|
|
6064
6692
|
if (result.removed.length > 0) {
|
|
6065
|
-
log.info(
|
|
6693
|
+
log.info(TAG30, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
|
|
6066
6694
|
}
|
|
6067
6695
|
if (result.errors.length > 0) {
|
|
6068
|
-
log.warn(
|
|
6696
|
+
log.warn(TAG30, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
|
|
6069
6697
|
}
|
|
6070
6698
|
return result;
|
|
6071
6699
|
}
|
|
@@ -6142,10 +6770,10 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
6142
6770
|
}
|
|
6143
6771
|
}
|
|
6144
6772
|
if (result.removed.length > 0) {
|
|
6145
|
-
log.info(
|
|
6773
|
+
log.info(TAG30, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
|
|
6146
6774
|
}
|
|
6147
6775
|
if (result.errors.length > 0) {
|
|
6148
|
-
log.warn(
|
|
6776
|
+
log.warn(TAG30, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
|
|
6149
6777
|
}
|
|
6150
6778
|
return result;
|
|
6151
6779
|
}
|
|
@@ -6176,13 +6804,13 @@ class WorktreeGc {
|
|
|
6176
6804
|
try {
|
|
6177
6805
|
runWorktreeGc(this.basePath, this.store);
|
|
6178
6806
|
} catch (err) {
|
|
6179
|
-
log.warn(
|
|
6807
|
+
log.warn(TAG30, `GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
6180
6808
|
}
|
|
6181
6809
|
if (this.remoteOpts) {
|
|
6182
6810
|
try {
|
|
6183
6811
|
pruneFailedRemoteBranches(this.remoteOpts);
|
|
6184
6812
|
} catch (err) {
|
|
6185
|
-
log.warn(
|
|
6813
|
+
log.warn(TAG30, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
6186
6814
|
}
|
|
6187
6815
|
}
|
|
6188
6816
|
}
|
|
@@ -6196,7 +6824,7 @@ function getRepoRoot2() {
|
|
|
6196
6824
|
return null;
|
|
6197
6825
|
}
|
|
6198
6826
|
}
|
|
6199
|
-
var
|
|
6827
|
+
var TAG30 = "worktree-gc";
|
|
6200
6828
|
var init_worktree_gc = __esm(() => {
|
|
6201
6829
|
init_log();
|
|
6202
6830
|
init_worktree();
|
|
@@ -6278,7 +6906,7 @@ async function main() {
|
|
|
6278
6906
|
} catch (err) {
|
|
6279
6907
|
if (err instanceof ConfigValidationError) {
|
|
6280
6908
|
banner.fail();
|
|
6281
|
-
log.error(
|
|
6909
|
+
log.error(TAG31, err.message);
|
|
6282
6910
|
process.exit(1);
|
|
6283
6911
|
}
|
|
6284
6912
|
throw err;
|
|
@@ -6387,25 +7015,28 @@ async function main() {
|
|
|
6387
7015
|
if (shuttingDown)
|
|
6388
7016
|
return;
|
|
6389
7017
|
shuttingDown = true;
|
|
6390
|
-
log.info(
|
|
7018
|
+
log.info(TAG31, `Received ${signal}, shutting down gracefully...`);
|
|
6391
7019
|
reconciler.stop();
|
|
6392
7020
|
mergeMonitor?.stop();
|
|
6393
7021
|
worktreeGc.stop();
|
|
6394
|
-
|
|
7022
|
+
if (httpServer) {
|
|
7023
|
+
clearDaemonPort(config.projectId, process.pid);
|
|
7024
|
+
await httpServer.stop();
|
|
7025
|
+
}
|
|
6395
7026
|
await watcher.stop();
|
|
6396
7027
|
await pool.shutdown();
|
|
6397
|
-
log.info(
|
|
7028
|
+
log.info(TAG31, "Daemon stopped.");
|
|
6398
7029
|
process.exit(exitCode);
|
|
6399
7030
|
};
|
|
6400
7031
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
6401
7032
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
6402
7033
|
process.on("uncaughtException", (err) => {
|
|
6403
|
-
log.error(
|
|
7034
|
+
log.error(TAG31, `Uncaught exception: ${err.message}`);
|
|
6404
7035
|
exitCode = 1;
|
|
6405
7036
|
shutdown("uncaughtException");
|
|
6406
7037
|
});
|
|
6407
7038
|
process.on("unhandledRejection", (reason) => {
|
|
6408
|
-
log.error(
|
|
7039
|
+
log.error(TAG31, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
|
|
6409
7040
|
exitCode = 1;
|
|
6410
7041
|
shutdown("unhandledRejection");
|
|
6411
7042
|
});
|
|
@@ -6415,7 +7046,13 @@ async function main() {
|
|
|
6415
7046
|
worktreeGc.start();
|
|
6416
7047
|
if (httpServer) {
|
|
6417
7048
|
try {
|
|
6418
|
-
await httpServer.start();
|
|
7049
|
+
const boundPort = await httpServer.start();
|
|
7050
|
+
recordDaemonPort(config.projectId, {
|
|
7051
|
+
port: boundPort,
|
|
7052
|
+
pid: process.pid,
|
|
7053
|
+
bindAddr: config.agent.http.bindAddr
|
|
7054
|
+
});
|
|
7055
|
+
banner.setHttpPort(boundPort);
|
|
6419
7056
|
} catch (err) {
|
|
6420
7057
|
banner.warn(`HTTP server failed to bind: ${err instanceof Error ? err.message : err}`);
|
|
6421
7058
|
}
|
|
@@ -6452,34 +7089,34 @@ async function handleBroadcast(event, client, pool, config, agentId) {
|
|
|
6452
7089
|
if (assignedAgentId === undefined)
|
|
6453
7090
|
return;
|
|
6454
7091
|
if (assignedAgentId === agentId) {
|
|
6455
|
-
log.info(
|
|
7092
|
+
log.info(TAG31, `Broadcast: card ${cardId} assigned to agent`);
|
|
6456
7093
|
try {
|
|
6457
7094
|
await tryEnqueueCard(cardId, client, pool, config, agentId);
|
|
6458
7095
|
} catch (err) {
|
|
6459
|
-
log.error(
|
|
7096
|
+
log.error(TAG31, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
|
|
6460
7097
|
}
|
|
6461
7098
|
} else if (pool.isCardKnown(cardId)) {
|
|
6462
|
-
log.info(
|
|
7099
|
+
log.info(TAG31, `Broadcast: card ${cardId} unassigned from agent`);
|
|
6463
7100
|
await pool.removeCard(cardId);
|
|
6464
7101
|
}
|
|
6465
7102
|
}
|
|
6466
7103
|
async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
6467
7104
|
const { card } = await client.getCard(cardId);
|
|
6468
7105
|
if (card.assigned_agent_id !== agentId) {
|
|
6469
|
-
log.debug(
|
|
7106
|
+
log.debug(TAG31, `Card ${cardId} no longer assigned to agent — skipping`);
|
|
6470
7107
|
return;
|
|
6471
7108
|
}
|
|
6472
7109
|
const board = await client.getBoard(config.projectId, { summary: true });
|
|
6473
7110
|
const columns = board.columns;
|
|
6474
7111
|
const column = columns.find((c) => c.id === card.column_id);
|
|
6475
7112
|
if (!column) {
|
|
6476
|
-
log.warn(
|
|
7113
|
+
log.warn(TAG31, `Column not found for card ${cardId}`);
|
|
6477
7114
|
return;
|
|
6478
7115
|
}
|
|
6479
7116
|
const isPickupColumn = config.agent.pickupColumns.some((name) => name.toLowerCase() === column.name.toLowerCase());
|
|
6480
7117
|
const isReviewColumn = config.agent.review.enabled && config.agent.review.pickupColumns.some((name) => name.toLowerCase() === column.name.toLowerCase());
|
|
6481
7118
|
if (!isPickupColumn && !isReviewColumn) {
|
|
6482
|
-
log.info(
|
|
7119
|
+
log.info(TAG31, `Card #${card.short_id} is in "${column.name}", not a pickup/review column — skipping`);
|
|
6483
7120
|
return;
|
|
6484
7121
|
}
|
|
6485
7122
|
const mode = isReviewColumn ? "review" : "implement";
|
|
@@ -6487,16 +7124,16 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
|
6487
7124
|
const cardLabels = resolveCardLabels(card, labelMap);
|
|
6488
7125
|
const subtasks = card.subtasks ?? [];
|
|
6489
7126
|
if (mode === "review" && config.agent.review.approvedLabel && hasLabel(cardLabels, config.agent.review.approvedLabel)) {
|
|
6490
|
-
log.debug(
|
|
7127
|
+
log.debug(TAG31, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
|
|
6491
7128
|
return;
|
|
6492
7129
|
}
|
|
6493
7130
|
if (mode === "review" && !extractBranchFromDescription(card.description)) {
|
|
6494
|
-
log.info(
|
|
7131
|
+
log.info(TAG31, `Card #${card.short_id} has no branch reference — skipping auto-review`);
|
|
6495
7132
|
return;
|
|
6496
7133
|
}
|
|
6497
7134
|
await pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
6498
7135
|
}
|
|
6499
|
-
var
|
|
7136
|
+
var TAG31 = "daemon", PKG_VERSION;
|
|
6500
7137
|
var init_src = __esm(() => {
|
|
6501
7138
|
init_board_helpers();
|
|
6502
7139
|
init_config();
|
|
@@ -6506,6 +7143,7 @@ var init_src = __esm(() => {
|
|
|
6506
7143
|
init_log();
|
|
6507
7144
|
init_merge_monitor();
|
|
6508
7145
|
init_pool();
|
|
7146
|
+
init_port_registry();
|
|
6509
7147
|
init_reconcile();
|
|
6510
7148
|
init_recovery();
|
|
6511
7149
|
init_review_worktree();
|