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