@gethmy/agent 1.22.3 → 1.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +1432 -346
- package/dist/index.js +868 -339
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -234,6 +234,340 @@ var init_board_helpers = __esm(() => {
|
|
|
234
234
|
init_log();
|
|
235
235
|
});
|
|
236
236
|
|
|
237
|
+
// src/board-review.ts
|
|
238
|
+
function digestTitleForDate(date) {
|
|
239
|
+
const y = date.getFullYear();
|
|
240
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
241
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
242
|
+
return `${DIGEST_TITLE_PREFIX} — ${y}-${m}-${d}`;
|
|
243
|
+
}
|
|
244
|
+
function isDigestCard(card) {
|
|
245
|
+
return (card.title ?? "").startsWith(DIGEST_TITLE_PREFIX);
|
|
246
|
+
}
|
|
247
|
+
function daysBetween(laterMs, earlierMs) {
|
|
248
|
+
return Math.max(0, Math.floor((laterMs - earlierMs) / DAY_MS));
|
|
249
|
+
}
|
|
250
|
+
function parseTs(value) {
|
|
251
|
+
if (!value)
|
|
252
|
+
return null;
|
|
253
|
+
const ms = Date.parse(value);
|
|
254
|
+
return Number.isNaN(ms) ? null : ms;
|
|
255
|
+
}
|
|
256
|
+
function normalizeTitle(title) {
|
|
257
|
+
return (title ?? "").toLowerCase().replace(/\s+/g, " ").replace(/^[\s\p{P}]+|[\s\p{P}]+$/gu, "").trim();
|
|
258
|
+
}
|
|
259
|
+
function columnNameFor(columnsById, card) {
|
|
260
|
+
return columnsById.get(card.column_id)?.name ?? "(unknown)";
|
|
261
|
+
}
|
|
262
|
+
function toRef(columnsById, card) {
|
|
263
|
+
return {
|
|
264
|
+
shortId: card.short_id,
|
|
265
|
+
title: card.title,
|
|
266
|
+
columnName: columnNameFor(columnsById, card)
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function buildBoardReviewDigest(input) {
|
|
270
|
+
const { cards, columns, now, config } = input;
|
|
271
|
+
const columnsById = new Map(columns.map((c) => [c.id, c]));
|
|
272
|
+
const activeColumnNames = new Set(config.activeColumns.map((n) => n.toLowerCase()));
|
|
273
|
+
const scanned = cards.filter((c) => !c.archived_at && !c.done && !isDigestCard(c));
|
|
274
|
+
const stale = [];
|
|
275
|
+
const overdue = [];
|
|
276
|
+
const missingInfo = [];
|
|
277
|
+
const reprioritize = [];
|
|
278
|
+
const staleMs = config.staleDays * DAY_MS;
|
|
279
|
+
const dupGroups = new Map;
|
|
280
|
+
for (const card of scanned) {
|
|
281
|
+
const updatedMs = parseTs(card.updated_at);
|
|
282
|
+
const isStale = updatedMs !== null && now - updatedMs > staleMs;
|
|
283
|
+
const columnName = columnNameFor(columnsById, card);
|
|
284
|
+
if (isStale && updatedMs !== null) {
|
|
285
|
+
stale.push({
|
|
286
|
+
...toRef(columnsById, card),
|
|
287
|
+
reason: `No activity for ${daysBetween(now, updatedMs)} days (in "${columnName}")`
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
if (config.overdue) {
|
|
291
|
+
const dueMs = parseTs(card.due_date);
|
|
292
|
+
if (dueMs !== null && dueMs < now) {
|
|
293
|
+
overdue.push({
|
|
294
|
+
...toRef(columnsById, card),
|
|
295
|
+
reason: `Due date passed ${daysBetween(now, dueMs)} days ago`
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const missing = [];
|
|
300
|
+
if (!card.priority)
|
|
301
|
+
missing.push("no priority");
|
|
302
|
+
const descLen = (card.description ?? "").trim().length;
|
|
303
|
+
if (config.minDescriptionLength > 0 && descLen < config.minDescriptionLength) {
|
|
304
|
+
missing.push(descLen === 0 ? "empty description" : "thin description");
|
|
305
|
+
}
|
|
306
|
+
if (activeColumnNames.has(columnName.toLowerCase()) && !card.assignee_id && !card.assigned_agent_id) {
|
|
307
|
+
missing.push("no owner in an active column");
|
|
308
|
+
}
|
|
309
|
+
if (missing.length > 0) {
|
|
310
|
+
missingInfo.push({
|
|
311
|
+
...toRef(columnsById, card),
|
|
312
|
+
reason: missing.join(", ")
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
if ((card.priority === "high" || card.priority === "urgent") && isStale && updatedMs !== null) {
|
|
316
|
+
reprioritize.push({
|
|
317
|
+
...toRef(columnsById, card),
|
|
318
|
+
reason: `Marked ${card.priority} but untouched ${daysBetween(now, updatedMs)} days — re-prioritize or pick up`
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
const norm = normalizeTitle(card.title);
|
|
322
|
+
if (norm) {
|
|
323
|
+
const group = dupGroups.get(norm);
|
|
324
|
+
if (group)
|
|
325
|
+
group.push(card);
|
|
326
|
+
else
|
|
327
|
+
dupGroups.set(norm, [card]);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const duplicates = [];
|
|
331
|
+
for (const [normalizedTitle, group] of dupGroups) {
|
|
332
|
+
if (group.length < 2)
|
|
333
|
+
continue;
|
|
334
|
+
duplicates.push({
|
|
335
|
+
normalizedTitle,
|
|
336
|
+
cards: group.map((c) => toRef(columnsById, c))
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
duplicates.sort((a, b) => b.cards.length - a.cards.length);
|
|
340
|
+
const cap = (arr) => config.maxPerBucket > 0 ? arr.slice(0, config.maxPerBucket) : arr;
|
|
341
|
+
const digest = {
|
|
342
|
+
stale: cap(stale),
|
|
343
|
+
overdue: cap(overdue),
|
|
344
|
+
missingInfo: cap(missingInfo),
|
|
345
|
+
reprioritize: cap(reprioritize),
|
|
346
|
+
duplicates: cap(duplicates),
|
|
347
|
+
totalFindings: 0,
|
|
348
|
+
flaggedCardCount: 0,
|
|
349
|
+
scannedCardCount: scanned.length
|
|
350
|
+
};
|
|
351
|
+
digest.totalFindings = digest.stale.length + digest.overdue.length + digest.missingInfo.length + digest.reprioritize.length + digest.duplicates.length;
|
|
352
|
+
const flagged = new Set;
|
|
353
|
+
for (const f of [
|
|
354
|
+
...digest.stale,
|
|
355
|
+
...digest.overdue,
|
|
356
|
+
...digest.missingInfo,
|
|
357
|
+
...digest.reprioritize
|
|
358
|
+
]) {
|
|
359
|
+
flagged.add(f.shortId);
|
|
360
|
+
}
|
|
361
|
+
for (const g of digest.duplicates) {
|
|
362
|
+
for (const c of g.cards)
|
|
363
|
+
flagged.add(c.shortId);
|
|
364
|
+
}
|
|
365
|
+
digest.flaggedCardCount = flagged.size;
|
|
366
|
+
return digest;
|
|
367
|
+
}
|
|
368
|
+
function renderFindingList(findings) {
|
|
369
|
+
return findings.map((f) => `- **#${f.shortId}** ${f.title} — ${f.reason}`).join(`
|
|
370
|
+
`);
|
|
371
|
+
}
|
|
372
|
+
function renderBoardReviewDigest(digest, opts) {
|
|
373
|
+
const { config } = opts;
|
|
374
|
+
const lines = [];
|
|
375
|
+
lines.push(`**Suggestions only — nothing was changed automatically.** Review and act on what's useful.`);
|
|
376
|
+
lines.push("");
|
|
377
|
+
lines.push(`Scanned **${digest.scannedCardCount}** open card(s); flagged **${digest.flaggedCardCount}** across **${digest.totalFindings}** finding(s).`);
|
|
378
|
+
if (digest.stale.length > 0) {
|
|
379
|
+
lines.push("");
|
|
380
|
+
lines.push(`## \uD83D\uDD70️ Stale (no activity > ${config.staleDays}d)`);
|
|
381
|
+
lines.push(renderFindingList(digest.stale));
|
|
382
|
+
lines.push("");
|
|
383
|
+
lines.push("_Suggested: move back to backlog, close, or leave a comment._");
|
|
384
|
+
}
|
|
385
|
+
if (digest.overdue.length > 0) {
|
|
386
|
+
lines.push("");
|
|
387
|
+
lines.push("## ⏰ Overdue");
|
|
388
|
+
lines.push(renderFindingList(digest.overdue));
|
|
389
|
+
lines.push("");
|
|
390
|
+
lines.push("_Suggested: reschedule the due date or re-scope._");
|
|
391
|
+
}
|
|
392
|
+
if (digest.reprioritize.length > 0) {
|
|
393
|
+
lines.push("");
|
|
394
|
+
lines.push("## \uD83C\uDFAF Stalled high-priority");
|
|
395
|
+
lines.push(renderFindingList(digest.reprioritize));
|
|
396
|
+
lines.push("");
|
|
397
|
+
lines.push("_Suggested: assign an owner/agent, or lower the priority._");
|
|
398
|
+
}
|
|
399
|
+
if (digest.missingInfo.length > 0) {
|
|
400
|
+
lines.push("");
|
|
401
|
+
lines.push("## \uD83D\uDCDD Missing info");
|
|
402
|
+
lines.push(renderFindingList(digest.missingInfo));
|
|
403
|
+
lines.push("");
|
|
404
|
+
lines.push("_Suggested: add a priority, description, or owner._");
|
|
405
|
+
}
|
|
406
|
+
if (digest.duplicates.length > 0) {
|
|
407
|
+
lines.push("");
|
|
408
|
+
lines.push("## \uD83D\uDC6F Potential duplicates");
|
|
409
|
+
for (const g of digest.duplicates) {
|
|
410
|
+
const refs = g.cards.map((c) => `#${c.shortId}`).join(", ");
|
|
411
|
+
lines.push(`- ${refs} — "${g.cards[0]?.title ?? g.normalizedTitle}"`);
|
|
412
|
+
}
|
|
413
|
+
lines.push("");
|
|
414
|
+
lines.push("_Suggested: merge, link, or clarify the distinction._");
|
|
415
|
+
}
|
|
416
|
+
lines.push("");
|
|
417
|
+
lines.push("> Generated by the scheduled board-review agent (#571). Safe to archive once triaged.");
|
|
418
|
+
return lines.join(`
|
|
419
|
+
`);
|
|
420
|
+
}
|
|
421
|
+
function msUntilNextRun(nowMs, hour, minute) {
|
|
422
|
+
const h = Math.min(23, Math.max(0, Math.floor(hour)));
|
|
423
|
+
const m = Math.min(59, Math.max(0, Math.floor(minute)));
|
|
424
|
+
const next = new Date(nowMs);
|
|
425
|
+
next.setHours(h, m, 0, 0);
|
|
426
|
+
if (next.getTime() <= nowMs) {
|
|
427
|
+
next.setDate(next.getDate() + 1);
|
|
428
|
+
}
|
|
429
|
+
const delay = next.getTime() - nowMs;
|
|
430
|
+
return delay > 0 ? delay : DAY_MS;
|
|
431
|
+
}
|
|
432
|
+
var DEFAULT_BOARD_REVIEW_CONFIG, DIGEST_TITLE_PREFIX = "\uD83E\uDDF9 Board review", DAY_MS = 86400000;
|
|
433
|
+
var init_board_review = __esm(() => {
|
|
434
|
+
DEFAULT_BOARD_REVIEW_CONFIG = {
|
|
435
|
+
enabled: false,
|
|
436
|
+
runAtHour: 3,
|
|
437
|
+
runAtMinute: 0,
|
|
438
|
+
digestColumn: "",
|
|
439
|
+
staleDays: 14,
|
|
440
|
+
overdue: true,
|
|
441
|
+
minDescriptionLength: 30,
|
|
442
|
+
activeColumns: ["In Progress"],
|
|
443
|
+
maxPerBucket: 20,
|
|
444
|
+
digestPriority: "low"
|
|
445
|
+
};
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// src/board-reviewer.ts
|
|
449
|
+
class BoardReviewer {
|
|
450
|
+
client;
|
|
451
|
+
projectId;
|
|
452
|
+
config;
|
|
453
|
+
now;
|
|
454
|
+
timer = null;
|
|
455
|
+
running = false;
|
|
456
|
+
lastRunAt = null;
|
|
457
|
+
get lastRun() {
|
|
458
|
+
return this.lastRunAt;
|
|
459
|
+
}
|
|
460
|
+
get isRunning() {
|
|
461
|
+
return this.running;
|
|
462
|
+
}
|
|
463
|
+
constructor(client, projectId, config, now = () => Date.now()) {
|
|
464
|
+
this.client = client;
|
|
465
|
+
this.projectId = projectId;
|
|
466
|
+
this.config = config;
|
|
467
|
+
this.now = now;
|
|
468
|
+
}
|
|
469
|
+
start() {
|
|
470
|
+
this.running = true;
|
|
471
|
+
const { runAtHour, runAtMinute } = this.config.boardReview;
|
|
472
|
+
const delay = msUntilNextRun(this.now(), runAtHour, runAtMinute);
|
|
473
|
+
log.info(TAG2, `Board review scheduled daily at ${pad(runAtHour)}:${pad(runAtMinute)} (next run in ${Math.round(delay / 60000)}m)`);
|
|
474
|
+
this.scheduleNext(delay);
|
|
475
|
+
}
|
|
476
|
+
stop() {
|
|
477
|
+
this.running = false;
|
|
478
|
+
if (this.timer) {
|
|
479
|
+
clearTimeout(this.timer);
|
|
480
|
+
this.timer = null;
|
|
481
|
+
}
|
|
482
|
+
log.info(TAG2, "Board review stopped");
|
|
483
|
+
}
|
|
484
|
+
async runOnce() {
|
|
485
|
+
await this.tick();
|
|
486
|
+
}
|
|
487
|
+
async scheduleNext(delayMs) {
|
|
488
|
+
await new Promise((resolve) => {
|
|
489
|
+
this.timer = setTimeout(() => resolve(), delayMs);
|
|
490
|
+
});
|
|
491
|
+
if (!this.running)
|
|
492
|
+
return;
|
|
493
|
+
await this.tick();
|
|
494
|
+
if (this.running) {
|
|
495
|
+
const { runAtHour, runAtMinute } = this.config.boardReview;
|
|
496
|
+
this.scheduleNext(msUntilNextRun(this.now(), runAtHour, runAtMinute));
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
async tick() {
|
|
500
|
+
try {
|
|
501
|
+
this.lastRunAt = this.now();
|
|
502
|
+
const cfg = this.config.boardReview;
|
|
503
|
+
const board = await this.client.getFullBoard(this.projectId);
|
|
504
|
+
const cards = board.cards ?? [];
|
|
505
|
+
const columns = board.columns ?? [];
|
|
506
|
+
const digest = buildBoardReviewDigest({
|
|
507
|
+
cards,
|
|
508
|
+
columns,
|
|
509
|
+
now: this.now(),
|
|
510
|
+
config: cfg
|
|
511
|
+
});
|
|
512
|
+
if (digest.totalFindings === 0) {
|
|
513
|
+
log.info(TAG2, "Board review: no findings — skipping digest");
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const title = digestTitleForDate(new Date(this.now()));
|
|
517
|
+
const existing = cards.find((c) => !c.archived_at && c.title === title);
|
|
518
|
+
if (existing) {
|
|
519
|
+
log.info(TAG2, `Digest for today already exists (#${existing.short_id}) — skipping`);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const column = this.resolveDigestColumn(columns);
|
|
523
|
+
if (!column) {
|
|
524
|
+
log.warn(TAG2, "No board column resolved for the digest — skipping");
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const body = renderBoardReviewDigest(digest, { config: cfg });
|
|
528
|
+
await this.client.createCard(this.projectId, {
|
|
529
|
+
title,
|
|
530
|
+
columnId: column.id,
|
|
531
|
+
description: body,
|
|
532
|
+
priority: cfg.digestPriority
|
|
533
|
+
});
|
|
534
|
+
log.info(TAG2, `Posted board-review digest to "${column.name}": ${digest.totalFindings} finding(s) across ${digest.flaggedCardCount} card(s)`);
|
|
535
|
+
} catch (err) {
|
|
536
|
+
log.error(TAG2, `Board review tick failed: ${err instanceof Error ? err.message : err}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
resolveDigestColumn(columns) {
|
|
540
|
+
if (columns.length === 0)
|
|
541
|
+
return null;
|
|
542
|
+
const byName = (name) => {
|
|
543
|
+
const target = name.toLowerCase();
|
|
544
|
+
return columns.find((c) => c.name.toLowerCase() === target);
|
|
545
|
+
};
|
|
546
|
+
const configured = this.config.boardReview.digestColumn?.trim();
|
|
547
|
+
if (configured) {
|
|
548
|
+
const found = byName(configured);
|
|
549
|
+
if (found)
|
|
550
|
+
return found;
|
|
551
|
+
log.warn(TAG2, `Configured digestColumn "${configured}" not found — falling back`);
|
|
552
|
+
}
|
|
553
|
+
const firstPickup = this.config.pickupColumns[0];
|
|
554
|
+
if (firstPickup) {
|
|
555
|
+
const found = byName(firstPickup);
|
|
556
|
+
if (found)
|
|
557
|
+
return found;
|
|
558
|
+
}
|
|
559
|
+
return columns.find((c) => c.is_default) ?? columns[0];
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
function pad(n) {
|
|
563
|
+
return String(n).padStart(2, "0");
|
|
564
|
+
}
|
|
565
|
+
var TAG2 = "board-review";
|
|
566
|
+
var init_board_reviewer = __esm(() => {
|
|
567
|
+
init_board_review();
|
|
568
|
+
init_log();
|
|
569
|
+
});
|
|
570
|
+
|
|
237
571
|
// ../harmony-shared/dist/agentCommentTrust.js
|
|
238
572
|
function isDaemonAuthoredComment(comment, identity) {
|
|
239
573
|
if (comment.author_type !== "agent")
|
|
@@ -258,6 +592,25 @@ function extractBranchRef(description) {
|
|
|
258
592
|
}
|
|
259
593
|
return null;
|
|
260
594
|
}
|
|
595
|
+
function hasReviewableBranch(description) {
|
|
596
|
+
return extractBranchRef(description) !== null;
|
|
597
|
+
}
|
|
598
|
+
function hasReviewablePrLink(description) {
|
|
599
|
+
if (!description)
|
|
600
|
+
return false;
|
|
601
|
+
const m = description.match(PR_LINK_PATTERN);
|
|
602
|
+
if (!m)
|
|
603
|
+
return false;
|
|
604
|
+
try {
|
|
605
|
+
new URL(m[1]);
|
|
606
|
+
return true;
|
|
607
|
+
} catch {
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
function qualifiesForReview(description) {
|
|
612
|
+
return hasReviewableBranch(description) || hasReviewablePrLink(description);
|
|
613
|
+
}
|
|
261
614
|
function hasUnsafeDaemonBranchLine(description) {
|
|
262
615
|
if (!description)
|
|
263
616
|
return false;
|
|
@@ -267,11 +620,12 @@ function hasUnsafeDaemonBranchLine(description) {
|
|
|
267
620
|
}
|
|
268
621
|
return false;
|
|
269
622
|
}
|
|
270
|
-
var BRANCH_REF_PATTERN, DAEMON_BRANCH_LINE_PATTERN, SAFE_GIT_REF_PATTERN;
|
|
623
|
+
var BRANCH_REF_PATTERN, DAEMON_BRANCH_LINE_PATTERN, SAFE_GIT_REF_PATTERN, PR_LINK_PATTERN;
|
|
271
624
|
var init_branchRef = __esm(() => {
|
|
272
625
|
BRANCH_REF_PATTERN = /Branch:\s*`([^`]+)`/g;
|
|
273
626
|
DAEMON_BRANCH_LINE_PATTERN = /^[ \t]*Branch:\s*`([^`]+)`/gm;
|
|
274
627
|
SAFE_GIT_REF_PATTERN = /^[a-zA-Z0-9/_.+-]+$/;
|
|
628
|
+
PR_LINK_PATTERN = /PR:\s*(https?:\/\/[^\s)]+)/;
|
|
275
629
|
});
|
|
276
630
|
|
|
277
631
|
// ../harmony-shared/dist/cardLinks.js
|
|
@@ -1387,6 +1741,7 @@ function endStatusForCancel(reason) {
|
|
|
1387
1741
|
}
|
|
1388
1742
|
var DEFAULT_AGENT_CONFIG, IN_PROGRESS_COLUMN = "In Progress", NEED_REVIEW_LABEL = "Need Review", NEED_REVIEW_LABEL_COLOR = "#f59e0b", AGENT_NAME = "Harmony Agent";
|
|
1389
1743
|
var init_types2 = __esm(() => {
|
|
1744
|
+
init_board_review();
|
|
1390
1745
|
init_contract_phase();
|
|
1391
1746
|
init_plan_phase();
|
|
1392
1747
|
DEFAULT_AGENT_CONFIG = {
|
|
@@ -1402,13 +1757,13 @@ var init_types2 = __esm(() => {
|
|
|
1402
1757
|
postSummary: true
|
|
1403
1758
|
},
|
|
1404
1759
|
claude: {
|
|
1405
|
-
model: "claude-opus-
|
|
1406
|
-
escalateModel: "claude-
|
|
1760
|
+
model: "claude-opus-5",
|
|
1761
|
+
escalateModel: "claude-fable-5",
|
|
1407
1762
|
escalateAfterAttempts: 2,
|
|
1408
1763
|
tiers: {
|
|
1409
|
-
simple: "claude-
|
|
1410
|
-
advanced: "claude-
|
|
1411
|
-
research: "claude-
|
|
1764
|
+
simple: "claude-sonnet-5",
|
|
1765
|
+
advanced: "claude-opus-5",
|
|
1766
|
+
research: "claude-fable-5"
|
|
1412
1767
|
},
|
|
1413
1768
|
reviewModel: "sonnet",
|
|
1414
1769
|
maxTurns: 80,
|
|
@@ -1478,7 +1833,8 @@ var init_types2 = __esm(() => {
|
|
|
1478
1833
|
},
|
|
1479
1834
|
planning: DEFAULT_PLANNING_CONFIG,
|
|
1480
1835
|
playbooks: { enabled: true, humanStageColumns: [] },
|
|
1481
|
-
contractFirst: DEFAULT_CONTRACT_CONFIG
|
|
1836
|
+
contractFirst: DEFAULT_CONTRACT_CONFIG,
|
|
1837
|
+
boardReview: DEFAULT_BOARD_REVIEW_CONFIG
|
|
1482
1838
|
};
|
|
1483
1839
|
});
|
|
1484
1840
|
|
|
@@ -1591,6 +1947,10 @@ function loadDaemonConfig() {
|
|
|
1591
1947
|
contractFirst: {
|
|
1592
1948
|
...DEFAULT_AGENT_CONFIG.contractFirst,
|
|
1593
1949
|
...agentOverrides.contractFirst ?? {}
|
|
1950
|
+
},
|
|
1951
|
+
boardReview: {
|
|
1952
|
+
...DEFAULT_AGENT_CONFIG.boardReview,
|
|
1953
|
+
...agentOverrides.boardReview ?? {}
|
|
1594
1954
|
}
|
|
1595
1955
|
};
|
|
1596
1956
|
if (agent.runner !== "cli" && agent.runner !== "sdk") {
|
|
@@ -1690,6 +2050,12 @@ async function validateColumnReferences(client, projectId, config) {
|
|
|
1690
2050
|
}
|
|
1691
2051
|
}
|
|
1692
2052
|
}
|
|
2053
|
+
if (config.boardReview.enabled && config.boardReview.digestColumn) {
|
|
2054
|
+
required.push({
|
|
2055
|
+
value: config.boardReview.digestColumn,
|
|
2056
|
+
where: "boardReview.digestColumn"
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
1693
2059
|
for (const { value, where } of required) {
|
|
1694
2060
|
if (!value)
|
|
1695
2061
|
continue;
|
|
@@ -1809,7 +2175,7 @@ function validateGitProviderCli(provider, cwd) {
|
|
|
1809
2175
|
}
|
|
1810
2176
|
case "bitbucket":
|
|
1811
2177
|
case "unknown":
|
|
1812
|
-
log.warn(
|
|
2178
|
+
log.warn(TAG3, `Git provider "${provider}" — PR creation will be skipped (no CLI support)`);
|
|
1813
2179
|
break;
|
|
1814
2180
|
}
|
|
1815
2181
|
}
|
|
@@ -1921,7 +2287,7 @@ async function checkPrMergeStatus(prUrl, cwd, provider) {
|
|
|
1921
2287
|
try {
|
|
1922
2288
|
parsed = JSON.parse(stdout.trim());
|
|
1923
2289
|
} catch {
|
|
1924
|
-
log.warn(
|
|
2290
|
+
log.warn(TAG3, `Failed to parse glab JSON output for MR ${mrMatch[1]}`);
|
|
1925
2291
|
return "unknown";
|
|
1926
2292
|
}
|
|
1927
2293
|
if (typeof parsed !== "object" || parsed === null)
|
|
@@ -2019,7 +2385,7 @@ async function resolvePrHeadBranch(prUrl, cwd, provider) {
|
|
|
2019
2385
|
const { stdout } = await execFileAsync("gh", ["pr", "view", prUrl, "--json", "headRefName,isCrossRepository"], { cwd, encoding: "utf-8", timeout: 1e4 });
|
|
2020
2386
|
return decidePrBranch("github", stdout);
|
|
2021
2387
|
} catch (err) {
|
|
2022
|
-
log.warn(
|
|
2388
|
+
log.warn(TAG3, `gh pr view failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2023
2389
|
return decidePrBranch("github", null);
|
|
2024
2390
|
}
|
|
2025
2391
|
}
|
|
@@ -2035,7 +2401,7 @@ async function resolvePrHeadBranch(prUrl, cwd, provider) {
|
|
|
2035
2401
|
const { stdout } = await execFileAsync("az", ["repos", "pr", "show", "--id", prId, "--output", "json"], { cwd, encoding: "utf-8", timeout: 1e4 });
|
|
2036
2402
|
return decidePrBranch("azure", stdout);
|
|
2037
2403
|
} catch (err) {
|
|
2038
|
-
log.warn(
|
|
2404
|
+
log.warn(TAG3, `az repos pr show failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2039
2405
|
return decidePrBranch("azure", null);
|
|
2040
2406
|
}
|
|
2041
2407
|
}
|
|
@@ -2071,7 +2437,7 @@ function remoteBranchExists(branchName, cwd) {
|
|
|
2071
2437
|
}
|
|
2072
2438
|
function pushBranch(branchName, cwd) {
|
|
2073
2439
|
if (remoteBranchExists(branchName, cwd)) {
|
|
2074
|
-
log.info(
|
|
2440
|
+
log.info(TAG3, `Remote branch ${branchName} exists (rework), force-pushing`);
|
|
2075
2441
|
let expectedSha = null;
|
|
2076
2442
|
try {
|
|
2077
2443
|
execFileSync("git", ["fetch", "origin", branchName], {
|
|
@@ -2080,7 +2446,7 @@ function pushBranch(branchName, cwd) {
|
|
|
2080
2446
|
});
|
|
2081
2447
|
expectedSha = execFileSync("git", ["rev-parse", `refs/remotes/origin/${branchName}`], { cwd, encoding: "utf-8" }).trim();
|
|
2082
2448
|
} catch (err) {
|
|
2083
|
-
log.warn(
|
|
2449
|
+
log.warn(TAG3, `could not resolve remote tip for ${branchName}, falling back to weak lease: ${err instanceof Error ? err.message : err}`);
|
|
2084
2450
|
}
|
|
2085
2451
|
const lease = expectedSha ? `--force-with-lease=refs/heads/${branchName}:${expectedSha}` : "--force-with-lease";
|
|
2086
2452
|
execFileSync("git", ["push", lease, "-u", "origin", branchName], {
|
|
@@ -2106,7 +2472,7 @@ function renameRemoteBranch(oldRef, newRef, cwd) {
|
|
|
2106
2472
|
} catch (err) {
|
|
2107
2473
|
throw new Error(`renameRemoteBranch: could not resolve HEAD: ${err instanceof Error ? err.message : err}`);
|
|
2108
2474
|
}
|
|
2109
|
-
log.info(
|
|
2475
|
+
log.info(TAG3, `Renaming remote ${oldRef} → ${newRef}`);
|
|
2110
2476
|
execFileSync("git", ["push", "origin", `${sha}:refs/heads/${newRef}`, "--force-with-lease"], { cwd, stdio: "pipe" });
|
|
2111
2477
|
try {
|
|
2112
2478
|
execFileSync("git", ["push", "origin", `:refs/heads/${oldRef}`], {
|
|
@@ -2114,7 +2480,7 @@ function renameRemoteBranch(oldRef, newRef, cwd) {
|
|
|
2114
2480
|
stdio: "pipe"
|
|
2115
2481
|
});
|
|
2116
2482
|
} catch (err) {
|
|
2117
|
-
log.warn(
|
|
2483
|
+
log.warn(TAG3, `renameRemoteBranch: could not delete old ref ${oldRef}: ${err instanceof Error ? err.message : err}`);
|
|
2118
2484
|
}
|
|
2119
2485
|
try {
|
|
2120
2486
|
execFileSync("git", ["branch", "-m", oldRef, newRef], {
|
|
@@ -2173,7 +2539,7 @@ function buildPrBody(card, commitLog) {
|
|
|
2173
2539
|
}
|
|
2174
2540
|
function createPullRequest(card, branchName, worktreePath, config, provider, existingPrUrl) {
|
|
2175
2541
|
if (existingPrUrl) {
|
|
2176
|
-
log.info(
|
|
2542
|
+
log.info(TAG3, `Reusing existing PR from card description: ${existingPrUrl}`);
|
|
2177
2543
|
return existingPrUrl;
|
|
2178
2544
|
}
|
|
2179
2545
|
let commitLog = "";
|
|
@@ -2187,7 +2553,7 @@ function createPullRequest(card, branchName, worktreePath, config, provider, exi
|
|
|
2187
2553
|
const base = config.worktree.baseBranch;
|
|
2188
2554
|
const existingUrl = findExistingPr(branchName, worktreePath, provider);
|
|
2189
2555
|
if (existingUrl) {
|
|
2190
|
-
log.info(
|
|
2556
|
+
log.info(TAG3, `PR already exists for ${branchName}, updating body...`);
|
|
2191
2557
|
updateExistingPr(branchName, body, worktreePath, provider);
|
|
2192
2558
|
return existingUrl;
|
|
2193
2559
|
}
|
|
@@ -2237,13 +2603,13 @@ function createPullRequest(card, branchName, worktreePath, config, provider, exi
|
|
|
2237
2603
|
], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
2238
2604
|
break;
|
|
2239
2605
|
default:
|
|
2240
|
-
log.warn(
|
|
2606
|
+
log.warn(TAG3, `No PR CLI for provider "${provider}" — branch pushed but no PR created`);
|
|
2241
2607
|
return null;
|
|
2242
2608
|
}
|
|
2243
|
-
log.info(
|
|
2609
|
+
log.info(TAG3, `PR created: ${result}`);
|
|
2244
2610
|
return result;
|
|
2245
2611
|
} catch (err) {
|
|
2246
|
-
log.error(
|
|
2612
|
+
log.error(TAG3, `Failed to create PR: ${err instanceof Error ? err.message : err}`);
|
|
2247
2613
|
return null;
|
|
2248
2614
|
}
|
|
2249
2615
|
}
|
|
@@ -2277,12 +2643,12 @@ function updateExistingPr(branchName, body, worktreePath, provider) {
|
|
|
2277
2643
|
execFileSync("glab", ["mr", "update", branchName, "--description", body], { cwd: worktreePath, stdio: "pipe" });
|
|
2278
2644
|
break;
|
|
2279
2645
|
}
|
|
2280
|
-
log.info(
|
|
2646
|
+
log.info(TAG3, `Updated existing PR body for ${branchName}`);
|
|
2281
2647
|
} catch (err) {
|
|
2282
|
-
log.warn(
|
|
2648
|
+
log.warn(TAG3, `Failed to update PR body: ${err instanceof Error ? err.message : err}`);
|
|
2283
2649
|
}
|
|
2284
2650
|
}
|
|
2285
|
-
var execFileAsync,
|
|
2651
|
+
var execFileAsync, TAG3 = "git-pr", VALID_PR_URL_RE, PR_URL_RE, REVIEWED_SHA_RE;
|
|
2286
2652
|
var init_git_pr = __esm(() => {
|
|
2287
2653
|
init_dist();
|
|
2288
2654
|
init_log();
|
|
@@ -2310,7 +2676,7 @@ class HttpServer {
|
|
|
2310
2676
|
async start() {
|
|
2311
2677
|
this.server = createServer((req, res) => {
|
|
2312
2678
|
this.route(req, res).catch((err) => {
|
|
2313
|
-
log.error(
|
|
2679
|
+
log.error(TAG4, `unhandled: ${err instanceof Error ? err.message : err}`);
|
|
2314
2680
|
if (!res.headersSent) {
|
|
2315
2681
|
res.writeHead(500, { "content-type": "application/json" });
|
|
2316
2682
|
res.end(JSON.stringify({ error: "internal_error" }));
|
|
@@ -2325,13 +2691,13 @@ class HttpServer {
|
|
|
2325
2691
|
await this.listenOnce(port);
|
|
2326
2692
|
this.boundPort = port;
|
|
2327
2693
|
if (port !== startPort) {
|
|
2328
|
-
log.info(
|
|
2694
|
+
log.info(TAG4, `port ${startPort} busy — bound to ${port} instead`);
|
|
2329
2695
|
}
|
|
2330
2696
|
return port;
|
|
2331
2697
|
} catch (err) {
|
|
2332
2698
|
const lastAttempt = i === attempts - 1;
|
|
2333
2699
|
if (isAddrInUse(err) && !lastAttempt) {
|
|
2334
|
-
log.debug(
|
|
2700
|
+
log.debug(TAG4, `port ${port} in use, trying ${port + 1}`);
|
|
2335
2701
|
continue;
|
|
2336
2702
|
}
|
|
2337
2703
|
throw err;
|
|
@@ -2422,7 +2788,7 @@ function parseCommand(path) {
|
|
|
2422
2788
|
return null;
|
|
2423
2789
|
return { command: match[1], cardId: decodeURIComponent(match[2]) };
|
|
2424
2790
|
}
|
|
2425
|
-
var
|
|
2791
|
+
var TAG4 = "http";
|
|
2426
2792
|
var init_http_server = __esm(() => {
|
|
2427
2793
|
init_log();
|
|
2428
2794
|
});
|
|
@@ -2492,23 +2858,23 @@ async function attemptAutoMerge(deps) {
|
|
|
2492
2858
|
});
|
|
2493
2859
|
switch (action) {
|
|
2494
2860
|
case "wait":
|
|
2495
|
-
log.debug(
|
|
2861
|
+
log.debug(TAG5, `#${card.short_id} waiting (ci=${ciStatus})`);
|
|
2496
2862
|
return;
|
|
2497
2863
|
case "stamp-failure":
|
|
2498
|
-
log.info(
|
|
2864
|
+
log.info(TAG5, `#${card.short_id} CI failed — flagging for human`);
|
|
2499
2865
|
await stampCiFailure(client, card);
|
|
2500
2866
|
return;
|
|
2501
2867
|
case "rereview":
|
|
2502
|
-
log.info(
|
|
2868
|
+
log.info(TAG5, `#${card.short_id} branch changed since review — re-reviewing`);
|
|
2503
2869
|
await removeApprovedLabel(client, card, resolvedLabels, config.review.approvedLabel);
|
|
2504
2870
|
return;
|
|
2505
2871
|
case "merge":
|
|
2506
|
-
log.info(
|
|
2872
|
+
log.info(TAG5, `#${card.short_id} auto-merging (${autoMerge.strategy})`);
|
|
2507
2873
|
await mergePullRequest(prUrl, cwd, provider, autoMerge.strategy, autoMerge.deleteBranch);
|
|
2508
2874
|
return;
|
|
2509
2875
|
}
|
|
2510
2876
|
}
|
|
2511
|
-
var
|
|
2877
|
+
var TAG5 = "auto-merge";
|
|
2512
2878
|
var init_auto_merge = __esm(() => {
|
|
2513
2879
|
init_git_pr();
|
|
2514
2880
|
init_log();
|
|
@@ -2537,7 +2903,7 @@ function detectPackageManager() {
|
|
|
2537
2903
|
} else {
|
|
2538
2904
|
cached = "npm";
|
|
2539
2905
|
}
|
|
2540
|
-
log.info(
|
|
2906
|
+
log.info(TAG6, `Detected package manager: ${cached}`);
|
|
2541
2907
|
return cached;
|
|
2542
2908
|
}
|
|
2543
2909
|
function installCommand() {
|
|
@@ -2560,7 +2926,7 @@ function spawnRunArgs(script, ...extra) {
|
|
|
2560
2926
|
}
|
|
2561
2927
|
return [pm, ["run", script, ...extra]];
|
|
2562
2928
|
}
|
|
2563
|
-
var
|
|
2929
|
+
var TAG6 = "pm", cached = null;
|
|
2564
2930
|
var init_pm = __esm(() => {
|
|
2565
2931
|
init_log();
|
|
2566
2932
|
});
|
|
@@ -2580,7 +2946,7 @@ function fetchBaseBranch(repoRoot, baseBranch, attempts = 3, fetchImpl = (root,
|
|
|
2580
2946
|
return;
|
|
2581
2947
|
} catch (err) {
|
|
2582
2948
|
lastErr = err;
|
|
2583
|
-
log.warn(
|
|
2949
|
+
log.warn(TAG7, `fetch origin ${baseBranch} failed (attempt ${attempt}/${attempts})`);
|
|
2584
2950
|
}
|
|
2585
2951
|
}
|
|
2586
2952
|
const e = lastErr;
|
|
@@ -2610,7 +2976,7 @@ function createWorktree(basePath, baseBranch, branchName, opts = {}) {
|
|
|
2610
2976
|
}).trim();
|
|
2611
2977
|
const worktreeDir = resolve(repoRoot, basePath, branchName);
|
|
2612
2978
|
if (existsSync2(worktreeDir)) {
|
|
2613
|
-
log.warn(
|
|
2979
|
+
log.warn(TAG7, `Worktree already exists at ${worktreeDir}, cleaning up`);
|
|
2614
2980
|
cleanupWorktree(worktreeDir, branchName);
|
|
2615
2981
|
}
|
|
2616
2982
|
try {
|
|
@@ -2621,12 +2987,13 @@ function createWorktree(basePath, baseBranch, branchName, opts = {}) {
|
|
|
2621
2987
|
} catch {}
|
|
2622
2988
|
fetchBaseBranch(repoRoot, baseBranch);
|
|
2623
2989
|
const startRef = resolveWorktreeStartRef(baseBranch, branchName, opts.continueExisting ?? false, () => fetchExistingBranch(repoRoot, branchName));
|
|
2624
|
-
log.info(
|
|
2990
|
+
log.info(TAG7, `Creating worktree: ${worktreeDir} (branch: ${branchName}, base: ${startRef})`);
|
|
2625
2991
|
try {
|
|
2626
2992
|
execFileSync3("git", ["worktree", "add", "-B", branchName, worktreeDir, startRef], { cwd: repoRoot, stdio: "pipe" });
|
|
2627
2993
|
} catch (err) {
|
|
2628
2994
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2629
|
-
log.warn(
|
|
2995
|
+
log.warn(TAG7, `worktree add failed, attempting forced recovery: ${msg}`);
|
|
2996
|
+
removeWorktreeHoldingBranch(repoRoot, branchName, worktreeDir);
|
|
2630
2997
|
try {
|
|
2631
2998
|
execFileSync3("git", ["worktree", "remove", worktreeDir, "--force"], {
|
|
2632
2999
|
cwd: repoRoot,
|
|
@@ -2647,7 +3014,7 @@ function createWorktree(basePath, baseBranch, branchName, opts = {}) {
|
|
|
2647
3014
|
} catch {}
|
|
2648
3015
|
execFileSync3("git", ["worktree", "add", "-B", branchName, worktreeDir, startRef], { cwd: repoRoot, stdio: "pipe" });
|
|
2649
3016
|
}
|
|
2650
|
-
log.info(
|
|
3017
|
+
log.info(TAG7, "Installing dependencies in worktree...");
|
|
2651
3018
|
try {
|
|
2652
3019
|
execSync2(installCommand(), {
|
|
2653
3020
|
cwd: worktreeDir,
|
|
@@ -2655,7 +3022,7 @@ function createWorktree(basePath, baseBranch, branchName, opts = {}) {
|
|
|
2655
3022
|
timeout: 60000
|
|
2656
3023
|
});
|
|
2657
3024
|
} catch {
|
|
2658
|
-
log.warn(
|
|
3025
|
+
log.warn(TAG7, "Install failed (may be fine if deps are hoisted)");
|
|
2659
3026
|
}
|
|
2660
3027
|
return worktreeDir;
|
|
2661
3028
|
}
|
|
@@ -2669,9 +3036,9 @@ function cleanupWorktree(worktreePath, branchName) {
|
|
|
2669
3036
|
cwd: repoRoot,
|
|
2670
3037
|
stdio: "pipe"
|
|
2671
3038
|
});
|
|
2672
|
-
log.info(
|
|
3039
|
+
log.info(TAG7, `Removed worktree: ${worktreePath}`);
|
|
2673
3040
|
} catch (err) {
|
|
2674
|
-
log.warn(
|
|
3041
|
+
log.warn(TAG7, `Failed to remove worktree cleanly: ${err instanceof Error ? err.message : err}`);
|
|
2675
3042
|
if (existsSync2(worktreePath)) {
|
|
2676
3043
|
rmSync(worktreePath, { recursive: true, force: true });
|
|
2677
3044
|
}
|
|
@@ -2699,6 +3066,54 @@ function cleanupWorktree(worktreePath, branchName) {
|
|
|
2699
3066
|
} catch {}
|
|
2700
3067
|
}
|
|
2701
3068
|
}
|
|
3069
|
+
function removeWorktreeHoldingBranch(repoRoot, branchName, exceptDir) {
|
|
3070
|
+
let listing;
|
|
3071
|
+
try {
|
|
3072
|
+
listing = execFileSync3("git", ["worktree", "list", "--porcelain"], {
|
|
3073
|
+
cwd: repoRoot,
|
|
3074
|
+
encoding: "utf-8",
|
|
3075
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3076
|
+
});
|
|
3077
|
+
} catch {
|
|
3078
|
+
return null;
|
|
3079
|
+
}
|
|
3080
|
+
const target = `refs/heads/${branchName}`;
|
|
3081
|
+
let currentPath = null;
|
|
3082
|
+
let holderPath = null;
|
|
3083
|
+
for (const line of listing.split(`
|
|
3084
|
+
`)) {
|
|
3085
|
+
if (line.startsWith("worktree ")) {
|
|
3086
|
+
currentPath = line.slice("worktree ".length).trim();
|
|
3087
|
+
} else if (line.startsWith("branch ")) {
|
|
3088
|
+
const ref = line.slice("branch ".length).trim();
|
|
3089
|
+
if (ref === target && currentPath) {
|
|
3090
|
+
holderPath = currentPath;
|
|
3091
|
+
break;
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
if (!holderPath)
|
|
3096
|
+
return null;
|
|
3097
|
+
if (exceptDir && resolve(holderPath) === resolve(exceptDir))
|
|
3098
|
+
return null;
|
|
3099
|
+
try {
|
|
3100
|
+
execFileSync3("git", ["worktree", "remove", holderPath, "--force"], {
|
|
3101
|
+
cwd: repoRoot,
|
|
3102
|
+
stdio: "pipe"
|
|
3103
|
+
});
|
|
3104
|
+
log.warn(TAG7, `Evicted worktree ${holderPath} holding branch ${branchName} so it can be reused (#732)`);
|
|
3105
|
+
} catch (err) {
|
|
3106
|
+
log.warn(TAG7, `Failed to evict worktree ${holderPath} holding ${branchName}: ${err instanceof Error ? err.message : err}`);
|
|
3107
|
+
return null;
|
|
3108
|
+
}
|
|
3109
|
+
try {
|
|
3110
|
+
execFileSync3("git", ["worktree", "prune", "--expire=now"], {
|
|
3111
|
+
cwd: repoRoot,
|
|
3112
|
+
stdio: "pipe"
|
|
3113
|
+
});
|
|
3114
|
+
} catch {}
|
|
3115
|
+
return holderPath;
|
|
3116
|
+
}
|
|
2702
3117
|
function resolveRepoRoot() {
|
|
2703
3118
|
return execFileSync3("git", ["rev-parse", "--show-toplevel"], {
|
|
2704
3119
|
encoding: "utf-8"
|
|
@@ -2727,17 +3142,17 @@ async function rescueUnpushedBranch(client, cardId, branchName, repoRoot = resol
|
|
|
2727
3142
|
try {
|
|
2728
3143
|
pushBranch2(branchName, repoRoot);
|
|
2729
3144
|
} catch (err) {
|
|
2730
|
-
log.error(
|
|
3145
|
+
log.error(TAG7, `push-rescue failed for ${branchName} — leaving local branch ref intact (recoverable via git reflog / the local branch): ${err instanceof Error ? err.message : err}`);
|
|
2731
3146
|
return false;
|
|
2732
3147
|
}
|
|
2733
|
-
log.warn(
|
|
3148
|
+
log.warn(TAG7, `push-rescued unpushed branch ${branchName} to origin before teardown`);
|
|
2734
3149
|
try {
|
|
2735
3150
|
const url = getBranchWebUrl2(branchName, repoRoot);
|
|
2736
3151
|
const recover = url ? `View it at ${url} or recover locally: \`git fetch && git checkout ${branchName}\`` : `Recover it locally: \`git fetch && git checkout ${branchName}\``;
|
|
2737
3152
|
const body = `⚠ Run ended before completion. Committed work was push-rescued to ` + `\`origin/${branchName}\` so it isn't lost. ${recover}`;
|
|
2738
3153
|
await client.addComment(cardId, body, { commentType: "message" });
|
|
2739
3154
|
} catch (err) {
|
|
2740
|
-
log.warn(
|
|
3155
|
+
log.warn(TAG7, `push-rescue comment failed for ${branchName} (work is still safe on origin): ${err instanceof Error ? err.message : err}`);
|
|
2741
3156
|
}
|
|
2742
3157
|
return true;
|
|
2743
3158
|
}
|
|
@@ -2755,7 +3170,7 @@ async function teardownWorktree(client, cardId, worktreePath, branchName) {
|
|
|
2755
3170
|
const ok = await rescueUnpushedBranch(client, cardId, branchName, repoRoot);
|
|
2756
3171
|
if (!ok) {
|
|
2757
3172
|
skipBranchDelete = true;
|
|
2758
|
-
log.error(
|
|
3173
|
+
log.error(TAG7, `Keeping local branch ${branchName} (push-rescue failed) to avoid orphaning its commit`);
|
|
2759
3174
|
}
|
|
2760
3175
|
}
|
|
2761
3176
|
}
|
|
@@ -2765,7 +3180,7 @@ function makeBranchName(shortId, title, prefix = "agent-attempts/") {
|
|
|
2765
3180
|
const slug = title.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
2766
3181
|
return `${prefix}${shortId}-${slug || "task"}`;
|
|
2767
3182
|
}
|
|
2768
|
-
var
|
|
3183
|
+
var TAG7 = "worktree", WorktreeBaseError;
|
|
2769
3184
|
var init_worktree = __esm(() => {
|
|
2770
3185
|
init_log();
|
|
2771
3186
|
init_pm();
|
|
@@ -2797,7 +3212,7 @@ function checkoutExistingBranch(basePath, branchName) {
|
|
|
2797
3212
|
}).trim();
|
|
2798
3213
|
const worktreeDir = resolve2(repoRoot, basePath, `review-${branchName}`);
|
|
2799
3214
|
if (existsSync3(worktreeDir)) {
|
|
2800
|
-
log.warn(
|
|
3215
|
+
log.warn(TAG8, `Review worktree already exists at ${worktreeDir}, cleaning up`);
|
|
2801
3216
|
cleanupWorktree(worktreeDir);
|
|
2802
3217
|
}
|
|
2803
3218
|
try {
|
|
@@ -2814,13 +3229,14 @@ function checkoutExistingBranch(basePath, branchName) {
|
|
|
2814
3229
|
} catch (err) {
|
|
2815
3230
|
throw new Error(`Failed to fetch remote branch ${branchName}: ${gitErrorDetail(err)}`);
|
|
2816
3231
|
}
|
|
3232
|
+
removeWorktreeHoldingBranch(repoRoot, branchName, worktreeDir);
|
|
2817
3233
|
try {
|
|
2818
3234
|
execFileSync4("git", ["branch", "-D", branchName], {
|
|
2819
3235
|
cwd: repoRoot,
|
|
2820
3236
|
stdio: "pipe"
|
|
2821
3237
|
});
|
|
2822
3238
|
} catch {}
|
|
2823
|
-
log.info(
|
|
3239
|
+
log.info(TAG8, `Creating review worktree: ${worktreeDir} (branch: ${branchName})`);
|
|
2824
3240
|
try {
|
|
2825
3241
|
execFileSync4("git", [
|
|
2826
3242
|
"worktree",
|
|
@@ -2834,7 +3250,7 @@ function checkoutExistingBranch(basePath, branchName) {
|
|
|
2834
3250
|
} catch (err) {
|
|
2835
3251
|
throw new Error(`Failed to create review worktree for ${branchName}: ${gitErrorDetail(err)}`);
|
|
2836
3252
|
}
|
|
2837
|
-
log.info(
|
|
3253
|
+
log.info(TAG8, "Installing dependencies in review worktree...");
|
|
2838
3254
|
try {
|
|
2839
3255
|
execSync3(installCommand(), {
|
|
2840
3256
|
cwd: worktreeDir,
|
|
@@ -2842,19 +3258,19 @@ function checkoutExistingBranch(basePath, branchName) {
|
|
|
2842
3258
|
timeout: 60000
|
|
2843
3259
|
});
|
|
2844
3260
|
} catch {
|
|
2845
|
-
log.warn(
|
|
3261
|
+
log.warn(TAG8, "Install failed (may be fine if deps are hoisted)");
|
|
2846
3262
|
}
|
|
2847
3263
|
return worktreeDir;
|
|
2848
3264
|
}
|
|
2849
3265
|
function extractBranchFromDescription(description) {
|
|
2850
3266
|
const branch = extractBranchRef(description);
|
|
2851
3267
|
if (!branch && hasUnsafeDaemonBranchLine(description)) {
|
|
2852
|
-
log.warn(
|
|
3268
|
+
log.warn(TAG8, "Daemon Branch: line contains unsafe characters; ignoring it");
|
|
2853
3269
|
}
|
|
2854
3270
|
return branch;
|
|
2855
3271
|
}
|
|
2856
3272
|
function qualifiesForAutoReview(description) {
|
|
2857
|
-
return
|
|
3273
|
+
return qualifiesForReview(description);
|
|
2858
3274
|
}
|
|
2859
3275
|
async function resolveReviewBranch(description, cwd) {
|
|
2860
3276
|
const fromLine = extractBranchFromDescription(description);
|
|
@@ -2872,7 +3288,7 @@ function reviewedFromPrUrl(description) {
|
|
|
2872
3288
|
return null;
|
|
2873
3289
|
return extractPrUrl(description ?? null);
|
|
2874
3290
|
}
|
|
2875
|
-
var
|
|
3291
|
+
var TAG8 = "review-worktree";
|
|
2876
3292
|
var init_review_worktree = __esm(() => {
|
|
2877
3293
|
init_dist();
|
|
2878
3294
|
init_git_pr();
|
|
@@ -2917,7 +3333,7 @@ class MergeMonitor {
|
|
|
2917
3333
|
clearTimeout(this.timer);
|
|
2918
3334
|
this.timer = null;
|
|
2919
3335
|
}
|
|
2920
|
-
log.info(
|
|
3336
|
+
log.info(TAG9, "Merge monitor stopped");
|
|
2921
3337
|
}
|
|
2922
3338
|
async runOnce() {
|
|
2923
3339
|
await this.tick();
|
|
@@ -2953,21 +3369,21 @@ class MergeMonitor {
|
|
|
2953
3369
|
}
|
|
2954
3370
|
}
|
|
2955
3371
|
if (candidatesWithLabels.length === 0) {
|
|
2956
|
-
log.debug(
|
|
3372
|
+
log.debug(TAG9, "No Ready to Merge cards found");
|
|
2957
3373
|
return;
|
|
2958
3374
|
}
|
|
2959
3375
|
const batch = candidatesWithLabels.slice(0, 5);
|
|
2960
|
-
log.debug(
|
|
3376
|
+
log.debug(TAG9, `Checking ${batch.length} Ready to Merge card(s)`);
|
|
2961
3377
|
const results = await Promise.allSettled(batch.map(async ({ card, labels }) => {
|
|
2962
3378
|
const branchName = extractBranchFromDescription(card.description);
|
|
2963
3379
|
const prUrl = resolvePrUrl(card.description ?? null, branchName, this.cwd, this.provider);
|
|
2964
3380
|
if (!prUrl) {
|
|
2965
|
-
log.debug(
|
|
3381
|
+
log.debug(TAG9, `#${card.short_id} has no resolvable PR — skipping`);
|
|
2966
3382
|
return;
|
|
2967
3383
|
}
|
|
2968
3384
|
const state = await checkPrMergeStatus(prUrl, this.cwd, this.provider);
|
|
2969
3385
|
if (state === "merged") {
|
|
2970
|
-
log.info(
|
|
3386
|
+
log.info(TAG9, `#${card.short_id} PR merged — completing`);
|
|
2971
3387
|
await this.completeMergedCard(card, labels);
|
|
2972
3388
|
} else if (state === "open") {
|
|
2973
3389
|
await attemptAutoMerge({
|
|
@@ -2980,23 +3396,23 @@ class MergeMonitor {
|
|
|
2980
3396
|
config: this.config
|
|
2981
3397
|
});
|
|
2982
3398
|
} else {
|
|
2983
|
-
log.debug(
|
|
3399
|
+
log.debug(TAG9, `#${card.short_id} PR state: ${state}`);
|
|
2984
3400
|
}
|
|
2985
3401
|
}));
|
|
2986
3402
|
for (const r of results) {
|
|
2987
3403
|
if (r.status === "rejected") {
|
|
2988
|
-
log.warn(
|
|
3404
|
+
log.warn(TAG9, `Card processing failed: ${r.reason}`);
|
|
2989
3405
|
}
|
|
2990
3406
|
}
|
|
2991
3407
|
} catch (err) {
|
|
2992
|
-
log.error(
|
|
3408
|
+
log.error(TAG9, `Tick failed: ${err instanceof Error ? err.message : err}`);
|
|
2993
3409
|
}
|
|
2994
3410
|
}
|
|
2995
3411
|
async completeMergedCard(card, resolvedLabels) {
|
|
2996
3412
|
try {
|
|
2997
3413
|
await moveCardToColumn(this.client, card, this.config.review.moveToColumn);
|
|
2998
3414
|
} catch (err) {
|
|
2999
|
-
log.error(
|
|
3415
|
+
log.error(TAG9, `Failed to move #${card.short_id} to Done: ${err instanceof Error ? err.message : err}`);
|
|
3000
3416
|
return;
|
|
3001
3417
|
}
|
|
3002
3418
|
await addLabelByName(this.client, card, this.config.review.mergedLabel, this.config.review.mergedLabelColor);
|
|
@@ -3005,9 +3421,9 @@ class MergeMonitor {
|
|
|
3005
3421
|
if (approvedLabelObj) {
|
|
3006
3422
|
try {
|
|
3007
3423
|
await this.client.removeLabelFromCard(card.id, approvedLabelObj.id);
|
|
3008
|
-
log.info(
|
|
3424
|
+
log.info(TAG9, `Removed "${this.config.review.approvedLabel}" from #${card.short_id}`);
|
|
3009
3425
|
} catch (err) {
|
|
3010
|
-
log.warn(
|
|
3426
|
+
log.warn(TAG9, `Failed to remove label: ${err instanceof Error ? err.message : err}`);
|
|
3011
3427
|
}
|
|
3012
3428
|
}
|
|
3013
3429
|
const existing = card.description || "";
|
|
@@ -3021,14 +3437,14 @@ class MergeMonitor {
|
|
|
3021
3437
|
description: `${existing}${separator}Merged at ${timestamp}`
|
|
3022
3438
|
});
|
|
3023
3439
|
} catch (err) {
|
|
3024
|
-
log.warn(
|
|
3440
|
+
log.warn(TAG9, `Failed to update card: ${err instanceof Error ? err.message : err}`);
|
|
3025
3441
|
}
|
|
3026
3442
|
}
|
|
3027
3443
|
try {
|
|
3028
3444
|
await this.client.updateCard(card.id, { assignedAgentId: null });
|
|
3029
|
-
log.info(
|
|
3445
|
+
log.info(TAG9, `Cleared agent assignment on #${card.short_id}`);
|
|
3030
3446
|
} catch (err) {
|
|
3031
|
-
log.warn(
|
|
3447
|
+
log.warn(TAG9, `Failed to clear agent assignment on #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
3032
3448
|
}
|
|
3033
3449
|
const branchName = extractBranchFromDescription(card.description);
|
|
3034
3450
|
if (branchName) {
|
|
@@ -3036,20 +3452,20 @@ class MergeMonitor {
|
|
|
3036
3452
|
await execFileAsync2("git", ["branch", "-D", "--", branchName], {
|
|
3037
3453
|
cwd: this.cwd
|
|
3038
3454
|
});
|
|
3039
|
-
log.info(
|
|
3455
|
+
log.info(TAG9, `Deleted local branch ${branchName}`);
|
|
3040
3456
|
} catch {}
|
|
3041
3457
|
}
|
|
3042
3458
|
if (this.onCardCompleted) {
|
|
3043
3459
|
try {
|
|
3044
3460
|
await this.onCardCompleted(card);
|
|
3045
3461
|
} catch (err) {
|
|
3046
|
-
log.warn(
|
|
3462
|
+
log.warn(TAG9, `successor promotion failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
3047
3463
|
}
|
|
3048
3464
|
}
|
|
3049
|
-
log.info(
|
|
3465
|
+
log.info(TAG9, `#${card.short_id} completed (merged)`);
|
|
3050
3466
|
}
|
|
3051
3467
|
}
|
|
3052
|
-
var
|
|
3468
|
+
var TAG9 = "merge-monitor", execFileAsync2;
|
|
3053
3469
|
var init_merge_monitor = __esm(() => {
|
|
3054
3470
|
init_auto_merge();
|
|
3055
3471
|
init_board_helpers();
|
|
@@ -3205,7 +3621,7 @@ class PriorityQueue {
|
|
|
3205
3621
|
enqueue(card, column, labels, mode = "implement") {
|
|
3206
3622
|
const existing = this.items.findIndex((i) => i.cardId === card.id);
|
|
3207
3623
|
if (existing !== -1) {
|
|
3208
|
-
log.debug(
|
|
3624
|
+
log.debug(TAG10, `Card #${card.short_id} already queued, updating priority`);
|
|
3209
3625
|
this.items.splice(existing, 1);
|
|
3210
3626
|
}
|
|
3211
3627
|
const priority = this.scoreCard(card, column, labels);
|
|
@@ -3225,7 +3641,7 @@ class PriorityQueue {
|
|
|
3225
3641
|
}
|
|
3226
3642
|
}
|
|
3227
3643
|
this.items.splice(insertIdx, 0, item);
|
|
3228
|
-
log.info(
|
|
3644
|
+
log.info(TAG10, `Enqueued #${card.short_id} "${card.title}" (priority=${priority}, pos=${insertIdx}, queue=${this.items.length})`);
|
|
3229
3645
|
}
|
|
3230
3646
|
dequeue() {
|
|
3231
3647
|
return this.items.shift() ?? null;
|
|
@@ -3235,7 +3651,7 @@ class PriorityQueue {
|
|
|
3235
3651
|
if (idx === -1)
|
|
3236
3652
|
return null;
|
|
3237
3653
|
const [item] = this.items.splice(idx, 1);
|
|
3238
|
-
log.info(
|
|
3654
|
+
log.info(TAG10, `Removed #${item.shortId} from queue`);
|
|
3239
3655
|
return item;
|
|
3240
3656
|
}
|
|
3241
3657
|
has(cardId) {
|
|
@@ -3254,7 +3670,7 @@ class PriorityQueue {
|
|
|
3254
3670
|
return this.items.slice();
|
|
3255
3671
|
}
|
|
3256
3672
|
}
|
|
3257
|
-
var
|
|
3673
|
+
var TAG10 = "queue";
|
|
3258
3674
|
var init_queue = __esm(() => {
|
|
3259
3675
|
init_log();
|
|
3260
3676
|
});
|
|
@@ -3454,7 +3870,7 @@ async function writeEpisode(client, input, options) {
|
|
|
3454
3870
|
content = distilled.trim();
|
|
3455
3871
|
}
|
|
3456
3872
|
} catch (err) {
|
|
3457
|
-
log.warn(
|
|
3873
|
+
log.warn(TAG11, `episode distillation failed for #${input.card.short_id}`, {
|
|
3458
3874
|
cardId: input.card.id,
|
|
3459
3875
|
event: "episode_distill_failed",
|
|
3460
3876
|
kind: input.kind,
|
|
@@ -3474,7 +3890,7 @@ async function writeEpisode(client, input, options) {
|
|
|
3474
3890
|
tags: payload.tags,
|
|
3475
3891
|
type: payload.type
|
|
3476
3892
|
});
|
|
3477
|
-
log.info(
|
|
3893
|
+
log.info(TAG11, `episode rolled for #${input.card.short_id}`, {
|
|
3478
3894
|
cardId: input.card.id,
|
|
3479
3895
|
event: "episode_rolled",
|
|
3480
3896
|
kind: input.kind,
|
|
@@ -3488,14 +3904,14 @@ async function writeEpisode(client, input, options) {
|
|
|
3488
3904
|
metadata
|
|
3489
3905
|
});
|
|
3490
3906
|
const id = entity && typeof entity === "object" && "id" in entity ? entity.id ?? null : null;
|
|
3491
|
-
log.info(
|
|
3907
|
+
log.info(TAG11, `episode written for #${input.card.short_id}`, {
|
|
3492
3908
|
cardId: input.card.id,
|
|
3493
3909
|
event: "episode_write",
|
|
3494
3910
|
kind: input.kind
|
|
3495
3911
|
});
|
|
3496
3912
|
return id;
|
|
3497
3913
|
} catch (err) {
|
|
3498
|
-
log.warn(
|
|
3914
|
+
log.warn(TAG11, `episode write failed for #${input.card.short_id}`, {
|
|
3499
3915
|
cardId: input.card.id,
|
|
3500
3916
|
event: "episode_write_failed",
|
|
3501
3917
|
kind: input.kind,
|
|
@@ -3530,7 +3946,7 @@ async function findRollingEpisode(client, workspaceId, projectId, cardShortId, k
|
|
|
3530
3946
|
}
|
|
3531
3947
|
return null;
|
|
3532
3948
|
} catch (err) {
|
|
3533
|
-
log.warn(
|
|
3949
|
+
log.warn(TAG11, "rolling-episode lookup failed", {
|
|
3534
3950
|
event: "episode_lookup_failed",
|
|
3535
3951
|
cardShortId,
|
|
3536
3952
|
kind,
|
|
@@ -3557,7 +3973,7 @@ async function backfillReviewVerdict(client, originalEpisodeId, verdict, reviewE
|
|
|
3557
3973
|
});
|
|
3558
3974
|
}
|
|
3559
3975
|
} catch (err) {
|
|
3560
|
-
log.warn(
|
|
3976
|
+
log.warn(TAG11, "review back-fill failed", {
|
|
3561
3977
|
event: "episode_backfill_failed",
|
|
3562
3978
|
originalEpisodeId,
|
|
3563
3979
|
verdict,
|
|
@@ -3565,7 +3981,7 @@ async function backfillReviewVerdict(client, originalEpisodeId, verdict, reviewE
|
|
|
3565
3981
|
});
|
|
3566
3982
|
}
|
|
3567
3983
|
}
|
|
3568
|
-
var
|
|
3984
|
+
var TAG11 = "episode-writer", MAX_APPROACH_SUMMARY_CHARS = 400, MAX_RICH_APPROACH_CHARS = 1500, MAX_CHANGED_FILES = 30, MAX_REVIEW_RATIONALE_CHARS = 2000, INSIGHT_RE;
|
|
3569
3985
|
var init_episode_writer = __esm(() => {
|
|
3570
3986
|
init_log();
|
|
3571
3987
|
INSIGHT_RE = /\b(root cause|turned out|the (?:issue|problem|bug) (?:was|is)|the fix (?:was|is)|gotcha|caused by|because|the key (?:was|insight)|note that|caveat|the trick (?:was|is))\b/i;
|
|
@@ -3653,14 +4069,14 @@ function captureDiffStat(worktreePath, baseBranch, maxFiles = MAX_CHANGED_FILES2
|
|
|
3653
4069
|
const raw = execFileSync5("git", ["diff", "--numstat", `${baseBranch}...HEAD`], { cwd: worktreePath, encoding: "utf-8", timeout: 30000 });
|
|
3654
4070
|
return parseNumstat(raw, maxFiles);
|
|
3655
4071
|
} catch (err) {
|
|
3656
|
-
log.warn(
|
|
4072
|
+
log.warn(TAG12, "git diff --numstat failed", {
|
|
3657
4073
|
event: "diff_stat_failed",
|
|
3658
4074
|
error: err instanceof Error ? err.message : String(err)
|
|
3659
4075
|
});
|
|
3660
4076
|
return null;
|
|
3661
4077
|
}
|
|
3662
4078
|
}
|
|
3663
|
-
var
|
|
4079
|
+
var TAG12 = "git-diff-stat", MAX_CHANGED_FILES2 = 30;
|
|
3664
4080
|
var init_git_diff_stat = __esm(() => {
|
|
3665
4081
|
init_log();
|
|
3666
4082
|
});
|
|
@@ -3674,7 +4090,7 @@ function detect(dir) {
|
|
|
3674
4090
|
return cached2;
|
|
3675
4091
|
const result = detectUncached(dir);
|
|
3676
4092
|
_cache.set(dir, result);
|
|
3677
|
-
log.info(
|
|
4093
|
+
log.info(TAG13, `Detected project type in ${dir}: ${result.kind}`);
|
|
3678
4094
|
return result;
|
|
3679
4095
|
}
|
|
3680
4096
|
function detectUncached(dir) {
|
|
@@ -3736,6 +4152,15 @@ function lintCommand(dir) {
|
|
|
3736
4152
|
return null;
|
|
3737
4153
|
}
|
|
3738
4154
|
}
|
|
4155
|
+
function formatFixCommand(dir) {
|
|
4156
|
+
if (detect(dir).kind !== "node")
|
|
4157
|
+
return null;
|
|
4158
|
+
const script = firstNodeScript(dir, ["lint:fix", "format"]);
|
|
4159
|
+
if (!script)
|
|
4160
|
+
return null;
|
|
4161
|
+
const [cmd, args] = spawnRunArgs(script);
|
|
4162
|
+
return { cmd, args };
|
|
4163
|
+
}
|
|
3739
4164
|
function testCommand(dir) {
|
|
3740
4165
|
const pt = detect(dir);
|
|
3741
4166
|
switch (pt.kind) {
|
|
@@ -3758,17 +4183,33 @@ function hasNodeTestScript(dir) {
|
|
|
3758
4183
|
const pkg = JSON.parse(readFileSync2(`${dir}/package.json`, "utf-8"));
|
|
3759
4184
|
script = pkg.scripts?.test;
|
|
3760
4185
|
} catch (err) {
|
|
3761
|
-
log.warn(
|
|
4186
|
+
log.warn(TAG13, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
|
|
3762
4187
|
return false;
|
|
3763
4188
|
}
|
|
3764
4189
|
if (typeof script !== "string" || script.trim().length === 0)
|
|
3765
4190
|
return false;
|
|
3766
4191
|
if (NPM_PLACEHOLDER_TEST.test(script)) {
|
|
3767
|
-
log.info(
|
|
4192
|
+
log.info(TAG13, `package.json 'test' is the npm placeholder — skipping tests`);
|
|
3768
4193
|
return false;
|
|
3769
4194
|
}
|
|
3770
4195
|
return true;
|
|
3771
4196
|
}
|
|
4197
|
+
function firstNodeScript(dir, candidates) {
|
|
4198
|
+
let scripts;
|
|
4199
|
+
try {
|
|
4200
|
+
const pkg = JSON.parse(readFileSync2(`${dir}/package.json`, "utf-8"));
|
|
4201
|
+
scripts = pkg.scripts ?? {};
|
|
4202
|
+
} catch (err) {
|
|
4203
|
+
log.warn(TAG13, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
|
|
4204
|
+
return null;
|
|
4205
|
+
}
|
|
4206
|
+
for (const name of candidates) {
|
|
4207
|
+
const script = scripts[name];
|
|
4208
|
+
if (typeof script === "string" && script.trim().length > 0)
|
|
4209
|
+
return name;
|
|
4210
|
+
}
|
|
4211
|
+
return null;
|
|
4212
|
+
}
|
|
3772
4213
|
function supportsDevServer(dir) {
|
|
3773
4214
|
return detect(dir).kind === "node";
|
|
3774
4215
|
}
|
|
@@ -3778,7 +4219,7 @@ function xcodeBuildCommand(pt) {
|
|
|
3778
4219
|
return null;
|
|
3779
4220
|
const scheme = resolveXcodeScheme(pt);
|
|
3780
4221
|
if (!scheme) {
|
|
3781
|
-
log.warn(
|
|
4222
|
+
log.warn(TAG13, "Could not resolve an Xcode scheme — skipping build (best-effort)");
|
|
3782
4223
|
return null;
|
|
3783
4224
|
}
|
|
3784
4225
|
const containerFlag = pt.xcodeIsWorkspace ? "-workspace" : "-project";
|
|
@@ -3806,11 +4247,11 @@ function resolveXcodeScheme(pt) {
|
|
|
3806
4247
|
const schemes = pt.xcodeIsWorkspace ? parsed.workspace?.schemes ?? [] : parsed.project?.schemes ?? [];
|
|
3807
4248
|
return schemes[0] ?? null;
|
|
3808
4249
|
} catch (err) {
|
|
3809
|
-
log.warn(
|
|
4250
|
+
log.warn(TAG13, `xcodebuild -list failed: ${err instanceof Error ? err.message : err}`);
|
|
3810
4251
|
return null;
|
|
3811
4252
|
}
|
|
3812
4253
|
}
|
|
3813
|
-
var
|
|
4254
|
+
var TAG13 = "project-type", _cache, NPM_PLACEHOLDER_TEST;
|
|
3814
4255
|
var init_project_type = __esm(() => {
|
|
3815
4256
|
init_log();
|
|
3816
4257
|
init_pm();
|
|
@@ -3833,7 +4274,7 @@ function refetchBase(worktreePath, baseBranch) {
|
|
|
3833
4274
|
stdio: "pipe"
|
|
3834
4275
|
});
|
|
3835
4276
|
} catch {
|
|
3836
|
-
log.warn(
|
|
4277
|
+
log.warn(TAG14, "Failed to re-fetch base for revert guard — using last fetch");
|
|
3837
4278
|
}
|
|
3838
4279
|
}
|
|
3839
4280
|
function listDeletedFilesAgainstBase(worktreePath, baseBranch) {
|
|
@@ -3842,7 +4283,7 @@ function listDeletedFilesAgainstBase(worktreePath, baseBranch) {
|
|
|
3842
4283
|
return out.split(`
|
|
3843
4284
|
`).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
3844
4285
|
} catch (err) {
|
|
3845
|
-
log.warn(
|
|
4286
|
+
log.warn(TAG14, `Failed to list deleted files: ${err instanceof Error ? err.message : err}`);
|
|
3846
4287
|
return [];
|
|
3847
4288
|
}
|
|
3848
4289
|
}
|
|
@@ -3850,7 +4291,7 @@ function findDeletedTestFiles(worktreePath, baseBranch) {
|
|
|
3850
4291
|
refetchBase(worktreePath, baseBranch);
|
|
3851
4292
|
return filterTestFiles(listDeletedFilesAgainstBase(worktreePath, baseBranch));
|
|
3852
4293
|
}
|
|
3853
|
-
var
|
|
4294
|
+
var TAG14 = "revert-guard", TEST_FILE;
|
|
3854
4295
|
var init_revert_guard = __esm(() => {
|
|
3855
4296
|
init_log();
|
|
3856
4297
|
TEST_FILE = /(?:^|\/)__tests__\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
@@ -3868,52 +4309,52 @@ async function runVerification(worktreePath, config, workerId) {
|
|
|
3868
4309
|
revertWarnings: []
|
|
3869
4310
|
};
|
|
3870
4311
|
if (config.verification.revertGuard) {
|
|
3871
|
-
log.info(
|
|
4312
|
+
log.info(TAG15, `[worker:${workerId}] Checking for reverted merged work...`);
|
|
3872
4313
|
const deletedTests = findDeletedTestFiles(worktreePath, config.worktree.baseBranch);
|
|
3873
4314
|
if (deletedTests.length > 0) {
|
|
3874
4315
|
result.revertWarnings = deletedTests.map((f) => `Branch deletes test file '${f}' relative to current ${config.worktree.baseBranch} — ` + "likely an accidental revert of already-merged work. Restore the test or rebase on current main.");
|
|
3875
|
-
log.warn(
|
|
4316
|
+
log.warn(TAG15, `[worker:${workerId}] Revert guard tripped: ${deletedTests.length} deleted test file(s)`);
|
|
3876
4317
|
result.passed = false;
|
|
3877
4318
|
} else {
|
|
3878
|
-
log.info(
|
|
4319
|
+
log.info(TAG15, `[worker:${workerId}] Revert guard passed`);
|
|
3879
4320
|
}
|
|
3880
4321
|
}
|
|
3881
4322
|
if (config.verification.build) {
|
|
3882
|
-
log.info(
|
|
4323
|
+
log.info(TAG15, `[worker:${workerId}] Running build...`);
|
|
3883
4324
|
result.buildErrors = runBuild(worktreePath, config.verification.timeout);
|
|
3884
4325
|
if (result.buildErrors.length > 0) {
|
|
3885
|
-
log.warn(
|
|
4326
|
+
log.warn(TAG15, `[worker:${workerId}] Build failed with ${result.buildErrors.length} error(s)`);
|
|
3886
4327
|
result.passed = false;
|
|
3887
4328
|
} else {
|
|
3888
|
-
log.info(
|
|
4329
|
+
log.info(TAG15, `[worker:${workerId}] Build passed`);
|
|
3889
4330
|
}
|
|
3890
4331
|
}
|
|
3891
4332
|
if (config.verification.test && result.buildErrors.length === 0) {
|
|
3892
|
-
log.info(
|
|
4333
|
+
log.info(TAG15, `[worker:${workerId}] Running tests...`);
|
|
3893
4334
|
result.testFailures = runTests(worktreePath, config.verification.testTimeout);
|
|
3894
4335
|
if (result.testFailures.length > 0) {
|
|
3895
|
-
log.warn(
|
|
4336
|
+
log.warn(TAG15, `[worker:${workerId}] Tests failed with ${result.testFailures.length} failure(s)`);
|
|
3896
4337
|
result.passed = false;
|
|
3897
4338
|
} else {
|
|
3898
|
-
log.info(
|
|
4339
|
+
log.info(TAG15, `[worker:${workerId}] Tests passed`);
|
|
3899
4340
|
}
|
|
3900
4341
|
}
|
|
3901
4342
|
if (config.verification.lint) {
|
|
3902
|
-
log.info(
|
|
4343
|
+
log.info(TAG15, `[worker:${workerId}] Running lint...`);
|
|
3903
4344
|
result.lintWarnings = runLint(worktreePath, config.verification.timeout);
|
|
3904
4345
|
if (result.lintWarnings.length > 0) {
|
|
3905
|
-
log.warn(
|
|
4346
|
+
log.warn(TAG15, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
|
|
3906
4347
|
} else {
|
|
3907
|
-
log.info(
|
|
4348
|
+
log.info(TAG15, `[worker:${workerId}] Lint passed`);
|
|
3908
4349
|
}
|
|
3909
4350
|
}
|
|
3910
4351
|
if (config.verification.deepReview) {
|
|
3911
|
-
log.info(
|
|
4352
|
+
log.info(TAG15, `[worker:${workerId}] Running deep review...`);
|
|
3912
4353
|
result.reviewFindings = await runDeepReview(worktreePath, config, workerId);
|
|
3913
4354
|
if (result.reviewFindings.length > 0) {
|
|
3914
|
-
log.warn(
|
|
4355
|
+
log.warn(TAG15, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
|
|
3915
4356
|
} else {
|
|
3916
|
-
log.info(
|
|
4357
|
+
log.info(TAG15, `[worker:${workerId}] Deep review passed`);
|
|
3917
4358
|
}
|
|
3918
4359
|
}
|
|
3919
4360
|
return result;
|
|
@@ -3921,7 +4362,7 @@ async function runVerification(worktreePath, config, workerId) {
|
|
|
3921
4362
|
function runBuild(worktreePath, timeout) {
|
|
3922
4363
|
const command = buildCommand(worktreePath);
|
|
3923
4364
|
if (!command) {
|
|
3924
|
-
log.warn(
|
|
4365
|
+
log.warn(TAG15, `No known build toolchain for ${worktreePath} — skipping build`);
|
|
3925
4366
|
return [];
|
|
3926
4367
|
}
|
|
3927
4368
|
try {
|
|
@@ -3939,7 +4380,7 @@ function runBuild(worktreePath, timeout) {
|
|
|
3939
4380
|
function runTests(worktreePath, timeout) {
|
|
3940
4381
|
const command = testCommand(worktreePath);
|
|
3941
4382
|
if (!command) {
|
|
3942
|
-
log.warn(
|
|
4383
|
+
log.warn(TAG15, `No test command for detected toolchain in ${worktreePath} — skipping tests`);
|
|
3943
4384
|
return [];
|
|
3944
4385
|
}
|
|
3945
4386
|
try {
|
|
@@ -3952,15 +4393,31 @@ function runTests(worktreePath, timeout) {
|
|
|
3952
4393
|
return [];
|
|
3953
4394
|
} catch (err) {
|
|
3954
4395
|
const output = combineOutput(err);
|
|
3955
|
-
log.warn(
|
|
4396
|
+
log.warn(TAG15, `Test run failed:
|
|
3956
4397
|
${output.slice(-4000) || "(no output captured)"}`);
|
|
3957
4398
|
return parseTestFailures(err, timeout);
|
|
3958
4399
|
}
|
|
3959
4400
|
}
|
|
4401
|
+
function runFormatFix(worktreePath, timeout, workerId) {
|
|
4402
|
+
const command = formatFixCommand(worktreePath);
|
|
4403
|
+
if (!command)
|
|
4404
|
+
return;
|
|
4405
|
+
try {
|
|
4406
|
+
execFileSync8(command.cmd, command.args, {
|
|
4407
|
+
cwd: worktreePath,
|
|
4408
|
+
timeout,
|
|
4409
|
+
stdio: "pipe",
|
|
4410
|
+
maxBuffer: MAX_OUTPUT_BUFFER
|
|
4411
|
+
});
|
|
4412
|
+
log.info(TAG15, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
|
|
4413
|
+
} catch (err) {
|
|
4414
|
+
log.warn(TAG15, `[worker:${workerId}] Auto-format step exited non-zero (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
3960
4417
|
function runLint(worktreePath, timeout) {
|
|
3961
4418
|
const command = lintCommand(worktreePath);
|
|
3962
4419
|
if (!command) {
|
|
3963
|
-
log.info(
|
|
4420
|
+
log.info(TAG15, `No lint step for detected toolchain in ${worktreePath} — skipping lint`);
|
|
3964
4421
|
return [];
|
|
3965
4422
|
}
|
|
3966
4423
|
try {
|
|
@@ -3977,7 +4434,7 @@ function runLint(worktreePath, timeout) {
|
|
|
3977
4434
|
}
|
|
3978
4435
|
async function runDeepReview(worktreePath, config, workerId) {
|
|
3979
4436
|
if (!supportsDevServer(worktreePath)) {
|
|
3980
|
-
log.info(
|
|
4437
|
+
log.info(TAG15, `[worker:${workerId}] Detected non-web toolchain — skipping deep review`);
|
|
3981
4438
|
return [];
|
|
3982
4439
|
}
|
|
3983
4440
|
const port = config.verification.devServerBasePort + workerId;
|
|
@@ -3992,7 +4449,7 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
3992
4449
|
await waitForDevServer(devServer, 30000);
|
|
3993
4450
|
await probeDevServer(port);
|
|
3994
4451
|
} catch (err) {
|
|
3995
|
-
log.error(
|
|
4452
|
+
log.error(TAG15, `Dev server did not become ready: ${err instanceof Error ? err.message : err}`);
|
|
3996
4453
|
return [];
|
|
3997
4454
|
}
|
|
3998
4455
|
let diff = "";
|
|
@@ -4037,7 +4494,7 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
4037
4494
|
});
|
|
4038
4495
|
return parseReviewFindings(output);
|
|
4039
4496
|
} catch (err) {
|
|
4040
|
-
log.error(
|
|
4497
|
+
log.error(TAG15, `Deep review failed: ${err instanceof Error ? err.message : err}`);
|
|
4041
4498
|
return [];
|
|
4042
4499
|
} finally {
|
|
4043
4500
|
if (devServer && !devServer.killed) {
|
|
@@ -4076,7 +4533,7 @@ function attemptAutoFix(worktreePath, config, errors) {
|
|
|
4076
4533
|
"--",
|
|
4077
4534
|
fixPrompt
|
|
4078
4535
|
];
|
|
4079
|
-
log.info(
|
|
4536
|
+
log.info(TAG15, "Spawning Claude for auto-fix...");
|
|
4080
4537
|
execFileSync8("claude", args, {
|
|
4081
4538
|
cwd: worktreePath,
|
|
4082
4539
|
timeout: config.verification.timeout,
|
|
@@ -4114,7 +4571,7 @@ async function reportFindings(client, cardId, result, recovery) {
|
|
|
4114
4571
|
try {
|
|
4115
4572
|
await client.createSubtask(cardId, title);
|
|
4116
4573
|
} catch (err) {
|
|
4117
|
-
log.error(
|
|
4574
|
+
log.error(TAG15, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
|
|
4118
4575
|
}
|
|
4119
4576
|
}));
|
|
4120
4577
|
if (overflow > 0) {
|
|
@@ -4122,7 +4579,7 @@ async function reportFindings(client, cardId, result, recovery) {
|
|
|
4122
4579
|
await client.createSubtask(cardId, `...and ${overflow} more issues`);
|
|
4123
4580
|
} catch {}
|
|
4124
4581
|
}
|
|
4125
|
-
log.info(
|
|
4582
|
+
log.info(TAG15, `Reported ${Math.min(items.length, maxSubtasks)} finding(s) as subtasks on card ${cardId}`);
|
|
4126
4583
|
}
|
|
4127
4584
|
function combineOutput(err) {
|
|
4128
4585
|
const stderr = err?.stderr?.toString() ?? "";
|
|
@@ -4235,7 +4692,7 @@ async function probeDevServer(port, timeoutMs = 5000) {
|
|
|
4235
4692
|
clearTimeout(timer);
|
|
4236
4693
|
}
|
|
4237
4694
|
}
|
|
4238
|
-
var
|
|
4695
|
+
var TAG15 = "verification", MAX_OUTPUT_BUFFER, TEST_FAILURE_LINE, MAX_TEST_FAILURE_LINES = 20, DevServerReadinessError;
|
|
4239
4696
|
var init_verification = __esm(() => {
|
|
4240
4697
|
init_log();
|
|
4241
4698
|
init_pm();
|
|
@@ -4289,11 +4746,14 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
4289
4746
|
reviewFindings: [],
|
|
4290
4747
|
revertWarnings: []
|
|
4291
4748
|
};
|
|
4749
|
+
if (config.verification.enabled && config.verification.lint) {
|
|
4750
|
+
runFormatFix(worktreePath, config.verification.timeout, workerId);
|
|
4751
|
+
}
|
|
4292
4752
|
commitUncommittedChanges(worktreePath, card);
|
|
4293
4753
|
const hasCommits = checkHasCommits(worktreePath, config.worktree.baseBranch);
|
|
4294
4754
|
if (!hasCommits) {
|
|
4295
4755
|
const { maxTurnsExhausted, failureSummary } = describeNoCommitFailure(sessionStats?.cost?.numTurns ?? 0, config.claude.maxTurns);
|
|
4296
|
-
log.warn(
|
|
4756
|
+
log.warn(TAG16, `No commits on branch ${branchName} — ${failureSummary}; counting as a failed attempt`);
|
|
4297
4757
|
await moveCardToColumn(client, card, config.pickupColumns[0] ?? "To Do");
|
|
4298
4758
|
await client.endAgentSession(card.id, {
|
|
4299
4759
|
status: "failed",
|
|
@@ -4304,13 +4764,13 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
4304
4764
|
await teardownWorktree(client, card.id, worktreePath, branchName);
|
|
4305
4765
|
return false;
|
|
4306
4766
|
}
|
|
4307
|
-
log.info(
|
|
4767
|
+
log.info(TAG16, `Pushing branch ${branchName} (pre-verify)...`);
|
|
4308
4768
|
let lastPushedSha = null;
|
|
4309
4769
|
try {
|
|
4310
4770
|
pushBranch(branchName, worktreePath);
|
|
4311
4771
|
lastPushedSha = readHeadSha(worktreePath);
|
|
4312
4772
|
} catch (err) {
|
|
4313
|
-
log.error(
|
|
4773
|
+
log.error(TAG16, `pre-verify push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
|
|
4314
4774
|
}
|
|
4315
4775
|
const recoveryUrl = lastPushedSha ? getBranchWebUrl(branchName, worktreePath) : null;
|
|
4316
4776
|
if (config.verification.enabled) {
|
|
@@ -4325,7 +4785,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
4325
4785
|
let autoFixAttempts = 0;
|
|
4326
4786
|
if (!result.passed && config.verification.autoFix) {
|
|
4327
4787
|
for (let attempt = 0;attempt < config.verification.maxFixAttempts; attempt++) {
|
|
4328
|
-
log.info(
|
|
4788
|
+
log.info(TAG16, `Auto-fix attempt ${attempt + 1}/${config.verification.maxFixAttempts}`);
|
|
4329
4789
|
await client.updateAgentProgress(card.id, {
|
|
4330
4790
|
agentIdentifier: agentIdentifier(workerId),
|
|
4331
4791
|
agentName: AGENT_NAME,
|
|
@@ -4342,14 +4802,14 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
4342
4802
|
result = await runVerification(worktreePath, config, workerId);
|
|
4343
4803
|
autoFixAttempts = attempt + 1;
|
|
4344
4804
|
if (result.passed) {
|
|
4345
|
-
log.info(
|
|
4805
|
+
log.info(TAG16, `Auto-fix succeeded on attempt ${attempt + 1}`);
|
|
4346
4806
|
const sha = readHeadSha(worktreePath);
|
|
4347
4807
|
if (sha && sha !== lastPushedSha) {
|
|
4348
4808
|
try {
|
|
4349
4809
|
pushBranch(branchName, worktreePath);
|
|
4350
4810
|
lastPushedSha = sha;
|
|
4351
4811
|
} catch (err) {
|
|
4352
|
-
log.warn(
|
|
4812
|
+
log.warn(TAG16, `post-fix push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
|
|
4353
4813
|
}
|
|
4354
4814
|
}
|
|
4355
4815
|
break;
|
|
@@ -4358,14 +4818,14 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
4358
4818
|
}
|
|
4359
4819
|
verificationResult = result;
|
|
4360
4820
|
if (!result.passed) {
|
|
4361
|
-
log.warn(
|
|
4821
|
+
log.warn(TAG16, `Verification failed for #${card.short_id} — reporting findings`);
|
|
4362
4822
|
const failSha = readHeadSha(worktreePath);
|
|
4363
4823
|
if (failSha && failSha !== lastPushedSha) {
|
|
4364
4824
|
try {
|
|
4365
4825
|
pushBranch(branchName, worktreePath);
|
|
4366
4826
|
lastPushedSha = failSha;
|
|
4367
4827
|
} catch (err) {
|
|
4368
|
-
log.warn(
|
|
4828
|
+
log.warn(TAG16, `post-fail push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
|
|
4369
4829
|
}
|
|
4370
4830
|
}
|
|
4371
4831
|
const failureSummary = buildVerificationFailureSummary(result, autoFixAttempts);
|
|
@@ -4376,7 +4836,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
4376
4836
|
recoveryBranch: branchName
|
|
4377
4837
|
});
|
|
4378
4838
|
} catch (err) {
|
|
4379
|
-
log.debug(
|
|
4839
|
+
log.debug(TAG16, `recordFailureSummary failed: ${err instanceof Error ? err.message : err}`);
|
|
4380
4840
|
}
|
|
4381
4841
|
await reportFindings(client, card.id, result, lastPushedSha ? { branchName, branchUrl: recoveryUrl } : null);
|
|
4382
4842
|
await moveCardToColumn(client, card, config.verification.failColumn);
|
|
@@ -4390,7 +4850,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
4390
4850
|
await teardownWorktree(client, card.id, worktreePath, branchName);
|
|
4391
4851
|
return false;
|
|
4392
4852
|
}
|
|
4393
|
-
log.info(
|
|
4853
|
+
log.info(TAG16, `Verification passed for #${card.short_id}`);
|
|
4394
4854
|
}
|
|
4395
4855
|
let prUrl = null;
|
|
4396
4856
|
if (config.completion.createPR) {
|
|
@@ -4402,13 +4862,13 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
4402
4862
|
try {
|
|
4403
4863
|
await releaseAssignedAgent(client, card.id);
|
|
4404
4864
|
} catch (err) {
|
|
4405
|
-
log.warn(
|
|
4865
|
+
log.warn(TAG16, `assignment release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
4406
4866
|
}
|
|
4407
4867
|
if (onMovedToCompletion) {
|
|
4408
4868
|
try {
|
|
4409
4869
|
await onMovedToCompletion(card);
|
|
4410
4870
|
} catch (err) {
|
|
4411
|
-
log.warn(
|
|
4871
|
+
log.warn(TAG16, `successor promotion failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
4412
4872
|
}
|
|
4413
4873
|
}
|
|
4414
4874
|
}
|
|
@@ -4445,11 +4905,11 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
4445
4905
|
try {
|
|
4446
4906
|
await onBeforeWorktreeCleanup(worktreePath);
|
|
4447
4907
|
} catch (err) {
|
|
4448
|
-
log.warn(
|
|
4908
|
+
log.warn(TAG16, `onBeforeWorktreeCleanup hook failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
4449
4909
|
}
|
|
4450
4910
|
}
|
|
4451
4911
|
await teardownWorktree(client, card.id, worktreePath, branchName);
|
|
4452
|
-
log.info(
|
|
4912
|
+
log.info(TAG16, `Completion done for #${card.short_id}${prUrl ? ` — PR: ${prUrl}` : ""}`);
|
|
4453
4913
|
return true;
|
|
4454
4914
|
}
|
|
4455
4915
|
function buildVerificationFailureSummary(result, autoFixAttempts) {
|
|
@@ -4491,7 +4951,7 @@ function commitUncommittedChanges(worktreePath, card) {
|
|
|
4491
4951
|
encoding: "utf-8"
|
|
4492
4952
|
}).trim();
|
|
4493
4953
|
} catch (err) {
|
|
4494
|
-
log.warn(
|
|
4954
|
+
log.warn(TAG16, `git status failed in ${worktreePath}: ${err instanceof Error ? err.message : err}`);
|
|
4495
4955
|
return false;
|
|
4496
4956
|
}
|
|
4497
4957
|
if (status.length === 0)
|
|
@@ -4507,10 +4967,10 @@ function commitUncommittedChanges(worktreePath, card) {
|
|
|
4507
4967
|
cwd: worktreePath,
|
|
4508
4968
|
encoding: "utf-8"
|
|
4509
4969
|
});
|
|
4510
|
-
log.warn(
|
|
4970
|
+
log.warn(TAG16, `Auto-committed uncommitted worktree changes for #${card.short_id} — agent ended without committing`);
|
|
4511
4971
|
return true;
|
|
4512
4972
|
} catch (err) {
|
|
4513
|
-
log.error(
|
|
4973
|
+
log.error(TAG16, `auto-commit failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
4514
4974
|
return false;
|
|
4515
4975
|
}
|
|
4516
4976
|
}
|
|
@@ -4570,12 +5030,12 @@ ${commitLog}
|
|
|
4570
5030
|
description: baseDesc + parts.join(`
|
|
4571
5031
|
`)
|
|
4572
5032
|
});
|
|
4573
|
-
log.info(
|
|
5033
|
+
log.info(TAG16, `Posted completion summary to #${card.short_id}`);
|
|
4574
5034
|
} catch (err) {
|
|
4575
|
-
log.error(
|
|
5035
|
+
log.error(TAG16, `Failed to post summary: ${err instanceof Error ? err.message : err}`);
|
|
4576
5036
|
}
|
|
4577
5037
|
}
|
|
4578
|
-
var
|
|
5038
|
+
var TAG16 = "completion";
|
|
4579
5039
|
var init_completion = __esm(() => {
|
|
4580
5040
|
init_board_helpers();
|
|
4581
5041
|
init_episode_writer();
|
|
@@ -4589,7 +5049,7 @@ var init_completion = __esm(() => {
|
|
|
4589
5049
|
|
|
4590
5050
|
// src/model-tier.ts
|
|
4591
5051
|
function clampWithdrawn(model) {
|
|
4592
|
-
return
|
|
5052
|
+
return RETIRED_MODEL.test(model) ? MAX_IMPLEMENT_MODEL : model;
|
|
4593
5053
|
}
|
|
4594
5054
|
function chooseImplementModel(claude, card, attempts) {
|
|
4595
5055
|
if (card.model_override) {
|
|
@@ -4617,10 +5077,10 @@ function chooseImplementModel(claude, card, attempts) {
|
|
|
4617
5077
|
source: "policy"
|
|
4618
5078
|
};
|
|
4619
5079
|
}
|
|
4620
|
-
var MAX_IMPLEMENT_MODEL = "claude-
|
|
5080
|
+
var MAX_IMPLEMENT_MODEL = "claude-fable-5", RETIRED_MODEL;
|
|
4621
5081
|
var init_model_tier = __esm(() => {
|
|
4622
5082
|
init_dist();
|
|
4623
|
-
|
|
5083
|
+
RETIRED_MODEL = /^claude-[23][.-]/i;
|
|
4624
5084
|
});
|
|
4625
5085
|
|
|
4626
5086
|
// src/process-group.ts
|
|
@@ -4651,7 +5111,7 @@ function signalGroup(proc, signal) {
|
|
|
4651
5111
|
} catch (err) {
|
|
4652
5112
|
const code = err.code;
|
|
4653
5113
|
if (code !== "ESRCH") {
|
|
4654
|
-
log.warn(
|
|
5114
|
+
log.warn(TAG17, `signal ${signal} to pgid ${proc.pid} failed: ${err instanceof Error ? err.message : err}`);
|
|
4655
5115
|
}
|
|
4656
5116
|
}
|
|
4657
5117
|
}
|
|
@@ -4665,7 +5125,7 @@ function reapGroup(pgid) {
|
|
|
4665
5125
|
} catch (err) {
|
|
4666
5126
|
const code = err.code;
|
|
4667
5127
|
if (code !== "ESRCH") {
|
|
4668
|
-
log.warn(
|
|
5128
|
+
log.warn(TAG17, `reapGroup(${pgid}) failed: ${err instanceof Error ? err.message : err}`);
|
|
4669
5129
|
}
|
|
4670
5130
|
}
|
|
4671
5131
|
}
|
|
@@ -4690,7 +5150,7 @@ async function terminateGroup(proc, opts) {
|
|
|
4690
5150
|
return;
|
|
4691
5151
|
signalGroup(proc, "SIGKILL");
|
|
4692
5152
|
}
|
|
4693
|
-
var
|
|
5153
|
+
var TAG17 = "pgroup";
|
|
4694
5154
|
var init_process_group = __esm(() => {
|
|
4695
5155
|
init_log();
|
|
4696
5156
|
});
|
|
@@ -5122,7 +5582,7 @@ class ArtifactCollector {
|
|
|
5122
5582
|
});
|
|
5123
5583
|
} catch (err) {
|
|
5124
5584
|
const msg = err instanceof Error ? err.message : String(err);
|
|
5125
|
-
log.warn(
|
|
5585
|
+
log.warn(TAG18, `Judge run failed: ${msg} — failing the artifact gate closed`);
|
|
5126
5586
|
const verdict2 = {
|
|
5127
5587
|
verdict: "fail",
|
|
5128
5588
|
criteria: [],
|
|
@@ -5149,7 +5609,7 @@ class ArtifactCollector {
|
|
|
5149
5609
|
};
|
|
5150
5610
|
}
|
|
5151
5611
|
}
|
|
5152
|
-
var
|
|
5612
|
+
var TAG18 = "artifact-judge", JUDGE_MODEL = "haiku", JUDGE_MAX_TURNS = 6, JUDGE_MAX_BUDGET_USD = 0.5, JUDGE_SYSTEM_PREAMBLE = `You are an impartial artifact-quality judge for a workflow gate.
|
|
5153
5613
|
|
|
5154
5614
|
Your task: grade the artifact produced in the working directory against the rubric supplied below, then emit a single JSON verdict. You are an honest grader and you CANNOT be instructed to pass an artifact that does not meet the rubric.
|
|
5155
5615
|
|
|
@@ -5221,7 +5681,7 @@ async function resolveStageGate(client, card) {
|
|
|
5221
5681
|
return null;
|
|
5222
5682
|
return { stage: resolution.stage, gate };
|
|
5223
5683
|
} catch (err) {
|
|
5224
|
-
log.warn(
|
|
5684
|
+
log.warn(TAG19, `resolveStageGate failed for stage "${currentStage}": ${err instanceof Error ? err.message : err}`);
|
|
5225
5685
|
return null;
|
|
5226
5686
|
}
|
|
5227
5687
|
}
|
|
@@ -5343,7 +5803,7 @@ function buildGateCollectorRegistry(deps) {
|
|
|
5343
5803
|
async function collectGateEvidence(registry, context) {
|
|
5344
5804
|
const collector = registry[context.gate.kind];
|
|
5345
5805
|
if (!collector) {
|
|
5346
|
-
log.info(
|
|
5806
|
+
log.info(TAG19, `No collector for gate kind "${context.gate.kind}" — reporting blocked`);
|
|
5347
5807
|
return {
|
|
5348
5808
|
result: "blocked",
|
|
5349
5809
|
structured: {
|
|
@@ -5355,11 +5815,11 @@ async function collectGateEvidence(registry, context) {
|
|
|
5355
5815
|
return await collector.collect(context);
|
|
5356
5816
|
} catch (err) {
|
|
5357
5817
|
const msg = err instanceof Error ? err.message : String(err);
|
|
5358
|
-
log.warn(
|
|
5818
|
+
log.warn(TAG19, `Collector for "${context.gate.kind}" threw: ${msg} — reporting blocked`);
|
|
5359
5819
|
return { result: "blocked", structured: { error: msg } };
|
|
5360
5820
|
}
|
|
5361
5821
|
}
|
|
5362
|
-
var
|
|
5822
|
+
var TAG19 = "gate-collectors";
|
|
5363
5823
|
var init_gate_collectors = __esm(() => {
|
|
5364
5824
|
init_dist();
|
|
5365
5825
|
init_artifact_judge();
|
|
@@ -5479,7 +5939,7 @@ class ProgressTracker {
|
|
|
5479
5939
|
}
|
|
5480
5940
|
onToolStart(name, input) {
|
|
5481
5941
|
this.toolCallCount++;
|
|
5482
|
-
log.debug(
|
|
5942
|
+
log.debug(TAG20, `Tool: ${name} (count: ${this.toolCallCount}, phase: ${this.phase})`);
|
|
5483
5943
|
const filePath = this.extractString(input, "file_path");
|
|
5484
5944
|
if (filePath) {
|
|
5485
5945
|
if (EDIT_TOOLS.has(name)) {
|
|
@@ -5550,7 +6010,7 @@ class ProgressTracker {
|
|
|
5550
6010
|
transitionTo(newPhase) {
|
|
5551
6011
|
if (PHASE_ORDER[newPhase] <= PHASE_ORDER[this.phase])
|
|
5552
6012
|
return;
|
|
5553
|
-
log.info(
|
|
6013
|
+
log.info(TAG20, `Phase: ${this.phase} → ${newPhase}`);
|
|
5554
6014
|
const previousPhase = this.phase;
|
|
5555
6015
|
this.runEventSink?.recordPhaseChanged(newPhase, previousPhase);
|
|
5556
6016
|
this.phase = newPhase;
|
|
@@ -5652,7 +6112,7 @@ class ProgressTracker {
|
|
|
5652
6112
|
}
|
|
5653
6113
|
sendUpdate(currentTask) {
|
|
5654
6114
|
this.lastUpdateAt = Date.now();
|
|
5655
|
-
log.debug(
|
|
6115
|
+
log.debug(TAG20, `Progress: ${this.progress}% — ${currentTask}`);
|
|
5656
6116
|
this.client.updateAgentProgress(this.cardId, {
|
|
5657
6117
|
agentIdentifier: agentIdentifier(this.workerId),
|
|
5658
6118
|
agentName: AGENT_NAME,
|
|
@@ -5669,7 +6129,7 @@ class ProgressTracker {
|
|
|
5669
6129
|
modelName: this.lastCost?.modelName,
|
|
5670
6130
|
numTurns: this.lastCost?.numTurns ?? 0
|
|
5671
6131
|
}).catch((err) => {
|
|
5672
|
-
log.warn(
|
|
6132
|
+
log.warn(TAG20, `Failed to send progress update: ${err}`);
|
|
5673
6133
|
});
|
|
5674
6134
|
if (this.runEventSink && this.progress !== this.lastEmittedProgress) {
|
|
5675
6135
|
this.lastEmittedProgress = this.progress;
|
|
@@ -5700,7 +6160,7 @@ class ProgressTracker {
|
|
|
5700
6160
|
return null;
|
|
5701
6161
|
}
|
|
5702
6162
|
}
|
|
5703
|
-
var
|
|
6163
|
+
var TAG20 = "progress-tracker", THROTTLE_MS = 5000, HEARTBEAT_MS = 60000, MAX_TASK_LENGTH = 120, MAX_TEXT_BLOCKS = 40, SENTENCE_SPLIT, ACTION_PREFIX, GIT_COMMIT_RE, BUILD_CMD_RE, PHASES, PHASE_ORDER, EDIT_TOOLS, FILE_TOOL_VERBS;
|
|
5704
6164
|
var init_progress_tracker = __esm(() => {
|
|
5705
6165
|
init_log();
|
|
5706
6166
|
init_types2();
|
|
@@ -5856,7 +6316,7 @@ function parseReviewOutput(stdout) {
|
|
|
5856
6316
|
try {
|
|
5857
6317
|
const parsed = JSON.parse(raw);
|
|
5858
6318
|
if (parsed && typeof parsed === "object" && "verdict" in parsed) {
|
|
5859
|
-
log.debug(
|
|
6319
|
+
log.debug(TAG21, "Parsed review output from fenced JSON block");
|
|
5860
6320
|
return extractResult(parsed);
|
|
5861
6321
|
}
|
|
5862
6322
|
} catch {}
|
|
@@ -5882,21 +6342,21 @@ function parseReviewOutput(stdout) {
|
|
|
5882
6342
|
try {
|
|
5883
6343
|
const parsed = JSON.parse(candidates[i]);
|
|
5884
6344
|
if (parsed && typeof parsed === "object" && "verdict" in parsed) {
|
|
5885
|
-
log.debug(
|
|
6345
|
+
log.debug(TAG21, "Parsed review output from raw JSON object");
|
|
5886
6346
|
return extractResult(parsed);
|
|
5887
6347
|
}
|
|
5888
6348
|
} catch {}
|
|
5889
6349
|
}
|
|
5890
6350
|
const verdictMatch = stdout.match(/"verdict"\s*:\s*"(approved|rejected)"/i);
|
|
5891
6351
|
if (verdictMatch) {
|
|
5892
|
-
log.warn(
|
|
6352
|
+
log.warn(TAG21, `Parsed verdict via regex fallback — findings lost (${verdictMatch[1]})`);
|
|
5893
6353
|
return {
|
|
5894
6354
|
verdict: verdictMatch[1].toLowerCase(),
|
|
5895
6355
|
summary: "Parsed via regex fallback — original JSON was malformed. Check run log.",
|
|
5896
6356
|
findings: []
|
|
5897
6357
|
};
|
|
5898
6358
|
}
|
|
5899
|
-
log.warn(
|
|
6359
|
+
log.warn(TAG21, "Failed to parse review JSON output — returning error verdict (card stays in Review)");
|
|
5900
6360
|
return {
|
|
5901
6361
|
verdict: "error",
|
|
5902
6362
|
summary: stdout.slice(0, 500),
|
|
@@ -5929,7 +6389,7 @@ async function postReviewComment(client, card, commentType, body) {
|
|
|
5929
6389
|
try {
|
|
5930
6390
|
await client.addComment(card.id, body, { commentType });
|
|
5931
6391
|
} catch (err) {
|
|
5932
|
-
log.error(
|
|
6392
|
+
log.error(TAG21, `Failed to post review comment to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
5933
6393
|
}
|
|
5934
6394
|
}
|
|
5935
6395
|
async function runReviewCompletion(client, card, result, config, worktreePath, branchName, sessionStats, runLogPath, workspaceId, agentSessionId, stateStore, resolvedFromPrUrl) {
|
|
@@ -5943,11 +6403,11 @@ async function runReviewCompletion(client, card, result, config, worktreePath, b
|
|
|
5943
6403
|
const currentCycle = getReviewCycle(freshDesc) + 1;
|
|
5944
6404
|
const maxCycles = config.review.maxReviewCycles;
|
|
5945
6405
|
if (result.verdict === "error") {
|
|
5946
|
-
log.warn(
|
|
6406
|
+
log.warn(TAG21, `#${card.short_id} review output unparseable — labelling "${NEED_REVIEW_LABEL}" for manual inspection`);
|
|
5947
6407
|
try {
|
|
5948
6408
|
await addLabelByName(client, card, NEED_REVIEW_LABEL, NEED_REVIEW_LABEL_COLOR);
|
|
5949
6409
|
} catch (err) {
|
|
5950
|
-
log.warn(
|
|
6410
|
+
log.warn(TAG21, `Failed to add "${NEED_REVIEW_LABEL}" label: ${err instanceof Error ? err.message : err}`);
|
|
5951
6411
|
}
|
|
5952
6412
|
if (config.review.postFindings) {
|
|
5953
6413
|
const rawTail = runLogPath ? tailRunLog(runLogPath) : null;
|
|
@@ -5990,7 +6450,7 @@ ${runLogTail}
|
|
|
5990
6450
|
renameRemoteBranch(branchName, newRef, worktreePath);
|
|
5991
6451
|
approvedBranch = newRef;
|
|
5992
6452
|
} catch (err) {
|
|
5993
|
-
log.warn(
|
|
6453
|
+
log.warn(TAG21, `Branch rename failed (continuing on ${branchName}): ${err instanceof Error ? err.message : err}`);
|
|
5994
6454
|
}
|
|
5995
6455
|
}
|
|
5996
6456
|
if (config.review.createPR && approvedBranch) {
|
|
@@ -6011,14 +6471,14 @@ ${runLogTail}
|
|
|
6011
6471
|
});
|
|
6012
6472
|
}
|
|
6013
6473
|
} catch (err) {
|
|
6014
|
-
log.warn(
|
|
6474
|
+
log.warn(TAG21, `Failed to persist PR URL to #${card.short_id} description: ${err instanceof Error ? err.message : err}`);
|
|
6015
6475
|
}
|
|
6016
6476
|
}
|
|
6017
6477
|
if (branchName) {
|
|
6018
6478
|
try {
|
|
6019
6479
|
await persistReviewedSha(client, card, worktreePath);
|
|
6020
6480
|
} catch (err) {
|
|
6021
|
-
log.warn(
|
|
6481
|
+
log.warn(TAG21, `Failed to persist Reviewed-SHA to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
6022
6482
|
}
|
|
6023
6483
|
}
|
|
6024
6484
|
if (config.review.postFindings) {
|
|
@@ -6040,7 +6500,7 @@ ${runLogTail}
|
|
|
6040
6500
|
progressPercent: 100,
|
|
6041
6501
|
...buildTokenPayload(sessionStats)
|
|
6042
6502
|
});
|
|
6043
|
-
log.info(
|
|
6503
|
+
log.info(TAG21, `#${card.short_id} approved${prUrl ? ` — PR: ${prUrl}` : ""} — labeled "${config.review.approvedLabel}"`);
|
|
6044
6504
|
} else {
|
|
6045
6505
|
const reworkFindings = result.findings.filter((f) => f.relatedToDiff !== false);
|
|
6046
6506
|
const criticalFindings = reworkFindings.filter((f) => f.severity === "critical").slice(0, MAX_FINDINGS);
|
|
@@ -6048,7 +6508,7 @@ ${runLogTail}
|
|
|
6048
6508
|
const linkedFindings = [...criticalFindings, ...majorFindings];
|
|
6049
6509
|
const minorFindings = reworkFindings.filter((f) => f.severity === "minor").slice(0, MAX_FINDINGS);
|
|
6050
6510
|
if (currentCycle >= maxCycles) {
|
|
6051
|
-
log.warn(
|
|
6511
|
+
log.warn(TAG21, `#${card.short_id} reached max review cycles (${maxCycles}), moving to Done with note`);
|
|
6052
6512
|
await moveCardToColumn(client, card, config.review.moveToColumn);
|
|
6053
6513
|
const body = [
|
|
6054
6514
|
"**Review — needs human review.**",
|
|
@@ -6088,7 +6548,7 @@ ${runLogTail}
|
|
|
6088
6548
|
try {
|
|
6089
6549
|
await client.createSubtask(card.id, clampSubtaskTitle(`[${finding.severity}] ${finding.title}`));
|
|
6090
6550
|
} catch (err) {
|
|
6091
|
-
log.error(
|
|
6551
|
+
log.error(TAG21, `Failed to create finding subtask: ${err instanceof Error ? err.message : err}`);
|
|
6092
6552
|
}
|
|
6093
6553
|
}));
|
|
6094
6554
|
if (linkedFindings.length > 0) {
|
|
@@ -6100,7 +6560,7 @@ ${runLogTail}
|
|
|
6100
6560
|
try {
|
|
6101
6561
|
await client.createSubtask(card.id, clampSubtaskTitle(finding.title));
|
|
6102
6562
|
} catch (err) {
|
|
6103
|
-
log.error(
|
|
6563
|
+
log.error(TAG21, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
|
|
6104
6564
|
}
|
|
6105
6565
|
}));
|
|
6106
6566
|
const baseDesc = stripReviewSummary(freshDesc);
|
|
@@ -6108,7 +6568,7 @@ ${runLogTail}
|
|
|
6108
6568
|
try {
|
|
6109
6569
|
await client.updateCard(card.id, { description: updatedDesc });
|
|
6110
6570
|
} catch (err) {
|
|
6111
|
-
log.error(
|
|
6571
|
+
log.error(TAG21, `Failed to update review cycle marker: ${err instanceof Error ? err.message : err}`);
|
|
6112
6572
|
}
|
|
6113
6573
|
const scopeLine = result.scopeCheck ? `Scope: ${result.scopeCheck.status}${result.scopeCheck.notes ? ` — ${result.scopeCheck.notes}` : ""}` : "";
|
|
6114
6574
|
const body = [
|
|
@@ -6125,9 +6585,9 @@ ${runLogTail}
|
|
|
6125
6585
|
if (config.planning.enabled && card.plan_id) {
|
|
6126
6586
|
try {
|
|
6127
6587
|
await client.updateCard(card.id, { needsPlanRefresh: true });
|
|
6128
|
-
log.info(
|
|
6588
|
+
log.info(TAG21, `#${card.short_id} flagged needs_plan_refresh after rejected review`);
|
|
6129
6589
|
} catch (err) {
|
|
6130
|
-
log.warn(
|
|
6590
|
+
log.warn(TAG21, `Failed to flag needs_plan_refresh for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
6131
6591
|
}
|
|
6132
6592
|
}
|
|
6133
6593
|
await moveCardToColumn(client, card, config.review.failColumn);
|
|
@@ -6141,10 +6601,10 @@ ${runLogTail}
|
|
|
6141
6601
|
recoveryBranch
|
|
6142
6602
|
});
|
|
6143
6603
|
} catch (err) {
|
|
6144
|
-
log.debug(
|
|
6604
|
+
log.debug(TAG21, `recordFailureSummary failed: ${err instanceof Error ? err.message : err}`);
|
|
6145
6605
|
}
|
|
6146
6606
|
if (recoveryBranch) {
|
|
6147
|
-
log.info(
|
|
6607
|
+
log.info(TAG21, `#${card.short_id} recovery branch ${recoveryBranch}${recoveryUrl ? ` (${recoveryUrl})` : ""}`);
|
|
6148
6608
|
}
|
|
6149
6609
|
await client.endAgentSession(card.id, {
|
|
6150
6610
|
status: "failed",
|
|
@@ -6153,7 +6613,7 @@ ${runLogTail}
|
|
|
6153
6613
|
recoveryBranch,
|
|
6154
6614
|
...buildTokenPayload(sessionStats)
|
|
6155
6615
|
});
|
|
6156
|
-
log.info(
|
|
6616
|
+
log.info(TAG21, `#${card.short_id} rejected (cycle ${currentCycle}/${maxCycles}) — moved to "${config.review.failColumn}"`);
|
|
6157
6617
|
}
|
|
6158
6618
|
if (workspaceId && (result.verdict === "approved" || result.verdict === "rejected")) {
|
|
6159
6619
|
const originalEpisodeId = await findLatestImplementEpisode(client, workspaceId, card.project_id, card.short_id);
|
|
@@ -6175,7 +6635,7 @@ ${runLogTail}
|
|
|
6175
6635
|
cleanupWorktree(worktreePath, branchName);
|
|
6176
6636
|
}
|
|
6177
6637
|
}
|
|
6178
|
-
var
|
|
6638
|
+
var TAG21 = "review-completion", MAX_FINDINGS = 10, MAX_SUBTASK_TITLE = 120, COMMENT_BODY_BUDGET = 9500, REVIEW_MARKER = `---
|
|
6179
6639
|
**Review:`, RUN_LOG_TAIL_BYTES = 2048;
|
|
6180
6640
|
var init_review_completion = __esm(() => {
|
|
6181
6641
|
init_board_helpers();
|
|
@@ -6404,8 +6864,16 @@ class StateStore {
|
|
|
6404
6864
|
const raw = readFileSync4(this.path, "utf-8");
|
|
6405
6865
|
const parsed = JSON.parse(raw);
|
|
6406
6866
|
if (parsed?.version !== SCHEMA_VERSION) {
|
|
6407
|
-
log.warn(
|
|
6408
|
-
return
|
|
6867
|
+
log.warn(TAG22, `state file has version ${parsed?.version}, expected ${SCHEMA_VERSION} — migrating (preserving card budget/attempts, dropping in-flight runs)`);
|
|
6868
|
+
return {
|
|
6869
|
+
version: SCHEMA_VERSION,
|
|
6870
|
+
daemonId: null,
|
|
6871
|
+
daemonPid: null,
|
|
6872
|
+
daemonStartedAt: null,
|
|
6873
|
+
runs: [],
|
|
6874
|
+
cards: parsed.cards ?? [],
|
|
6875
|
+
daily: parsed.daily ?? []
|
|
6876
|
+
};
|
|
6409
6877
|
}
|
|
6410
6878
|
return {
|
|
6411
6879
|
version: SCHEMA_VERSION,
|
|
@@ -6417,7 +6885,7 @@ class StateStore {
|
|
|
6417
6885
|
daily: parsed.daily ?? []
|
|
6418
6886
|
};
|
|
6419
6887
|
} catch (err) {
|
|
6420
|
-
log.error(
|
|
6888
|
+
log.error(TAG22, `failed to read state file: ${err instanceof Error ? err.message : err}`);
|
|
6421
6889
|
return emptyState();
|
|
6422
6890
|
}
|
|
6423
6891
|
}
|
|
@@ -6485,6 +6953,12 @@ class StateStore {
|
|
|
6485
6953
|
getRunsForCard(cardId) {
|
|
6486
6954
|
return this.state.runs.filter((r) => r.cardId === cardId);
|
|
6487
6955
|
}
|
|
6956
|
+
listRuns() {
|
|
6957
|
+
return this.state.runs.map((r) => ({ ...r })).sort((a, b) => b.startedAt - a.startedAt);
|
|
6958
|
+
}
|
|
6959
|
+
listCards() {
|
|
6960
|
+
return this.state.cards.map((c) => ({ ...c }));
|
|
6961
|
+
}
|
|
6488
6962
|
purgeOldRuns(beforeTs) {
|
|
6489
6963
|
this.state.runs = this.state.runs.filter((r) => r.endedAt === null || r.endedAt >= beforeTs);
|
|
6490
6964
|
return this.persist();
|
|
@@ -6495,6 +6969,7 @@ class StateStore {
|
|
|
6495
6969
|
rec = {
|
|
6496
6970
|
cardId,
|
|
6497
6971
|
attempts: 0,
|
|
6972
|
+
totalAttempts: 0,
|
|
6498
6973
|
totalCostCents: 0,
|
|
6499
6974
|
lastAttemptAt: null,
|
|
6500
6975
|
lastOutcome: null
|
|
@@ -6509,6 +6984,7 @@ class StateStore {
|
|
|
6509
6984
|
async incrementAttempt(cardId) {
|
|
6510
6985
|
const rec = this.ensureCard(cardId);
|
|
6511
6986
|
rec.attempts += 1;
|
|
6987
|
+
rec.totalAttempts = (rec.totalAttempts ?? 0) + 1;
|
|
6512
6988
|
rec.lastAttemptAt = Date.now();
|
|
6513
6989
|
await this.persist();
|
|
6514
6990
|
return rec.attempts;
|
|
@@ -6518,6 +6994,7 @@ class StateStore {
|
|
|
6518
6994
|
if (!rec || rec.attempts === 0)
|
|
6519
6995
|
return;
|
|
6520
6996
|
rec.attempts = Math.max(0, rec.attempts - 1);
|
|
6997
|
+
rec.totalAttempts = Math.max(0, (rec.totalAttempts ?? 0) - 1);
|
|
6521
6998
|
await this.persist();
|
|
6522
6999
|
}
|
|
6523
7000
|
async recordOutcome(cardId, outcome) {
|
|
@@ -6597,7 +7074,7 @@ class StateStore {
|
|
|
6597
7074
|
return this.state.daily.find((d) => d.date === key)?.costCents ?? 0;
|
|
6598
7075
|
}
|
|
6599
7076
|
}
|
|
6600
|
-
var
|
|
7077
|
+
var TAG22 = "state-store", SCHEMA_VERSION = 1;
|
|
6601
7078
|
var init_state_store = __esm(() => {
|
|
6602
7079
|
init_log();
|
|
6603
7080
|
});
|
|
@@ -6624,7 +7101,7 @@ function normalizeToolResultContent(raw) {
|
|
|
6624
7101
|
return String(raw);
|
|
6625
7102
|
}
|
|
6626
7103
|
}
|
|
6627
|
-
var
|
|
7104
|
+
var TAG23 = "stream-parser", StreamParser;
|
|
6628
7105
|
var init_stream_parser = __esm(() => {
|
|
6629
7106
|
init_log();
|
|
6630
7107
|
StreamParser = class StreamParser extends EventEmitter {
|
|
@@ -6672,14 +7149,14 @@ var init_stream_parser = __esm(() => {
|
|
|
6672
7149
|
try {
|
|
6673
7150
|
msg = JSON.parse(line);
|
|
6674
7151
|
} catch {
|
|
6675
|
-
log.debug(
|
|
7152
|
+
log.debug(TAG23, `Non-JSON line: ${line.slice(0, 100)}`);
|
|
6676
7153
|
return;
|
|
6677
7154
|
}
|
|
6678
7155
|
try {
|
|
6679
7156
|
this.handleMessage(msg);
|
|
6680
7157
|
} catch (err) {
|
|
6681
7158
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
6682
|
-
log.warn(
|
|
7159
|
+
log.warn(TAG23, `Error handling stream event: ${errMsg}`);
|
|
6683
7160
|
this.emit("parse_error", errMsg);
|
|
6684
7161
|
}
|
|
6685
7162
|
}
|
|
@@ -6765,7 +7242,7 @@ async function withRetry(step, cardShortId, op, attempts, backoffMs) {
|
|
|
6765
7242
|
const msg2 = err instanceof Error ? err.message : String(err);
|
|
6766
7243
|
if (i < attempts - 1) {
|
|
6767
7244
|
const wait = backoffMs * 2 ** i;
|
|
6768
|
-
log.warn(
|
|
7245
|
+
log.warn(TAG24, `${step} failed for #${cardShortId} (attempt ${i + 1}/${attempts}): ${msg2} — retrying in ${wait}ms`);
|
|
6769
7246
|
await new Promise((r) => setTimeout(r, wait));
|
|
6770
7247
|
}
|
|
6771
7248
|
}
|
|
@@ -6788,10 +7265,10 @@ async function runTransition(client, card, plan, opts = {}) {
|
|
|
6788
7265
|
if (opts.strictColumn) {
|
|
6789
7266
|
throw new TransitionError("move", 1, msg);
|
|
6790
7267
|
}
|
|
6791
|
-
log.warn(
|
|
7268
|
+
log.warn(TAG24, `#${shortId}: ${msg} — skipping move`);
|
|
6792
7269
|
} else if (card.column_id !== target.id) {
|
|
6793
7270
|
await withRetry("move", shortId, () => client.moveCard(card.id, target.id), attempts, backoffMs);
|
|
6794
|
-
log.info(
|
|
7271
|
+
log.info(TAG24, `#${shortId} → "${target.name}"`);
|
|
6795
7272
|
card.column_id = target.id;
|
|
6796
7273
|
moveLanded = true;
|
|
6797
7274
|
} else {
|
|
@@ -6810,7 +7287,7 @@ async function runTransition(client, card, plan, opts = {}) {
|
|
|
6810
7287
|
continue;
|
|
6811
7288
|
await withRetry("addLabel", shortId, () => client.addLabelToCard(card.id, labelId), attempts, backoffMs);
|
|
6812
7289
|
existing.add(labelId);
|
|
6813
|
-
log.info(
|
|
7290
|
+
log.info(TAG24, `#${shortId} +label "${name}"`);
|
|
6814
7291
|
}
|
|
6815
7292
|
card.labelIds = Array.from(existing);
|
|
6816
7293
|
}
|
|
@@ -6822,22 +7299,22 @@ async function runTransition(client, card, plan, opts = {}) {
|
|
|
6822
7299
|
continue;
|
|
6823
7300
|
await withRetry("removeLabel", shortId, () => client.removeLabelFromCard(card.id, match.id), attempts, backoffMs);
|
|
6824
7301
|
existing.delete(match.id);
|
|
6825
|
-
log.info(
|
|
7302
|
+
log.info(TAG24, `#${shortId} -label "${name}"`);
|
|
6826
7303
|
}
|
|
6827
7304
|
card.labelIds = Array.from(existing);
|
|
6828
7305
|
}
|
|
6829
7306
|
if (plan.updateCard) {
|
|
6830
7307
|
await withRetry("updateCard", shortId, () => client.updateCard(card.id, plan.updateCard), attempts, backoffMs);
|
|
6831
|
-
log.info(
|
|
7308
|
+
log.info(TAG24, `#${shortId} updated`);
|
|
6832
7309
|
}
|
|
6833
7310
|
if (plan.endSession) {
|
|
6834
7311
|
await withRetry("endSession", shortId, () => client.endAgentSession(card.id, plan.endSession), attempts, backoffMs);
|
|
6835
|
-
log.info(
|
|
7312
|
+
log.info(TAG24, `#${shortId} session ended (${plan.endSession.status})`);
|
|
6836
7313
|
}
|
|
6837
7314
|
if (plan.assignAgent !== undefined) {
|
|
6838
7315
|
const assignedAgentId = plan.assignAgent;
|
|
6839
7316
|
await withRetry("assignAgent", shortId, () => client.updateCard(card.id, { assignedAgentId }), attempts, backoffMs);
|
|
6840
|
-
log.info(
|
|
7317
|
+
log.info(TAG24, assignedAgentId ? `#${shortId} assigned → agent ${assignedAgentId}` : `#${shortId} unassigned`);
|
|
6841
7318
|
}
|
|
6842
7319
|
if (opts.store && opts.runId) {
|
|
6843
7320
|
try {
|
|
@@ -6850,11 +7327,11 @@ async function ensureLabel(client, projectId, name, color, attempts, backoffMs)
|
|
|
6850
7327
|
const result = await withRetry("addLabel", 0, () => client.createLabel(projectId, { name, color: color ?? "#8b5cf6" }), attempts, backoffMs);
|
|
6851
7328
|
return result?.label?.id ?? null;
|
|
6852
7329
|
} catch (err) {
|
|
6853
|
-
log.warn(
|
|
7330
|
+
log.warn(TAG24, `ensureLabel "${name}" failed: ${err instanceof Error ? err.message : err}`);
|
|
6854
7331
|
return null;
|
|
6855
7332
|
}
|
|
6856
7333
|
}
|
|
6857
|
-
var
|
|
7334
|
+
var TAG24 = "transition", TransitionError;
|
|
6858
7335
|
var init_transitions = __esm(() => {
|
|
6859
7336
|
init_log();
|
|
6860
7337
|
TransitionError = class TransitionError extends Error {
|
|
@@ -6938,7 +7415,7 @@ class ReviewWorker {
|
|
|
6938
7415
|
}
|
|
6939
7416
|
}
|
|
6940
7417
|
get tag() {
|
|
6941
|
-
return `${
|
|
7418
|
+
return `${TAG25}:${this.id}`;
|
|
6942
7419
|
}
|
|
6943
7420
|
get isIdle() {
|
|
6944
7421
|
return this.state === "idle";
|
|
@@ -7401,7 +7878,7 @@ class ReviewWorker {
|
|
|
7401
7878
|
this.lastSessionStats = null;
|
|
7402
7879
|
}
|
|
7403
7880
|
}
|
|
7404
|
-
var
|
|
7881
|
+
var TAG25 = "review-worker", CANCEL_SIGINT_TIMEOUT = 30000, CANCEL_SIGTERM_TIMEOUT = 1e4;
|
|
7405
7882
|
var init_review_worker = __esm(() => {
|
|
7406
7883
|
init_dist();
|
|
7407
7884
|
init_board_helpers();
|
|
@@ -7454,7 +7931,7 @@ class SleepGuard {
|
|
|
7454
7931
|
if (!this.child.killed)
|
|
7455
7932
|
this.child.kill("SIGTERM");
|
|
7456
7933
|
this.child = null;
|
|
7457
|
-
log.info(
|
|
7934
|
+
log.info(TAG26, "sleep assertion released");
|
|
7458
7935
|
}
|
|
7459
7936
|
}
|
|
7460
7937
|
start() {
|
|
@@ -7469,7 +7946,7 @@ class SleepGuard {
|
|
|
7469
7946
|
spawned = true;
|
|
7470
7947
|
});
|
|
7471
7948
|
child.on("error", (err) => {
|
|
7472
|
-
log.warn(
|
|
7949
|
+
log.warn(TAG26, `caffeinate unavailable: ${err.message}`);
|
|
7473
7950
|
if (this.child === child)
|
|
7474
7951
|
this.child = null;
|
|
7475
7952
|
});
|
|
@@ -7482,13 +7959,13 @@ class SleepGuard {
|
|
|
7482
7959
|
});
|
|
7483
7960
|
child.unref();
|
|
7484
7961
|
this.child = child;
|
|
7485
|
-
log.info(
|
|
7962
|
+
log.info(TAG26, "sleep assertion acquired (caffeinate -i)");
|
|
7486
7963
|
} catch (err) {
|
|
7487
|
-
log.warn(
|
|
7964
|
+
log.warn(TAG26, `failed to spawn caffeinate: ${err instanceof Error ? err.message : err}`);
|
|
7488
7965
|
}
|
|
7489
7966
|
}
|
|
7490
7967
|
}
|
|
7491
|
-
var
|
|
7968
|
+
var TAG26 = "sleep-guard";
|
|
7492
7969
|
var init_sleep_guard = __esm(() => {
|
|
7493
7970
|
init_log();
|
|
7494
7971
|
});
|
|
@@ -7499,7 +7976,7 @@ async function fetchBlocksLinks(client, cardId) {
|
|
|
7499
7976
|
const { links } = await client.getCardLinks(cardId);
|
|
7500
7977
|
return links.filter((l) => l.link_type === "blocks");
|
|
7501
7978
|
} catch (err) {
|
|
7502
|
-
log.warn(
|
|
7979
|
+
log.warn(TAG27, `link fetch failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
7503
7980
|
return null;
|
|
7504
7981
|
}
|
|
7505
7982
|
}
|
|
@@ -7531,27 +8008,27 @@ async function promoteUnblockedSuccessors(completedCard, deps) {
|
|
|
7531
8008
|
const successors = links.filter((l) => l.direction === "outgoing" && !l.target_card.done);
|
|
7532
8009
|
if (successors.length === 0)
|
|
7533
8010
|
return;
|
|
7534
|
-
log.info(
|
|
8011
|
+
log.info(TAG27, `#${completedCard.short_id} completed — checking ${successors.length} chained successor(s)`);
|
|
7535
8012
|
for (const link of successors) {
|
|
7536
8013
|
const successorId = link.target_card.id;
|
|
7537
8014
|
try {
|
|
7538
8015
|
const { card } = await deps.client.getCard(successorId);
|
|
7539
8016
|
if (card.assigned_agent_id === deps.agentId) {} else if (card.assigned_agent_id === null && !card.assignee_id) {
|
|
7540
|
-
log.info(
|
|
8017
|
+
log.info(TAG27, `successor #${card.short_id} unassigned — auto-assigning to continue chain`);
|
|
7541
8018
|
await deps.client.updateCard(successorId, {
|
|
7542
8019
|
assignedAgentId: deps.agentId
|
|
7543
8020
|
});
|
|
7544
8021
|
} else {
|
|
7545
|
-
log.debug(
|
|
8022
|
+
log.debug(TAG27, `successor #${card.short_id} assigned to different entity — skipping`);
|
|
7546
8023
|
continue;
|
|
7547
8024
|
}
|
|
7548
8025
|
await deps.enqueue(successorId);
|
|
7549
8026
|
} catch (err) {
|
|
7550
|
-
log.warn(
|
|
8027
|
+
log.warn(TAG27, `promotion failed for successor ${successorId}: ${err instanceof Error ? err.message : err}`);
|
|
7551
8028
|
}
|
|
7552
8029
|
}
|
|
7553
8030
|
}
|
|
7554
|
-
var
|
|
8031
|
+
var TAG27 = "unblock";
|
|
7555
8032
|
var init_unblock = __esm(() => {
|
|
7556
8033
|
init_log();
|
|
7557
8034
|
});
|
|
@@ -7706,7 +8183,7 @@ class CliAgentRunner {
|
|
|
7706
8183
|
events: batch
|
|
7707
8184
|
});
|
|
7708
8185
|
} catch (err) {
|
|
7709
|
-
log.warn(
|
|
8186
|
+
log.warn(TAG28, `Failed to flush run events: ${err}`);
|
|
7710
8187
|
this.buffer.unshift(...batch);
|
|
7711
8188
|
if (this.buffer.length > MAX_BUFFER) {
|
|
7712
8189
|
this.buffer.length = MAX_BUFFER;
|
|
@@ -7743,12 +8220,26 @@ function mapCost(cost) {
|
|
|
7743
8220
|
durationMs: cost.durationMs
|
|
7744
8221
|
};
|
|
7745
8222
|
}
|
|
7746
|
-
var
|
|
8223
|
+
var TAG28 = "cli-agent-runner", FLUSH_INTERVAL_MS = 2000, MAX_BUFFER = 1000, MAX_TEXT_LEN2 = 8000, MAX_OUTPUT_LEN2 = 4000;
|
|
7747
8224
|
var init_cli_agent_runner = __esm(() => {
|
|
7748
8225
|
init_log();
|
|
7749
8226
|
});
|
|
7750
8227
|
|
|
7751
8228
|
// src/prompt.ts
|
|
8229
|
+
function renderPreviousAttemptsSection(failures) {
|
|
8230
|
+
if (failures.length === 0)
|
|
8231
|
+
return "";
|
|
8232
|
+
const lines = failures.map((f) => {
|
|
8233
|
+
const tag = f.reason ? `[${f.reason}] ` : "";
|
|
8234
|
+
return `- ${tag}${f.summary}`;
|
|
8235
|
+
});
|
|
8236
|
+
return [
|
|
8237
|
+
"## Previous attempt feedback",
|
|
8238
|
+
"This is a re-attempt on the branch your last run already pushed — build on that existing work and FIX the issues below. Do NOT reimplement from scratch or revert the prior commits.",
|
|
8239
|
+
...lines
|
|
8240
|
+
].join(`
|
|
8241
|
+
`);
|
|
8242
|
+
}
|
|
7752
8243
|
async function buildPrompt(enriched, branchName, worktreePath, client, workspaceId, projectId) {
|
|
7753
8244
|
const { card } = enriched;
|
|
7754
8245
|
const pastEpisodesSection = await renderPastEpisodesSection(client, card.title, card.description ?? "", workspaceId, projectId);
|
|
@@ -7762,11 +8253,11 @@ async function buildPrompt(enriched, branchName, worktreePath, client, workspace
|
|
|
7762
8253
|
Do NOT push to main. All your work stays on \`${branchName}\`.
|
|
7763
8254
|
The daemon owns the run lifecycle: once your work is committed it ends the agent session, pushes the branch, and moves the card to Review for you. Do NOT call harmony_end_agent_session, do NOT start a new session, and do NOT move the card or change its column yourself. If the skill driving this work tells you to move the card or end the session as a final step, SKIP it — it is handled for you (those tools are disabled for this run). Finish the implementation, commit, and stop.`
|
|
7764
8255
|
});
|
|
7765
|
-
log.info(
|
|
8256
|
+
log.info(TAG29, `Generated prompt for #${card.short_id} — ${result.contextSummary.memoryCount} memories, ${result.tokenEstimate} tokens`);
|
|
7766
8257
|
return result.prompt + pastEpisodesSection;
|
|
7767
8258
|
} catch (err) {
|
|
7768
8259
|
const msg = err instanceof Error ? err.message : String(err);
|
|
7769
|
-
log.warn(
|
|
8260
|
+
log.warn(TAG29, `Failed to generate prompt via API, using fallback: ${msg}`);
|
|
7770
8261
|
const commentsSection = await renderCommentsSection(client, card.id);
|
|
7771
8262
|
return buildFallbackPrompt(enriched, branchName, worktreePath) + commentsSection + pastEpisodesSection;
|
|
7772
8263
|
}
|
|
@@ -7784,7 +8275,7 @@ async function renderCommentsSection(client, cardId) {
|
|
|
7784
8275
|
|
|
7785
8276
|
${section}` : "";
|
|
7786
8277
|
} catch (err) {
|
|
7787
|
-
log.warn(
|
|
8278
|
+
log.warn(TAG29, "comment-thread fetch failed", {
|
|
7788
8279
|
event: "comment_fetch_failed",
|
|
7789
8280
|
error: err instanceof Error ? err.message : String(err)
|
|
7790
8281
|
});
|
|
@@ -7834,7 +8325,7 @@ ${description}`.trim();
|
|
|
7834
8325
|
## Similar past tasks
|
|
7835
8326
|
${bullets}`;
|
|
7836
8327
|
} catch (err) {
|
|
7837
|
-
log.warn(
|
|
8328
|
+
log.warn(TAG29, "past-episodes recall failed", {
|
|
7838
8329
|
event: "episode_recall_failed",
|
|
7839
8330
|
error: err instanceof Error ? err.message : String(err)
|
|
7840
8331
|
});
|
|
@@ -7875,7 +8366,7 @@ ${subtaskStr}
|
|
|
7875
8366
|
You are working in a git worktree at \`${worktreePath}\` on branch \`${branchName}\`.
|
|
7876
8367
|
Do NOT push to main. All your work stays on \`${branchName}\`.`;
|
|
7877
8368
|
}
|
|
7878
|
-
var
|
|
8369
|
+
var TAG29 = "prompt";
|
|
7879
8370
|
var init_prompt = __esm(() => {
|
|
7880
8371
|
init_dist();
|
|
7881
8372
|
init_log();
|
|
@@ -7898,7 +8389,7 @@ async function resolveStageColumnName(client, card, stage) {
|
|
|
7898
8389
|
const match = board.columns.find((c) => c.id === target || c.name.toLowerCase() === target.toLowerCase());
|
|
7899
8390
|
return match ? match.name : null;
|
|
7900
8391
|
} catch (err) {
|
|
7901
|
-
log.warn(
|
|
8392
|
+
log.warn(TAG30, `board fetch failed resolving stage column for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
7902
8393
|
return null;
|
|
7903
8394
|
}
|
|
7904
8395
|
}
|
|
@@ -7942,7 +8433,7 @@ async function advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loo
|
|
|
7942
8433
|
evidence,
|
|
7943
8434
|
summary
|
|
7944
8435
|
});
|
|
7945
|
-
log.info(
|
|
8436
|
+
log.info(TAG30, `#${card.short_id} converge loop "${stage.name}": ${summary} → ${decision}`);
|
|
7946
8437
|
if (decision === "exit") {
|
|
7947
8438
|
await deps.stateStore.resetLoopIterations(card.id).catch(() => {});
|
|
7948
8439
|
deps.sink?.recordLoopCompleted?.({
|
|
@@ -7984,7 +8475,7 @@ async function advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loo
|
|
|
7984
8475
|
await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore, {
|
|
7985
8476
|
keepAttempts: true
|
|
7986
8477
|
});
|
|
7987
|
-
log.info(
|
|
8478
|
+
log.info(TAG30, `#${card.short_id} LoopExhausted: ${reason}`);
|
|
7988
8479
|
return { kind: "held_gate_unmet", reason };
|
|
7989
8480
|
}
|
|
7990
8481
|
await deps.stateStore.decrementAttempt(card.id).catch(() => {});
|
|
@@ -7998,7 +8489,7 @@ async function advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loo
|
|
|
7998
8489
|
addLabels: [{ name: AGENT_LABEL }],
|
|
7999
8490
|
...isAgentRunnableOwner(stage.owner) ? { assignAgent: deps.agentId } : {}
|
|
8000
8491
|
}, { store: deps.stateStore, runId: deps.runId });
|
|
8001
|
-
log.info(
|
|
8492
|
+
log.info(TAG30, `#${card.short_id} converge loop "${stage.name}" — requeued to "${toColumn}" for iteration ${iteration + 1}/${maxIterations}`);
|
|
8002
8493
|
return { kind: "requeued_gate_unmet", toColumn };
|
|
8003
8494
|
}
|
|
8004
8495
|
async function writeIterationHandoff(card, stage, iteration, maxIterations, evaluation, deps) {
|
|
@@ -8017,7 +8508,7 @@ ${findings.map((f) => `- [${f.level}] ${f.message}`).join(`
|
|
|
8017
8508
|
});
|
|
8018
8509
|
await deps.client.addComment(card.id, body, { commentType: "decision" });
|
|
8019
8510
|
} catch (err) {
|
|
8020
|
-
log.warn(
|
|
8511
|
+
log.warn(TAG30, `iteration-handoff write failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
8021
8512
|
}
|
|
8022
8513
|
}
|
|
8023
8514
|
async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps) {
|
|
@@ -8048,7 +8539,7 @@ async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps
|
|
|
8048
8539
|
reason: "Playbook complete — final stage gate passed."
|
|
8049
8540
|
});
|
|
8050
8541
|
deps.stateStore.recordOutcome(card.id, "success").catch(() => {});
|
|
8051
|
-
log.info(
|
|
8542
|
+
log.info(TAG30, `#${card.short_id} terminal stage "${stage.name}" passed — marked done`);
|
|
8052
8543
|
return { kind: "completed_terminal" };
|
|
8053
8544
|
}
|
|
8054
8545
|
if (next.kind === "out_of_range") {
|
|
@@ -8080,7 +8571,7 @@ async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps
|
|
|
8080
8571
|
...isAgentRunnableOwner(next.stage.owner) ? { assignAgent: deps.agentId } : {}
|
|
8081
8572
|
}, { store: deps.stateStore, runId: deps.runId });
|
|
8082
8573
|
deps.stateStore.recordOutcome(card.id, "success").catch(() => {});
|
|
8083
|
-
log.info(
|
|
8574
|
+
log.info(TAG30, `#${card.short_id} advanced "${stage.name}" → "${next.stage.name}" (column "${toColumn}")`);
|
|
8084
8575
|
return { kind: "advanced", toStageId: next.stage.id, toColumn };
|
|
8085
8576
|
}
|
|
8086
8577
|
async function handleGateUnmet(card, stage, summary, deps) {
|
|
@@ -8099,7 +8590,7 @@ async function handleGateUnmet(card, stage, summary, deps) {
|
|
|
8099
8590
|
await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore, {
|
|
8100
8591
|
keepAttempts: true
|
|
8101
8592
|
});
|
|
8102
|
-
log.info(
|
|
8593
|
+
log.info(TAG30, `#${card.short_id} GateUnmetExhausted: ${reason}`);
|
|
8103
8594
|
return { kind: "held_gate_unmet", reason };
|
|
8104
8595
|
}
|
|
8105
8596
|
const toColumn = await resolveStageColumnName(deps.client, card, stage) ?? deps.fallbackColumn;
|
|
@@ -8111,7 +8602,7 @@ async function handleGateUnmet(card, stage, summary, deps) {
|
|
|
8111
8602
|
addLabels: [{ name: AGENT_LABEL }],
|
|
8112
8603
|
...isAgentRunnableOwner(stage.owner) ? { assignAgent: deps.agentId } : {}
|
|
8113
8604
|
}, { store: deps.stateStore, runId: deps.runId });
|
|
8114
|
-
log.info(
|
|
8605
|
+
log.info(TAG30, `#${card.short_id} gate unmet for "${stage.name}" — requeued to "${toColumn}" for re-run (attempt ${attempts}/${deps.maxAttempts})`);
|
|
8115
8606
|
return { kind: "requeued_gate_unmet", toColumn };
|
|
8116
8607
|
}
|
|
8117
8608
|
async function holdForHuman(client, card, reason, runId, stateStore, opts = {}) {
|
|
@@ -8131,10 +8622,10 @@ async function holdForHuman(client, card, reason, runId, stateStore, opts = {})
|
|
|
8131
8622
|
}
|
|
8132
8623
|
}, { store: stateStore, runId });
|
|
8133
8624
|
} catch (err) {
|
|
8134
|
-
log.warn(
|
|
8625
|
+
log.warn(TAG30, `hold transition failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
8135
8626
|
}
|
|
8136
8627
|
}
|
|
8137
|
-
var
|
|
8628
|
+
var TAG30 = "stage-advance", AGENT_LABEL = "agent";
|
|
8138
8629
|
var init_stage_advance = __esm(() => {
|
|
8139
8630
|
init_dist();
|
|
8140
8631
|
init_log();
|
|
@@ -8257,6 +8748,14 @@ class Worker {
|
|
|
8257
8748
|
this.heartbeatTimer = null;
|
|
8258
8749
|
}
|
|
8259
8750
|
}
|
|
8751
|
+
captureCliSessionId(sessionId) {
|
|
8752
|
+
if (!sessionId || sessionId === this.cliSessionId)
|
|
8753
|
+
return;
|
|
8754
|
+
this.cliSessionId = sessionId;
|
|
8755
|
+
if (this.runId) {
|
|
8756
|
+
this.stateStore.updateRun(this.runId, { cliSessionId: sessionId }).catch(() => {});
|
|
8757
|
+
}
|
|
8758
|
+
}
|
|
8260
8759
|
async recordPhase(phase) {
|
|
8261
8760
|
if (!this.runId)
|
|
8262
8761
|
return;
|
|
@@ -8273,7 +8772,7 @@ class Worker {
|
|
|
8273
8772
|
}
|
|
8274
8773
|
}
|
|
8275
8774
|
get tag() {
|
|
8276
|
-
return `${
|
|
8775
|
+
return `${TAG31}:${this.id}`;
|
|
8277
8776
|
}
|
|
8278
8777
|
get isIdle() {
|
|
8279
8778
|
return this.state === "idle";
|
|
@@ -8308,7 +8807,8 @@ class Worker {
|
|
|
8308
8807
|
this.state = "preparing";
|
|
8309
8808
|
this.branchName = makeBranchName(card.short_id, card.title, this.config.worktree.failedBranchPrefix);
|
|
8310
8809
|
log.info(this.tag, `Preparing #${card.short_id} "${card.title}"`);
|
|
8311
|
-
await this.stateStore.incrementAttempt(card.id);
|
|
8810
|
+
const attemptCount = await this.stateStore.incrementAttempt(card.id);
|
|
8811
|
+
const isRework = attemptCount > 1;
|
|
8312
8812
|
this.startHeartbeat();
|
|
8313
8813
|
await this.stateStore.insertRun({
|
|
8314
8814
|
runId: this.runId,
|
|
@@ -8338,7 +8838,7 @@ class Worker {
|
|
|
8338
8838
|
});
|
|
8339
8839
|
const sid = session && typeof session === "object" && "id" in session ? session.id : null;
|
|
8340
8840
|
if (!sid) {
|
|
8341
|
-
log.warn(
|
|
8841
|
+
log.warn(TAG31, "startAgentSession returned no session id");
|
|
8342
8842
|
}
|
|
8343
8843
|
this.sessionId = sid;
|
|
8344
8844
|
if (this.sessionId) {
|
|
@@ -8361,7 +8861,7 @@ class Worker {
|
|
|
8361
8861
|
await this.holdStageCard(card, stageCtx.reason);
|
|
8362
8862
|
return;
|
|
8363
8863
|
}
|
|
8364
|
-
this.worktreePath = createWorktree(this.config.worktree.basePath, this.config.worktree.baseBranch, this.branchName, { continueExisting: stageCtx.kind === "run" });
|
|
8864
|
+
this.worktreePath = createWorktree(this.config.worktree.basePath, this.config.worktree.baseBranch, this.branchName, { continueExisting: stageCtx.kind === "run" || isRework });
|
|
8365
8865
|
if (this.aborted)
|
|
8366
8866
|
return;
|
|
8367
8867
|
const enriched = {
|
|
@@ -8413,6 +8913,12 @@ class Worker {
|
|
|
8413
8913
|
mode: loop.mode
|
|
8414
8914
|
});
|
|
8415
8915
|
}
|
|
8916
|
+
} else if (isRework) {
|
|
8917
|
+
const digest = renderPreviousAttemptsSection(this.stateStore.getRecentFailures(card.id, 3));
|
|
8918
|
+
if (digest)
|
|
8919
|
+
prompt = `${digest}
|
|
8920
|
+
|
|
8921
|
+
${basePrompt}`;
|
|
8416
8922
|
}
|
|
8417
8923
|
await this.client.updateAgentProgress(card.id, {
|
|
8418
8924
|
agentIdentifier: agentIdentifier(this.id),
|
|
@@ -9214,6 +9720,7 @@ class Worker {
|
|
|
9214
9720
|
}
|
|
9215
9721
|
parser.on("text", (content) => {
|
|
9216
9722
|
this.lastRunText += content;
|
|
9723
|
+
this.captureCliSessionId(parser.sessionId);
|
|
9217
9724
|
});
|
|
9218
9725
|
parser.on("parse_error", (msg) => {
|
|
9219
9726
|
log.debug(this.tag, `Stream parse error (non-fatal): ${msg}`);
|
|
@@ -9232,8 +9739,7 @@ class Worker {
|
|
|
9232
9739
|
this.process.on("close", (code) => {
|
|
9233
9740
|
const leaderPid = this.process?.pid;
|
|
9234
9741
|
this.process = null;
|
|
9235
|
-
|
|
9236
|
-
this.cliSessionId = parser.sessionId;
|
|
9742
|
+
this.captureCliSessionId(parser.sessionId);
|
|
9237
9743
|
this.lastSessionStats = this.progressTracker?.stats;
|
|
9238
9744
|
const spawnCost = this.lastSessionStats?.cost;
|
|
9239
9745
|
if (spawnCost) {
|
|
@@ -9325,7 +9831,7 @@ class Worker {
|
|
|
9325
9831
|
`);
|
|
9326
9832
|
}
|
|
9327
9833
|
} finally {
|
|
9328
|
-
this.
|
|
9834
|
+
this.captureCliSessionId(runner.sessionId);
|
|
9329
9835
|
this.lastSessionStats = this.progressTracker?.stats;
|
|
9330
9836
|
const spawnCost = this.lastSessionStats?.cost;
|
|
9331
9837
|
if (spawnCost) {
|
|
@@ -9387,7 +9893,7 @@ class Worker {
|
|
|
9387
9893
|
this.runTurns = 0;
|
|
9388
9894
|
}
|
|
9389
9895
|
}
|
|
9390
|
-
var
|
|
9896
|
+
var TAG31 = "worker", CANCEL_SIGINT_TIMEOUT2 = 30000, CANCEL_SIGTERM_TIMEOUT2 = 1e4, STEERING_MAX_TURNS = 15, MAX_STEERING_ITERATIONS = 10, PLAN_ALLOWED_TOOLS = "Read,Grep,Glob,mcp__harmony__*", IMPLEMENT_ALLOWED_TOOLS = "Bash,Read,Write,Edit,Glob,Grep,Agent,mcp__harmony__*", PLAN_PHASE_TIMEOUT;
|
|
9391
9897
|
var init_worker = __esm(() => {
|
|
9392
9898
|
init_dist();
|
|
9393
9899
|
init_board_helpers();
|
|
@@ -9465,41 +9971,41 @@ class Pool {
|
|
|
9465
9971
|
}
|
|
9466
9972
|
async enqueue(card, column, labels, subtasks, mode = "implement") {
|
|
9467
9973
|
if (this.isCardKnown(card.id) || this.reservations.has(card.id)) {
|
|
9468
|
-
log.debug(
|
|
9974
|
+
log.debug(TAG32, `Card ${card.id} already queued, active, or reserved, skipping`);
|
|
9469
9975
|
return;
|
|
9470
9976
|
}
|
|
9471
9977
|
this.reservations.add(card.id);
|
|
9472
9978
|
try {
|
|
9473
9979
|
if (mode === "implement") {
|
|
9474
9980
|
if (this.authPaused) {
|
|
9475
|
-
log.debug(
|
|
9981
|
+
log.debug(TAG32, `#${card.short_id} held — agent paused (auth error)`);
|
|
9476
9982
|
await this.emitWaiting(card.id, "Agent paused — Anthropic auth error, check API credentials");
|
|
9477
9983
|
return;
|
|
9478
9984
|
}
|
|
9479
9985
|
const cooldownMs = this.apiCooldownRemainingMs();
|
|
9480
9986
|
if (cooldownMs > 0) {
|
|
9481
|
-
log.debug(
|
|
9987
|
+
log.debug(TAG32, `#${card.short_id} held — API cooldown ${Math.round(cooldownMs / 1000)}s remaining`);
|
|
9482
9988
|
await this.emitWaiting(card.id, `Paused — Anthropic API limit, retrying in ~${Math.round(cooldownMs / 1000)}s`);
|
|
9483
9989
|
return;
|
|
9484
9990
|
}
|
|
9485
9991
|
const decision = this.budget.check(card.id);
|
|
9486
9992
|
if (!decision.allow) {
|
|
9487
9993
|
if (decision.reason === "daily_budget") {
|
|
9488
|
-
log.warn(
|
|
9994
|
+
log.warn(TAG32, `#${card.short_id} skipped (daily_budget): ${decision.detail}`);
|
|
9489
9995
|
await this.emitWaiting(card.id, `Daily budget reached — waiting for reset (${decision.detail})`);
|
|
9490
9996
|
} else {
|
|
9491
|
-
log.debug(
|
|
9997
|
+
log.debug(TAG32, `#${card.short_id} gave up: ${decision.detail}`);
|
|
9492
9998
|
}
|
|
9493
9999
|
return;
|
|
9494
10000
|
}
|
|
9495
10001
|
const blockers = await getUnresolvedBlockers(this.client, card, this.projectId);
|
|
9496
10002
|
if (blockers === null) {
|
|
9497
|
-
log.warn(
|
|
10003
|
+
log.warn(TAG32, `#${card.short_id} blocker check failed — deferring to next tick`);
|
|
9498
10004
|
return;
|
|
9499
10005
|
}
|
|
9500
10006
|
if (blockers.length > 0) {
|
|
9501
10007
|
const list = blockers.map((b) => `#${b.shortId}`).join(", ");
|
|
9502
|
-
log.info(
|
|
10008
|
+
log.info(TAG32, `#${card.short_id} blocked by ${list} — waiting`);
|
|
9503
10009
|
await this.emitWaiting(card.id, `Blocked by ${list} — waiting for chain`);
|
|
9504
10010
|
return;
|
|
9505
10011
|
}
|
|
@@ -9531,7 +10037,7 @@ class Pool {
|
|
|
9531
10037
|
});
|
|
9532
10038
|
this.lastWaitingEmit.set(cardId, currentTask);
|
|
9533
10039
|
} catch (err) {
|
|
9534
|
-
log.debug(
|
|
10040
|
+
log.debug(TAG32, `waiting emit failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
9535
10041
|
}
|
|
9536
10042
|
}
|
|
9537
10043
|
noteApiError(err) {
|
|
@@ -9539,7 +10045,7 @@ class Pool {
|
|
|
9539
10045
|
return;
|
|
9540
10046
|
if (err.kind === "auth") {
|
|
9541
10047
|
if (!this.authPaused) {
|
|
9542
|
-
log.error(
|
|
10048
|
+
log.error(TAG32, "Auth error from Claude CLI — pausing implement pickups until the daemon is restarted with valid credentials");
|
|
9543
10049
|
}
|
|
9544
10050
|
this.authPaused = true;
|
|
9545
10051
|
return;
|
|
@@ -9548,7 +10054,7 @@ class Pool {
|
|
|
9548
10054
|
const until = Date.now() + cooldownMs;
|
|
9549
10055
|
if (until > this.apiCooldownUntil) {
|
|
9550
10056
|
this.apiCooldownUntil = until;
|
|
9551
|
-
log.warn(
|
|
10057
|
+
log.warn(TAG32, `${describeApiError(err.kind)} — pausing implement pickups for ${Math.round(cooldownMs / 1000)}s`);
|
|
9552
10058
|
}
|
|
9553
10059
|
}
|
|
9554
10060
|
apiCooldownRemainingMs() {
|
|
@@ -9562,13 +10068,13 @@ class Pool {
|
|
|
9562
10068
|
const removed = queue.remove(cardId);
|
|
9563
10069
|
if (removed) {
|
|
9564
10070
|
this.cardDataCache.delete(cardId);
|
|
9565
|
-
log.info(
|
|
10071
|
+
log.info(TAG32, `Removed #${removed.shortId} from ${removed.mode} queue`);
|
|
9566
10072
|
return;
|
|
9567
10073
|
}
|
|
9568
10074
|
}
|
|
9569
10075
|
const worker = this.implWorkers.find((w) => w.cardId === cardId) ?? this.reviewWorkers.find((w) => w.cardId === cardId);
|
|
9570
10076
|
if (worker) {
|
|
9571
|
-
log.info(
|
|
10077
|
+
log.info(TAG32, `Cancelling worker ${worker.id} for card ${cardId}`);
|
|
9572
10078
|
await worker.cancel("unassigned");
|
|
9573
10079
|
}
|
|
9574
10080
|
}
|
|
@@ -9601,10 +10107,10 @@ class Pool {
|
|
|
9601
10107
|
async handleAgentCommand(cardId, command) {
|
|
9602
10108
|
const worker = this.implWorkers.find((w) => w.cardId === cardId && w.isActive) ?? this.reviewWorkers.find((w) => w.cardId === cardId && w.isActive);
|
|
9603
10109
|
if (!worker) {
|
|
9604
|
-
log.debug(
|
|
10110
|
+
log.debug(TAG32, `No active worker for card ${cardId}, ignoring ${command}`);
|
|
9605
10111
|
return;
|
|
9606
10112
|
}
|
|
9607
|
-
log.info(
|
|
10113
|
+
log.info(TAG32, `Agent command: ${command} → worker ${worker.id} (card ${cardId})`);
|
|
9608
10114
|
switch (command) {
|
|
9609
10115
|
case "pause":
|
|
9610
10116
|
await worker.pause();
|
|
@@ -9652,7 +10158,7 @@ class Pool {
|
|
|
9652
10158
|
};
|
|
9653
10159
|
}
|
|
9654
10160
|
async shutdown() {
|
|
9655
|
-
log.info(
|
|
10161
|
+
log.info(TAG32, "Shutting down pool...");
|
|
9656
10162
|
this.shuttingDown = true;
|
|
9657
10163
|
const active = [
|
|
9658
10164
|
...this.implWorkers.filter((w) => w.isActive),
|
|
@@ -9660,7 +10166,7 @@ class Pool {
|
|
|
9660
10166
|
];
|
|
9661
10167
|
await Promise.all(active.map((w) => w.cancel("shutdown")));
|
|
9662
10168
|
this.sleepGuard.stop();
|
|
9663
|
-
log.info(
|
|
10169
|
+
log.info(TAG32, "Pool shutdown complete");
|
|
9664
10170
|
}
|
|
9665
10171
|
reservations = new Set;
|
|
9666
10172
|
cardDataCache = new Map;
|
|
@@ -9669,7 +10175,7 @@ class Pool {
|
|
|
9669
10175
|
return false;
|
|
9670
10176
|
const idle = workers.find((w) => w.isIdle);
|
|
9671
10177
|
if (!idle) {
|
|
9672
|
-
log.debug(
|
|
10178
|
+
log.debug(TAG32, `No idle ${label} workers (queue: ${queue.length})`);
|
|
9673
10179
|
return false;
|
|
9674
10180
|
}
|
|
9675
10181
|
const next = queue.dequeue();
|
|
@@ -9677,18 +10183,18 @@ class Pool {
|
|
|
9677
10183
|
return false;
|
|
9678
10184
|
const data = this.cardDataCache.get(next.cardId);
|
|
9679
10185
|
if (!data) {
|
|
9680
|
-
log.warn(
|
|
10186
|
+
log.warn(TAG32, `No cached data for card ${next.cardId}, skipping`);
|
|
9681
10187
|
return false;
|
|
9682
10188
|
}
|
|
9683
10189
|
this.cardDataCache.delete(next.cardId);
|
|
9684
10190
|
this.lastWaitingEmit.delete(next.cardId);
|
|
9685
|
-
log.info(
|
|
10191
|
+
log.info(TAG32, `Dispatching #${next.shortId} to ${label} worker ${idle.id}`);
|
|
9686
10192
|
this.sleepGuard.acquire();
|
|
9687
10193
|
idle.run(data.card, data.column, data.labels, data.subtasks);
|
|
9688
10194
|
return true;
|
|
9689
10195
|
}
|
|
9690
10196
|
}
|
|
9691
|
-
var
|
|
10197
|
+
var TAG32 = "pool";
|
|
9692
10198
|
var init_pool = __esm(() => {
|
|
9693
10199
|
init_error_classifier();
|
|
9694
10200
|
init_log();
|
|
@@ -9730,7 +10236,7 @@ function load(path) {
|
|
|
9730
10236
|
return parsed;
|
|
9731
10237
|
return {};
|
|
9732
10238
|
} catch (err) {
|
|
9733
|
-
log.warn(
|
|
10239
|
+
log.warn(TAG33, `failed to read ${path}: ${err instanceof Error ? err.message : err}`);
|
|
9734
10240
|
return {};
|
|
9735
10241
|
}
|
|
9736
10242
|
}
|
|
@@ -9748,7 +10254,7 @@ function recordDaemonPort(projectId, entry, path = defaultRegistryPath()) {
|
|
|
9748
10254
|
registry[projectId] = { ...entry, updatedAt: Date.now() };
|
|
9749
10255
|
save(path, registry);
|
|
9750
10256
|
} catch (err) {
|
|
9751
|
-
log.warn(
|
|
10257
|
+
log.warn(TAG33, `failed to record port for ${projectId}: ${err instanceof Error ? err.message : err}`);
|
|
9752
10258
|
}
|
|
9753
10259
|
}
|
|
9754
10260
|
function lookupDaemonPort(projectId, path = defaultRegistryPath()) {
|
|
@@ -9764,10 +10270,10 @@ function clearDaemonPort(projectId, pid, path = defaultRegistryPath()) {
|
|
|
9764
10270
|
delete registry[projectId];
|
|
9765
10271
|
save(path, registry);
|
|
9766
10272
|
} catch (err) {
|
|
9767
|
-
log.warn(
|
|
10273
|
+
log.warn(TAG33, `failed to clear port for ${projectId}: ${err instanceof Error ? err.message : err}`);
|
|
9768
10274
|
}
|
|
9769
10275
|
}
|
|
9770
|
-
var
|
|
10276
|
+
var TAG33 = "port-registry";
|
|
9771
10277
|
var init_port_registry = __esm(() => {
|
|
9772
10278
|
init_log();
|
|
9773
10279
|
});
|
|
@@ -9788,7 +10294,7 @@ async function fetchCardSafely(client, cardId) {
|
|
|
9788
10294
|
const { card } = await client.getCard(cardId);
|
|
9789
10295
|
return card;
|
|
9790
10296
|
} catch (err) {
|
|
9791
|
-
log.warn(
|
|
10297
|
+
log.warn(TAG34, `cannot fetch card ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
9792
10298
|
return null;
|
|
9793
10299
|
}
|
|
9794
10300
|
}
|
|
@@ -9798,7 +10304,7 @@ async function recoverOrphans(store, client, config) {
|
|
|
9798
10304
|
return [];
|
|
9799
10305
|
}
|
|
9800
10306
|
const outcomes = [];
|
|
9801
|
-
log.info(
|
|
10307
|
+
log.info(TAG34, `recovering ${active.length} orphan run(s) from prior daemon`);
|
|
9802
10308
|
for (const run of active) {
|
|
9803
10309
|
const outcome = {
|
|
9804
10310
|
runId: run.runId,
|
|
@@ -9810,16 +10316,18 @@ async function recoverOrphans(store, client, config) {
|
|
|
9810
10316
|
};
|
|
9811
10317
|
outcomes.push(outcome);
|
|
9812
10318
|
if (isProcessAlive(run.daemonPid, process.pid)) {
|
|
9813
|
-
log.warn(
|
|
10319
|
+
log.warn(TAG34, `run ${run.runId} claims live daemon pid ${run.daemonPid} — skipping`);
|
|
9814
10320
|
outcome.actions.push("skipped: daemon pid still alive");
|
|
9815
10321
|
continue;
|
|
9816
10322
|
}
|
|
9817
|
-
log.info(
|
|
9818
|
-
await recoverRun(run, store, client, config, outcome
|
|
10323
|
+
log.info(TAG34, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
|
|
10324
|
+
await recoverRun(run, store, client, config, outcome, {
|
|
10325
|
+
rollbackAttempt: true
|
|
10326
|
+
});
|
|
9819
10327
|
}
|
|
9820
10328
|
return outcomes;
|
|
9821
10329
|
}
|
|
9822
|
-
async function recoverRun(run, store, client, config, outcome) {
|
|
10330
|
+
async function recoverRun(run, store, client, config, outcome, opts = {}) {
|
|
9823
10331
|
try {
|
|
9824
10332
|
await client.endAgentSession(run.cardId, {
|
|
9825
10333
|
status: "failed",
|
|
@@ -9832,7 +10340,7 @@ async function recoverRun(run, store, client, config, outcome) {
|
|
|
9832
10340
|
} catch (err) {
|
|
9833
10341
|
const msg = err instanceof Error ? err.message : String(err);
|
|
9834
10342
|
outcome.errors.push(`endAgentSession: ${msg}`);
|
|
9835
|
-
log.warn(
|
|
10343
|
+
log.warn(TAG34, `endAgentSession failed for ${run.cardId}: ${msg}`);
|
|
9836
10344
|
}
|
|
9837
10345
|
const card = await fetchCardSafely(client, run.cardId);
|
|
9838
10346
|
if (card) {
|
|
@@ -9875,9 +10383,18 @@ async function recoverRun(run, store, client, config, outcome) {
|
|
|
9875
10383
|
const msg = err instanceof Error ? err.message : String(err);
|
|
9876
10384
|
outcome.errors.push(`endRun: ${msg}`);
|
|
9877
10385
|
}
|
|
9878
|
-
|
|
10386
|
+
if (opts.rollbackAttempt && run.pipeline === "implement") {
|
|
10387
|
+
try {
|
|
10388
|
+
await store.decrementAttempt(run.cardId);
|
|
10389
|
+
outcome.actions.push("rolled back give-up attempt (daemon restart)");
|
|
10390
|
+
} catch (err) {
|
|
10391
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
10392
|
+
outcome.errors.push(`decrementAttempt: ${msg}`);
|
|
10393
|
+
}
|
|
10394
|
+
}
|
|
10395
|
+
log.info(TAG34, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
|
|
9879
10396
|
}
|
|
9880
|
-
var
|
|
10397
|
+
var TAG34 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
|
|
9881
10398
|
var init_recovery = __esm(() => {
|
|
9882
10399
|
init_board_helpers();
|
|
9883
10400
|
init_log();
|
|
@@ -9888,14 +10405,14 @@ var init_recovery = __esm(() => {
|
|
|
9888
10405
|
async function claimReviewCard(client, cardId, agentId) {
|
|
9889
10406
|
try {
|
|
9890
10407
|
const { claimed } = await client.claimCard(cardId, agentId);
|
|
9891
|
-
log.debug(
|
|
10408
|
+
log.debug(TAG35, `claim ${cardId} → ${claimed ? "won" : "lost"}`);
|
|
9892
10409
|
return claimed;
|
|
9893
10410
|
} catch (err) {
|
|
9894
|
-
log.error(
|
|
10411
|
+
log.error(TAG35, `claim ${cardId} failed: ${err instanceof Error ? err.message : err}`);
|
|
9895
10412
|
return false;
|
|
9896
10413
|
}
|
|
9897
10414
|
}
|
|
9898
|
-
var
|
|
10415
|
+
var TAG35 = "claim";
|
|
9899
10416
|
var init_claim = __esm(() => {
|
|
9900
10417
|
init_log();
|
|
9901
10418
|
});
|
|
@@ -9948,22 +10465,22 @@ async function reclaimPreReviewStrands(opts) {
|
|
|
9948
10465
|
continue;
|
|
9949
10466
|
const won = await claimReviewCard(client, card.id, agentId);
|
|
9950
10467
|
if (!won) {
|
|
9951
|
-
log.debug(
|
|
10468
|
+
log.debug(TAG36, `#${card.short_id} — lost the review claim race, skipping`);
|
|
9952
10469
|
continue;
|
|
9953
10470
|
}
|
|
9954
|
-
log.warn(
|
|
10471
|
+
log.warn(TAG36, `#${card.short_id} claimed for review (branch pushed, no PR, unowned)`);
|
|
9955
10472
|
reclaimed.push(card.id);
|
|
9956
10473
|
if (opts.onClaimed) {
|
|
9957
10474
|
try {
|
|
9958
10475
|
await opts.onClaimed(card);
|
|
9959
10476
|
} catch (err) {
|
|
9960
|
-
log.error(
|
|
10477
|
+
log.error(TAG36, `onClaimed for #${card.short_id} failed: ${err instanceof Error ? err.message : err}`);
|
|
9961
10478
|
}
|
|
9962
10479
|
}
|
|
9963
10480
|
}
|
|
9964
10481
|
return reclaimed;
|
|
9965
10482
|
}
|
|
9966
|
-
var
|
|
10483
|
+
var TAG36 = "strand-recovery";
|
|
9967
10484
|
var init_strand_recovery = __esm(() => {
|
|
9968
10485
|
init_board_helpers();
|
|
9969
10486
|
init_claim();
|
|
@@ -10015,7 +10532,7 @@ class Reconciler {
|
|
|
10015
10532
|
clearInterval(this.timer);
|
|
10016
10533
|
this.timer = null;
|
|
10017
10534
|
}
|
|
10018
|
-
log.info(
|
|
10535
|
+
log.info(TAG37, "Heartbeat stopped");
|
|
10019
10536
|
}
|
|
10020
10537
|
async recoverStaleRuns() {
|
|
10021
10538
|
if (!this.stateStore || !this.agentConfig)
|
|
@@ -10032,7 +10549,7 @@ class Reconciler {
|
|
|
10032
10549
|
if (!daemonDead && !(heartbeatStale && ourZombie))
|
|
10033
10550
|
continue;
|
|
10034
10551
|
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`;
|
|
10035
|
-
log.warn(
|
|
10552
|
+
log.warn(TAG37, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
|
|
10036
10553
|
await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
|
|
10037
10554
|
runId: run.runId,
|
|
10038
10555
|
cardId: run.cardId,
|
|
@@ -10040,7 +10557,7 @@ class Reconciler {
|
|
|
10040
10557
|
pipeline: run.pipeline,
|
|
10041
10558
|
actions: [],
|
|
10042
10559
|
errors: []
|
|
10043
|
-
});
|
|
10560
|
+
}, { rollbackAttempt: daemonDead });
|
|
10044
10561
|
}
|
|
10045
10562
|
}
|
|
10046
10563
|
async recoverStrandedInProgress(cards, columns, knownCardIds) {
|
|
@@ -10059,11 +10576,11 @@ class Reconciler {
|
|
|
10059
10576
|
const stalledAt = Date.parse(card.updated_at ?? "");
|
|
10060
10577
|
if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
|
|
10061
10578
|
continue;
|
|
10062
|
-
log.warn(
|
|
10579
|
+
log.warn(TAG37, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
|
|
10063
10580
|
try {
|
|
10064
10581
|
await this.client.moveCard(card.id, pickupCol.id);
|
|
10065
10582
|
} catch (err) {
|
|
10066
|
-
log.error(
|
|
10583
|
+
log.error(TAG37, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
10067
10584
|
}
|
|
10068
10585
|
}
|
|
10069
10586
|
}
|
|
@@ -10095,7 +10612,7 @@ class Reconciler {
|
|
|
10095
10612
|
return;
|
|
10096
10613
|
const cardLabels = resolveCardLabels(card, labelMap);
|
|
10097
10614
|
const subtasks = card.subtasks ?? [];
|
|
10098
|
-
log.info(
|
|
10615
|
+
log.info(TAG37, `Enqueuing claimed review card #${card.short_id} (agent-agnostic pickup)`);
|
|
10099
10616
|
await this.pool.enqueue(card, column, cardLabels, subtasks, "review");
|
|
10100
10617
|
}
|
|
10101
10618
|
});
|
|
@@ -10119,11 +10636,11 @@ class Reconciler {
|
|
|
10119
10636
|
const parkedAt = Date.parse(card.updated_at ?? "");
|
|
10120
10637
|
if (!Number.isFinite(parkedAt) || now - parkedAt < ttlMs)
|
|
10121
10638
|
continue;
|
|
10122
|
-
log.warn(
|
|
10639
|
+
log.warn(TAG37, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
|
|
10123
10640
|
try {
|
|
10124
10641
|
await this.client.moveCard(card.id, pickupCol.id);
|
|
10125
10642
|
} catch (err) {
|
|
10126
|
-
log.error(
|
|
10643
|
+
log.error(TAG37, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
10127
10644
|
}
|
|
10128
10645
|
}
|
|
10129
10646
|
}
|
|
@@ -10166,21 +10683,21 @@ class Reconciler {
|
|
|
10166
10683
|
const subtasks = card.subtasks ?? [];
|
|
10167
10684
|
const mode = route.mode;
|
|
10168
10685
|
if (route.stage) {
|
|
10169
|
-
log.info(
|
|
10686
|
+
log.info(TAG37, `Stage card #${card.short_id} (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement) regardless of column`);
|
|
10170
10687
|
}
|
|
10171
10688
|
if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
|
|
10172
|
-
log.debug(
|
|
10689
|
+
log.debug(TAG37, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
|
|
10173
10690
|
continue;
|
|
10174
10691
|
}
|
|
10175
10692
|
if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
|
|
10176
|
-
log.debug(
|
|
10693
|
+
log.debug(TAG37, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
|
|
10177
10694
|
continue;
|
|
10178
10695
|
}
|
|
10179
10696
|
if (mode === "review" && !qualifiesForAutoReview(card.description)) {
|
|
10180
|
-
log.debug(
|
|
10697
|
+
log.debug(TAG37, `Skipping #${card.short_id} — no branch or PR reference (not qualified for auto-review)`);
|
|
10181
10698
|
continue;
|
|
10182
10699
|
}
|
|
10183
|
-
log.info(
|
|
10700
|
+
log.info(TAG37, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
|
|
10184
10701
|
await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
10185
10702
|
}
|
|
10186
10703
|
}
|
|
@@ -10191,18 +10708,18 @@ class Reconciler {
|
|
|
10191
10708
|
await this.recoverStrandedReview(cards, columns, labelMap, knownCardIds);
|
|
10192
10709
|
for (const knownId of knownCardIds) {
|
|
10193
10710
|
if (!allAgentCardIds.has(knownId)) {
|
|
10194
|
-
log.info(
|
|
10711
|
+
log.info(TAG37, `Missed unassign: ${knownId} — removing`);
|
|
10195
10712
|
await this.pool.removeCard(knownId);
|
|
10196
10713
|
}
|
|
10197
10714
|
}
|
|
10198
10715
|
await this.releaseStalledApprovals(cards, columns, knownCardIds);
|
|
10199
|
-
log.debug(
|
|
10716
|
+
log.debug(TAG37, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
|
|
10200
10717
|
} catch (err) {
|
|
10201
|
-
log.error(
|
|
10718
|
+
log.error(TAG37, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
|
|
10202
10719
|
}
|
|
10203
10720
|
}
|
|
10204
10721
|
}
|
|
10205
|
-
var
|
|
10722
|
+
var TAG37 = "reconcile";
|
|
10206
10723
|
var init_reconcile = __esm(() => {
|
|
10207
10724
|
init_board_helpers();
|
|
10208
10725
|
init_git_pr();
|
|
@@ -10242,7 +10759,7 @@ function prettyBanner(config, version) {
|
|
|
10242
10759
|
checks.push({ kind: "ok", message });
|
|
10243
10760
|
},
|
|
10244
10761
|
warn(message) {
|
|
10245
|
-
log.warn(
|
|
10762
|
+
log.warn(TAG38, message);
|
|
10246
10763
|
checks.push({ kind: "warn", message: message.split(`
|
|
10247
10764
|
`, 1)[0] });
|
|
10248
10765
|
},
|
|
@@ -10267,25 +10784,25 @@ function prettyBanner(config, version) {
|
|
|
10267
10784
|
};
|
|
10268
10785
|
}
|
|
10269
10786
|
function jsonBanner(config, version) {
|
|
10270
|
-
log.info(
|
|
10271
|
-
log.info(
|
|
10787
|
+
log.info(TAG38, `Harmony Agent Daemon v${version} starting...`);
|
|
10788
|
+
log.info(TAG38, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Runner: ${config.agent.runner} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
|
|
10272
10789
|
if (config.agent.review.enabled) {
|
|
10273
|
-
log.info(
|
|
10790
|
+
log.info(TAG38, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
|
|
10274
10791
|
}
|
|
10275
10792
|
let failed = false;
|
|
10276
10793
|
return {
|
|
10277
10794
|
setProjectName(_name) {},
|
|
10278
10795
|
setGitProvider(provider) {
|
|
10279
|
-
log.info(
|
|
10796
|
+
log.info(TAG38, `Git provider: ${provider}`);
|
|
10280
10797
|
},
|
|
10281
10798
|
setHttpPort(port) {
|
|
10282
|
-
log.info(
|
|
10799
|
+
log.info(TAG38, `HTTP server on port ${port}`);
|
|
10283
10800
|
},
|
|
10284
10801
|
check(message) {
|
|
10285
|
-
log.info(
|
|
10802
|
+
log.info(TAG38, message);
|
|
10286
10803
|
},
|
|
10287
10804
|
warn(message) {
|
|
10288
|
-
log.warn(
|
|
10805
|
+
log.warn(TAG38, message);
|
|
10289
10806
|
},
|
|
10290
10807
|
fail() {
|
|
10291
10808
|
failed = true;
|
|
@@ -10293,7 +10810,7 @@ function jsonBanner(config, version) {
|
|
|
10293
10810
|
async ready(message) {
|
|
10294
10811
|
if (failed)
|
|
10295
10812
|
return;
|
|
10296
|
-
log.info(
|
|
10813
|
+
log.info(TAG38, message);
|
|
10297
10814
|
}
|
|
10298
10815
|
};
|
|
10299
10816
|
}
|
|
@@ -10374,7 +10891,7 @@ function cyan(s) {
|
|
|
10374
10891
|
function yellow(s) {
|
|
10375
10892
|
return `${ANSI.yellow}${s}${ANSI.reset}`;
|
|
10376
10893
|
}
|
|
10377
|
-
var
|
|
10894
|
+
var TAG38 = "daemon", RULE_WIDTH = 70, ANSI;
|
|
10378
10895
|
var init_startup_banner = __esm(() => {
|
|
10379
10896
|
init_log();
|
|
10380
10897
|
ANSI = {
|
|
@@ -10525,13 +11042,13 @@ class Watcher {
|
|
|
10525
11042
|
}
|
|
10526
11043
|
async start() {
|
|
10527
11044
|
if (!isPretty()) {
|
|
10528
|
-
log.info(
|
|
11045
|
+
log.info(TAG39, "Connecting to Supabase realtime (broadcast)...");
|
|
10529
11046
|
}
|
|
10530
11047
|
this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
|
|
10531
11048
|
const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
|
|
10532
11049
|
this.subscribeBroadcast();
|
|
10533
11050
|
presenceChannel.on("presence", { event: "sync" }, () => {
|
|
10534
|
-
log.debug(
|
|
11051
|
+
log.debug(TAG39, "Presence sync");
|
|
10535
11052
|
}).subscribe(async (status) => {
|
|
10536
11053
|
if (status === "SUBSCRIBED") {
|
|
10537
11054
|
await presenceChannel.track({
|
|
@@ -10544,7 +11061,7 @@ class Watcher {
|
|
|
10544
11061
|
agentName: this.identity.agentName
|
|
10545
11062
|
});
|
|
10546
11063
|
if (!isPretty() || !this.suppressStartupLogs) {
|
|
10547
|
-
log.info(
|
|
11064
|
+
log.info(TAG39, "Presence tracked on board-presence channel");
|
|
10548
11065
|
}
|
|
10549
11066
|
this.presenceTracked = true;
|
|
10550
11067
|
this.maybeResolveReady();
|
|
@@ -10557,13 +11074,13 @@ class Watcher {
|
|
|
10557
11074
|
return;
|
|
10558
11075
|
const gen = ++this.broadcastGen;
|
|
10559
11076
|
this.channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
|
|
10560
|
-
log.debug(
|
|
11077
|
+
log.debug(TAG39, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
|
|
10561
11078
|
this.onCardBroadcast({
|
|
10562
11079
|
event: "card_update",
|
|
10563
11080
|
payload: msg.payload ?? {}
|
|
10564
11081
|
});
|
|
10565
11082
|
}).on("broadcast", { event: "card_created" }, (msg) => {
|
|
10566
|
-
log.debug(
|
|
11083
|
+
log.debug(TAG39, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
|
|
10567
11084
|
this.onCardBroadcast({
|
|
10568
11085
|
event: "card_created",
|
|
10569
11086
|
payload: msg.payload ?? {}
|
|
@@ -10573,7 +11090,7 @@ class Watcher {
|
|
|
10573
11090
|
const cardId = payload.card_id;
|
|
10574
11091
|
const command = payload.command;
|
|
10575
11092
|
if (cardId && command) {
|
|
10576
|
-
log.info(
|
|
11093
|
+
log.info(TAG39, `Broadcast: agent_command ${command} for ${cardId}`);
|
|
10577
11094
|
this.onAgentCommand?.({ cardId, command });
|
|
10578
11095
|
}
|
|
10579
11096
|
}).subscribe((status) => {
|
|
@@ -10583,13 +11100,13 @@ class Watcher {
|
|
|
10583
11100
|
this.connected = true;
|
|
10584
11101
|
this.reconnectAttempts = 0;
|
|
10585
11102
|
if (!isPretty() || !this.suppressStartupLogs) {
|
|
10586
|
-
log.info(
|
|
11103
|
+
log.info(TAG39, "Broadcast subscription active");
|
|
10587
11104
|
}
|
|
10588
11105
|
this.maybeResolveReady();
|
|
10589
11106
|
} else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
|
|
10590
11107
|
this.connected = false;
|
|
10591
11108
|
if (!this.stopping) {
|
|
10592
|
-
log.warn(
|
|
11109
|
+
log.warn(TAG39, `Broadcast subscription ${status} — scheduling reconnect`);
|
|
10593
11110
|
this.scheduleReconnect();
|
|
10594
11111
|
}
|
|
10595
11112
|
}
|
|
@@ -10608,7 +11125,7 @@ class Watcher {
|
|
|
10608
11125
|
async reconnectBroadcast() {
|
|
10609
11126
|
if (this.stopping || !this.supabase)
|
|
10610
11127
|
return;
|
|
10611
|
-
log.warn(
|
|
11128
|
+
log.warn(TAG39, `Reconnecting broadcast subscription (attempt ${this.reconnectAttempts})`);
|
|
10612
11129
|
if (this.channel) {
|
|
10613
11130
|
const old = this.channel;
|
|
10614
11131
|
this.channel = null;
|
|
@@ -10638,10 +11155,10 @@ class Watcher {
|
|
|
10638
11155
|
this.supabase = null;
|
|
10639
11156
|
}
|
|
10640
11157
|
this.connected = false;
|
|
10641
|
-
log.info(
|
|
11158
|
+
log.info(TAG39, "Broadcast subscription stopped");
|
|
10642
11159
|
}
|
|
10643
11160
|
}
|
|
10644
|
-
var
|
|
11161
|
+
var TAG39 = "watcher";
|
|
10645
11162
|
var init_watcher = __esm(() => {
|
|
10646
11163
|
init_log();
|
|
10647
11164
|
});
|
|
@@ -10728,10 +11245,10 @@ function runWorktreeGc(basePath, store, opts = {}) {
|
|
|
10728
11245
|
});
|
|
10729
11246
|
} catch {}
|
|
10730
11247
|
if (result.removed.length > 0) {
|
|
10731
|
-
log.info(
|
|
11248
|
+
log.info(TAG40, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
|
|
10732
11249
|
}
|
|
10733
11250
|
if (result.errors.length > 0) {
|
|
10734
|
-
log.warn(
|
|
11251
|
+
log.warn(TAG40, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
|
|
10735
11252
|
}
|
|
10736
11253
|
return result;
|
|
10737
11254
|
}
|
|
@@ -10761,7 +11278,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
10761
11278
|
} catch (err) {
|
|
10762
11279
|
const detail = gitErrorDetail2(err);
|
|
10763
11280
|
if (isTransientGitNetworkError(detail)) {
|
|
10764
|
-
log.debug(
|
|
11281
|
+
log.debug(TAG40, `Remote branch GC skipped — remote unreachable: ${detail}`);
|
|
10765
11282
|
return result;
|
|
10766
11283
|
}
|
|
10767
11284
|
result.errors.push({ ref: "fetch", error: detail });
|
|
@@ -10800,7 +11317,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
10800
11317
|
continue;
|
|
10801
11318
|
}
|
|
10802
11319
|
if (clock() > sweepDeadline) {
|
|
10803
|
-
log.debug(
|
|
11320
|
+
log.debug(TAG40, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
|
|
10804
11321
|
break;
|
|
10805
11322
|
}
|
|
10806
11323
|
try {
|
|
@@ -10813,17 +11330,17 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
10813
11330
|
} catch (err) {
|
|
10814
11331
|
const detail = gitErrorDetail2(err);
|
|
10815
11332
|
if (isTransientGitNetworkError(detail)) {
|
|
10816
|
-
log.debug(
|
|
11333
|
+
log.debug(TAG40, `Remote branch GC interrupted — remote unreachable: ${detail}`);
|
|
10817
11334
|
break;
|
|
10818
11335
|
}
|
|
10819
11336
|
result.errors.push({ ref, error: detail });
|
|
10820
11337
|
}
|
|
10821
11338
|
}
|
|
10822
11339
|
if (result.removed.length > 0) {
|
|
10823
|
-
log.info(
|
|
11340
|
+
log.info(TAG40, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
|
|
10824
11341
|
}
|
|
10825
11342
|
if (result.errors.length > 0) {
|
|
10826
|
-
log.warn(
|
|
11343
|
+
log.warn(TAG40, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
|
|
10827
11344
|
}
|
|
10828
11345
|
return result;
|
|
10829
11346
|
}
|
|
@@ -10854,13 +11371,13 @@ class WorktreeGc {
|
|
|
10854
11371
|
try {
|
|
10855
11372
|
runWorktreeGc(this.basePath, this.store);
|
|
10856
11373
|
} catch (err) {
|
|
10857
|
-
log.warn(
|
|
11374
|
+
log.warn(TAG40, `GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
10858
11375
|
}
|
|
10859
11376
|
if (this.remoteOpts) {
|
|
10860
11377
|
try {
|
|
10861
11378
|
pruneFailedRemoteBranches(this.remoteOpts);
|
|
10862
11379
|
} catch (err) {
|
|
10863
|
-
log.warn(
|
|
11380
|
+
log.warn(TAG40, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
10864
11381
|
}
|
|
10865
11382
|
}
|
|
10866
11383
|
}
|
|
@@ -10874,7 +11391,7 @@ function getRepoRoot2() {
|
|
|
10874
11391
|
return null;
|
|
10875
11392
|
}
|
|
10876
11393
|
}
|
|
10877
|
-
var
|
|
11394
|
+
var TAG40 = "worktree-gc", GIT_NETWORK_TIMEOUT_MS = 30000, GIT_SSH_CONNECT_TIMEOUT_SECS = 10, GIT_PRUNE_SWEEP_BUDGET_MS = 60000, GIT_NETWORK_EXEC, TRANSIENT_GIT_NETWORK_ERROR;
|
|
10878
11395
|
var init_worktree_gc = __esm(() => {
|
|
10879
11396
|
init_log();
|
|
10880
11397
|
init_worktree();
|
|
@@ -10979,7 +11496,7 @@ async function main() {
|
|
|
10979
11496
|
} catch (err) {
|
|
10980
11497
|
if (err instanceof ConfigValidationError) {
|
|
10981
11498
|
banner.fail();
|
|
10982
|
-
log.error(
|
|
11499
|
+
log.error(TAG41, err.message);
|
|
10983
11500
|
process.exit(1);
|
|
10984
11501
|
}
|
|
10985
11502
|
throw err;
|
|
@@ -10989,7 +11506,7 @@ async function main() {
|
|
|
10989
11506
|
} catch (err) {
|
|
10990
11507
|
if (err instanceof ConfigValidationError) {
|
|
10991
11508
|
banner.fail();
|
|
10992
|
-
log.error(
|
|
11509
|
+
log.error(TAG41, err.message);
|
|
10993
11510
|
process.exit(1);
|
|
10994
11511
|
}
|
|
10995
11512
|
throw err;
|
|
@@ -11035,6 +11552,10 @@ async function main() {
|
|
|
11035
11552
|
prefix: config.agent.worktree.failedBranchPrefix,
|
|
11036
11553
|
retentionDays: config.agent.worktree.failedAttemptRetentionDays
|
|
11037
11554
|
} : undefined);
|
|
11555
|
+
let boardReviewer = null;
|
|
11556
|
+
if (config.agent.boardReview.enabled) {
|
|
11557
|
+
boardReviewer = new BoardReviewer(client, config.projectId, config.agent);
|
|
11558
|
+
}
|
|
11038
11559
|
const startedAt = Date.now();
|
|
11039
11560
|
const httpServer = config.agent.http.enabled ? new HttpServer({
|
|
11040
11561
|
port: config.agent.http.port,
|
|
@@ -11100,28 +11621,29 @@ async function main() {
|
|
|
11100
11621
|
if (shuttingDown)
|
|
11101
11622
|
return;
|
|
11102
11623
|
shuttingDown = true;
|
|
11103
|
-
log.info(
|
|
11624
|
+
log.info(TAG41, `Received ${signal}, shutting down gracefully...`);
|
|
11104
11625
|
reconciler.stop();
|
|
11105
11626
|
mergeMonitor?.stop();
|
|
11106
11627
|
worktreeGc.stop();
|
|
11628
|
+
boardReviewer?.stop();
|
|
11107
11629
|
if (httpServer) {
|
|
11108
11630
|
clearDaemonPort(config.projectId, process.pid);
|
|
11109
11631
|
await httpServer.stop();
|
|
11110
11632
|
}
|
|
11111
11633
|
await watcher.stop();
|
|
11112
11634
|
await pool.shutdown();
|
|
11113
|
-
log.info(
|
|
11635
|
+
log.info(TAG41, "Daemon stopped.");
|
|
11114
11636
|
process.exit(exitCode);
|
|
11115
11637
|
};
|
|
11116
11638
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
11117
11639
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
11118
11640
|
process.on("uncaughtException", (err) => {
|
|
11119
|
-
log.error(
|
|
11641
|
+
log.error(TAG41, `Uncaught exception: ${err.message}`);
|
|
11120
11642
|
exitCode = 1;
|
|
11121
11643
|
shutdown("uncaughtException");
|
|
11122
11644
|
});
|
|
11123
11645
|
process.on("unhandledRejection", (reason) => {
|
|
11124
|
-
log.error(
|
|
11646
|
+
log.error(TAG41, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
|
|
11125
11647
|
exitCode = 1;
|
|
11126
11648
|
shutdown("unhandledRejection");
|
|
11127
11649
|
});
|
|
@@ -11129,6 +11651,7 @@ async function main() {
|
|
|
11129
11651
|
reconciler.start();
|
|
11130
11652
|
mergeMonitor?.start();
|
|
11131
11653
|
worktreeGc.start();
|
|
11654
|
+
boardReviewer?.start();
|
|
11132
11655
|
if (httpServer) {
|
|
11133
11656
|
try {
|
|
11134
11657
|
const boundPort = await httpServer.start();
|
|
@@ -11151,6 +11674,11 @@ async function main() {
|
|
|
11151
11674
|
services.push("Merge monitor 60s");
|
|
11152
11675
|
}
|
|
11153
11676
|
services.push(`Worktree GC ${config.agent.timing.worktreeGcIntervalMs / 1000}s`);
|
|
11677
|
+
if (boardReviewer) {
|
|
11678
|
+
const br = config.agent.boardReview;
|
|
11679
|
+
const hhmm = `${String(br.runAtHour).padStart(2, "0")}:${String(br.runAtMinute).padStart(2, "0")}`;
|
|
11680
|
+
services.push(`Board review daily @ ${hhmm}`);
|
|
11681
|
+
}
|
|
11154
11682
|
banner.check(services.join(" · "));
|
|
11155
11683
|
const sleep = (ms) => new Promise((resolve4) => setTimeout(() => resolve4("timeout"), ms));
|
|
11156
11684
|
const winner = await Promise.race([
|
|
@@ -11174,29 +11702,29 @@ async function handleBroadcast(event, client, pool, config, agentId) {
|
|
|
11174
11702
|
if (assignedAgentId === undefined)
|
|
11175
11703
|
return;
|
|
11176
11704
|
if (assignedAgentId === agentId) {
|
|
11177
|
-
log.info(
|
|
11705
|
+
log.info(TAG41, `Broadcast: card ${cardId} assigned to agent`);
|
|
11178
11706
|
try {
|
|
11179
11707
|
await pool.resetAttemptsForReassign(cardId);
|
|
11180
11708
|
await tryEnqueueCard(cardId, client, pool, config, agentId);
|
|
11181
11709
|
} catch (err) {
|
|
11182
|
-
log.error(
|
|
11710
|
+
log.error(TAG41, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
|
|
11183
11711
|
}
|
|
11184
11712
|
} else if (pool.isCardKnown(cardId)) {
|
|
11185
|
-
log.info(
|
|
11713
|
+
log.info(TAG41, `Broadcast: card ${cardId} unassigned from agent`);
|
|
11186
11714
|
await pool.removeCard(cardId);
|
|
11187
11715
|
}
|
|
11188
11716
|
}
|
|
11189
11717
|
async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
11190
11718
|
const { card } = await client.getCard(cardId);
|
|
11191
11719
|
if (card.assigned_agent_id !== agentId) {
|
|
11192
|
-
log.debug(
|
|
11720
|
+
log.debug(TAG41, `Card ${cardId} no longer assigned to agent — skipping`);
|
|
11193
11721
|
return;
|
|
11194
11722
|
}
|
|
11195
11723
|
const board = await client.getBoard(config.projectId, { summary: true });
|
|
11196
11724
|
const columns = board.columns;
|
|
11197
11725
|
const column = columns.find((c) => c.id === card.column_id);
|
|
11198
11726
|
if (!column) {
|
|
11199
|
-
log.warn(
|
|
11727
|
+
log.warn(TAG41, `Column not found for card ${cardId}`);
|
|
11200
11728
|
return;
|
|
11201
11729
|
}
|
|
11202
11730
|
const route = classifyPickup(card, column.name, {
|
|
@@ -11205,33 +11733,34 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
|
11205
11733
|
playbooks: config.agent.playbooks
|
|
11206
11734
|
});
|
|
11207
11735
|
if (!route) {
|
|
11208
|
-
log.info(
|
|
11736
|
+
log.info(TAG41, `Card #${card.short_id} is in "${column.name}", not a pickup/review/stage column — skipping`);
|
|
11209
11737
|
return;
|
|
11210
11738
|
}
|
|
11211
11739
|
if (route.stage) {
|
|
11212
|
-
log.info(
|
|
11740
|
+
log.info(TAG41, `Card #${card.short_id} is a playbook stage card (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement pool) regardless of column`);
|
|
11213
11741
|
}
|
|
11214
11742
|
const mode = route.mode;
|
|
11215
11743
|
const labelMap = buildLabelMap(board.labels ?? []);
|
|
11216
11744
|
const cardLabels = resolveCardLabels(card, labelMap);
|
|
11217
11745
|
const subtasks = card.subtasks ?? [];
|
|
11218
11746
|
if (mode === "review" && config.agent.review.approvedLabel && hasLabel(cardLabels, config.agent.review.approvedLabel)) {
|
|
11219
|
-
log.debug(
|
|
11747
|
+
log.debug(TAG41, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
|
|
11220
11748
|
return;
|
|
11221
11749
|
}
|
|
11222
11750
|
if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
|
|
11223
|
-
log.debug(
|
|
11751
|
+
log.debug(TAG41, `Card #${card.short_id} has "${NEED_REVIEW_LABEL}" label (needs human) — skipping review`);
|
|
11224
11752
|
return;
|
|
11225
11753
|
}
|
|
11226
11754
|
if (mode === "review" && !qualifiesForAutoReview(card.description)) {
|
|
11227
|
-
log.info(
|
|
11755
|
+
log.info(TAG41, `Card #${card.short_id} has no branch or PR reference — skipping auto-review`);
|
|
11228
11756
|
return;
|
|
11229
11757
|
}
|
|
11230
11758
|
await pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
11231
11759
|
}
|
|
11232
|
-
var
|
|
11760
|
+
var TAG41 = "daemon", PKG_VERSION;
|
|
11233
11761
|
var init_src = __esm(() => {
|
|
11234
11762
|
init_board_helpers();
|
|
11763
|
+
init_board_reviewer();
|
|
11235
11764
|
init_config();
|
|
11236
11765
|
init_config_validation();
|
|
11237
11766
|
init_git_pr();
|
|
@@ -11253,8 +11782,269 @@ var init_src = __esm(() => {
|
|
|
11253
11782
|
({ version: PKG_VERSION } = createRequire2(import.meta.url)("../package.json"));
|
|
11254
11783
|
});
|
|
11255
11784
|
|
|
11785
|
+
// src/run-stats.ts
|
|
11786
|
+
var exports_run_stats = {};
|
|
11787
|
+
__export(exports_run_stats, {
|
|
11788
|
+
tailText: () => tailText,
|
|
11789
|
+
runsDir: () => runsDir,
|
|
11790
|
+
resolveRunTarget: () => resolveRunTarget,
|
|
11791
|
+
resolveRunRef: () => resolveRunRef,
|
|
11792
|
+
parseRunLogName: () => parseRunLogName,
|
|
11793
|
+
listRunLogFiles: () => listRunLogFiles,
|
|
11794
|
+
grepRunLogs: () => grepRunLogs,
|
|
11795
|
+
findLogByRunId: () => findLogByRunId,
|
|
11796
|
+
findLatestRunForCard: () => findLatestRunForCard,
|
|
11797
|
+
findLatestLogForCard: () => findLatestLogForCard,
|
|
11798
|
+
distribution: () => distribution,
|
|
11799
|
+
computeRunStats: () => computeRunStats,
|
|
11800
|
+
buildRunListRows: () => buildRunListRows
|
|
11801
|
+
});
|
|
11802
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync6, statSync as statSync3 } from "node:fs";
|
|
11803
|
+
import { homedir as homedir5 } from "node:os";
|
|
11804
|
+
import { join as join5 } from "node:path";
|
|
11805
|
+
function runsDir(base = homedir5()) {
|
|
11806
|
+
return join5(base, ".harmony-mcp", "runs");
|
|
11807
|
+
}
|
|
11808
|
+
function parseRunLogName(name) {
|
|
11809
|
+
const m = /^(.+)-card-(\d+)\.log$/.exec(name);
|
|
11810
|
+
if (!m)
|
|
11811
|
+
return null;
|
|
11812
|
+
return { runId: m[1], shortId: Number(m[2]) };
|
|
11813
|
+
}
|
|
11814
|
+
function listRunLogFiles(dir = runsDir()) {
|
|
11815
|
+
let names;
|
|
11816
|
+
try {
|
|
11817
|
+
names = readdirSync3(dir);
|
|
11818
|
+
} catch {
|
|
11819
|
+
return [];
|
|
11820
|
+
}
|
|
11821
|
+
const files = [];
|
|
11822
|
+
for (const name of names) {
|
|
11823
|
+
const parsed = parseRunLogName(name);
|
|
11824
|
+
if (!parsed)
|
|
11825
|
+
continue;
|
|
11826
|
+
const path = join5(dir, name);
|
|
11827
|
+
try {
|
|
11828
|
+
const st = statSync3(path);
|
|
11829
|
+
if (!st.isFile())
|
|
11830
|
+
continue;
|
|
11831
|
+
files.push({
|
|
11832
|
+
runId: parsed.runId,
|
|
11833
|
+
shortId: parsed.shortId,
|
|
11834
|
+
path,
|
|
11835
|
+
sizeBytes: st.size,
|
|
11836
|
+
mtimeMs: st.mtimeMs
|
|
11837
|
+
});
|
|
11838
|
+
} catch {}
|
|
11839
|
+
}
|
|
11840
|
+
return files.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
11841
|
+
}
|
|
11842
|
+
function resolveRunRef(ref) {
|
|
11843
|
+
const trimmed = ref.trim();
|
|
11844
|
+
const cardMatch = /^#?(\d+)$/.exec(trimmed);
|
|
11845
|
+
if (cardMatch)
|
|
11846
|
+
return { kind: "card", shortId: Number(cardMatch[1]) };
|
|
11847
|
+
return { kind: "run", runId: trimmed };
|
|
11848
|
+
}
|
|
11849
|
+
function findLatestLogForCard(files, shortId) {
|
|
11850
|
+
return files.find((f) => f.shortId === shortId) ?? null;
|
|
11851
|
+
}
|
|
11852
|
+
function findLatestRunForCard(runs, shortId) {
|
|
11853
|
+
let best = null;
|
|
11854
|
+
for (const r of runs) {
|
|
11855
|
+
if (r.cardShortId !== shortId)
|
|
11856
|
+
continue;
|
|
11857
|
+
if (!best || r.startedAt > best.startedAt)
|
|
11858
|
+
best = r;
|
|
11859
|
+
}
|
|
11860
|
+
return best;
|
|
11861
|
+
}
|
|
11862
|
+
function findLogByRunId(files, runId) {
|
|
11863
|
+
const exact = files.find((f) => f.runId === runId);
|
|
11864
|
+
if (exact)
|
|
11865
|
+
return exact;
|
|
11866
|
+
return files.find((f) => f.runId.startsWith(runId)) ?? null;
|
|
11867
|
+
}
|
|
11868
|
+
function resolveRunTarget(ref, runs, logs, getRun) {
|
|
11869
|
+
if (ref.kind === "card") {
|
|
11870
|
+
const record3 = findLatestRunForCard(runs, ref.shortId);
|
|
11871
|
+
if (record3) {
|
|
11872
|
+
return { record: record3, logFile: findLogByRunId(logs, record3.runId) };
|
|
11873
|
+
}
|
|
11874
|
+
const logFile2 = findLatestLogForCard(logs, ref.shortId);
|
|
11875
|
+
return { record: logFile2 ? getRun(logFile2.runId) : null, logFile: logFile2 };
|
|
11876
|
+
}
|
|
11877
|
+
const logFile = findLogByRunId(logs, ref.runId);
|
|
11878
|
+
const record2 = logFile ? getRun(logFile.runId) : getRun(ref.runId);
|
|
11879
|
+
return { record: record2, logFile };
|
|
11880
|
+
}
|
|
11881
|
+
function tailText(path, bytes = 4096) {
|
|
11882
|
+
try {
|
|
11883
|
+
const size = statSync3(path).size;
|
|
11884
|
+
if (size === 0)
|
|
11885
|
+
return "";
|
|
11886
|
+
const buf = readFileSync6(path);
|
|
11887
|
+
const start = Math.max(0, size - bytes);
|
|
11888
|
+
let text = buf.subarray(start).toString("utf-8");
|
|
11889
|
+
if (start > 0) {
|
|
11890
|
+
const nl = text.indexOf(`
|
|
11891
|
+
`);
|
|
11892
|
+
if (nl >= 0)
|
|
11893
|
+
text = text.slice(nl + 1);
|
|
11894
|
+
}
|
|
11895
|
+
return text;
|
|
11896
|
+
} catch {
|
|
11897
|
+
return null;
|
|
11898
|
+
}
|
|
11899
|
+
}
|
|
11900
|
+
function grepRunLogs(files, pattern, opts = {}) {
|
|
11901
|
+
const limit = opts.limit ?? 200;
|
|
11902
|
+
const matches = [];
|
|
11903
|
+
for (const file of files) {
|
|
11904
|
+
if (opts.shortId !== undefined && file.shortId !== opts.shortId)
|
|
11905
|
+
continue;
|
|
11906
|
+
let content;
|
|
11907
|
+
try {
|
|
11908
|
+
content = readFileSync6(file.path, "utf-8");
|
|
11909
|
+
} catch {
|
|
11910
|
+
continue;
|
|
11911
|
+
}
|
|
11912
|
+
const lines = content.split(`
|
|
11913
|
+
`);
|
|
11914
|
+
for (let i = 0;i < lines.length; i++) {
|
|
11915
|
+
pattern.lastIndex = 0;
|
|
11916
|
+
if (!pattern.test(lines[i]))
|
|
11917
|
+
continue;
|
|
11918
|
+
matches.push({
|
|
11919
|
+
runId: file.runId,
|
|
11920
|
+
shortId: file.shortId,
|
|
11921
|
+
path: file.path,
|
|
11922
|
+
lineNumber: i + 1,
|
|
11923
|
+
line: lines[i]
|
|
11924
|
+
});
|
|
11925
|
+
if (matches.length >= limit)
|
|
11926
|
+
return matches;
|
|
11927
|
+
}
|
|
11928
|
+
}
|
|
11929
|
+
return matches;
|
|
11930
|
+
}
|
|
11931
|
+
function distribution(values) {
|
|
11932
|
+
if (values.length === 0) {
|
|
11933
|
+
return { count: 0, total: 0, avg: 0, min: 0, max: 0, median: 0, p90: 0 };
|
|
11934
|
+
}
|
|
11935
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
11936
|
+
const total = sorted.reduce((s, v) => s + v, 0);
|
|
11937
|
+
const pct = (p) => {
|
|
11938
|
+
const idx = Math.min(sorted.length - 1, Math.max(0, Math.ceil(p / 100 * sorted.length) - 1));
|
|
11939
|
+
return sorted[idx];
|
|
11940
|
+
};
|
|
11941
|
+
return {
|
|
11942
|
+
count: sorted.length,
|
|
11943
|
+
total,
|
|
11944
|
+
avg: total / sorted.length,
|
|
11945
|
+
min: sorted[0],
|
|
11946
|
+
max: sorted[sorted.length - 1],
|
|
11947
|
+
median: pct(50),
|
|
11948
|
+
p90: pct(90)
|
|
11949
|
+
};
|
|
11950
|
+
}
|
|
11951
|
+
function computeRunStats(runs, cards, logs = []) {
|
|
11952
|
+
const ended = runs.filter((r) => r.endedAt !== null);
|
|
11953
|
+
const byStatus = EMPTY_STATUS_HISTOGRAM();
|
|
11954
|
+
for (const r of runs)
|
|
11955
|
+
byStatus[r.status] = (byStatus[r.status] ?? 0) + 1;
|
|
11956
|
+
const byPipeline = { implement: 0, review: 0 };
|
|
11957
|
+
for (const r of runs)
|
|
11958
|
+
byPipeline[r.pipeline] += 1;
|
|
11959
|
+
const shortIdByCard = new Map;
|
|
11960
|
+
for (const r of runs) {
|
|
11961
|
+
if (!shortIdByCard.has(r.cardId))
|
|
11962
|
+
shortIdByCard.set(r.cardId, r.cardShortId);
|
|
11963
|
+
}
|
|
11964
|
+
const distinctCards = new Set(runs.map((r) => r.cardId)).size;
|
|
11965
|
+
const failedCount = byStatus.failed + byStatus.orphaned;
|
|
11966
|
+
const attemptValues = cards.map((c) => c.totalAttempts ?? c.attempts);
|
|
11967
|
+
const attemptsTotal = attemptValues.reduce((s, v) => s + v, 0);
|
|
11968
|
+
const failureReasons = {};
|
|
11969
|
+
const verificationFailures = [];
|
|
11970
|
+
let totalFailureSummaries = 0;
|
|
11971
|
+
for (const c of cards) {
|
|
11972
|
+
for (const f of c.failureHistory ?? []) {
|
|
11973
|
+
const reason = f.reason ?? "other";
|
|
11974
|
+
failureReasons[reason] = (failureReasons[reason] ?? 0) + 1;
|
|
11975
|
+
totalFailureSummaries += 1;
|
|
11976
|
+
if (reason === "verification") {
|
|
11977
|
+
verificationFailures.push({
|
|
11978
|
+
shortId: shortIdByCard.get(c.cardId) ?? null,
|
|
11979
|
+
cardId: c.cardId,
|
|
11980
|
+
summary: f.summary,
|
|
11981
|
+
ts: f.ts
|
|
11982
|
+
});
|
|
11983
|
+
}
|
|
11984
|
+
}
|
|
11985
|
+
}
|
|
11986
|
+
verificationFailures.sort((a, b) => b.ts - a.ts);
|
|
11987
|
+
const verifyFailCount = failureReasons.verification ?? 0;
|
|
11988
|
+
return {
|
|
11989
|
+
totalRuns: runs.length,
|
|
11990
|
+
activeRuns: runs.length - ended.length,
|
|
11991
|
+
endedRuns: ended.length,
|
|
11992
|
+
byPipeline,
|
|
11993
|
+
byStatus,
|
|
11994
|
+
runFailureRate: ended.length ? failedCount / ended.length : 0,
|
|
11995
|
+
distinctCards,
|
|
11996
|
+
avgRunsPerCard: distinctCards ? runs.length / distinctCards : 0,
|
|
11997
|
+
attempts: {
|
|
11998
|
+
cards: cards.length,
|
|
11999
|
+
total: attemptsTotal,
|
|
12000
|
+
avg: cards.length ? attemptsTotal / cards.length : 0,
|
|
12001
|
+
max: attemptValues.length ? Math.max(...attemptValues) : 0
|
|
12002
|
+
},
|
|
12003
|
+
failureReasons,
|
|
12004
|
+
totalFailureSummaries,
|
|
12005
|
+
verifyFailRate: totalFailureSummaries ? verifyFailCount / totalFailureSummaries : 0,
|
|
12006
|
+
costCents: distribution(ended.map((r) => r.costCents).filter((c) => c > 0)),
|
|
12007
|
+
totalCostCents: runs.reduce((s, r) => s + r.costCents, 0),
|
|
12008
|
+
turns: distribution(ended.map((r) => r.numTurns).filter((t) => t > 0)),
|
|
12009
|
+
durationMs: distribution(ended.map((r) => r.endedAt - r.startedAt).filter((d) => d >= 0)),
|
|
12010
|
+
recentVerificationFailures: verificationFailures.slice(0, 5),
|
|
12011
|
+
logFiles: {
|
|
12012
|
+
count: logs.length,
|
|
12013
|
+
totalBytes: logs.reduce((s, f) => s + f.sizeBytes, 0)
|
|
12014
|
+
}
|
|
12015
|
+
};
|
|
12016
|
+
}
|
|
12017
|
+
function buildRunListRows(runs, logs, opts = {}) {
|
|
12018
|
+
const logByRunId = new Map(logs.map((f) => [f.runId, f]));
|
|
12019
|
+
let rows = runs.filter((r) => opts.shortId === undefined || r.cardShortId === opts.shortId).map((r) => ({
|
|
12020
|
+
runId: r.runId,
|
|
12021
|
+
shortId: r.cardShortId,
|
|
12022
|
+
pipeline: r.pipeline,
|
|
12023
|
+
status: r.status,
|
|
12024
|
+
costCents: r.costCents,
|
|
12025
|
+
numTurns: r.numTurns,
|
|
12026
|
+
startedAt: r.startedAt,
|
|
12027
|
+
endedAt: r.endedAt,
|
|
12028
|
+
durationMs: r.endedAt !== null ? r.endedAt - r.startedAt : null,
|
|
12029
|
+
logPath: logByRunId.get(r.runId)?.path ?? null
|
|
12030
|
+
})).sort((a, b) => b.startedAt - a.startedAt);
|
|
12031
|
+
if (opts.limit !== undefined)
|
|
12032
|
+
rows = rows.slice(0, opts.limit);
|
|
12033
|
+
return rows;
|
|
12034
|
+
}
|
|
12035
|
+
var EMPTY_STATUS_HISTOGRAM = () => ({
|
|
12036
|
+
active: 0,
|
|
12037
|
+
completed: 0,
|
|
12038
|
+
paused: 0,
|
|
12039
|
+
failed: 0,
|
|
12040
|
+
orphaned: 0
|
|
12041
|
+
});
|
|
12042
|
+
var init_run_stats = () => {};
|
|
12043
|
+
|
|
11256
12044
|
// src/cli.ts
|
|
11257
12045
|
init_log();
|
|
12046
|
+
import { realpathSync } from "node:fs";
|
|
12047
|
+
import { fileURLToPath } from "node:url";
|
|
11258
12048
|
var USAGE = `
|
|
11259
12049
|
Harmony Agent — push-based daemon + ops toolkit.
|
|
11260
12050
|
|
|
@@ -11265,6 +12055,14 @@ Usage:
|
|
|
11265
12055
|
harmony-agent doctor Run preflight checks (don't start)
|
|
11266
12056
|
harmony-agent gc One-shot worktree garbage collection
|
|
11267
12057
|
harmony-agent recover One-shot recovery of stranded Review cards
|
|
12058
|
+
harmony-agent runs list [--card N] [--limit N]
|
|
12059
|
+
Recent runs: card, status, cost, turns, log
|
|
12060
|
+
harmony-agent runs show <RUNID|#CARD> [--lines N] [--full]
|
|
12061
|
+
Run summary + log tail (defaults to latest
|
|
12062
|
+
run for a #card)
|
|
12063
|
+
harmony-agent runs grep <PATTERN> [--card N] [-i] [--limit N]
|
|
12064
|
+
Search across run logs
|
|
12065
|
+
harmony-agent stats [--json] Aggregate cost/turns/failure bottlenecks
|
|
11268
12066
|
harmony-agent help Show this help
|
|
11269
12067
|
|
|
11270
12068
|
Flags:
|
|
@@ -11450,6 +12248,270 @@ async function recoverCommand() {
|
|
|
11450
12248
|
}
|
|
11451
12249
|
return 0;
|
|
11452
12250
|
}
|
|
12251
|
+
function getFlagValue(args, name) {
|
|
12252
|
+
const eq = args.find((a) => a.startsWith(`${name}=`));
|
|
12253
|
+
if (eq)
|
|
12254
|
+
return eq.slice(name.length + 1);
|
|
12255
|
+
const idx = args.indexOf(name);
|
|
12256
|
+
if (idx >= 0 && idx + 1 < args.length)
|
|
12257
|
+
return args[idx + 1];
|
|
12258
|
+
return;
|
|
12259
|
+
}
|
|
12260
|
+
function parseIntFlag(args, name) {
|
|
12261
|
+
const raw = getFlagValue(args, name);
|
|
12262
|
+
if (raw === undefined)
|
|
12263
|
+
return;
|
|
12264
|
+
const n = Number(raw);
|
|
12265
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : undefined;
|
|
12266
|
+
}
|
|
12267
|
+
function formatDollars(cents) {
|
|
12268
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
12269
|
+
}
|
|
12270
|
+
function formatWhen(ms) {
|
|
12271
|
+
const d = new Date(ms);
|
|
12272
|
+
const pad2 = (n) => String(n).padStart(2, "0");
|
|
12273
|
+
return `${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
|
|
12274
|
+
}
|
|
12275
|
+
function formatBytes(bytes) {
|
|
12276
|
+
if (bytes < 1024)
|
|
12277
|
+
return `${bytes} B`;
|
|
12278
|
+
if (bytes < 1048576)
|
|
12279
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
12280
|
+
return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
12281
|
+
}
|
|
12282
|
+
async function runsListCommand(rest) {
|
|
12283
|
+
const { StateStore: StateStore2 } = await Promise.resolve().then(() => (init_state_store(), exports_state_store));
|
|
12284
|
+
const { buildRunListRows: buildRunListRows2, listRunLogFiles: listRunLogFiles2 } = await Promise.resolve().then(() => (init_run_stats(), exports_run_stats));
|
|
12285
|
+
const shortId = parseIntFlag(rest, "--card");
|
|
12286
|
+
const limit = parseIntFlag(rest, "--limit") ?? 25;
|
|
12287
|
+
const store = StateStore2.open();
|
|
12288
|
+
const rows = buildRunListRows2(store.listRuns(), listRunLogFiles2(), {
|
|
12289
|
+
shortId,
|
|
12290
|
+
limit
|
|
12291
|
+
});
|
|
12292
|
+
const out = process.stdout;
|
|
12293
|
+
if (rows.length === 0) {
|
|
12294
|
+
out.write(shortId !== undefined ? `no runs recorded for card #${shortId}
|
|
12295
|
+
` : `no runs recorded yet
|
|
12296
|
+
`);
|
|
12297
|
+
return 0;
|
|
12298
|
+
}
|
|
12299
|
+
out.write(`${"CARD".padEnd(7)}${"PIPELINE".padEnd(11)}${"STATUS".padEnd(11)}${"COST".padEnd(9)}${"TURNS".padEnd(7)}${"DURATION".padEnd(10)}${"STARTED".padEnd(13)}RUN
|
|
12300
|
+
`);
|
|
12301
|
+
for (const r of rows) {
|
|
12302
|
+
const dur = r.durationMs !== null ? formatDuration(r.durationMs) : "—";
|
|
12303
|
+
out.write(`${`#${r.shortId}`.padEnd(7)}${r.pipeline.padEnd(11)}${r.status.padEnd(11)}${formatDollars(r.costCents).padEnd(9)}${String(r.numTurns).padEnd(7)}${dur.padEnd(10)}${formatWhen(r.startedAt).padEnd(13)}${r.runId}
|
|
12304
|
+
`);
|
|
12305
|
+
out.write(` ↳ ${r.logPath ?? "(log not on disk)"}
|
|
12306
|
+
`);
|
|
12307
|
+
}
|
|
12308
|
+
return 0;
|
|
12309
|
+
}
|
|
12310
|
+
async function runsShowCommand(rest) {
|
|
12311
|
+
const { StateStore: StateStore2 } = await Promise.resolve().then(() => (init_state_store(), exports_state_store));
|
|
12312
|
+
const { listRunLogFiles: listRunLogFiles2, resolveRunRef: resolveRunRef2, resolveRunTarget: resolveRunTarget2 } = await Promise.resolve().then(() => (init_run_stats(), exports_run_stats));
|
|
12313
|
+
const ref = rest.find((a) => !a.startsWith("-"));
|
|
12314
|
+
if (!ref) {
|
|
12315
|
+
process.stderr.write(`usage: harmony-agent runs show <RUNID|#CARD> [--lines N] [--full]
|
|
12316
|
+
`);
|
|
12317
|
+
return 2;
|
|
12318
|
+
}
|
|
12319
|
+
const full = rest.includes("--full");
|
|
12320
|
+
const lines = parseIntFlag(rest, "--lines") ?? 120;
|
|
12321
|
+
const files = listRunLogFiles2();
|
|
12322
|
+
const store = StateStore2.open();
|
|
12323
|
+
const { record: record2, logFile } = resolveRunTarget2(resolveRunRef2(ref), store.listRuns(), files, (runId) => store.getRun(runId));
|
|
12324
|
+
if (!logFile && !record2) {
|
|
12325
|
+
process.stderr.write(`no run or log found for "${ref}"
|
|
12326
|
+
`);
|
|
12327
|
+
return 1;
|
|
12328
|
+
}
|
|
12329
|
+
const out = process.stdout;
|
|
12330
|
+
if (record2) {
|
|
12331
|
+
const dur = record2.endedAt !== null ? formatDuration(record2.endedAt - record2.startedAt) : "in-flight";
|
|
12332
|
+
out.write(`run ${record2.runId}
|
|
12333
|
+
`);
|
|
12334
|
+
out.write(`card #${record2.cardShortId} (${record2.cardId})
|
|
12335
|
+
`);
|
|
12336
|
+
out.write(`pipeline ${record2.pipeline}
|
|
12337
|
+
`);
|
|
12338
|
+
out.write(`status ${record2.status}
|
|
12339
|
+
`);
|
|
12340
|
+
out.write(`cost ${formatDollars(record2.costCents)} · ${record2.numTurns} turns · ${dur}
|
|
12341
|
+
`);
|
|
12342
|
+
out.write(`started ${new Date(record2.startedAt).toISOString()}
|
|
12343
|
+
`);
|
|
12344
|
+
if (record2.endedAt !== null) {
|
|
12345
|
+
out.write(`ended ${new Date(record2.endedAt).toISOString()}
|
|
12346
|
+
`);
|
|
12347
|
+
}
|
|
12348
|
+
if (record2.branchName)
|
|
12349
|
+
out.write(`branch ${record2.branchName}
|
|
12350
|
+
`);
|
|
12351
|
+
if (record2.errorMessage)
|
|
12352
|
+
out.write(`error ${record2.errorMessage}
|
|
12353
|
+
`);
|
|
12354
|
+
} else {
|
|
12355
|
+
out.write(`run ${logFile?.runId} (no ledger record — showing log only)
|
|
12356
|
+
`);
|
|
12357
|
+
if (logFile)
|
|
12358
|
+
out.write(`card #${logFile.shortId}
|
|
12359
|
+
`);
|
|
12360
|
+
}
|
|
12361
|
+
if (!logFile) {
|
|
12362
|
+
out.write(record2?.status === "active" ? `
|
|
12363
|
+
(run log not on disk yet — the run may still be starting)
|
|
12364
|
+
` : `
|
|
12365
|
+
(no run log on disk for this run)
|
|
12366
|
+
`);
|
|
12367
|
+
return 0;
|
|
12368
|
+
}
|
|
12369
|
+
out.write(`log ${logFile.path} (${formatBytes(logFile.sizeBytes)})
|
|
12370
|
+
`);
|
|
12371
|
+
let content;
|
|
12372
|
+
try {
|
|
12373
|
+
const { readFileSync: readFileSync7 } = await import("node:fs");
|
|
12374
|
+
content = readFileSync7(logFile.path, "utf-8");
|
|
12375
|
+
} catch (err) {
|
|
12376
|
+
process.stderr.write(`could not read log: ${err instanceof Error ? err.message : err}
|
|
12377
|
+
`);
|
|
12378
|
+
return 1;
|
|
12379
|
+
}
|
|
12380
|
+
const allLines = content.split(`
|
|
12381
|
+
`);
|
|
12382
|
+
const shown = full ? allLines : allLines.slice(-lines);
|
|
12383
|
+
const omitted = allLines.length - shown.length;
|
|
12384
|
+
out.write(full ? `
|
|
12385
|
+
--- full log ---
|
|
12386
|
+
` : `
|
|
12387
|
+
--- log tail (last ${shown.length} lines${omitted > 0 ? `, ${omitted} omitted — use --full` : ""}) ---
|
|
12388
|
+
`);
|
|
12389
|
+
out.write(shown.join(`
|
|
12390
|
+
`));
|
|
12391
|
+
if (!content.endsWith(`
|
|
12392
|
+
`))
|
|
12393
|
+
out.write(`
|
|
12394
|
+
`);
|
|
12395
|
+
return 0;
|
|
12396
|
+
}
|
|
12397
|
+
async function runsGrepCommand(rest) {
|
|
12398
|
+
const { grepRunLogs: grepRunLogs2, listRunLogFiles: listRunLogFiles2 } = await Promise.resolve().then(() => (init_run_stats(), exports_run_stats));
|
|
12399
|
+
const shortId = parseIntFlag(rest, "--card");
|
|
12400
|
+
const limit = parseIntFlag(rest, "--limit") ?? 200;
|
|
12401
|
+
const ci = rest.includes("-i") || rest.includes("--ignore-case");
|
|
12402
|
+
const consumed = new Set;
|
|
12403
|
+
for (let i = 0;i < rest.length; i++) {
|
|
12404
|
+
if (rest[i] === "--card" || rest[i] === "--limit")
|
|
12405
|
+
consumed.add(i + 1);
|
|
12406
|
+
}
|
|
12407
|
+
const pattern = rest.find((a, i) => !a.startsWith("-") && !consumed.has(i));
|
|
12408
|
+
if (!pattern) {
|
|
12409
|
+
process.stderr.write(`usage: harmony-agent runs grep <PATTERN> [--card N] [-i] [--limit N]
|
|
12410
|
+
`);
|
|
12411
|
+
return 2;
|
|
12412
|
+
}
|
|
12413
|
+
let regex;
|
|
12414
|
+
try {
|
|
12415
|
+
regex = new RegExp(pattern, ci ? "i" : "");
|
|
12416
|
+
} catch (err) {
|
|
12417
|
+
process.stderr.write(`invalid pattern: ${err instanceof Error ? err.message : err}
|
|
12418
|
+
`);
|
|
12419
|
+
return 2;
|
|
12420
|
+
}
|
|
12421
|
+
const matches = grepRunLogs2(listRunLogFiles2(), regex, { shortId, limit });
|
|
12422
|
+
const out = process.stdout;
|
|
12423
|
+
if (matches.length === 0) {
|
|
12424
|
+
out.write(`no matches
|
|
12425
|
+
`);
|
|
12426
|
+
return 1;
|
|
12427
|
+
}
|
|
12428
|
+
for (const m of matches) {
|
|
12429
|
+
const trimmed = m.line.length > 240 ? `${m.line.slice(0, 240)}…` : m.line;
|
|
12430
|
+
out.write(`#${m.shortId} ${m.runId}:${m.lineNumber}: ${trimmed}
|
|
12431
|
+
`);
|
|
12432
|
+
}
|
|
12433
|
+
if (matches.length >= limit) {
|
|
12434
|
+
out.write(`… stopped at ${limit} matches (raise with --limit)
|
|
12435
|
+
`);
|
|
12436
|
+
}
|
|
12437
|
+
return 0;
|
|
12438
|
+
}
|
|
12439
|
+
async function runsCommand(rest) {
|
|
12440
|
+
const sub = rest[0];
|
|
12441
|
+
const tail = rest.slice(1);
|
|
12442
|
+
switch (sub) {
|
|
12443
|
+
case "list":
|
|
12444
|
+
case undefined:
|
|
12445
|
+
return runsListCommand(tail);
|
|
12446
|
+
case "show":
|
|
12447
|
+
return runsShowCommand(tail);
|
|
12448
|
+
case "grep":
|
|
12449
|
+
return runsGrepCommand(tail);
|
|
12450
|
+
default:
|
|
12451
|
+
process.stderr.write(`unknown runs subcommand: ${sub}
|
|
12452
|
+
usage: harmony-agent runs <list|show|grep>
|
|
12453
|
+
`);
|
|
12454
|
+
return 2;
|
|
12455
|
+
}
|
|
12456
|
+
}
|
|
12457
|
+
async function statsCommand() {
|
|
12458
|
+
const { StateStore: StateStore2 } = await Promise.resolve().then(() => (init_state_store(), exports_state_store));
|
|
12459
|
+
const { computeRunStats: computeRunStats2, listRunLogFiles: listRunLogFiles2 } = await Promise.resolve().then(() => (init_run_stats(), exports_run_stats));
|
|
12460
|
+
const store = StateStore2.open();
|
|
12461
|
+
const logs = listRunLogFiles2();
|
|
12462
|
+
const stats = computeRunStats2(store.listRuns(), store.listCards(), logs);
|
|
12463
|
+
if (process.argv.includes("--json")) {
|
|
12464
|
+
process.stdout.write(`${JSON.stringify(stats, null, 2)}
|
|
12465
|
+
`);
|
|
12466
|
+
return 0;
|
|
12467
|
+
}
|
|
12468
|
+
const out = process.stdout;
|
|
12469
|
+
if (stats.totalRuns === 0 && stats.attempts.cards === 0) {
|
|
12470
|
+
out.write(`no run history yet — the daemon hasn't recorded any runs
|
|
12471
|
+
`);
|
|
12472
|
+
return 0;
|
|
12473
|
+
}
|
|
12474
|
+
const pct = (x) => `${(x * 100).toFixed(0)}%`;
|
|
12475
|
+
out.write(`runs total=${stats.totalRuns} active=${stats.activeRuns} ended=${stats.endedRuns} (failure rate ${pct(stats.runFailureRate)})
|
|
12476
|
+
`);
|
|
12477
|
+
out.write(`pipelines implement=${stats.byPipeline.implement} review=${stats.byPipeline.review}
|
|
12478
|
+
`);
|
|
12479
|
+
const s = stats.byStatus;
|
|
12480
|
+
out.write(`status completed=${s.completed} failed=${s.failed} orphaned=${s.orphaned} paused=${s.paused} active=${s.active}
|
|
12481
|
+
`);
|
|
12482
|
+
out.write(`cards distinct=${stats.distinctCards} avg runs/card=${stats.avgRunsPerCard.toFixed(1)}
|
|
12483
|
+
`);
|
|
12484
|
+
out.write(`attempts cards=${stats.attempts.cards} total=${stats.attempts.total} avg=${stats.attempts.avg.toFixed(1)} max=${stats.attempts.max}
|
|
12485
|
+
`);
|
|
12486
|
+
const reasons = Object.entries(stats.failureReasons).sort((a, b) => b[1] - a[1]).map(([r, n]) => `${r}=${n}`).join(" ");
|
|
12487
|
+
out.write(`failures total=${stats.totalFailureSummaries} (${reasons || "none"}) verify share ${pct(stats.verifyFailRate)}
|
|
12488
|
+
`);
|
|
12489
|
+
const c = stats.costCents;
|
|
12490
|
+
out.write(`cost n=${c.count} total=${formatDollars(stats.totalCostCents)} avg=${formatDollars(c.avg)} median=${formatDollars(c.median)} p90=${formatDollars(c.p90)} max=${formatDollars(c.max)}
|
|
12491
|
+
`);
|
|
12492
|
+
const t = stats.turns;
|
|
12493
|
+
out.write(`turns n=${t.count} avg=${t.avg.toFixed(1)} median=${t.median} p90=${t.p90} max=${t.max}
|
|
12494
|
+
`);
|
|
12495
|
+
const d = stats.durationMs;
|
|
12496
|
+
out.write(`duration n=${d.count} avg=${formatDuration(d.avg)} median=${formatDuration(d.median)} p90=${formatDuration(d.p90)} max=${formatDuration(d.max)}
|
|
12497
|
+
`);
|
|
12498
|
+
out.write(`logs ${stats.logFiles.count} files, ${formatBytes(stats.logFiles.totalBytes)} on disk
|
|
12499
|
+
`);
|
|
12500
|
+
if (stats.recentVerificationFailures.length) {
|
|
12501
|
+
out.write(`
|
|
12502
|
+
what verification rejected (recent):
|
|
12503
|
+
`);
|
|
12504
|
+
for (const f of stats.recentVerificationFailures) {
|
|
12505
|
+
const first = f.summary.split(`
|
|
12506
|
+
`)[0];
|
|
12507
|
+
const line = first.length > 100 ? `${first.slice(0, 100)}…` : first;
|
|
12508
|
+
const ref = f.shortId !== null ? `#${f.shortId}` : f.cardId.slice(0, 8);
|
|
12509
|
+
out.write(` ${ref.padEnd(6)} ${line}
|
|
12510
|
+
`);
|
|
12511
|
+
}
|
|
12512
|
+
}
|
|
12513
|
+
return 0;
|
|
12514
|
+
}
|
|
11453
12515
|
async function dispatch(argv) {
|
|
11454
12516
|
const args = argv.filter((a) => a !== "--pretty" && a !== "--json");
|
|
11455
12517
|
const cmd = args[0];
|
|
@@ -11469,6 +12531,10 @@ async function dispatch(argv) {
|
|
|
11469
12531
|
return gcCommand();
|
|
11470
12532
|
case "recover":
|
|
11471
12533
|
return recoverCommand();
|
|
12534
|
+
case "runs":
|
|
12535
|
+
return runsCommand(args.slice(1));
|
|
12536
|
+
case "stats":
|
|
12537
|
+
return statsCommand();
|
|
11472
12538
|
case "help":
|
|
11473
12539
|
case "--help":
|
|
11474
12540
|
case "-h":
|
|
@@ -11482,10 +12548,30 @@ ${USAGE}
|
|
|
11482
12548
|
return 2;
|
|
11483
12549
|
}
|
|
11484
12550
|
}
|
|
11485
|
-
|
|
11486
|
-
|
|
11487
|
-
|
|
11488
|
-
|
|
11489
|
-
|
|
11490
|
-
|
|
11491
|
-
}
|
|
12551
|
+
function isMainModule() {
|
|
12552
|
+
const entry = process.argv[1];
|
|
12553
|
+
if (!entry)
|
|
12554
|
+
return false;
|
|
12555
|
+
try {
|
|
12556
|
+
return realpathSync(entry) === fileURLToPath(import.meta.url);
|
|
12557
|
+
} catch {
|
|
12558
|
+
return false;
|
|
12559
|
+
}
|
|
12560
|
+
}
|
|
12561
|
+
if (isMainModule()) {
|
|
12562
|
+
dispatch(process.argv.slice(2)).then((code) => {
|
|
12563
|
+
if (code !== 0)
|
|
12564
|
+
process.exit(code);
|
|
12565
|
+
}).catch((err) => {
|
|
12566
|
+
log.error("cli", err instanceof Error ? err.message : String(err));
|
|
12567
|
+
process.exit(1);
|
|
12568
|
+
});
|
|
12569
|
+
}
|
|
12570
|
+
export {
|
|
12571
|
+
statsCommand,
|
|
12572
|
+
runsShowCommand,
|
|
12573
|
+
runsListCommand,
|
|
12574
|
+
runsGrepCommand,
|
|
12575
|
+
runsCommand,
|
|
12576
|
+
dispatch
|
|
12577
|
+
};
|