@gethmy/agent 1.28.0 → 1.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1082 -457
- package/dist/index.js +1080 -455
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -783,6 +783,76 @@ var init_constants = __esm(() => {
|
|
|
783
783
|
QUERY_GC_TIME: 1000 * 60 * 60 * 24
|
|
784
784
|
};
|
|
785
785
|
});
|
|
786
|
+
// ../harmony-shared/dist/fanoutSource.js
|
|
787
|
+
function fanoutItemKey(item) {
|
|
788
|
+
return item.sourceId ? `src:${item.sourceId}` : `idx:${item.index}`;
|
|
789
|
+
}
|
|
790
|
+
function parseFanoutKeyMarker(description) {
|
|
791
|
+
if (typeof description !== "string")
|
|
792
|
+
return null;
|
|
793
|
+
const match = FANOUT_KEY_RE.exec(description);
|
|
794
|
+
const key = match?.[1]?.trim();
|
|
795
|
+
return key ? key : null;
|
|
796
|
+
}
|
|
797
|
+
function subtaskItems(subtasks) {
|
|
798
|
+
return [...subtasks].sort((a, b) => (a.position ?? 0) - (b.position ?? 0)).map((s) => ({ label: s.title ?? "", sourceId: s.id }));
|
|
799
|
+
}
|
|
800
|
+
function parseMarkdownListItems(text) {
|
|
801
|
+
if (typeof text !== "string" || !text)
|
|
802
|
+
return [];
|
|
803
|
+
const items = [];
|
|
804
|
+
for (const rawLine of text.split(`
|
|
805
|
+
`)) {
|
|
806
|
+
const match = /^ {0,1}(?:[-*+]|\d+[.)])\s+(.*)$/.exec(rawLine);
|
|
807
|
+
if (!match)
|
|
808
|
+
continue;
|
|
809
|
+
const withoutCheckbox = match[1].replace(/^\[[ xX]\]\s*/, "");
|
|
810
|
+
const entry = withoutCheckbox.trim();
|
|
811
|
+
if (entry)
|
|
812
|
+
items.push(entry);
|
|
813
|
+
}
|
|
814
|
+
return items;
|
|
815
|
+
}
|
|
816
|
+
function checklistItemsFromDescription(description, field) {
|
|
817
|
+
if (typeof description !== "string" || !description)
|
|
818
|
+
return [];
|
|
819
|
+
const wanted = field.trim().toLowerCase();
|
|
820
|
+
if (!wanted)
|
|
821
|
+
return [];
|
|
822
|
+
const lines = description.split(`
|
|
823
|
+
`);
|
|
824
|
+
const headingAt = lines.findIndex((line) => {
|
|
825
|
+
const heading = /^#{1,6}\s+(.*)$/.exec(line);
|
|
826
|
+
if (!heading)
|
|
827
|
+
return false;
|
|
828
|
+
const text = heading[1].replace(/[*_`]/g, "").replace(/[::]\s*$/, "").trim().toLowerCase();
|
|
829
|
+
return text === wanted;
|
|
830
|
+
});
|
|
831
|
+
if (headingAt < 0)
|
|
832
|
+
return [];
|
|
833
|
+
const body = [];
|
|
834
|
+
for (let i = headingAt + 1;i < lines.length; i++) {
|
|
835
|
+
if (/^#{1,6}\s+/.test(lines[i]))
|
|
836
|
+
break;
|
|
837
|
+
body.push(lines[i]);
|
|
838
|
+
}
|
|
839
|
+
return parseMarkdownListItems(body.join(`
|
|
840
|
+
`)).map((label) => ({ label }));
|
|
841
|
+
}
|
|
842
|
+
function handoffListItems(handoff) {
|
|
843
|
+
if (!handoff)
|
|
844
|
+
return [];
|
|
845
|
+
const explicit = Array.isArray(handoff.producedItems) ? handoff.producedItems.filter((s) => typeof s === "string").map((s) => s.trim()).filter(Boolean) : [];
|
|
846
|
+
if (explicit.length > 0)
|
|
847
|
+
return explicit.map((label) => ({ label }));
|
|
848
|
+
return parseMarkdownListItems(handoff.produced ?? "").map((label) => ({
|
|
849
|
+
label
|
|
850
|
+
}));
|
|
851
|
+
}
|
|
852
|
+
var FANOUT_KEY_MARKER = "harmony:fanout-item", FANOUT_KEY_RE;
|
|
853
|
+
var init_fanoutSource = __esm(() => {
|
|
854
|
+
FANOUT_KEY_RE = new RegExp(`^\\[${FANOUT_KEY_MARKER}\\]:\\s*#(\\S+)\\s*$`, "m");
|
|
855
|
+
});
|
|
786
856
|
// ../harmony-shared/dist/gateConfigError.js
|
|
787
857
|
var GATE_CONFIG_ERROR_KEY = "configError", GATE_CONFIG_ERROR_MARK;
|
|
788
858
|
var init_gateConfigError = __esm(() => {
|
|
@@ -1185,6 +1255,78 @@ function getStageLoop(stage) {
|
|
|
1185
1255
|
function isConvergeLoop(loop) {
|
|
1186
1256
|
return loop !== null && loop.mode === "converge";
|
|
1187
1257
|
}
|
|
1258
|
+
function isFanoutLoop(loop) {
|
|
1259
|
+
return loop !== null && loop.mode === "fanout";
|
|
1260
|
+
}
|
|
1261
|
+
function normalizeItemSource(raw) {
|
|
1262
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
1263
|
+
return null;
|
|
1264
|
+
const obj = raw;
|
|
1265
|
+
switch (obj.kind) {
|
|
1266
|
+
case "subtasks":
|
|
1267
|
+
return { kind: "subtasks" };
|
|
1268
|
+
case "list_handoff": {
|
|
1269
|
+
const from = typeof obj.from_stage === "string" ? obj.from_stage.trim() : "";
|
|
1270
|
+
return from ? { kind: "list_handoff", from_stage: from } : null;
|
|
1271
|
+
}
|
|
1272
|
+
case "checklist": {
|
|
1273
|
+
const field = typeof obj.field === "string" ? obj.field.trim() : "";
|
|
1274
|
+
return field ? { kind: "checklist", field } : null;
|
|
1275
|
+
}
|
|
1276
|
+
default:
|
|
1277
|
+
return null;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
function getLoopItemSource(loop) {
|
|
1281
|
+
return normalizeItemSource(loop.item_source);
|
|
1282
|
+
}
|
|
1283
|
+
function resolveLoopConcurrency(loop) {
|
|
1284
|
+
const raw = loop.concurrency;
|
|
1285
|
+
if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 1) {
|
|
1286
|
+
return DEFAULT_LOOP_CONCURRENCY;
|
|
1287
|
+
}
|
|
1288
|
+
return Math.floor(raw);
|
|
1289
|
+
}
|
|
1290
|
+
function resolveOnItemFail(loop) {
|
|
1291
|
+
return loop.on_item_fail ?? DEFAULT_ON_ITEM_FAIL;
|
|
1292
|
+
}
|
|
1293
|
+
function planFanoutItems(raw, loop) {
|
|
1294
|
+
const max = Math.max(1, Math.floor(loop.max_iterations) || 1);
|
|
1295
|
+
let invalid = 0;
|
|
1296
|
+
const valid = [];
|
|
1297
|
+
for (const entry of raw) {
|
|
1298
|
+
const label = typeof entry?.label === "string" ? entry.label.trim() : "";
|
|
1299
|
+
if (!label) {
|
|
1300
|
+
invalid += 1;
|
|
1301
|
+
continue;
|
|
1302
|
+
}
|
|
1303
|
+
valid.push({ ...entry, label });
|
|
1304
|
+
}
|
|
1305
|
+
const kept = valid.slice(0, max);
|
|
1306
|
+
const items = kept.map((entry, index) => {
|
|
1307
|
+
const item = { index, label: entry.label };
|
|
1308
|
+
const detail = typeof entry.detail === "string" ? entry.detail.trim() : "";
|
|
1309
|
+
if (detail)
|
|
1310
|
+
item.detail = detail;
|
|
1311
|
+
const sourceId = typeof entry.sourceId === "string" ? entry.sourceId.trim() : "";
|
|
1312
|
+
if (sourceId)
|
|
1313
|
+
item.sourceId = sourceId;
|
|
1314
|
+
return item;
|
|
1315
|
+
});
|
|
1316
|
+
return {
|
|
1317
|
+
items,
|
|
1318
|
+
total: valid.length,
|
|
1319
|
+
truncated: valid.length - kept.length,
|
|
1320
|
+
invalid
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
function decideFanoutAggregation(args) {
|
|
1324
|
+
const { loop, outcomes } = args;
|
|
1325
|
+
if (resolveOnItemFail(loop) === "halt" && outcomes.some((o) => o === "failed")) {
|
|
1326
|
+
return "halt";
|
|
1327
|
+
}
|
|
1328
|
+
return outcomes.every((o) => o !== "pending") ? "complete" : "pending";
|
|
1329
|
+
}
|
|
1188
1330
|
function resolveLoopExitGate(stage, loop) {
|
|
1189
1331
|
return loop.exit_gate ?? stage.gate ?? null;
|
|
1190
1332
|
}
|
|
@@ -1289,7 +1431,7 @@ function referencedGateMetrics(def) {
|
|
|
1289
1431
|
}
|
|
1290
1432
|
return out;
|
|
1291
1433
|
}
|
|
1292
|
-
var DEFAULT_LOOP_MAX_ITERATIONS = 5, PLAYBOOK_STAGE_ROLES, SKILL_TOOL_ALLOWLIST, HARMONY_TOOL_RE, STAGE_DAEMON_OWNED_TOOLS;
|
|
1434
|
+
var DEFAULT_LOOP_MAX_ITERATIONS = 5, DEFAULT_LOOP_CONCURRENCY = 1, DEFAULT_ON_ITEM_FAIL = "continue", PLAYBOOK_STAGE_ROLES, SKILL_TOOL_ALLOWLIST, HARMONY_TOOL_RE, STAGE_DAEMON_OWNED_TOOLS;
|
|
1293
1435
|
var init_playbookStage = __esm(() => {
|
|
1294
1436
|
PLAYBOOK_STAGE_ROLES = [
|
|
1295
1437
|
"author",
|
|
@@ -1474,6 +1616,12 @@ var init_reviewTools = __esm(() => {
|
|
|
1474
1616
|
];
|
|
1475
1617
|
});
|
|
1476
1618
|
// ../harmony-shared/dist/stageHandoff.js
|
|
1619
|
+
function isFanoutHandoffItem(value) {
|
|
1620
|
+
if (typeof value !== "object" || value === null)
|
|
1621
|
+
return false;
|
|
1622
|
+
const v = value;
|
|
1623
|
+
return typeof v.parentCardId === "string" && typeof v.stageId === "string" && typeof v.index === "number" && Number.isFinite(v.index) && typeof v.total === "number" && Number.isFinite(v.total) && typeof v.label === "string" && (v.detail === undefined || typeof v.detail === "string") && (v.sourceId === undefined || typeof v.sourceId === "string");
|
|
1624
|
+
}
|
|
1477
1625
|
function buildHandoffCommentBody(input) {
|
|
1478
1626
|
const handoff = {
|
|
1479
1627
|
version: STAGE_HANDOFF_VERSION,
|
|
@@ -1485,11 +1633,22 @@ function buildHandoffCommentBody(input) {
|
|
|
1485
1633
|
nextStageNeeds: input.nextStageNeeds,
|
|
1486
1634
|
producedAt: input.producedAt ?? new Date().toISOString()
|
|
1487
1635
|
};
|
|
1636
|
+
if (input.fanoutItem)
|
|
1637
|
+
handoff.fanoutItem = input.fanoutItem;
|
|
1638
|
+
if (input.producedItems && input.producedItems.length > 0) {
|
|
1639
|
+
handoff.producedItems = input.producedItems;
|
|
1640
|
+
}
|
|
1488
1641
|
const decisionLines = handoff.decisions.length > 0 ? handoff.decisions.map((d) => `- ${d}`).join(`
|
|
1489
1642
|
`) : "_None._";
|
|
1643
|
+
const item = handoff.fanoutItem;
|
|
1490
1644
|
const prose = [
|
|
1491
1645
|
`**Stage handoff — ${handoff.stageName}**`,
|
|
1492
1646
|
"",
|
|
1647
|
+
...item ? [
|
|
1648
|
+
`**Your item (${item.index + 1} of ${item.total}):** ${item.label}`,
|
|
1649
|
+
...item.detail ? ["", item.detail] : [],
|
|
1650
|
+
""
|
|
1651
|
+
] : [],
|
|
1493
1652
|
`**Produced:** ${handoff.produced}`,
|
|
1494
1653
|
"",
|
|
1495
1654
|
"**Decisions (settled — do not re-litigate):**",
|
|
@@ -1521,7 +1680,18 @@ function parseHandoffCommentBody(body) {
|
|
|
1521
1680
|
return null;
|
|
1522
1681
|
try {
|
|
1523
1682
|
const parsed = JSON.parse(match[1]);
|
|
1524
|
-
|
|
1683
|
+
if (!isTypedStageHandoff(parsed))
|
|
1684
|
+
return null;
|
|
1685
|
+
const cleaned = { ...parsed };
|
|
1686
|
+
if (cleaned.fanoutItem !== undefined && !isFanoutHandoffItem(cleaned.fanoutItem)) {
|
|
1687
|
+
delete cleaned.fanoutItem;
|
|
1688
|
+
}
|
|
1689
|
+
if (cleaned.producedItems !== undefined) {
|
|
1690
|
+
if (!Array.isArray(cleaned.producedItems) || !cleaned.producedItems.every((s) => typeof s === "string")) {
|
|
1691
|
+
delete cleaned.producedItems;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
return cleaned;
|
|
1525
1695
|
} catch {
|
|
1526
1696
|
return null;
|
|
1527
1697
|
}
|
|
@@ -1547,6 +1717,25 @@ function extractLatestHandoff(comments, identity, opts = {}) {
|
|
|
1547
1717
|
function renderInheritedHandoffSection(handoff) {
|
|
1548
1718
|
const decisions = handoff.decisions.length > 0 ? handoff.decisions.map((d) => `- ${d}`).join(`
|
|
1549
1719
|
`) : "- (none recorded)";
|
|
1720
|
+
const item = handoff.fanoutItem;
|
|
1721
|
+
if (item) {
|
|
1722
|
+
return [
|
|
1723
|
+
"## Your fan-out item",
|
|
1724
|
+
"",
|
|
1725
|
+
`You are one of ${item.total} cards working this stage in parallel. Do **only** your own item — the others are being handled on their own cards.`,
|
|
1726
|
+
"",
|
|
1727
|
+
`**Item ${item.index + 1} of ${item.total}:** ${item.label}`,
|
|
1728
|
+
...item.detail ? ["", item.detail] : [],
|
|
1729
|
+
"",
|
|
1730
|
+
`**Context from the dispatching stage:** ${handoff.produced}`,
|
|
1731
|
+
"",
|
|
1732
|
+
"**Decisions you must respect:**",
|
|
1733
|
+
decisions,
|
|
1734
|
+
"",
|
|
1735
|
+
`**What you need to do with it:** ${handoff.nextStageNeeds}`
|
|
1736
|
+
].join(`
|
|
1737
|
+
`);
|
|
1738
|
+
}
|
|
1550
1739
|
return [
|
|
1551
1740
|
"## Inherited handoff (from the previous stage)",
|
|
1552
1741
|
"",
|
|
@@ -1579,6 +1768,7 @@ var init_dist = __esm(() => {
|
|
|
1579
1768
|
init_columnSort();
|
|
1580
1769
|
init_commentSerializer();
|
|
1581
1770
|
init_constants();
|
|
1771
|
+
init_fanoutSource();
|
|
1582
1772
|
init_gateConfigError();
|
|
1583
1773
|
init_gateEvaluate();
|
|
1584
1774
|
init_logger();
|
|
@@ -3380,6 +3570,40 @@ var init_episode_writer = __esm(() => {
|
|
|
3380
3570
|
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;
|
|
3381
3571
|
});
|
|
3382
3572
|
|
|
3573
|
+
// src/run-closeout.ts
|
|
3574
|
+
import { log as log9 } from "@gethmy/harness";
|
|
3575
|
+
async function transferCardToCompletion(deps, card, moveToColumn, onPromoted) {
|
|
3576
|
+
await moveCardToColumn(deps.client, card, moveToColumn);
|
|
3577
|
+
try {
|
|
3578
|
+
await releaseAssignedAgent(deps.client, card.id);
|
|
3579
|
+
} catch (err) {
|
|
3580
|
+
log9.warn(deps.tag, `assignment release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
3581
|
+
}
|
|
3582
|
+
if (onPromoted) {
|
|
3583
|
+
try {
|
|
3584
|
+
await onPromoted(card);
|
|
3585
|
+
} catch (err) {
|
|
3586
|
+
log9.warn(deps.tag, `successor promotion failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
async function endRunSession(deps, card, disposition, extraPayload, onError) {
|
|
3591
|
+
try {
|
|
3592
|
+
await deps.client.endAgentSession(card.id, {
|
|
3593
|
+
...disposition,
|
|
3594
|
+
progressPercent: 100,
|
|
3595
|
+
...extraPayload
|
|
3596
|
+
});
|
|
3597
|
+
} catch (err) {
|
|
3598
|
+
if (onError === "throw")
|
|
3599
|
+
throw err;
|
|
3600
|
+
log9.error(deps.tag, `endAgentSession after the run failed on #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
var init_run_closeout = __esm(() => {
|
|
3604
|
+
init_board_helpers();
|
|
3605
|
+
});
|
|
3606
|
+
|
|
3383
3607
|
// src/completion.ts
|
|
3384
3608
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
3385
3609
|
import {
|
|
@@ -3388,7 +3612,7 @@ import {
|
|
|
3388
3612
|
createPullRequest,
|
|
3389
3613
|
detectGitProvider as detectGitProvider3,
|
|
3390
3614
|
getBranchWebUrl,
|
|
3391
|
-
log as
|
|
3615
|
+
log as log10,
|
|
3392
3616
|
pushBranch,
|
|
3393
3617
|
reportFindings,
|
|
3394
3618
|
runFormatFix,
|
|
@@ -3425,7 +3649,7 @@ function buildTokenPayload(stats) {
|
|
|
3425
3649
|
numTurns: stats.cost.numTurns
|
|
3426
3650
|
};
|
|
3427
3651
|
}
|
|
3428
|
-
async function runCompletion(client, card, branchName, worktreePath, config, workerId, sessionStats, workspaceId, agentSessionId, stateStore, onMovedToCompletion, onBeforeWorktreeCleanup, runBaselineSha, effectiveMaxTurns) {
|
|
3652
|
+
async function runCompletion(client, card, branchName, worktreePath, config, workerId, sessionIdentifier, sessionStats, workspaceId, agentSessionId, stateStore, onMovedToCompletion, onBeforeWorktreeCleanup, runBaselineSha, effectiveMaxTurns) {
|
|
3429
3653
|
let verificationResult = {
|
|
3430
3654
|
passed: true,
|
|
3431
3655
|
buildErrors: [],
|
|
@@ -3442,10 +3666,10 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
3442
3666
|
if (!hasCommits) {
|
|
3443
3667
|
const { maxTurnsExhausted, failureSummary } = describeNoCommitFailure(sessionStats?.cost?.numTurns ?? 0, effectiveMaxTurns ?? config.claude.maxTurns);
|
|
3444
3668
|
if (noCommitOutcome(maxTurnsExhausted, config.budget.pause.enabled) === "park") {
|
|
3445
|
-
|
|
3669
|
+
log10.warn(TAG9, `No commits on branch ${branchName} — ${failureSummary}; parking for a decision`);
|
|
3446
3670
|
return "park";
|
|
3447
3671
|
}
|
|
3448
|
-
|
|
3672
|
+
log10.warn(TAG9, `No commits on branch ${branchName} — ${failureSummary}; counting as a failed attempt`);
|
|
3449
3673
|
await moveCardToColumn(client, card, config.pickupColumns[0] ?? "To Do");
|
|
3450
3674
|
await client.endAgentSession(card.id, {
|
|
3451
3675
|
status: "failed",
|
|
@@ -3456,18 +3680,18 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
3456
3680
|
await teardownWorktree(client, card.id, worktreePath, branchName);
|
|
3457
3681
|
return false;
|
|
3458
3682
|
}
|
|
3459
|
-
|
|
3683
|
+
log10.info(TAG9, `Pushing branch ${branchName} (pre-verify)...`);
|
|
3460
3684
|
let lastPushedSha = null;
|
|
3461
3685
|
try {
|
|
3462
3686
|
pushBranch(branchName, worktreePath);
|
|
3463
3687
|
lastPushedSha = readHeadSha(worktreePath);
|
|
3464
3688
|
} catch (err) {
|
|
3465
|
-
|
|
3689
|
+
log10.error(TAG9, `pre-verify push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
|
|
3466
3690
|
}
|
|
3467
3691
|
const recoveryUrl = lastPushedSha ? getBranchWebUrl(branchName, worktreePath) : null;
|
|
3468
3692
|
if (config.verification.enabled) {
|
|
3469
3693
|
await client.updateAgentProgress(card.id, {
|
|
3470
|
-
agentIdentifier:
|
|
3694
|
+
agentIdentifier: sessionIdentifier,
|
|
3471
3695
|
agentName: AGENT_NAME,
|
|
3472
3696
|
status: "working",
|
|
3473
3697
|
currentTask: "Verifying build...",
|
|
@@ -3477,9 +3701,9 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
3477
3701
|
let autoFixAttempts = 0;
|
|
3478
3702
|
if (!result.passed && config.verification.autoFix) {
|
|
3479
3703
|
for (let attempt = 0;attempt < config.verification.maxFixAttempts; attempt++) {
|
|
3480
|
-
|
|
3704
|
+
log10.info(TAG9, `Auto-fix attempt ${attempt + 1}/${config.verification.maxFixAttempts}`);
|
|
3481
3705
|
await client.updateAgentProgress(card.id, {
|
|
3482
|
-
agentIdentifier:
|
|
3706
|
+
agentIdentifier: sessionIdentifier,
|
|
3483
3707
|
agentName: AGENT_NAME,
|
|
3484
3708
|
status: "working",
|
|
3485
3709
|
currentTask: `Fixing issues (attempt ${attempt + 1})...`,
|
|
@@ -3494,14 +3718,14 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
3494
3718
|
result = await runVerification(worktreePath, config, workerId);
|
|
3495
3719
|
autoFixAttempts = attempt + 1;
|
|
3496
3720
|
if (result.passed) {
|
|
3497
|
-
|
|
3721
|
+
log10.info(TAG9, `Auto-fix succeeded on attempt ${attempt + 1}`);
|
|
3498
3722
|
const sha = readHeadSha(worktreePath);
|
|
3499
3723
|
if (sha && sha !== lastPushedSha) {
|
|
3500
3724
|
try {
|
|
3501
3725
|
pushBranch(branchName, worktreePath);
|
|
3502
3726
|
lastPushedSha = sha;
|
|
3503
3727
|
} catch (err) {
|
|
3504
|
-
|
|
3728
|
+
log10.warn(TAG9, `post-fix push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
|
|
3505
3729
|
}
|
|
3506
3730
|
}
|
|
3507
3731
|
break;
|
|
@@ -3510,14 +3734,14 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
3510
3734
|
}
|
|
3511
3735
|
verificationResult = result;
|
|
3512
3736
|
if (!result.passed) {
|
|
3513
|
-
|
|
3737
|
+
log10.warn(TAG9, `Verification failed for #${card.short_id} — reporting findings`);
|
|
3514
3738
|
const failSha = readHeadSha(worktreePath);
|
|
3515
3739
|
if (failSha && failSha !== lastPushedSha) {
|
|
3516
3740
|
try {
|
|
3517
3741
|
pushBranch(branchName, worktreePath);
|
|
3518
3742
|
lastPushedSha = failSha;
|
|
3519
3743
|
} catch (err) {
|
|
3520
|
-
|
|
3744
|
+
log10.warn(TAG9, `post-fail push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
|
|
3521
3745
|
}
|
|
3522
3746
|
}
|
|
3523
3747
|
const failureSummary = buildVerificationFailureSummary(result, autoFixAttempts);
|
|
@@ -3528,7 +3752,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
3528
3752
|
recoveryBranch: branchName
|
|
3529
3753
|
});
|
|
3530
3754
|
} catch (err) {
|
|
3531
|
-
|
|
3755
|
+
log10.debug(TAG9, `recordFailureSummary failed: ${err instanceof Error ? err.message : err}`);
|
|
3532
3756
|
}
|
|
3533
3757
|
await reportFindings(client, card.id, result, lastPushedSha ? { branchName, branchUrl: recoveryUrl } : null);
|
|
3534
3758
|
await moveCardToColumn(client, card, config.verification.failColumn);
|
|
@@ -3542,7 +3766,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
3542
3766
|
await teardownWorktree(client, card.id, worktreePath, branchName);
|
|
3543
3767
|
return false;
|
|
3544
3768
|
}
|
|
3545
|
-
|
|
3769
|
+
log10.info(TAG9, `Verification passed for #${card.short_id}`);
|
|
3546
3770
|
}
|
|
3547
3771
|
let prUrl = null;
|
|
3548
3772
|
if (config.completion.createPR) {
|
|
@@ -3550,19 +3774,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
3550
3774
|
prUrl = createPullRequest(card, branchName, worktreePath, config, provider);
|
|
3551
3775
|
}
|
|
3552
3776
|
if (config.completion.moveToColumn) {
|
|
3553
|
-
await
|
|
3554
|
-
try {
|
|
3555
|
-
await releaseAssignedAgent(client, card.id);
|
|
3556
|
-
} catch (err) {
|
|
3557
|
-
log9.warn(TAG9, `assignment release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
3558
|
-
}
|
|
3559
|
-
if (onMovedToCompletion) {
|
|
3560
|
-
try {
|
|
3561
|
-
await onMovedToCompletion(card);
|
|
3562
|
-
} catch (err) {
|
|
3563
|
-
log9.warn(TAG9, `successor promotion failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
3564
|
-
}
|
|
3565
|
-
}
|
|
3777
|
+
await transferCardToCompletion({ client, tag: TAG9 }, card, config.completion.moveToColumn, onMovedToCompletion);
|
|
3566
3778
|
}
|
|
3567
3779
|
if (config.completion.postSummary) {
|
|
3568
3780
|
await postSummary(client, card, branchName, worktreePath, prUrl, config.worktree.baseBranch, sessionStats);
|
|
@@ -3574,14 +3786,10 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
3574
3786
|
if (disposition)
|
|
3575
3787
|
endDisposition = disposition;
|
|
3576
3788
|
} catch (err) {
|
|
3577
|
-
|
|
3789
|
+
log10.warn(TAG9, `onBeforeWorktreeCleanup hook failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
3578
3790
|
}
|
|
3579
3791
|
}
|
|
3580
|
-
await client
|
|
3581
|
-
...endDisposition,
|
|
3582
|
-
progressPercent: 100,
|
|
3583
|
-
...buildTokenPayload(sessionStats)
|
|
3584
|
-
});
|
|
3792
|
+
await endRunSession({ client, tag: TAG9 }, card, endDisposition, buildTokenPayload(sessionStats), "throw");
|
|
3585
3793
|
if (workspaceId) {
|
|
3586
3794
|
const diffStat = captureDiffStat(worktreePath, config.worktree.baseBranch);
|
|
3587
3795
|
const changedFiles = diffStat && diffStat.files.length > 0 ? diffStat.files : sessionStats?.filesEditedPaths ?? [];
|
|
@@ -3604,7 +3812,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
3604
3812
|
});
|
|
3605
3813
|
}
|
|
3606
3814
|
await teardownWorktree(client, card.id, worktreePath, branchName);
|
|
3607
|
-
|
|
3815
|
+
log10.info(TAG9, `Completion done for #${card.short_id}${prUrl ? ` — PR: ${prUrl}` : ""}`);
|
|
3608
3816
|
return true;
|
|
3609
3817
|
}
|
|
3610
3818
|
function buildVerificationFailureSummary(result, autoFixAttempts) {
|
|
@@ -3646,7 +3854,7 @@ function commitUncommittedChanges(worktreePath, card) {
|
|
|
3646
3854
|
encoding: "utf-8"
|
|
3647
3855
|
}).trim();
|
|
3648
3856
|
} catch (err) {
|
|
3649
|
-
|
|
3857
|
+
log10.warn(TAG9, `git status failed in ${worktreePath}: ${err instanceof Error ? err.message : err}`);
|
|
3650
3858
|
return false;
|
|
3651
3859
|
}
|
|
3652
3860
|
if (status.length === 0)
|
|
@@ -3662,10 +3870,10 @@ function commitUncommittedChanges(worktreePath, card) {
|
|
|
3662
3870
|
cwd: worktreePath,
|
|
3663
3871
|
encoding: "utf-8"
|
|
3664
3872
|
});
|
|
3665
|
-
|
|
3873
|
+
log10.warn(TAG9, `Auto-committed uncommitted worktree changes for #${card.short_id} — agent ended without committing`);
|
|
3666
3874
|
return true;
|
|
3667
3875
|
} catch (err) {
|
|
3668
|
-
|
|
3876
|
+
log10.error(TAG9, `auto-commit failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
3669
3877
|
return false;
|
|
3670
3878
|
}
|
|
3671
3879
|
}
|
|
@@ -3733,20 +3941,21 @@ ${commitLog}
|
|
|
3733
3941
|
description: baseDesc + parts.join(`
|
|
3734
3942
|
`)
|
|
3735
3943
|
});
|
|
3736
|
-
|
|
3944
|
+
log10.info(TAG9, `Posted completion summary to #${card.short_id}`);
|
|
3737
3945
|
} catch (err) {
|
|
3738
|
-
|
|
3946
|
+
log10.error(TAG9, `Failed to post summary: ${err instanceof Error ? err.message : err}`);
|
|
3739
3947
|
}
|
|
3740
3948
|
}
|
|
3741
3949
|
var TAG9 = "completion";
|
|
3742
3950
|
var init_completion = __esm(() => {
|
|
3743
3951
|
init_board_helpers();
|
|
3744
3952
|
init_episode_writer();
|
|
3953
|
+
init_run_closeout();
|
|
3745
3954
|
init_types2();
|
|
3746
3955
|
});
|
|
3747
3956
|
|
|
3748
3957
|
// src/progress-tracker.ts
|
|
3749
|
-
import { log as
|
|
3958
|
+
import { log as log11 } from "@gethmy/harness";
|
|
3750
3959
|
function truncate(str, max) {
|
|
3751
3960
|
return str.length > max ? `${str.slice(0, max - 3)}...` : str;
|
|
3752
3961
|
}
|
|
@@ -3754,7 +3963,7 @@ function truncate(str, max) {
|
|
|
3754
3963
|
class ProgressTracker {
|
|
3755
3964
|
client;
|
|
3756
3965
|
cardId;
|
|
3757
|
-
|
|
3966
|
+
sessionIdentifier;
|
|
3758
3967
|
phase = "exploring";
|
|
3759
3968
|
progress = 10;
|
|
3760
3969
|
toolCallCount = 0;
|
|
@@ -3776,10 +3985,10 @@ class ProgressTracker {
|
|
|
3776
3985
|
lastEmittedProgress = -1;
|
|
3777
3986
|
lastAssistantText = "";
|
|
3778
3987
|
assistantTextBlocks = [];
|
|
3779
|
-
constructor(client, cardId,
|
|
3988
|
+
constructor(client, cardId, sessionIdentifier, subtasks, initialPhase = "exploring") {
|
|
3780
3989
|
this.client = client;
|
|
3781
3990
|
this.cardId = cardId;
|
|
3782
|
-
this.
|
|
3991
|
+
this.sessionIdentifier = sessionIdentifier;
|
|
3783
3992
|
this.subtaskTotal = subtasks.length;
|
|
3784
3993
|
this.subtaskCompleted = subtasks.filter((s) => s.completed).length;
|
|
3785
3994
|
this.subtaskMode = subtasks.length > 0;
|
|
@@ -3865,7 +4074,7 @@ class ProgressTracker {
|
|
|
3865
4074
|
}
|
|
3866
4075
|
onToolStart(name, input) {
|
|
3867
4076
|
this.toolCallCount++;
|
|
3868
|
-
|
|
4077
|
+
log11.debug(TAG10, `Tool: ${name} (count: ${this.toolCallCount}, phase: ${this.phase})`);
|
|
3869
4078
|
const filePath = this.extractString(input, "file_path");
|
|
3870
4079
|
if (filePath) {
|
|
3871
4080
|
if (EDIT_TOOLS.has(name)) {
|
|
@@ -3936,7 +4145,7 @@ class ProgressTracker {
|
|
|
3936
4145
|
transitionTo(newPhase) {
|
|
3937
4146
|
if (PHASE_ORDER[newPhase] <= PHASE_ORDER[this.phase])
|
|
3938
4147
|
return;
|
|
3939
|
-
|
|
4148
|
+
log11.info(TAG10, `Phase: ${this.phase} → ${newPhase}`);
|
|
3940
4149
|
const previousPhase = this.phase;
|
|
3941
4150
|
this.runEventSink?.recordPhaseChanged(newPhase, previousPhase);
|
|
3942
4151
|
this.phase = newPhase;
|
|
@@ -4041,9 +4250,9 @@ class ProgressTracker {
|
|
|
4041
4250
|
}
|
|
4042
4251
|
sendUpdate(currentTask) {
|
|
4043
4252
|
this.lastUpdateAt = Date.now();
|
|
4044
|
-
|
|
4253
|
+
log11.debug(TAG10, `Progress: ${this.progress}% — ${currentTask}`);
|
|
4045
4254
|
this.client.updateAgentProgress(this.cardId, {
|
|
4046
|
-
agentIdentifier:
|
|
4255
|
+
agentIdentifier: this.sessionIdentifier,
|
|
4047
4256
|
agentName: AGENT_NAME,
|
|
4048
4257
|
status: "working",
|
|
4049
4258
|
currentTask: truncate(currentTask, MAX_TASK_LENGTH),
|
|
@@ -4058,7 +4267,7 @@ class ProgressTracker {
|
|
|
4058
4267
|
modelName: this.lastCost?.modelName ?? this.requestedModel ?? undefined,
|
|
4059
4268
|
numTurns: this.lastCost?.numTurns ?? 0
|
|
4060
4269
|
}).catch((err) => {
|
|
4061
|
-
|
|
4270
|
+
log11.warn(TAG10, `Failed to send progress update: ${err}`);
|
|
4062
4271
|
});
|
|
4063
4272
|
if (this.runEventSink && this.progress !== this.lastEmittedProgress) {
|
|
4064
4273
|
this.lastEmittedProgress = this.progress;
|
|
@@ -4123,7 +4332,7 @@ var init_progress_tracker = __esm(() => {
|
|
|
4123
4332
|
});
|
|
4124
4333
|
|
|
4125
4334
|
// src/prompt.ts
|
|
4126
|
-
import { log as
|
|
4335
|
+
import { log as log12 } from "@gethmy/harness";
|
|
4127
4336
|
function buildSteeringPrompt(messages) {
|
|
4128
4337
|
if (messages.length === 1)
|
|
4129
4338
|
return messages[0];
|
|
@@ -4160,11 +4369,11 @@ async function buildPrompt(enriched, branchName, worktreePath, client, workspace
|
|
|
4160
4369
|
Do NOT push to main. All your work stays on \`${branchName}\`.
|
|
4161
4370
|
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.`
|
|
4162
4371
|
});
|
|
4163
|
-
|
|
4372
|
+
log12.info(TAG11, `Generated prompt for #${card.short_id} — ${result.contextSummary.memoryCount} memories, ${result.tokenEstimate} tokens`);
|
|
4164
4373
|
return result.prompt + pastEpisodesSection + referenceSection;
|
|
4165
4374
|
} catch (err) {
|
|
4166
4375
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4167
|
-
|
|
4376
|
+
log12.warn(TAG11, `Failed to generate prompt via API, using fallback: ${msg}`);
|
|
4168
4377
|
const commentsSection = await renderCommentsSection(client, card.id);
|
|
4169
4378
|
return buildFallbackPrompt(enriched, branchName, worktreePath) + commentsSection + pastEpisodesSection + referenceSection;
|
|
4170
4379
|
}
|
|
@@ -4182,7 +4391,7 @@ async function renderCommentsSection(client, cardId) {
|
|
|
4182
4391
|
|
|
4183
4392
|
${section}` : "";
|
|
4184
4393
|
} catch (err) {
|
|
4185
|
-
|
|
4394
|
+
log12.warn(TAG11, "comment-thread fetch failed", {
|
|
4186
4395
|
event: "comment_fetch_failed",
|
|
4187
4396
|
error: err instanceof Error ? err.message : String(err)
|
|
4188
4397
|
});
|
|
@@ -4234,7 +4443,7 @@ ${description}`.trim();
|
|
|
4234
4443
|
## Similar past tasks
|
|
4235
4444
|
${bullets}`;
|
|
4236
4445
|
} catch (err) {
|
|
4237
|
-
|
|
4446
|
+
log12.warn(TAG11, "past-episodes recall failed", {
|
|
4238
4447
|
event: "episode_recall_failed",
|
|
4239
4448
|
error: err instanceof Error ? err.message : String(err)
|
|
4240
4449
|
});
|
|
@@ -4267,7 +4476,7 @@ ${description}`.trim();
|
|
|
4267
4476
|
## How we work here
|
|
4268
4477
|
${bullets}`;
|
|
4269
4478
|
} catch (err) {
|
|
4270
|
-
|
|
4479
|
+
log12.warn(TAG11, "reference recall failed", {
|
|
4271
4480
|
event: "reference_recall_failed",
|
|
4272
4481
|
error: err instanceof Error ? err.message : String(err)
|
|
4273
4482
|
});
|
|
@@ -4322,7 +4531,7 @@ import {
|
|
|
4322
4531
|
extractPrUrl as extractPrUrl2,
|
|
4323
4532
|
getBranchWebUrl as getBranchWebUrl2,
|
|
4324
4533
|
getHeadSha,
|
|
4325
|
-
log as
|
|
4534
|
+
log as log13,
|
|
4326
4535
|
pushBranch as pushBranch2,
|
|
4327
4536
|
renameRemoteBranch,
|
|
4328
4537
|
upsertReviewedSha
|
|
@@ -4447,7 +4656,7 @@ function parseReviewOutput(stdout) {
|
|
|
4447
4656
|
try {
|
|
4448
4657
|
const parsed = JSON.parse(raw);
|
|
4449
4658
|
if (parsed && typeof parsed === "object" && "verdict" in parsed) {
|
|
4450
|
-
|
|
4659
|
+
log13.debug(TAG12, "Parsed review output from fenced JSON block");
|
|
4451
4660
|
return extractResult(parsed);
|
|
4452
4661
|
}
|
|
4453
4662
|
} catch {}
|
|
@@ -4473,21 +4682,21 @@ function parseReviewOutput(stdout) {
|
|
|
4473
4682
|
try {
|
|
4474
4683
|
const parsed = JSON.parse(candidates[i]);
|
|
4475
4684
|
if (parsed && typeof parsed === "object" && "verdict" in parsed) {
|
|
4476
|
-
|
|
4685
|
+
log13.debug(TAG12, "Parsed review output from raw JSON object");
|
|
4477
4686
|
return extractResult(parsed);
|
|
4478
4687
|
}
|
|
4479
4688
|
} catch {}
|
|
4480
4689
|
}
|
|
4481
4690
|
const verdictMatch = stdout.match(/"verdict"\s*:\s*"(approved|rejected)"/i);
|
|
4482
4691
|
if (verdictMatch) {
|
|
4483
|
-
|
|
4692
|
+
log13.warn(TAG12, `Parsed verdict via regex fallback — findings lost (${verdictMatch[1]})`);
|
|
4484
4693
|
return {
|
|
4485
4694
|
verdict: verdictMatch[1].toLowerCase(),
|
|
4486
4695
|
summary: "Parsed via regex fallback — original JSON was malformed. Check run log.",
|
|
4487
4696
|
findings: []
|
|
4488
4697
|
};
|
|
4489
4698
|
}
|
|
4490
|
-
|
|
4699
|
+
log13.warn(TAG12, "Failed to parse review JSON output — returning error verdict (card stays in Review)");
|
|
4491
4700
|
return {
|
|
4492
4701
|
verdict: "error",
|
|
4493
4702
|
summary: stdout.slice(0, 500),
|
|
@@ -4520,7 +4729,7 @@ async function postReviewComment(client, card, commentType, body) {
|
|
|
4520
4729
|
try {
|
|
4521
4730
|
await client.addComment(card.id, body, { commentType });
|
|
4522
4731
|
} catch (err) {
|
|
4523
|
-
|
|
4732
|
+
log13.error(TAG12, `Failed to post review comment to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
4524
4733
|
}
|
|
4525
4734
|
}
|
|
4526
4735
|
async function runReviewCompletion(client, card, result, config, worktreePath, branchName, sessionStats, runLogPath, workspaceId, agentSessionId, stateStore, resolvedFromPrUrl) {
|
|
@@ -4534,11 +4743,11 @@ async function runReviewCompletion(client, card, result, config, worktreePath, b
|
|
|
4534
4743
|
const currentCycle = getReviewCycle(freshDesc) + 1;
|
|
4535
4744
|
const maxCycles = config.review.maxReviewCycles;
|
|
4536
4745
|
if (result.verdict === "error") {
|
|
4537
|
-
|
|
4746
|
+
log13.warn(TAG12, `#${card.short_id} review output unparseable — labelling "${NEED_REVIEW_LABEL}" for manual inspection`);
|
|
4538
4747
|
try {
|
|
4539
4748
|
await addLabelByName(client, card, NEED_REVIEW_LABEL, NEED_REVIEW_LABEL_COLOR);
|
|
4540
4749
|
} catch (err) {
|
|
4541
|
-
|
|
4750
|
+
log13.warn(TAG12, `Failed to add "${NEED_REVIEW_LABEL}" label: ${err instanceof Error ? err.message : err}`);
|
|
4542
4751
|
}
|
|
4543
4752
|
if (config.review.postFindings) {
|
|
4544
4753
|
const rawTail = runLogPath ? tailRunLog(runLogPath) : null;
|
|
@@ -4581,7 +4790,7 @@ ${runLogTail}
|
|
|
4581
4790
|
renameRemoteBranch(branchName, newRef, worktreePath);
|
|
4582
4791
|
approvedBranch = newRef;
|
|
4583
4792
|
} catch (err) {
|
|
4584
|
-
|
|
4793
|
+
log13.warn(TAG12, `Branch rename failed (continuing on ${branchName}): ${err instanceof Error ? err.message : err}`);
|
|
4585
4794
|
}
|
|
4586
4795
|
}
|
|
4587
4796
|
if (config.review.createPR && approvedBranch) {
|
|
@@ -4602,14 +4811,14 @@ ${runLogTail}
|
|
|
4602
4811
|
});
|
|
4603
4812
|
}
|
|
4604
4813
|
} catch (err) {
|
|
4605
|
-
|
|
4814
|
+
log13.warn(TAG12, `Failed to persist PR URL to #${card.short_id} description: ${err instanceof Error ? err.message : err}`);
|
|
4606
4815
|
}
|
|
4607
4816
|
}
|
|
4608
4817
|
if (branchName) {
|
|
4609
4818
|
try {
|
|
4610
4819
|
await persistReviewedSha(client, card, worktreePath);
|
|
4611
4820
|
} catch (err) {
|
|
4612
|
-
|
|
4821
|
+
log13.warn(TAG12, `Failed to persist Reviewed-SHA to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
4613
4822
|
}
|
|
4614
4823
|
}
|
|
4615
4824
|
if (config.review.postFindings) {
|
|
@@ -4631,7 +4840,7 @@ ${runLogTail}
|
|
|
4631
4840
|
progressPercent: 100,
|
|
4632
4841
|
...buildTokenPayload(sessionStats)
|
|
4633
4842
|
});
|
|
4634
|
-
|
|
4843
|
+
log13.info(TAG12, `#${card.short_id} approved${prUrl ? ` — PR: ${prUrl}` : ""} — labeled "${config.review.approvedLabel}"`);
|
|
4635
4844
|
} else {
|
|
4636
4845
|
const reworkFindings = result.findings.filter((f) => f.relatedToDiff !== false);
|
|
4637
4846
|
const criticalFindings = reworkFindings.filter((f) => f.severity === "critical").slice(0, MAX_FINDINGS);
|
|
@@ -4639,7 +4848,7 @@ ${runLogTail}
|
|
|
4639
4848
|
const linkedFindings = [...criticalFindings, ...majorFindings];
|
|
4640
4849
|
const minorFindings = reworkFindings.filter((f) => f.severity === "minor").slice(0, MAX_FINDINGS);
|
|
4641
4850
|
if (currentCycle >= maxCycles) {
|
|
4642
|
-
|
|
4851
|
+
log13.warn(TAG12, `#${card.short_id} reached max review cycles (${maxCycles}), moving to Done with note`);
|
|
4643
4852
|
await moveCardToColumn(client, card, config.review.moveToColumn);
|
|
4644
4853
|
const body = [
|
|
4645
4854
|
"**Review — needs human review.**",
|
|
@@ -4679,7 +4888,7 @@ ${runLogTail}
|
|
|
4679
4888
|
try {
|
|
4680
4889
|
await client.createSubtask(card.id, clampSubtaskTitle(`[${finding.severity}] ${finding.title}`));
|
|
4681
4890
|
} catch (err) {
|
|
4682
|
-
|
|
4891
|
+
log13.error(TAG12, `Failed to create finding subtask: ${err instanceof Error ? err.message : err}`);
|
|
4683
4892
|
}
|
|
4684
4893
|
}));
|
|
4685
4894
|
if (linkedFindings.length > 0) {
|
|
@@ -4691,7 +4900,7 @@ ${runLogTail}
|
|
|
4691
4900
|
try {
|
|
4692
4901
|
await client.createSubtask(card.id, clampSubtaskTitle(finding.title));
|
|
4693
4902
|
} catch (err) {
|
|
4694
|
-
|
|
4903
|
+
log13.error(TAG12, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
|
|
4695
4904
|
}
|
|
4696
4905
|
}));
|
|
4697
4906
|
const baseDesc = stripReviewSummary(freshDesc);
|
|
@@ -4699,7 +4908,7 @@ ${runLogTail}
|
|
|
4699
4908
|
try {
|
|
4700
4909
|
await client.updateCard(card.id, { description: updatedDesc });
|
|
4701
4910
|
} catch (err) {
|
|
4702
|
-
|
|
4911
|
+
log13.error(TAG12, `Failed to update review cycle marker: ${err instanceof Error ? err.message : err}`);
|
|
4703
4912
|
}
|
|
4704
4913
|
const scopeLine = result.scopeCheck ? `Scope: ${result.scopeCheck.status}${result.scopeCheck.notes ? ` — ${result.scopeCheck.notes}` : ""}` : "";
|
|
4705
4914
|
const body = [
|
|
@@ -4716,9 +4925,9 @@ ${runLogTail}
|
|
|
4716
4925
|
if (config.planning.enabled && card.plan_id) {
|
|
4717
4926
|
try {
|
|
4718
4927
|
await client.updateCard(card.id, { needsPlanRefresh: true });
|
|
4719
|
-
|
|
4928
|
+
log13.info(TAG12, `#${card.short_id} flagged needs_plan_refresh after rejected review`);
|
|
4720
4929
|
} catch (err) {
|
|
4721
|
-
|
|
4930
|
+
log13.warn(TAG12, `Failed to flag needs_plan_refresh for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
4722
4931
|
}
|
|
4723
4932
|
}
|
|
4724
4933
|
await moveCardToColumn(client, card, config.review.failColumn);
|
|
@@ -4732,10 +4941,10 @@ ${runLogTail}
|
|
|
4732
4941
|
recoveryBranch
|
|
4733
4942
|
});
|
|
4734
4943
|
} catch (err) {
|
|
4735
|
-
|
|
4944
|
+
log13.debug(TAG12, `recordFailureSummary failed: ${err instanceof Error ? err.message : err}`);
|
|
4736
4945
|
}
|
|
4737
4946
|
if (recoveryBranch) {
|
|
4738
|
-
|
|
4947
|
+
log13.info(TAG12, `#${card.short_id} recovery branch ${recoveryBranch}${recoveryUrl ? ` (${recoveryUrl})` : ""}`);
|
|
4739
4948
|
}
|
|
4740
4949
|
await client.endAgentSession(card.id, {
|
|
4741
4950
|
status: "failed",
|
|
@@ -4744,7 +4953,7 @@ ${runLogTail}
|
|
|
4744
4953
|
recoveryBranch,
|
|
4745
4954
|
...buildTokenPayload(sessionStats)
|
|
4746
4955
|
});
|
|
4747
|
-
|
|
4956
|
+
log13.info(TAG12, `#${card.short_id} rejected (cycle ${currentCycle}/${maxCycles}) — moved to "${config.review.failColumn}"`);
|
|
4748
4957
|
}
|
|
4749
4958
|
if (workspaceId && (result.verdict === "approved" || result.verdict === "rejected")) {
|
|
4750
4959
|
const originalEpisodeId = await findLatestImplementEpisode(client, workspaceId, card.project_id, card.short_id);
|
|
@@ -4913,7 +5122,7 @@ var init_review_prompt = __esm(() => {
|
|
|
4913
5122
|
import { createWriteStream, mkdirSync } from "node:fs";
|
|
4914
5123
|
import { homedir as homedir2 } from "node:os";
|
|
4915
5124
|
import { join as join2 } from "node:path";
|
|
4916
|
-
import { log as
|
|
5125
|
+
import { log as log14 } from "@gethmy/harness";
|
|
4917
5126
|
function openRunLog(tag, runId, shortId) {
|
|
4918
5127
|
if (!runId)
|
|
4919
5128
|
return null;
|
|
@@ -4924,7 +5133,7 @@ function openRunLog(tag, runId, shortId) {
|
|
|
4924
5133
|
const stream = createWriteStream(path, { flags: "a" });
|
|
4925
5134
|
return { path, stream };
|
|
4926
5135
|
} catch (err) {
|
|
4927
|
-
|
|
5136
|
+
log14.warn(tag, `Failed to open run log: ${err instanceof Error ? err.message : err}`);
|
|
4928
5137
|
return null;
|
|
4929
5138
|
}
|
|
4930
5139
|
}
|
|
@@ -4959,7 +5168,7 @@ import {
|
|
|
4959
5168
|
} from "node:fs";
|
|
4960
5169
|
import { homedir as homedir3 } from "node:os";
|
|
4961
5170
|
import { dirname, join as join3 } from "node:path";
|
|
4962
|
-
import { log as
|
|
5171
|
+
import { log as log15 } from "@gethmy/harness";
|
|
4963
5172
|
function emptyState() {
|
|
4964
5173
|
return {
|
|
4965
5174
|
version: SCHEMA_VERSION,
|
|
@@ -5015,7 +5224,7 @@ class StateStore {
|
|
|
5015
5224
|
const raw = readFileSync3(this.path, "utf-8");
|
|
5016
5225
|
const parsed = JSON.parse(raw);
|
|
5017
5226
|
if (parsed?.version !== SCHEMA_VERSION) {
|
|
5018
|
-
|
|
5227
|
+
log15.warn(TAG13, `state file has version ${parsed?.version}, expected ${SCHEMA_VERSION} — migrating (preserving card budget/attempts, dropping in-flight runs)`);
|
|
5019
5228
|
return {
|
|
5020
5229
|
version: SCHEMA_VERSION,
|
|
5021
5230
|
daemonId: null,
|
|
@@ -5036,7 +5245,7 @@ class StateStore {
|
|
|
5036
5245
|
daily: parsed.daily ?? []
|
|
5037
5246
|
};
|
|
5038
5247
|
} catch (err) {
|
|
5039
|
-
|
|
5248
|
+
log15.error(TAG13, `failed to read state file: ${err instanceof Error ? err.message : err}`);
|
|
5040
5249
|
return emptyState();
|
|
5041
5250
|
}
|
|
5042
5251
|
}
|
|
@@ -5207,6 +5416,28 @@ class StateStore {
|
|
|
5207
5416
|
rec.loopIterations = 0;
|
|
5208
5417
|
await this.persist();
|
|
5209
5418
|
}
|
|
5419
|
+
async markFanoutSettled(cardId, stageId, childCardId) {
|
|
5420
|
+
const rec = this.ensureCard(cardId);
|
|
5421
|
+
if (rec.fanoutStageId !== stageId) {
|
|
5422
|
+
rec.fanoutStageId = stageId;
|
|
5423
|
+
rec.fanoutSettledChildIds = [];
|
|
5424
|
+
}
|
|
5425
|
+
const seen = rec.fanoutSettledChildIds ?? [];
|
|
5426
|
+
if (seen.includes(childCardId))
|
|
5427
|
+
return false;
|
|
5428
|
+
seen.push(childCardId);
|
|
5429
|
+
rec.fanoutSettledChildIds = seen;
|
|
5430
|
+
await this.persist();
|
|
5431
|
+
return true;
|
|
5432
|
+
}
|
|
5433
|
+
async resetFanoutSettled(cardId) {
|
|
5434
|
+
const rec = this.getCard(cardId);
|
|
5435
|
+
if (!rec || rec.fanoutStageId == null && rec.fanoutSettledChildIds == null)
|
|
5436
|
+
return;
|
|
5437
|
+
rec.fanoutStageId = null;
|
|
5438
|
+
rec.fanoutSettledChildIds = [];
|
|
5439
|
+
await this.persist();
|
|
5440
|
+
}
|
|
5210
5441
|
async markAwaitingDecision(cardId, opts) {
|
|
5211
5442
|
const rec = this.ensureCard(cardId);
|
|
5212
5443
|
rec.awaitingDecisionUntil = opts.until;
|
|
@@ -5274,7 +5505,7 @@ var init_state_store = () => {};
|
|
|
5274
5505
|
|
|
5275
5506
|
// src/stream-parser.ts
|
|
5276
5507
|
import { EventEmitter } from "node:events";
|
|
5277
|
-
import { log as
|
|
5508
|
+
import { log as log16 } from "@gethmy/harness";
|
|
5278
5509
|
function normalizeToolResultContent(raw) {
|
|
5279
5510
|
if (raw == null)
|
|
5280
5511
|
return;
|
|
@@ -5342,14 +5573,14 @@ var init_stream_parser = __esm(() => {
|
|
|
5342
5573
|
try {
|
|
5343
5574
|
msg = JSON.parse(line);
|
|
5344
5575
|
} catch {
|
|
5345
|
-
|
|
5576
|
+
log16.debug(TAG14, `Non-JSON line: ${line.slice(0, 100)}`);
|
|
5346
5577
|
return;
|
|
5347
5578
|
}
|
|
5348
5579
|
try {
|
|
5349
5580
|
this.handleMessage(msg);
|
|
5350
5581
|
} catch (err) {
|
|
5351
5582
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
5352
|
-
|
|
5583
|
+
log16.warn(TAG14, `Error handling stream event: ${errMsg}`);
|
|
5353
5584
|
this.emit("parse_error", errMsg);
|
|
5354
5585
|
}
|
|
5355
5586
|
}
|
|
@@ -5425,7 +5656,7 @@ var init_stream_parser = __esm(() => {
|
|
|
5425
5656
|
});
|
|
5426
5657
|
|
|
5427
5658
|
// src/transitions.ts
|
|
5428
|
-
import { log as
|
|
5659
|
+
import { log as log17 } from "@gethmy/harness";
|
|
5429
5660
|
async function withRetry(step, cardShortId, op, attempts, backoffMs) {
|
|
5430
5661
|
let lastErr;
|
|
5431
5662
|
for (let i = 0;i < attempts; i++) {
|
|
@@ -5436,7 +5667,7 @@ async function withRetry(step, cardShortId, op, attempts, backoffMs) {
|
|
|
5436
5667
|
const msg2 = err instanceof Error ? err.message : String(err);
|
|
5437
5668
|
if (i < attempts - 1) {
|
|
5438
5669
|
const wait = backoffMs * 2 ** i;
|
|
5439
|
-
|
|
5670
|
+
log17.warn(TAG15, `${step} failed for #${cardShortId} (attempt ${i + 1}/${attempts}): ${msg2} — retrying in ${wait}ms`);
|
|
5440
5671
|
await new Promise((r) => setTimeout(r, wait));
|
|
5441
5672
|
}
|
|
5442
5673
|
}
|
|
@@ -5460,10 +5691,10 @@ async function runTransition(client, card, plan, opts = {}) {
|
|
|
5460
5691
|
if (opts.strictColumn) {
|
|
5461
5692
|
throw new TransitionError("move", 1, msg);
|
|
5462
5693
|
}
|
|
5463
|
-
|
|
5694
|
+
log17.warn(TAG15, `#${shortId}: ${msg} — skipping move`);
|
|
5464
5695
|
} else if (card.column_id !== target.id) {
|
|
5465
5696
|
await withRetry("move", shortId, () => client.moveCard(card.id, target.id), attempts, backoffMs);
|
|
5466
|
-
|
|
5697
|
+
log17.info(TAG15, `#${shortId} → "${target.name}"`);
|
|
5467
5698
|
card.column_id = target.id;
|
|
5468
5699
|
moveLanded = true;
|
|
5469
5700
|
} else {
|
|
@@ -5482,7 +5713,7 @@ async function runTransition(client, card, plan, opts = {}) {
|
|
|
5482
5713
|
continue;
|
|
5483
5714
|
await withRetry("addLabel", shortId, () => client.addLabelToCard(card.id, labelId), attempts, backoffMs);
|
|
5484
5715
|
existing.add(labelId);
|
|
5485
|
-
|
|
5716
|
+
log17.info(TAG15, `#${shortId} +label "${name}"`);
|
|
5486
5717
|
}
|
|
5487
5718
|
card.labelIds = Array.from(existing);
|
|
5488
5719
|
}
|
|
@@ -5494,23 +5725,23 @@ async function runTransition(client, card, plan, opts = {}) {
|
|
|
5494
5725
|
continue;
|
|
5495
5726
|
await withRetry("removeLabel", shortId, () => client.removeLabelFromCard(card.id, match.id), attempts, backoffMs);
|
|
5496
5727
|
existing.delete(match.id);
|
|
5497
|
-
|
|
5728
|
+
log17.info(TAG15, `#${shortId} -label "${name}"`);
|
|
5498
5729
|
}
|
|
5499
5730
|
card.labelIds = Array.from(existing);
|
|
5500
5731
|
}
|
|
5501
5732
|
if (plan.updateCard) {
|
|
5502
5733
|
await withRetry("updateCard", shortId, () => client.updateCard(card.id, plan.updateCard), attempts, backoffMs);
|
|
5503
|
-
|
|
5734
|
+
log17.info(TAG15, `#${shortId} updated`);
|
|
5504
5735
|
}
|
|
5505
5736
|
if (plan.endSession) {
|
|
5506
5737
|
const endResult = await withRetry("endSession", shortId, () => client.endAgentSession(card.id, plan.endSession), attempts, backoffMs);
|
|
5507
5738
|
result.endSession = endResult;
|
|
5508
|
-
|
|
5739
|
+
log17.info(TAG15, `#${shortId} session ended (${plan.endSession.status})`);
|
|
5509
5740
|
}
|
|
5510
5741
|
if (plan.assignAgent !== undefined) {
|
|
5511
5742
|
const assignedAgentId = plan.assignAgent;
|
|
5512
5743
|
await withRetry("assignAgent", shortId, () => client.updateCard(card.id, { assignedAgentId }), attempts, backoffMs);
|
|
5513
|
-
|
|
5744
|
+
log17.info(TAG15, assignedAgentId ? `#${shortId} assigned → agent ${assignedAgentId}` : `#${shortId} unassigned`);
|
|
5514
5745
|
}
|
|
5515
5746
|
if (opts.store && opts.runId) {
|
|
5516
5747
|
try {
|
|
@@ -5524,7 +5755,7 @@ async function ensureLabel(client, projectId, name, color, attempts, backoffMs)
|
|
|
5524
5755
|
const result = await withRetry("addLabel", 0, () => client.createLabel(projectId, { name, color: color ?? "#8b5cf6" }), attempts, backoffMs);
|
|
5525
5756
|
return result?.label?.id ?? null;
|
|
5526
5757
|
} catch (err) {
|
|
5527
|
-
|
|
5758
|
+
log17.warn(TAG15, `ensureLabel "${name}" failed: ${err instanceof Error ? err.message : err}`);
|
|
5528
5759
|
return null;
|
|
5529
5760
|
}
|
|
5530
5761
|
}
|
|
@@ -5552,7 +5783,7 @@ import {
|
|
|
5552
5783
|
collectGateEvidence,
|
|
5553
5784
|
DevServerReadinessError,
|
|
5554
5785
|
formatDiffSummary,
|
|
5555
|
-
log as
|
|
5786
|
+
log as log18,
|
|
5556
5787
|
probeDevServer,
|
|
5557
5788
|
resolveStageGate,
|
|
5558
5789
|
signalGroup,
|
|
@@ -5591,6 +5822,7 @@ class ReviewWorker {
|
|
|
5591
5822
|
cliSessionId = null;
|
|
5592
5823
|
grantedTurns = null;
|
|
5593
5824
|
resumeMessage = null;
|
|
5825
|
+
sessionIdentifier = "";
|
|
5594
5826
|
get effectiveMaxTurns() {
|
|
5595
5827
|
return this.grantedTurns ?? this.config.claude.reviewMaxTurns;
|
|
5596
5828
|
}
|
|
@@ -5602,6 +5834,7 @@ class ReviewWorker {
|
|
|
5602
5834
|
this.stateStore = stateStore;
|
|
5603
5835
|
this.workspaceId = workspaceId;
|
|
5604
5836
|
this.id = id;
|
|
5837
|
+
this.sessionIdentifier = agentIdentifier(id);
|
|
5605
5838
|
}
|
|
5606
5839
|
startHeartbeat() {
|
|
5607
5840
|
this.stopHeartbeat();
|
|
@@ -5640,7 +5873,7 @@ class ReviewWorker {
|
|
|
5640
5873
|
cliSessionId: this.cliSessionId
|
|
5641
5874
|
});
|
|
5642
5875
|
} catch (err) {
|
|
5643
|
-
|
|
5876
|
+
log18.warn(this.tag, `state store updateRun failed: ${err instanceof Error ? err.message : err}`);
|
|
5644
5877
|
}
|
|
5645
5878
|
}
|
|
5646
5879
|
get tag() {
|
|
@@ -5680,6 +5913,7 @@ class ReviewWorker {
|
|
|
5680
5913
|
this.startedAt = Date.now();
|
|
5681
5914
|
this.runId = newRunId();
|
|
5682
5915
|
const resuming = this.stateStore.getResumableRunForCard(card.id);
|
|
5916
|
+
this.sessionIdentifier = agentIdentifier(resuming?.workerId ?? this.id);
|
|
5683
5917
|
if (resuming) {
|
|
5684
5918
|
this.runId = resuming.runId;
|
|
5685
5919
|
this.worktreePath = resuming.worktreePath;
|
|
@@ -5694,12 +5928,12 @@ class ReviewWorker {
|
|
|
5694
5928
|
resumeMessage: null
|
|
5695
5929
|
});
|
|
5696
5930
|
} catch (err) {
|
|
5697
|
-
|
|
5931
|
+
log18.warn(this.tag, `Failed to clear the consumed grant: ${err}`);
|
|
5698
5932
|
}
|
|
5699
5933
|
}
|
|
5700
5934
|
try {
|
|
5701
5935
|
this.state = "preparing";
|
|
5702
|
-
|
|
5936
|
+
log18.info(this.tag, resuming ? `Resuming review of #${card.short_id} "${card.title}" with ${this.grantedTurns ?? "the default"} more turns` : `Preparing review for #${card.short_id} "${card.title}"`);
|
|
5703
5937
|
this.startHeartbeat();
|
|
5704
5938
|
if (!resuming) {
|
|
5705
5939
|
await this.stateStore.insertRun({
|
|
@@ -5727,16 +5961,16 @@ class ReviewWorker {
|
|
|
5727
5961
|
const resolution = await resolveReviewBranch(card.description, repoRoot);
|
|
5728
5962
|
if (resolution.kind !== "branch") {
|
|
5729
5963
|
const why = resolution.kind === "skip" ? resolution.reason : "no branch or PR reference";
|
|
5730
|
-
|
|
5964
|
+
log18.info(this.tag, `#${card.short_id} not auto-reviewable (${why}) — marking for human review (staying in Review)`);
|
|
5731
5965
|
await addLabelByName(this.client, card, NEED_REVIEW_LABEL, NEED_REVIEW_LABEL_COLOR);
|
|
5732
5966
|
return;
|
|
5733
5967
|
}
|
|
5734
5968
|
this.branchName = resolution.branch;
|
|
5735
|
-
|
|
5969
|
+
log18.info(this.tag, `Review branch: ${this.branchName}`);
|
|
5736
5970
|
let reviewSession;
|
|
5737
5971
|
try {
|
|
5738
5972
|
const started = await this.client.startAgentSession(card.id, {
|
|
5739
|
-
agentIdentifier:
|
|
5973
|
+
agentIdentifier: this.sessionIdentifier,
|
|
5740
5974
|
agentName: `${AGENT_NAME} (Review)`,
|
|
5741
5975
|
agentId: this.identity.agentId,
|
|
5742
5976
|
status: "working",
|
|
@@ -5750,7 +5984,7 @@ class ReviewWorker {
|
|
|
5750
5984
|
} catch (err) {
|
|
5751
5985
|
if (isSessionConflict(err)) {
|
|
5752
5986
|
this.sessionConflict = true;
|
|
5753
|
-
|
|
5987
|
+
log18.info(this.tag, `Skipping review of #${card.short_id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
5754
5988
|
return;
|
|
5755
5989
|
}
|
|
5756
5990
|
throw err;
|
|
@@ -5769,7 +6003,7 @@ class ReviewWorker {
|
|
|
5769
6003
|
}
|
|
5770
6004
|
const port = this.reviewPort;
|
|
5771
6005
|
const cwd = this.worktreePath;
|
|
5772
|
-
|
|
6006
|
+
log18.info(this.tag, `Starting dev server on port ${port}...`);
|
|
5773
6007
|
const [devCmd, devArgs] = spawnRunArgs("dev", "--port", String(port));
|
|
5774
6008
|
this.devServerProcess = spawnInGroup(devCmd, devArgs, {
|
|
5775
6009
|
cwd,
|
|
@@ -5780,7 +6014,7 @@ class ReviewWorker {
|
|
|
5780
6014
|
devServerSpawnError = err;
|
|
5781
6015
|
});
|
|
5782
6016
|
await this.client.updateAgentProgress(card.id, {
|
|
5783
|
-
agentIdentifier:
|
|
6017
|
+
agentIdentifier: this.sessionIdentifier,
|
|
5784
6018
|
agentName: `${AGENT_NAME} (Review)`,
|
|
5785
6019
|
status: "waiting",
|
|
5786
6020
|
currentTask: `Starting dev server on port ${port}…`,
|
|
@@ -5791,9 +6025,9 @@ class ReviewWorker {
|
|
|
5791
6025
|
}
|
|
5792
6026
|
await waitForDevServer(this.devServerProcess, 30000);
|
|
5793
6027
|
await probeDevServer(port);
|
|
5794
|
-
|
|
6028
|
+
log18.info(this.tag, `Dev server ready on port ${port}`);
|
|
5795
6029
|
await this.client.updateAgentProgress(card.id, {
|
|
5796
|
-
agentIdentifier:
|
|
6030
|
+
agentIdentifier: this.sessionIdentifier,
|
|
5797
6031
|
agentName: `${AGENT_NAME} (Review)`,
|
|
5798
6032
|
status: "working",
|
|
5799
6033
|
currentTask: "Reviewing changes",
|
|
@@ -5824,10 +6058,10 @@ class ReviewWorker {
|
|
|
5824
6058
|
pinnedContract = extractPinnedContract(comments, this.identity);
|
|
5825
6059
|
}
|
|
5826
6060
|
} catch (err) {
|
|
5827
|
-
|
|
6061
|
+
log18.warn(this.tag, `pinned-contract fetch failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
5828
6062
|
}
|
|
5829
6063
|
if (pinnedContract) {
|
|
5830
|
-
|
|
6064
|
+
log18.info(this.tag, `Grading pinned contract for #${card.short_id} (${pinnedContract.assertions.length} criteria)`);
|
|
5831
6065
|
}
|
|
5832
6066
|
}
|
|
5833
6067
|
const systemPrompt = buildReviewSystemPrompt();
|
|
@@ -5845,21 +6079,21 @@ ${userPrompt}`;
|
|
|
5845
6079
|
contextIncluded: { source: "review-knowledge", mode: "review" }
|
|
5846
6080
|
});
|
|
5847
6081
|
} catch (err) {
|
|
5848
|
-
|
|
6082
|
+
log18.warn(this.tag, `prompt_history persistence skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
5849
6083
|
}
|
|
5850
6084
|
await this.client.updateAgentProgress(card.id, {
|
|
5851
|
-
agentIdentifier:
|
|
6085
|
+
agentIdentifier: this.sessionIdentifier,
|
|
5852
6086
|
agentName: `${AGENT_NAME} (Review)`,
|
|
5853
6087
|
status: "working",
|
|
5854
6088
|
currentTask: "Running Claude review",
|
|
5855
6089
|
progressPercent: 20
|
|
5856
6090
|
});
|
|
5857
6091
|
this.timeoutTimer = setTimeout(() => {
|
|
5858
|
-
|
|
6092
|
+
log18.warn(this.tag, `Review timeout reached (${this.config.review.maxTimeout}ms), cancelling`);
|
|
5859
6093
|
this.timedOut = true;
|
|
5860
6094
|
this.cancel("timeout");
|
|
5861
6095
|
}, this.config.review.maxTimeout);
|
|
5862
|
-
this.progressTracker = new ProgressTracker(this.client, card.id, this.
|
|
6096
|
+
this.progressTracker = new ProgressTracker(this.client, card.id, this.sessionIdentifier, subtasks);
|
|
5863
6097
|
this.progressTracker.setRequestedModel(this.config.claude.reviewModel);
|
|
5864
6098
|
const stdout = await this.spawnClaude(userPrompt, systemPrompt, this.progressTracker, card.short_id, {
|
|
5865
6099
|
maxTurns: this.grantedTurns ?? undefined,
|
|
@@ -5877,12 +6111,12 @@ ${userPrompt}`;
|
|
|
5877
6111
|
}
|
|
5878
6112
|
this.state = "completing";
|
|
5879
6113
|
await this.recordPhase("completing");
|
|
5880
|
-
|
|
6114
|
+
log18.info(this.tag, `Claude review finished for #${card.short_id}`);
|
|
5881
6115
|
this.killDevServer();
|
|
5882
6116
|
const result = parseReviewOutput(stdout);
|
|
5883
|
-
|
|
6117
|
+
log18.info(this.tag, `Review verdict: ${result.verdict} (${result.findings.length} finding(s))`);
|
|
5884
6118
|
await this.client.updateAgentProgress(card.id, {
|
|
5885
|
-
agentIdentifier:
|
|
6119
|
+
agentIdentifier: this.sessionIdentifier,
|
|
5886
6120
|
agentName: `${AGENT_NAME} (Review)`,
|
|
5887
6121
|
status: "working",
|
|
5888
6122
|
currentTask: `Processing ${result.verdict} verdict`,
|
|
@@ -5901,7 +6135,7 @@ ${userPrompt}`;
|
|
|
5901
6135
|
}
|
|
5902
6136
|
this.state = "error";
|
|
5903
6137
|
const msg = err instanceof Error ? err.message : String(err);
|
|
5904
|
-
|
|
6138
|
+
log18.error(this.tag, `Error reviewing #${card.short_id}: ${msg}`);
|
|
5905
6139
|
try {
|
|
5906
6140
|
const stats = this.lastSessionStats ?? this.progressTracker?.stats;
|
|
5907
6141
|
await runTransition(this.client, card, {
|
|
@@ -5911,21 +6145,21 @@ ${userPrompt}`;
|
|
|
5911
6145
|
}
|
|
5912
6146
|
});
|
|
5913
6147
|
} catch (tErr) {
|
|
5914
|
-
|
|
6148
|
+
log18.error(this.tag, `endAgentSession unrecoverable on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
|
|
5915
6149
|
}
|
|
5916
6150
|
if (err instanceof DevServerReadinessError) {
|
|
5917
6151
|
try {
|
|
5918
6152
|
await addLabelByName(this.client, card, NEED_REVIEW_LABEL, NEED_REVIEW_LABEL_COLOR);
|
|
5919
|
-
|
|
6153
|
+
log18.info(this.tag, `#${card.short_id} kept in Review — dev server unavailable, human review needed`);
|
|
5920
6154
|
} catch {
|
|
5921
|
-
|
|
6155
|
+
log18.warn(this.tag, "Failed to add Need Review label after dev-server failure");
|
|
5922
6156
|
}
|
|
5923
6157
|
} else {
|
|
5924
6158
|
try {
|
|
5925
6159
|
await moveCardToColumn(this.client, card, this.config.review.failColumn);
|
|
5926
|
-
|
|
6160
|
+
log18.info(this.tag, `Moved #${card.short_id} to "${this.config.review.failColumn}" after error`);
|
|
5927
6161
|
} catch {
|
|
5928
|
-
|
|
6162
|
+
log18.warn(this.tag, "Failed to move card to fail column after error");
|
|
5929
6163
|
}
|
|
5930
6164
|
}
|
|
5931
6165
|
if (this.runId) {
|
|
@@ -5965,7 +6199,7 @@ ${userPrompt}`;
|
|
|
5965
6199
|
const holderMessage = err instanceof Error ? err.message : String(err);
|
|
5966
6200
|
const waitHours = this.config.budget.pause.waitHours;
|
|
5967
6201
|
const until = computeDecisionDeadline(waitHours);
|
|
5968
|
-
|
|
6202
|
+
log18.warn(this.tag, `#${card.short_id} stays parked — the resume could not reclaim its session: ${holderMessage}`);
|
|
5969
6203
|
try {
|
|
5970
6204
|
await this.client.addComment(card.id, formatResumeConflictComment({
|
|
5971
6205
|
holderMessage,
|
|
@@ -5977,7 +6211,7 @@ ${userPrompt}`;
|
|
|
5977
6211
|
agentSessionId: this.sessionId ?? undefined
|
|
5978
6212
|
});
|
|
5979
6213
|
} catch (commentErr) {
|
|
5980
|
-
|
|
6214
|
+
log18.warn(this.tag, `Failed to post the resume-conflict note for #${card.short_id}: ${commentErr}`);
|
|
5981
6215
|
}
|
|
5982
6216
|
if (this.runId) {
|
|
5983
6217
|
const run = this.stateStore.getRun(this.runId);
|
|
@@ -5988,7 +6222,7 @@ ${userPrompt}`;
|
|
|
5988
6222
|
awaitingDecisionUntil: until
|
|
5989
6223
|
});
|
|
5990
6224
|
} catch (storeErr) {
|
|
5991
|
-
|
|
6225
|
+
log18.error(this.tag, `#${card.short_id} could not be re-parked after a resume conflict — the daemon has no hold on it: ${storeErr}`);
|
|
5992
6226
|
}
|
|
5993
6227
|
}
|
|
5994
6228
|
}
|
|
@@ -6000,7 +6234,7 @@ ${userPrompt}`;
|
|
|
6000
6234
|
this.progressTracker = null;
|
|
6001
6235
|
const waitHours = this.config.budget.pause.waitHours;
|
|
6002
6236
|
const until = computeDecisionDeadline(waitHours);
|
|
6003
|
-
|
|
6237
|
+
log18.warn(this.tag, `#${card.short_id} parked (${trigger}) — awaiting a human decision for ${waitHours}h`);
|
|
6004
6238
|
const body = formatBudgetComment({
|
|
6005
6239
|
trigger,
|
|
6006
6240
|
numTurns: stats?.cost?.numTurns ?? 0,
|
|
@@ -6019,18 +6253,18 @@ ${userPrompt}`;
|
|
|
6019
6253
|
});
|
|
6020
6254
|
commentId = res?.comment?.id ?? null;
|
|
6021
6255
|
} catch (err) {
|
|
6022
|
-
|
|
6256
|
+
log18.warn(this.tag, `Failed to post the budget-pause comment: ${err}`);
|
|
6023
6257
|
}
|
|
6024
6258
|
try {
|
|
6025
6259
|
await this.client.updateAgentProgress(card.id, {
|
|
6026
|
-
agentIdentifier:
|
|
6260
|
+
agentIdentifier: this.sessionIdentifier,
|
|
6027
6261
|
agentName: `${AGENT_NAME} (Review)`,
|
|
6028
6262
|
status: "blocked",
|
|
6029
6263
|
currentTask: "Waiting for your decision on the turn budget",
|
|
6030
6264
|
awaitingDecisionUntil: new Date(until).toISOString()
|
|
6031
6265
|
});
|
|
6032
6266
|
} catch (err) {
|
|
6033
|
-
|
|
6267
|
+
log18.warn(this.tag, `Failed to mark the session blocked: ${err}`);
|
|
6034
6268
|
}
|
|
6035
6269
|
if (this.runId) {
|
|
6036
6270
|
try {
|
|
@@ -6042,14 +6276,14 @@ ${userPrompt}`;
|
|
|
6042
6276
|
numTurns: stats?.cost?.numTurns ?? 0
|
|
6043
6277
|
});
|
|
6044
6278
|
} catch (err) {
|
|
6045
|
-
|
|
6279
|
+
log18.error(this.tag, `#${card.short_id} parkRun failed after the blocker comment + blocked session were already written — the daemon has no local record of this park: ${err}`);
|
|
6046
6280
|
}
|
|
6047
6281
|
}
|
|
6048
6282
|
}
|
|
6049
6283
|
async pause() {
|
|
6050
6284
|
if (!this.isActive || !this.process || this.process.killed)
|
|
6051
6285
|
return;
|
|
6052
|
-
|
|
6286
|
+
log18.info(this.tag, `Pausing review on ${this.cardId}`);
|
|
6053
6287
|
signalGroup(this.process, "SIGSTOP");
|
|
6054
6288
|
if (this.timeoutTimer) {
|
|
6055
6289
|
clearTimeout(this.timeoutTimer);
|
|
@@ -6058,34 +6292,34 @@ ${userPrompt}`;
|
|
|
6058
6292
|
if (this.cardId) {
|
|
6059
6293
|
try {
|
|
6060
6294
|
await this.client.updateAgentProgress(this.cardId, {
|
|
6061
|
-
agentIdentifier:
|
|
6062
|
-
agentName: AGENT_NAME
|
|
6295
|
+
agentIdentifier: this.sessionIdentifier,
|
|
6296
|
+
agentName: `${AGENT_NAME} (Review)`,
|
|
6063
6297
|
status: "paused"
|
|
6064
6298
|
});
|
|
6065
6299
|
} catch {
|
|
6066
|
-
|
|
6300
|
+
log18.warn(this.tag, "Failed to update agent session to paused");
|
|
6067
6301
|
}
|
|
6068
6302
|
}
|
|
6069
6303
|
}
|
|
6070
6304
|
async resume() {
|
|
6071
6305
|
if (!this.isActive || !this.process || this.process.killed)
|
|
6072
6306
|
return;
|
|
6073
|
-
|
|
6307
|
+
log18.info(this.tag, `Resuming review on ${this.cardId}`);
|
|
6074
6308
|
signalGroup(this.process, "SIGCONT");
|
|
6075
6309
|
this.timeoutTimer = setTimeout(() => {
|
|
6076
|
-
|
|
6310
|
+
log18.warn(this.tag, `Timeout reached (${this.config.review.maxTimeout}ms), cancelling`);
|
|
6077
6311
|
this.timedOut = true;
|
|
6078
6312
|
this.cancel("timeout");
|
|
6079
6313
|
}, this.config.review.maxTimeout);
|
|
6080
6314
|
if (this.cardId) {
|
|
6081
6315
|
try {
|
|
6082
6316
|
await this.client.updateAgentProgress(this.cardId, {
|
|
6083
|
-
agentIdentifier:
|
|
6084
|
-
agentName: AGENT_NAME
|
|
6317
|
+
agentIdentifier: this.sessionIdentifier,
|
|
6318
|
+
agentName: `${AGENT_NAME} (Review)`,
|
|
6085
6319
|
status: "working"
|
|
6086
6320
|
});
|
|
6087
6321
|
} catch {
|
|
6088
|
-
|
|
6322
|
+
log18.warn(this.tag, "Failed to update agent session to working");
|
|
6089
6323
|
}
|
|
6090
6324
|
}
|
|
6091
6325
|
}
|
|
@@ -6094,7 +6328,7 @@ ${userPrompt}`;
|
|
|
6094
6328
|
return;
|
|
6095
6329
|
this.aborted = true;
|
|
6096
6330
|
this.state = "cancelling";
|
|
6097
|
-
|
|
6331
|
+
log18.info(this.tag, `Cancelling review on ${this.cardId}`);
|
|
6098
6332
|
const snapshotStats = this.lastSessionStats ?? this.progressTracker?.stats;
|
|
6099
6333
|
if (this.progressTracker) {
|
|
6100
6334
|
this.progressTracker?.stop();
|
|
@@ -6144,11 +6378,11 @@ ${userPrompt}`;
|
|
|
6144
6378
|
"--",
|
|
6145
6379
|
prompt
|
|
6146
6380
|
];
|
|
6147
|
-
|
|
6381
|
+
log18.info(this.tag, `Spawning review: claude ${args.slice(0, 5).join(" ")} ...`);
|
|
6148
6382
|
const runLog = openRunLog(this.tag, this.runId, shortId);
|
|
6149
6383
|
this.lastRunLogPath = runLog?.path ?? null;
|
|
6150
6384
|
if (runLog) {
|
|
6151
|
-
|
|
6385
|
+
log18.info(this.tag, `Run log: ${runLog.path}`);
|
|
6152
6386
|
runLog.stream.write(`# run=${this.runId} card=#${shortId} pipeline=review started=${new Date().toISOString()}
|
|
6153
6387
|
` + `# args: ${args.slice(0, -2).join(" ")} -- <prompt:${prompt.length} chars>
|
|
6154
6388
|
|
|
@@ -6166,7 +6400,7 @@ ${userPrompt}`;
|
|
|
6166
6400
|
this.captureCliSessionId(parser.sessionId);
|
|
6167
6401
|
});
|
|
6168
6402
|
parser.on("parse_error", (msg) => {
|
|
6169
|
-
|
|
6403
|
+
log18.debug(this.tag, `Stream parse error (non-fatal): ${msg}`);
|
|
6170
6404
|
runLog?.stream.write(`
|
|
6171
6405
|
[parse_error] ${msg}
|
|
6172
6406
|
`);
|
|
@@ -6245,16 +6479,16 @@ ${userPrompt}`;
|
|
|
6245
6479
|
const evidence = await collectGateEvidence(registry, context);
|
|
6246
6480
|
const evaluation = gateEvaluate(resolved.gate, evidence);
|
|
6247
6481
|
await this.client.request("POST", `/cards/${encodeURIComponent(card.id)}/stage-gate-evidence`, toStageGateEvidenceInsert(context, evidence));
|
|
6248
|
-
|
|
6482
|
+
log18.info(this.tag, `Recorded review_passed gate evidence for #${card.short_id} stage "${resolved.stage.name}": result=${evidence.result} passed=${evaluation.passed}`);
|
|
6249
6483
|
} catch (err) {
|
|
6250
|
-
|
|
6484
|
+
log18.warn(this.tag, `review gate evidence collection failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
6251
6485
|
}
|
|
6252
6486
|
}
|
|
6253
6487
|
killDevServer() {
|
|
6254
6488
|
if (this.devServerProcess && !this.devServerProcess.killed) {
|
|
6255
6489
|
signalGroup(this.devServerProcess, "SIGTERM");
|
|
6256
6490
|
this.devServerProcess = null;
|
|
6257
|
-
|
|
6491
|
+
log18.debug(this.tag, "Killed dev server group");
|
|
6258
6492
|
}
|
|
6259
6493
|
}
|
|
6260
6494
|
cleanup() {
|
|
@@ -6272,7 +6506,7 @@ ${userPrompt}`;
|
|
|
6272
6506
|
try {
|
|
6273
6507
|
cleanupWorktree3(this.worktreePath);
|
|
6274
6508
|
} catch {
|
|
6275
|
-
|
|
6509
|
+
log18.warn(this.tag, "Failed to cleanup review worktree");
|
|
6276
6510
|
}
|
|
6277
6511
|
}
|
|
6278
6512
|
this.process = null;
|
|
@@ -6305,7 +6539,7 @@ var init_review_worker = __esm(() => {
|
|
|
6305
6539
|
|
|
6306
6540
|
// src/sleep-guard.ts
|
|
6307
6541
|
import { spawn } from "node:child_process";
|
|
6308
|
-
import { log as
|
|
6542
|
+
import { log as log19 } from "@gethmy/harness";
|
|
6309
6543
|
|
|
6310
6544
|
class SleepGuard {
|
|
6311
6545
|
platform;
|
|
@@ -6333,7 +6567,7 @@ class SleepGuard {
|
|
|
6333
6567
|
if (!this.child.killed)
|
|
6334
6568
|
this.child.kill("SIGTERM");
|
|
6335
6569
|
this.child = null;
|
|
6336
|
-
|
|
6570
|
+
log19.info(TAG17, "sleep assertion released");
|
|
6337
6571
|
}
|
|
6338
6572
|
}
|
|
6339
6573
|
start() {
|
|
@@ -6348,7 +6582,7 @@ class SleepGuard {
|
|
|
6348
6582
|
spawned = true;
|
|
6349
6583
|
});
|
|
6350
6584
|
child.on("error", (err) => {
|
|
6351
|
-
|
|
6585
|
+
log19.warn(TAG17, `caffeinate unavailable: ${err.message}`);
|
|
6352
6586
|
if (this.child === child)
|
|
6353
6587
|
this.child = null;
|
|
6354
6588
|
});
|
|
@@ -6361,9 +6595,9 @@ class SleepGuard {
|
|
|
6361
6595
|
});
|
|
6362
6596
|
child.unref();
|
|
6363
6597
|
this.child = child;
|
|
6364
|
-
|
|
6598
|
+
log19.info(TAG17, "sleep assertion acquired (caffeinate -i)");
|
|
6365
6599
|
} catch (err) {
|
|
6366
|
-
|
|
6600
|
+
log19.warn(TAG17, `failed to spawn caffeinate: ${err instanceof Error ? err.message : err}`);
|
|
6367
6601
|
}
|
|
6368
6602
|
}
|
|
6369
6603
|
}
|
|
@@ -6371,13 +6605,13 @@ var TAG17 = "sleep-guard";
|
|
|
6371
6605
|
var init_sleep_guard = () => {};
|
|
6372
6606
|
|
|
6373
6607
|
// src/unblock.ts
|
|
6374
|
-
import { log as
|
|
6608
|
+
import { log as log20 } from "@gethmy/harness";
|
|
6375
6609
|
async function fetchBlocksLinks(client, cardId) {
|
|
6376
6610
|
try {
|
|
6377
6611
|
const { links } = await client.getCardLinks(cardId);
|
|
6378
6612
|
return links.filter((l) => l.link_type === "blocks");
|
|
6379
6613
|
} catch (err) {
|
|
6380
|
-
|
|
6614
|
+
log20.warn(TAG18, `link fetch failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
6381
6615
|
return null;
|
|
6382
6616
|
}
|
|
6383
6617
|
}
|
|
@@ -6409,23 +6643,23 @@ async function promoteUnblockedSuccessors(completedCard, deps) {
|
|
|
6409
6643
|
const successors = links.filter((l) => l.direction === "outgoing" && !l.target_card.done);
|
|
6410
6644
|
if (successors.length === 0)
|
|
6411
6645
|
return;
|
|
6412
|
-
|
|
6646
|
+
log20.info(TAG18, `#${completedCard.short_id} completed — checking ${successors.length} chained successor(s)`);
|
|
6413
6647
|
for (const link of successors) {
|
|
6414
6648
|
const successorId = link.target_card.id;
|
|
6415
6649
|
try {
|
|
6416
6650
|
const { card } = await deps.client.getCard(successorId);
|
|
6417
6651
|
if (card.assigned_agent_id === deps.agentId) {} else if (card.assigned_agent_id === null && !card.assignee_id) {
|
|
6418
|
-
|
|
6652
|
+
log20.info(TAG18, `successor #${card.short_id} unassigned — auto-assigning to continue chain`);
|
|
6419
6653
|
await deps.client.updateCard(successorId, {
|
|
6420
6654
|
assignedAgentId: deps.agentId
|
|
6421
6655
|
});
|
|
6422
6656
|
} else {
|
|
6423
|
-
|
|
6657
|
+
log20.debug(TAG18, `successor #${card.short_id} assigned to different entity — skipping`);
|
|
6424
6658
|
continue;
|
|
6425
6659
|
}
|
|
6426
6660
|
await deps.enqueue(successorId);
|
|
6427
6661
|
} catch (err) {
|
|
6428
|
-
|
|
6662
|
+
log20.warn(TAG18, `promotion failed for successor ${successorId}: ${err instanceof Error ? err.message : err}`);
|
|
6429
6663
|
}
|
|
6430
6664
|
}
|
|
6431
6665
|
}
|
|
@@ -6433,7 +6667,7 @@ var TAG18 = "unblock";
|
|
|
6433
6667
|
var init_unblock = () => {};
|
|
6434
6668
|
|
|
6435
6669
|
// src/cli-agent-runner.ts
|
|
6436
|
-
import { log as
|
|
6670
|
+
import { log as log21 } from "@gethmy/harness";
|
|
6437
6671
|
function truncateOutput(value) {
|
|
6438
6672
|
return value === undefined ? undefined : value.slice(0, MAX_OUTPUT_LEN);
|
|
6439
6673
|
}
|
|
@@ -6555,6 +6789,14 @@ class CliAgentRunner {
|
|
|
6555
6789
|
this.enqueue({ kind: "loop_exhausted", source: "system", payload });
|
|
6556
6790
|
this.startTimer();
|
|
6557
6791
|
}
|
|
6792
|
+
recordLoopItemDispatched(payload) {
|
|
6793
|
+
this.enqueue({ kind: "loop_item_dispatched", source: "system", payload });
|
|
6794
|
+
this.startTimer();
|
|
6795
|
+
}
|
|
6796
|
+
recordLoopItemSettled(payload) {
|
|
6797
|
+
this.enqueue({ kind: "loop_item_settled", source: "system", payload });
|
|
6798
|
+
this.startTimer();
|
|
6799
|
+
}
|
|
6558
6800
|
record(body) {
|
|
6559
6801
|
this.enqueue(body);
|
|
6560
6802
|
this.startTimer();
|
|
@@ -6583,7 +6825,7 @@ class CliAgentRunner {
|
|
|
6583
6825
|
events: batch
|
|
6584
6826
|
});
|
|
6585
6827
|
} catch (err) {
|
|
6586
|
-
|
|
6828
|
+
log21.warn(TAG19, `Failed to flush run events: ${err}`);
|
|
6587
6829
|
this.buffer.unshift(...batch);
|
|
6588
6830
|
if (this.buffer.length > MAX_BUFFER) {
|
|
6589
6831
|
this.buffer.length = MAX_BUFFER;
|
|
@@ -6623,6 +6865,268 @@ function mapCost(cost) {
|
|
|
6623
6865
|
var TAG19 = "cli-agent-runner", FLUSH_INTERVAL_MS = 2000, MAX_BUFFER = 1000, MAX_TEXT_LEN = 8000, MAX_OUTPUT_LEN = 4000;
|
|
6624
6866
|
var init_cli_agent_runner = () => {};
|
|
6625
6867
|
|
|
6868
|
+
// src/fanout.ts
|
|
6869
|
+
import { log as log22 } from "@gethmy/harness";
|
|
6870
|
+
async function fetchLinks(client, cardId) {
|
|
6871
|
+
try {
|
|
6872
|
+
const { links } = await client.getCardLinks(cardId);
|
|
6873
|
+
return links;
|
|
6874
|
+
} catch (err) {
|
|
6875
|
+
log22.warn(TAG20, `link fetch failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
6876
|
+
return null;
|
|
6877
|
+
}
|
|
6878
|
+
}
|
|
6879
|
+
async function isFanoutChildOf(card, stage, client) {
|
|
6880
|
+
const links = await fetchLinks(client, card.id);
|
|
6881
|
+
if (links === null) {
|
|
6882
|
+
log22.warn(TAG20, `#${card.short_id}: link read failed — treating as a fan-out child (fail closed, no recursive dispatch)`);
|
|
6883
|
+
return true;
|
|
6884
|
+
}
|
|
6885
|
+
const parents = links.filter((l) => l.direction === "outgoing" && l.link_type === "is_part_of");
|
|
6886
|
+
for (const link of parents) {
|
|
6887
|
+
try {
|
|
6888
|
+
const { card: parent } = await client.getCard(link.target_card.id);
|
|
6889
|
+
if (parent.current_stage === stage.id && parent.playbook_id === card.playbook_id) {
|
|
6890
|
+
return true;
|
|
6891
|
+
}
|
|
6892
|
+
} catch {}
|
|
6893
|
+
}
|
|
6894
|
+
return false;
|
|
6895
|
+
}
|
|
6896
|
+
function childOutcome(child, deps) {
|
|
6897
|
+
if (child.done)
|
|
6898
|
+
return "passed";
|
|
6899
|
+
if (child.playbook_id && child.current_stage === null)
|
|
6900
|
+
return "passed";
|
|
6901
|
+
if (deps.isGivenUp(child.id))
|
|
6902
|
+
return "failed";
|
|
6903
|
+
return "pending";
|
|
6904
|
+
}
|
|
6905
|
+
async function readChildren(parent, deps) {
|
|
6906
|
+
const links = await fetchLinks(deps.client, parent.id);
|
|
6907
|
+
if (links === null)
|
|
6908
|
+
return null;
|
|
6909
|
+
const childLinks = links.filter((l) => l.direction === "incoming" && l.link_type === "is_part_of");
|
|
6910
|
+
const children = [];
|
|
6911
|
+
for (const link of childLinks) {
|
|
6912
|
+
const stub = link.target_card;
|
|
6913
|
+
let outcome = "pending";
|
|
6914
|
+
let key = null;
|
|
6915
|
+
try {
|
|
6916
|
+
const { card: child } = await deps.client.getCard(stub.id);
|
|
6917
|
+
outcome = childOutcome(child, deps);
|
|
6918
|
+
key = parseFanoutKeyMarker(child.description);
|
|
6919
|
+
} catch {
|
|
6920
|
+
outcome = "pending";
|
|
6921
|
+
}
|
|
6922
|
+
children.push({
|
|
6923
|
+
cardId: stub.id,
|
|
6924
|
+
shortId: stub.short_id,
|
|
6925
|
+
title: stub.title,
|
|
6926
|
+
key,
|
|
6927
|
+
outcome
|
|
6928
|
+
});
|
|
6929
|
+
}
|
|
6930
|
+
return children;
|
|
6931
|
+
}
|
|
6932
|
+
async function resolveItems(card, stage, loop, deps) {
|
|
6933
|
+
const source = getLoopItemSource(loop);
|
|
6934
|
+
if (!source) {
|
|
6935
|
+
return {
|
|
6936
|
+
error: `Stage "${stage.name}" is a fan-out loop but its item source is missing or malformed, so nothing can be dispatched. Set an item source on the stage.`
|
|
6937
|
+
};
|
|
6938
|
+
}
|
|
6939
|
+
switch (source.kind) {
|
|
6940
|
+
case "subtasks":
|
|
6941
|
+
return { items: subtaskItems(card.subtasks ?? []) };
|
|
6942
|
+
case "checklist":
|
|
6943
|
+
return {
|
|
6944
|
+
items: checklistItemsFromDescription(card.description, source.field)
|
|
6945
|
+
};
|
|
6946
|
+
case "list_handoff": {
|
|
6947
|
+
const handoff = await readStageHandoff(card, source.from_stage, deps);
|
|
6948
|
+
return { items: handoffListItems(handoff) };
|
|
6949
|
+
}
|
|
6950
|
+
default: {
|
|
6951
|
+
const unknown = source;
|
|
6952
|
+
return {
|
|
6953
|
+
error: `Stage "${stage.name}" has an item source this daemon does not understand (${JSON.stringify(unknown)}). Upgrade the daemon or change the source.`
|
|
6954
|
+
};
|
|
6955
|
+
}
|
|
6956
|
+
}
|
|
6957
|
+
}
|
|
6958
|
+
async function readStageHandoff(card, fromStage, deps) {
|
|
6959
|
+
try {
|
|
6960
|
+
const res = await deps.client.request("GET", `/cards/${encodeURIComponent(card.id)}/comments?order=desc&comment_type=decision`);
|
|
6961
|
+
const comments = res.comments ?? [];
|
|
6962
|
+
for (const candidate of comments) {
|
|
6963
|
+
const handoff = extractLatestHandoff([candidate], deps.identity);
|
|
6964
|
+
if (handoff && handoff.stageId === fromStage)
|
|
6965
|
+
return handoff;
|
|
6966
|
+
}
|
|
6967
|
+
return null;
|
|
6968
|
+
} catch (err) {
|
|
6969
|
+
log22.warn(TAG20, `handoff read failed for #${card.short_id} stage "${fromStage}": ${err instanceof Error ? err.message : err}`);
|
|
6970
|
+
return null;
|
|
6971
|
+
}
|
|
6972
|
+
}
|
|
6973
|
+
async function runFanoutTick(card, stage, loop, deps) {
|
|
6974
|
+
const resolved = await resolveItems(card, stage, loop, deps);
|
|
6975
|
+
if ("error" in resolved)
|
|
6976
|
+
return { kind: "held", reason: resolved.error };
|
|
6977
|
+
const plan = planFanoutItems(resolved.items, loop);
|
|
6978
|
+
if (plan.items.length === 0) {
|
|
6979
|
+
const why = plan.invalid > 0 ? `its item source produced ${plan.invalid} entr${plan.invalid === 1 ? "y" : "ies"} with no usable text` : "its item source is empty";
|
|
6980
|
+
return {
|
|
6981
|
+
kind: "held",
|
|
6982
|
+
reason: `Stage "${stage.name}" is a fan-out loop but ${why}, so there is nothing to dispatch.`
|
|
6983
|
+
};
|
|
6984
|
+
}
|
|
6985
|
+
const children = await readChildren(card, deps);
|
|
6986
|
+
if (children === null) {
|
|
6987
|
+
return {
|
|
6988
|
+
kind: "held",
|
|
6989
|
+
reason: `Could not read the child cards for stage "${stage.name}" — holding rather than risking a duplicate dispatch.`
|
|
6990
|
+
};
|
|
6991
|
+
}
|
|
6992
|
+
const byKey = new Map;
|
|
6993
|
+
for (const child of children) {
|
|
6994
|
+
if (child.key && !byKey.has(child.key))
|
|
6995
|
+
byKey.set(child.key, child);
|
|
6996
|
+
}
|
|
6997
|
+
const itemFor = (child) => plan.items.find((i) => fanoutItemKey(i) === child.key);
|
|
6998
|
+
const policy = resolveOnItemFail(loop);
|
|
6999
|
+
for (const child of children) {
|
|
7000
|
+
if (child.outcome === "pending")
|
|
7001
|
+
continue;
|
|
7002
|
+
const first = await deps.stateStore.markFanoutSettled(card.id, stage.id, child.cardId).catch(() => false);
|
|
7003
|
+
if (!first)
|
|
7004
|
+
continue;
|
|
7005
|
+
deps.sink?.recordLoopItemSettled?.({
|
|
7006
|
+
stageId: stage.id,
|
|
7007
|
+
index: itemFor(child)?.index ?? 0,
|
|
7008
|
+
total: plan.items.length,
|
|
7009
|
+
label: child.title,
|
|
7010
|
+
childCardId: child.cardId,
|
|
7011
|
+
childShortId: child.shortId,
|
|
7012
|
+
result: child.outcome === "passed" ? "passed" : "failed",
|
|
7013
|
+
policy
|
|
7014
|
+
});
|
|
7015
|
+
}
|
|
7016
|
+
const aggregate = decideFanoutAggregation({
|
|
7017
|
+
loop,
|
|
7018
|
+
outcomes: children.map((c) => c.outcome)
|
|
7019
|
+
});
|
|
7020
|
+
if (aggregate === "halt") {
|
|
7021
|
+
const failed = children.filter((c) => c.outcome === "failed");
|
|
7022
|
+
const named = failed.map((c) => `#${c.shortId}`).join(", ");
|
|
7023
|
+
return {
|
|
7024
|
+
kind: "halted",
|
|
7025
|
+
reason: `Fan-out stage "${stage.name}" stopped: ${failed.length} item${failed.length === 1 ? "" : "s"} failed (${named}) and this loop is set to halt on a failed item. Settled items are left exactly as they are — decide whether to retry them or advance the parent by hand.`
|
|
7026
|
+
};
|
|
7027
|
+
}
|
|
7028
|
+
const inFlight = children.filter((c) => c.outcome === "pending").length;
|
|
7029
|
+
const settled = children.length - inFlight;
|
|
7030
|
+
const undispatched = plan.items.filter((i) => !byKey.has(fanoutItemKey(i)));
|
|
7031
|
+
if (undispatched.length > 0) {
|
|
7032
|
+
const slots = Math.max(0, resolveLoopConcurrency(loop) - inFlight);
|
|
7033
|
+
if (slots > 0) {
|
|
7034
|
+
const created = await dispatchWave(card, stage, plan, undispatched.slice(0, slots), deps);
|
|
7035
|
+
return {
|
|
7036
|
+
kind: "dispatched",
|
|
7037
|
+
created,
|
|
7038
|
+
inFlight: inFlight + created,
|
|
7039
|
+
total: plan.items.length
|
|
7040
|
+
};
|
|
7041
|
+
}
|
|
7042
|
+
return { kind: "waiting", settled, total: plan.items.length };
|
|
7043
|
+
}
|
|
7044
|
+
if (aggregate === "pending") {
|
|
7045
|
+
return { kind: "waiting", settled, total: plan.items.length };
|
|
7046
|
+
}
|
|
7047
|
+
return {
|
|
7048
|
+
kind: "complete",
|
|
7049
|
+
passed: children.filter((c) => c.outcome === "passed").length,
|
|
7050
|
+
failed: children.filter((c) => c.outcome === "failed").length
|
|
7051
|
+
};
|
|
7052
|
+
}
|
|
7053
|
+
async function dispatchWave(parent, stage, plan, items, deps) {
|
|
7054
|
+
if (items.length === 0)
|
|
7055
|
+
return 0;
|
|
7056
|
+
let spawned = [];
|
|
7057
|
+
try {
|
|
7058
|
+
const res = await deps.client.request("POST", `/cards/${encodeURIComponent(parent.id)}/fanout-children`, {
|
|
7059
|
+
stageId: stage.id,
|
|
7060
|
+
batchTotal: plan.items.length,
|
|
7061
|
+
items: items.map((i) => ({
|
|
7062
|
+
index: i.index,
|
|
7063
|
+
label: i.label,
|
|
7064
|
+
...i.detail ? { detail: i.detail } : {},
|
|
7065
|
+
...i.sourceId ? { sourceId: i.sourceId } : {}
|
|
7066
|
+
}))
|
|
7067
|
+
});
|
|
7068
|
+
spawned = res.children ?? [];
|
|
7069
|
+
} catch (err) {
|
|
7070
|
+
log22.warn(TAG20, `child spawn failed for #${parent.short_id} stage "${stage.id}": ${err instanceof Error ? err.message : err}`);
|
|
7071
|
+
return 0;
|
|
7072
|
+
}
|
|
7073
|
+
let created = 0;
|
|
7074
|
+
for (const child of spawned) {
|
|
7075
|
+
if (!child.created)
|
|
7076
|
+
continue;
|
|
7077
|
+
const item = items.find((i) => i.index === child.index);
|
|
7078
|
+
if (!item)
|
|
7079
|
+
continue;
|
|
7080
|
+
await seedChildHandoff(parent, stage, item, plan.items.length, child.id, deps);
|
|
7081
|
+
deps.sink?.recordLoopItemDispatched?.({
|
|
7082
|
+
stageId: stage.id,
|
|
7083
|
+
stageName: stage.name,
|
|
7084
|
+
index: item.index,
|
|
7085
|
+
total: plan.items.length,
|
|
7086
|
+
label: item.label,
|
|
7087
|
+
childCardId: child.id,
|
|
7088
|
+
childShortId: child.shortId ?? undefined,
|
|
7089
|
+
...plan.truncated > 0 ? { truncated: plan.truncated } : {},
|
|
7090
|
+
...plan.invalid > 0 ? { invalid: plan.invalid } : {}
|
|
7091
|
+
});
|
|
7092
|
+
created += 1;
|
|
7093
|
+
}
|
|
7094
|
+
if (created > 0 && plan.truncated > 0) {
|
|
7095
|
+
await deps.client.addComment(parent.id, `Fan-out stage "${stage.name}" capped this batch at ${plan.items.length} of ${plan.total} items (max_iterations). ${plan.truncated} item${plan.truncated === 1 ? " was" : "s were"} not dispatched. Raise the loop's max iterations to cover the rest.`, { commentType: "finding" }).catch(() => {});
|
|
7096
|
+
}
|
|
7097
|
+
return created;
|
|
7098
|
+
}
|
|
7099
|
+
async function seedChildHandoff(parent, stage, item, total, childId, deps) {
|
|
7100
|
+
const body = buildHandoffCommentBody({
|
|
7101
|
+
stageId: stage.id,
|
|
7102
|
+
stageName: stage.name,
|
|
7103
|
+
artifactType: stage.artifact_type,
|
|
7104
|
+
produced: `Fanned out from #${parent.short_id} "${parent.title}".`,
|
|
7105
|
+
decisions: [
|
|
7106
|
+
"Do only your own item — the other items are being handled on their own cards."
|
|
7107
|
+
],
|
|
7108
|
+
nextStageNeeds: `Complete this one item: ${item.label}`,
|
|
7109
|
+
fanoutItem: {
|
|
7110
|
+
parentCardId: parent.id,
|
|
7111
|
+
stageId: stage.id,
|
|
7112
|
+
index: item.index,
|
|
7113
|
+
total,
|
|
7114
|
+
label: item.label,
|
|
7115
|
+
...item.detail ? { detail: item.detail } : {},
|
|
7116
|
+
...item.sourceId ? { sourceId: item.sourceId } : {}
|
|
7117
|
+
}
|
|
7118
|
+
});
|
|
7119
|
+
try {
|
|
7120
|
+
await deps.client.addComment(childId, body, { commentType: "decision" });
|
|
7121
|
+
} catch (err) {
|
|
7122
|
+
log22.warn(TAG20, `seed handoff failed for child ${childId}: ${err instanceof Error ? err.message : err}`);
|
|
7123
|
+
}
|
|
7124
|
+
}
|
|
7125
|
+
var TAG20 = "fanout";
|
|
7126
|
+
var init_fanout = __esm(() => {
|
|
7127
|
+
init_dist();
|
|
7128
|
+
});
|
|
7129
|
+
|
|
6626
7130
|
// src/motor-driver.ts
|
|
6627
7131
|
import { mkdtempSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
6628
7132
|
import { createRequire as createRequire2 } from "node:module";
|
|
@@ -6804,7 +7308,7 @@ var ABORT_SIGINT_GRACE_MS = 12000, ABORT_SIGTERM_GRACE_MS = 6000;
|
|
|
6804
7308
|
var init_motor_driver = () => {};
|
|
6805
7309
|
|
|
6806
7310
|
// src/stage-advance.ts
|
|
6807
|
-
import { gateConfigErrorReason, log as
|
|
7311
|
+
import { gateConfigErrorReason, log as log23 } from "@gethmy/harness";
|
|
6808
7312
|
function handoffText(stage) {
|
|
6809
7313
|
if (stage.handoff && typeof stage.handoff === "object") {
|
|
6810
7314
|
const summary = stage.handoff.summary ?? stage.handoff.description;
|
|
@@ -6842,7 +7346,7 @@ async function resolveStageColumnName(client, card, stage) {
|
|
|
6842
7346
|
const match = board.columns.find((c) => c.id === target || c.name.toLowerCase() === target.toLowerCase());
|
|
6843
7347
|
return match ? match.name : null;
|
|
6844
7348
|
} catch (err) {
|
|
6845
|
-
|
|
7349
|
+
log23.warn(TAG21, `board fetch failed resolving stage column for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
6846
7350
|
return null;
|
|
6847
7351
|
}
|
|
6848
7352
|
}
|
|
@@ -6881,7 +7385,7 @@ async function holdGateMisconfigured(card, stage, detail, deps) {
|
|
|
6881
7385
|
});
|
|
6882
7386
|
} catch {}
|
|
6883
7387
|
await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore);
|
|
6884
|
-
|
|
7388
|
+
log23.info(TAG21, `#${card.short_id} GateMisconfigured: ${reason}`);
|
|
6885
7389
|
return { kind: "held_misconfigured", reason };
|
|
6886
7390
|
}
|
|
6887
7391
|
function firstErrorMessage(evaluation) {
|
|
@@ -6912,7 +7416,7 @@ async function advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loo
|
|
|
6912
7416
|
evidence,
|
|
6913
7417
|
summary
|
|
6914
7418
|
});
|
|
6915
|
-
|
|
7419
|
+
log23.info(TAG21, `#${card.short_id} converge loop "${stage.name}": ${summary} → ${decision}`);
|
|
6916
7420
|
if (decision === "exit") {
|
|
6917
7421
|
await deps.stateStore.resetLoopIterations(card.id).catch(() => {});
|
|
6918
7422
|
deps.sink?.recordLoopCompleted?.({
|
|
@@ -6956,7 +7460,7 @@ async function advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loo
|
|
|
6956
7460
|
endStatus: "blocked",
|
|
6957
7461
|
blockers: [reason]
|
|
6958
7462
|
});
|
|
6959
|
-
|
|
7463
|
+
log23.info(TAG21, `#${card.short_id} LoopExhausted: ${reason}`);
|
|
6960
7464
|
return { kind: "held_gate_unmet", reason };
|
|
6961
7465
|
}
|
|
6962
7466
|
await deps.stateStore.decrementAttempt(card.id).catch(() => {});
|
|
@@ -6970,7 +7474,7 @@ async function advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loo
|
|
|
6970
7474
|
addLabels: [{ name: AGENT_LABEL }],
|
|
6971
7475
|
...isAgentRunnableOwner(stage.owner) ? { assignAgent: deps.agentId } : {}
|
|
6972
7476
|
}, { store: deps.stateStore, runId: deps.runId });
|
|
6973
|
-
|
|
7477
|
+
log23.info(TAG21, `#${card.short_id} converge loop "${stage.name}" — requeued to "${toColumn}" for iteration ${iteration + 1}/${maxIterations}`);
|
|
6974
7478
|
return { kind: "requeued_gate_unmet", toColumn };
|
|
6975
7479
|
}
|
|
6976
7480
|
async function writeIterationHandoff(card, stage, iteration, maxIterations, evaluation, deps) {
|
|
@@ -6989,7 +7493,7 @@ ${findings.map((f) => `- [${f.level}] ${f.message}`).join(`
|
|
|
6989
7493
|
});
|
|
6990
7494
|
await deps.client.addComment(card.id, body, { commentType: "decision" });
|
|
6991
7495
|
} catch (err) {
|
|
6992
|
-
|
|
7496
|
+
log23.warn(TAG21, `iteration-handoff write failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
6993
7497
|
}
|
|
6994
7498
|
}
|
|
6995
7499
|
async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps) {
|
|
@@ -7017,7 +7521,7 @@ async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps
|
|
|
7017
7521
|
reason: "Playbook complete — final stage gate passed."
|
|
7018
7522
|
});
|
|
7019
7523
|
deps.stateStore.recordOutcome(card.id, "success").catch(() => {});
|
|
7020
|
-
|
|
7524
|
+
log23.info(TAG21, `#${card.short_id} terminal stage "${stage.name}" passed — playbook complete (done left to the column)`);
|
|
7021
7525
|
return { kind: "completed_terminal" };
|
|
7022
7526
|
}
|
|
7023
7527
|
if (next.kind === "out_of_range") {
|
|
@@ -7049,7 +7553,7 @@ async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps
|
|
|
7049
7553
|
...isAgentRunnableOwner(next.stage.owner) ? { assignAgent: deps.agentId } : {}
|
|
7050
7554
|
}, { store: deps.stateStore, runId: deps.runId });
|
|
7051
7555
|
deps.stateStore.recordOutcome(card.id, "success").catch(() => {});
|
|
7052
|
-
|
|
7556
|
+
log23.info(TAG21, `#${card.short_id} advanced "${stage.name}" → "${next.stage.name}" (column "${toColumn}")`);
|
|
7053
7557
|
if (next.stage.owner === "human") {
|
|
7054
7558
|
const reason = `Stage "${next.stage.name}" is yours: ${handoffText(next.stage)}`;
|
|
7055
7559
|
await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore, {
|
|
@@ -7078,7 +7582,7 @@ async function handleGateUnmet(card, stage, summary, deps) {
|
|
|
7078
7582
|
endStatus: "blocked",
|
|
7079
7583
|
blockers: [reason]
|
|
7080
7584
|
});
|
|
7081
|
-
|
|
7585
|
+
log23.info(TAG21, `#${card.short_id} GateUnmetExhausted: ${reason}`);
|
|
7082
7586
|
return { kind: "held_gate_unmet", reason };
|
|
7083
7587
|
}
|
|
7084
7588
|
const toColumn = await resolveStageColumnName(deps.client, card, stage) ?? deps.fallbackColumn;
|
|
@@ -7090,7 +7594,7 @@ async function handleGateUnmet(card, stage, summary, deps) {
|
|
|
7090
7594
|
addLabels: [{ name: AGENT_LABEL }],
|
|
7091
7595
|
...isAgentRunnableOwner(stage.owner) ? { assignAgent: deps.agentId } : {}
|
|
7092
7596
|
}, { store: deps.stateStore, runId: deps.runId });
|
|
7093
|
-
|
|
7597
|
+
log23.info(TAG21, `#${card.short_id} gate unmet for "${stage.name}" — requeued to "${toColumn}" for re-run (attempt ${attempts}/${deps.maxAttempts})`);
|
|
7094
7598
|
return { kind: "requeued_gate_unmet", toColumn };
|
|
7095
7599
|
}
|
|
7096
7600
|
async function holdForHuman(client, card, reason, runId, stateStore, opts = {}) {
|
|
@@ -7111,13 +7615,13 @@ async function holdForHuman(client, card, reason, runId, stateStore, opts = {})
|
|
|
7111
7615
|
}
|
|
7112
7616
|
}, { store: stateStore, runId });
|
|
7113
7617
|
if (opts.endStatus === "blocked" && result.endSession?.ended === false) {
|
|
7114
|
-
|
|
7618
|
+
log23.warn(TAG21, `#${card.short_id} hold intended to end the session BLOCKED, but it was already ended (${result.endSession.reason ?? "unknown reason"}) — no agent_blocked push fired from this write.`);
|
|
7115
7619
|
}
|
|
7116
7620
|
} catch (err) {
|
|
7117
|
-
|
|
7621
|
+
log23.warn(TAG21, `hold transition failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
7118
7622
|
}
|
|
7119
7623
|
}
|
|
7120
|
-
var
|
|
7624
|
+
var TAG21 = "stage-advance", AGENT_LABEL = "agent";
|
|
7121
7625
|
var init_stage_advance = __esm(() => {
|
|
7122
7626
|
init_dist();
|
|
7123
7627
|
init_transitions();
|
|
@@ -7134,7 +7638,7 @@ import {
|
|
|
7134
7638
|
collectGateEvidence as collectGateEvidence2,
|
|
7135
7639
|
createWorktree,
|
|
7136
7640
|
describeApiError,
|
|
7137
|
-
log as
|
|
7641
|
+
log as log24,
|
|
7138
7642
|
makeBranchName,
|
|
7139
7643
|
normalizeGateSpec,
|
|
7140
7644
|
pushBranch as pushBranch3,
|
|
@@ -7252,6 +7756,7 @@ class Worker {
|
|
|
7252
7756
|
lastDrainedSeq = 0;
|
|
7253
7757
|
grantedTurns = null;
|
|
7254
7758
|
resumeMessage = null;
|
|
7759
|
+
sessionIdentifier = "";
|
|
7255
7760
|
get effectiveMaxTurns() {
|
|
7256
7761
|
return this.grantedTurns ?? this.config.claude.maxTurns;
|
|
7257
7762
|
}
|
|
@@ -7269,6 +7774,7 @@ class Worker {
|
|
|
7269
7774
|
this.onCardCompleted = onCardCompleted;
|
|
7270
7775
|
this.onApiError = onApiError;
|
|
7271
7776
|
this.id = id;
|
|
7777
|
+
this.sessionIdentifier = agentIdentifier(id);
|
|
7272
7778
|
}
|
|
7273
7779
|
startHeartbeat() {
|
|
7274
7780
|
this.stopHeartbeat();
|
|
@@ -7306,11 +7812,11 @@ class Worker {
|
|
|
7306
7812
|
sessionId: this.sessionId
|
|
7307
7813
|
});
|
|
7308
7814
|
} catch (err) {
|
|
7309
|
-
|
|
7815
|
+
log24.warn(this.tag, `state store updateRun failed: ${err instanceof Error ? err.message : err}`);
|
|
7310
7816
|
}
|
|
7311
7817
|
}
|
|
7312
7818
|
get tag() {
|
|
7313
|
-
return `${
|
|
7819
|
+
return `${TAG22}:${this.id}`;
|
|
7314
7820
|
}
|
|
7315
7821
|
get isIdle() {
|
|
7316
7822
|
return this.state === "idle";
|
|
@@ -7349,6 +7855,7 @@ class Worker {
|
|
|
7349
7855
|
this.startedAt = Date.now();
|
|
7350
7856
|
this.runId = newRunId();
|
|
7351
7857
|
const resuming = this.stateStore.getResumableRunForCard(card.id);
|
|
7858
|
+
this.sessionIdentifier = agentIdentifier(resuming?.workerId ?? this.id);
|
|
7352
7859
|
if (resuming) {
|
|
7353
7860
|
this.runId = resuming.runId;
|
|
7354
7861
|
this.worktreePath = resuming.worktreePath;
|
|
@@ -7363,7 +7870,7 @@ class Worker {
|
|
|
7363
7870
|
resumeMessage: null
|
|
7364
7871
|
});
|
|
7365
7872
|
} catch (err) {
|
|
7366
|
-
|
|
7873
|
+
log24.warn(this.tag, `Failed to clear the consumed grant: ${err}`);
|
|
7367
7874
|
}
|
|
7368
7875
|
}
|
|
7369
7876
|
try {
|
|
@@ -7371,15 +7878,15 @@ class Worker {
|
|
|
7371
7878
|
if (!resuming) {
|
|
7372
7879
|
this.branchName = makeBranchName(card.short_id, card.title, this.config.worktree.failedBranchPrefix);
|
|
7373
7880
|
}
|
|
7374
|
-
|
|
7881
|
+
log24.info(this.tag, resuming ? `Resuming #${card.short_id} "${card.title}" with ${this.grantedTurns ?? "the default"} more turns` : `Preparing #${card.short_id} "${card.title}"`);
|
|
7375
7882
|
const attemptCount = await this.stateStore.incrementAttempt(card.id);
|
|
7376
7883
|
const isRework = attemptCount > 1;
|
|
7377
7884
|
const recordedBranch = extractBranchRef(card.description);
|
|
7378
7885
|
const continuesPushedWork = isRework || recordsPushedWorkOn(card.description, this.branchName);
|
|
7379
7886
|
if (continuesPushedWork && !isRework) {
|
|
7380
|
-
|
|
7887
|
+
log24.info(this.tag, `Card records completed work on ${this.branchName} — continuing that branch instead of rebuilding from ${this.config.worktree.baseBranch}`);
|
|
7381
7888
|
} else if (recordedBranch && recordedBranch !== this.branchName) {
|
|
7382
|
-
|
|
7889
|
+
log24.warn(this.tag, `Card records branch ${recordedBranch} but this run targets ${this.branchName} — starting fresh; the recorded branch is left untouched`);
|
|
7383
7890
|
}
|
|
7384
7891
|
this.startHeartbeat();
|
|
7385
7892
|
this.sizing = await this.sizeThisRun(card);
|
|
@@ -7407,7 +7914,7 @@ class Worker {
|
|
|
7407
7914
|
let session;
|
|
7408
7915
|
try {
|
|
7409
7916
|
const started = await this.client.startAgentSession(card.id, {
|
|
7410
|
-
agentIdentifier:
|
|
7917
|
+
agentIdentifier: this.sessionIdentifier,
|
|
7411
7918
|
agentName: AGENT_NAME,
|
|
7412
7919
|
agentId: this.identity.agentId,
|
|
7413
7920
|
status: "working",
|
|
@@ -7421,7 +7928,7 @@ class Worker {
|
|
|
7421
7928
|
} catch (err) {
|
|
7422
7929
|
if (isSessionConflict(err)) {
|
|
7423
7930
|
this.sessionConflict = true;
|
|
7424
|
-
|
|
7931
|
+
log24.info(this.tag, `Skipping #${card.short_id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
7425
7932
|
await this.stateStore.decrementAttempt(card.id);
|
|
7426
7933
|
return;
|
|
7427
7934
|
}
|
|
@@ -7429,7 +7936,7 @@ class Worker {
|
|
|
7429
7936
|
}
|
|
7430
7937
|
const sid = session && typeof session === "object" && "id" in session ? session.id : null;
|
|
7431
7938
|
if (!sid) {
|
|
7432
|
-
|
|
7939
|
+
log24.warn(TAG22, "startAgentSession returned no session id");
|
|
7433
7940
|
}
|
|
7434
7941
|
this.sessionId = sid;
|
|
7435
7942
|
}
|
|
@@ -7447,7 +7954,7 @@ class Worker {
|
|
|
7447
7954
|
if (!resuming) {
|
|
7448
7955
|
const moved = await moveCardAndAddLabel(this.client, card, IN_PROGRESS_COLUMN, "agent");
|
|
7449
7956
|
if (!moved) {
|
|
7450
|
-
|
|
7957
|
+
log24.warn(this.tag, `Card #${card.short_id} was NOT moved to "In Progress" — check API logs`);
|
|
7451
7958
|
}
|
|
7452
7959
|
}
|
|
7453
7960
|
if (this.aborted)
|
|
@@ -7463,6 +7970,10 @@ class Worker {
|
|
|
7463
7970
|
await this.holdStageCard(card, stageCtx.reason, stageCtx.wait);
|
|
7464
7971
|
return;
|
|
7465
7972
|
}
|
|
7973
|
+
if (stageCtx.kind === "fanout") {
|
|
7974
|
+
await this.runFanoutStageCtx(card, stageCtx);
|
|
7975
|
+
return;
|
|
7976
|
+
}
|
|
7466
7977
|
if (!resuming) {
|
|
7467
7978
|
this.worktreePath = createWorktree(this.config.worktree.basePath, this.config.worktree.baseBranch, this.branchName, {
|
|
7468
7979
|
continueExisting: stageCtx.kind === "run" || stageCtx.kind === "motor" || continuesPushedWork
|
|
@@ -7494,7 +8005,7 @@ class Worker {
|
|
|
7494
8005
|
if (this.aborted)
|
|
7495
8006
|
return;
|
|
7496
8007
|
if (parked) {
|
|
7497
|
-
|
|
8008
|
+
log24.info(this.tag, `#${card.short_id} parked for plan approval — ending run`);
|
|
7498
8009
|
return;
|
|
7499
8010
|
}
|
|
7500
8011
|
}
|
|
@@ -7505,7 +8016,7 @@ class Worker {
|
|
|
7505
8016
|
if (stageCtx.kind === "run") {
|
|
7506
8017
|
const loop = getStageLoop(stageCtx.stage);
|
|
7507
8018
|
const isLoop = isConvergeLoop(loop);
|
|
7508
|
-
const inherited = await this.loadInheritedHandoffSection(card.id, stageCtx.stage.id, { includeOwnStage: isLoop });
|
|
8019
|
+
const inherited = await this.loadInheritedHandoffSection(card.id, stageCtx.stage.id, { includeOwnStage: isLoop || stageCtx.isFanoutChild === true });
|
|
7509
8020
|
prompt = [buildStagePreamble(stageCtx.stage), inherited, basePrompt].filter(Boolean).join(`
|
|
7510
8021
|
|
|
7511
8022
|
`);
|
|
@@ -7539,14 +8050,14 @@ ${basePrompt}`;
|
|
|
7539
8050
|
${prompt}`;
|
|
7540
8051
|
}
|
|
7541
8052
|
await this.client.updateAgentProgress(card.id, {
|
|
7542
|
-
agentIdentifier:
|
|
8053
|
+
agentIdentifier: this.sessionIdentifier,
|
|
7543
8054
|
agentName: AGENT_NAME,
|
|
7544
8055
|
status: "working",
|
|
7545
8056
|
currentTask: resuming ? "Resuming Claude CLI" : stageCtx.kind === "run" ? `Running stage "${stageCtx.stage.name}"` : "Running Claude CLI",
|
|
7546
8057
|
progressPercent: 10
|
|
7547
8058
|
});
|
|
7548
8059
|
this.timeoutTimer = setTimeout(() => {
|
|
7549
|
-
|
|
8060
|
+
log24.warn(this.tag, `Timeout reached (${this.config.maxTimeout}ms), cancelling`);
|
|
7550
8061
|
this.timedOut = true;
|
|
7551
8062
|
this.cancel("timeout");
|
|
7552
8063
|
}, this.config.maxTimeout);
|
|
@@ -7571,9 +8082,9 @@ ${prompt}`;
|
|
|
7571
8082
|
}
|
|
7572
8083
|
this.state = "verifying";
|
|
7573
8084
|
await this.recordPhase("verifying");
|
|
7574
|
-
|
|
8085
|
+
log24.info(this.tag, `Claude finished for #${card.short_id}, running verification & completion`);
|
|
7575
8086
|
await this.client.updateAgentProgress(card.id, {
|
|
7576
|
-
agentIdentifier:
|
|
8087
|
+
agentIdentifier: this.sessionIdentifier,
|
|
7577
8088
|
agentName: AGENT_NAME,
|
|
7578
8089
|
status: "working",
|
|
7579
8090
|
currentTask: "Verifying implementation",
|
|
@@ -7588,7 +8099,7 @@ ${prompt}`;
|
|
|
7588
8099
|
stageGateEvaluation = await this.collectStageGateEvidence(card, stageRun.stage, worktreePath, subtasks);
|
|
7589
8100
|
return stageEndDisposition(stageGateEvaluation, stageRun.stage, stageRun.index, stageRun.def);
|
|
7590
8101
|
} : undefined;
|
|
7591
|
-
const completed = await runCompletion(this.client, card, this.branchName, this.worktreePath, this.config, this.id, this.lastSessionStats, this.workspaceId, this.sessionId, this.stateStore, this.onCardCompleted, onBeforeWorktreeCleanup, this.runBaselineSha, this.effectiveMaxTurns);
|
|
8102
|
+
const completed = await runCompletion(this.client, card, this.branchName, this.worktreePath, this.config, this.id, this.sessionIdentifier, this.lastSessionStats, this.workspaceId, this.sessionId, this.stateStore, this.onCardCompleted, onBeforeWorktreeCleanup, this.runBaselineSha, this.effectiveMaxTurns);
|
|
7592
8103
|
if (completed === "park") {
|
|
7593
8104
|
await this.parkForDecision(card, "max_turns");
|
|
7594
8105
|
return;
|
|
@@ -7623,7 +8134,7 @@ ${prompt}`;
|
|
|
7623
8134
|
}
|
|
7624
8135
|
this.state = "error";
|
|
7625
8136
|
const msg = err instanceof Error ? err.message : String(err);
|
|
7626
|
-
|
|
8137
|
+
log24.error(this.tag, `Error on #${card.short_id}: ${msg}`);
|
|
7627
8138
|
const rawStderr = err?.stderr;
|
|
7628
8139
|
const errClass = classifyRunError(typeof rawStderr === "string" && rawStderr ? rawStderr : msg);
|
|
7629
8140
|
const sdkKind = err?.errorKind;
|
|
@@ -7646,7 +8157,7 @@ ${prompt}`;
|
|
|
7646
8157
|
try {
|
|
7647
8158
|
await teardownWorktree2(this.client, card.id, this.worktreePath, this.branchName ?? undefined);
|
|
7648
8159
|
} catch {
|
|
7649
|
-
|
|
8160
|
+
log24.warn(this.tag, "Failed to cleanup worktree before requeue");
|
|
7650
8161
|
}
|
|
7651
8162
|
this.worktreePath = null;
|
|
7652
8163
|
}
|
|
@@ -7663,7 +8174,7 @@ ${prompt}`;
|
|
|
7663
8174
|
}
|
|
7664
8175
|
});
|
|
7665
8176
|
} catch (tErr) {
|
|
7666
|
-
|
|
8177
|
+
log24.error(this.tag, `error transition failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
|
|
7667
8178
|
}
|
|
7668
8179
|
if (this.runId) {
|
|
7669
8180
|
try {
|
|
@@ -7699,7 +8210,7 @@ ${prompt}`;
|
|
|
7699
8210
|
try {
|
|
7700
8211
|
await teardownWorktree2(this.client, card.id, this.worktreePath, this.branchName ?? undefined);
|
|
7701
8212
|
} catch {
|
|
7702
|
-
|
|
8213
|
+
log24.warn(this.tag, "Failed to cleanup worktree before requeue");
|
|
7703
8214
|
}
|
|
7704
8215
|
this.worktreePath = null;
|
|
7705
8216
|
}
|
|
@@ -7714,7 +8225,7 @@ ${prompt}`;
|
|
|
7714
8225
|
}
|
|
7715
8226
|
});
|
|
7716
8227
|
} catch (tErr) {
|
|
7717
|
-
|
|
8228
|
+
log24.error(this.tag, `timeout transition failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
|
|
7718
8229
|
}
|
|
7719
8230
|
try {
|
|
7720
8231
|
await this.stateStore.endRun(this.runId, "failed", {
|
|
@@ -7728,15 +8239,15 @@ ${prompt}`;
|
|
|
7728
8239
|
try {
|
|
7729
8240
|
await this.client.updateCard(card.id, { assignedAgentId: null });
|
|
7730
8241
|
} catch (err) {
|
|
7731
|
-
|
|
8242
|
+
log24.warn(this.tag, `failed to release card after stop: ${err instanceof Error ? err.message : err}`);
|
|
7732
8243
|
}
|
|
7733
8244
|
try {
|
|
7734
8245
|
await runTransition(this.client, card, { removeLabels: ["agent"] });
|
|
7735
8246
|
} catch (tErr) {
|
|
7736
|
-
|
|
8247
|
+
log24.warn(this.tag, `stop label cleanup failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
|
|
7737
8248
|
}
|
|
7738
8249
|
} else {
|
|
7739
|
-
|
|
8250
|
+
log24.info(this.tag, `cancel arrived after completion on #${card.short_id} — keeping assignment so review picks it up (#585)`);
|
|
7740
8251
|
}
|
|
7741
8252
|
try {
|
|
7742
8253
|
await this.stateStore.endRun(this.runId, "paused", {
|
|
@@ -7808,23 +8319,23 @@ ${prompt}`;
|
|
|
7808
8319
|
};
|
|
7809
8320
|
const { pick, reason } = selectAutoPlaybook(subject, playbooks ?? []);
|
|
7810
8321
|
if (!pick) {
|
|
7811
|
-
|
|
8322
|
+
log24.info(this.tag, `No playbook auto-bound to #${card.short_id}: ${reason}`);
|
|
7812
8323
|
return card;
|
|
7813
8324
|
}
|
|
7814
8325
|
const applyResult = await this.client.request("POST", `/cards/${card.id}/apply-playbook`, {
|
|
7815
8326
|
playbookId: pick.id
|
|
7816
8327
|
});
|
|
7817
|
-
|
|
8328
|
+
log24.info(this.tag, `Auto-bound #${card.short_id} to playbook "${pick.name}": ${reason}`);
|
|
7818
8329
|
try {
|
|
7819
8330
|
await this.client.addComment(card.id, `Bound playbook "${pick.name}" automatically — ${reason} Apply a different playbook from the card's stage rail to override, or turn the rule off in the playbook editor.`);
|
|
7820
8331
|
} catch (commentErr) {
|
|
7821
|
-
|
|
8332
|
+
log24.warn(this.tag, `Auto-bind comment failed for #${card.short_id}: ${commentErr instanceof Error ? commentErr.message : String(commentErr)}`);
|
|
7822
8333
|
}
|
|
7823
8334
|
try {
|
|
7824
8335
|
const { card: fresh } = await this.client.getCard(card.id);
|
|
7825
8336
|
return fresh;
|
|
7826
8337
|
} catch (fetchErr) {
|
|
7827
|
-
|
|
8338
|
+
log24.warn(this.tag, `Auto-bind re-fetch failed for #${card.short_id} after binding playbook "${pick.name}" — using the apply-playbook response's fields for this pickup: ${fetchErr instanceof Error ? fetchErr.message : String(fetchErr)}`);
|
|
7828
8339
|
return {
|
|
7829
8340
|
...card,
|
|
7830
8341
|
playbook_id: applyResult.card.playbook_id,
|
|
@@ -7833,7 +8344,7 @@ ${prompt}`;
|
|
|
7833
8344
|
};
|
|
7834
8345
|
}
|
|
7835
8346
|
} catch (err) {
|
|
7836
|
-
|
|
8347
|
+
log24.warn(this.tag, `Auto-bind playbook check failed for #${card.short_id}, continuing unbound: ${err instanceof Error ? err.message : String(err)}`);
|
|
7837
8348
|
return card;
|
|
7838
8349
|
}
|
|
7839
8350
|
}
|
|
@@ -7889,6 +8400,20 @@ ${prompt}`;
|
|
|
7889
8400
|
}
|
|
7890
8401
|
return { kind: "motor", stage, index: resolution.index, def, role };
|
|
7891
8402
|
}
|
|
8403
|
+
const stageLoop = getStageLoop(stage);
|
|
8404
|
+
let isFanoutChild = false;
|
|
8405
|
+
if (isFanoutLoop(stageLoop) && stageLoop) {
|
|
8406
|
+
isFanoutChild = await isFanoutChildOf(card, stage, this.client);
|
|
8407
|
+
if (!isFanoutChild) {
|
|
8408
|
+
return {
|
|
8409
|
+
kind: "fanout",
|
|
8410
|
+
stage,
|
|
8411
|
+
index: resolution.index,
|
|
8412
|
+
def,
|
|
8413
|
+
loop: stageLoop
|
|
8414
|
+
};
|
|
8415
|
+
}
|
|
8416
|
+
}
|
|
7892
8417
|
const allowedTools = entryActionAllowlist(stage.entry_action);
|
|
7893
8418
|
if (!allowedTools) {
|
|
7894
8419
|
return {
|
|
@@ -7905,11 +8430,89 @@ ${prompt}`;
|
|
|
7905
8430
|
allowedTools,
|
|
7906
8431
|
index: resolution.index,
|
|
7907
8432
|
priorStage,
|
|
7908
|
-
def
|
|
8433
|
+
def,
|
|
8434
|
+
...isFanoutChild ? { isFanoutChild: true } : {}
|
|
7909
8435
|
};
|
|
7910
8436
|
}
|
|
8437
|
+
async runFanoutStageCtx(card, ctx) {
|
|
8438
|
+
this.held = true;
|
|
8439
|
+
this.cliRunner?.recordStageEntered({
|
|
8440
|
+
stageId: ctx.stage.id,
|
|
8441
|
+
stageName: ctx.stage.name,
|
|
8442
|
+
owner: ctx.stage.owner
|
|
8443
|
+
});
|
|
8444
|
+
let outcome;
|
|
8445
|
+
try {
|
|
8446
|
+
outcome = await runFanoutTick(card, ctx.stage, ctx.loop, {
|
|
8447
|
+
client: this.client,
|
|
8448
|
+
stateStore: this.stateStore,
|
|
8449
|
+
identity: {
|
|
8450
|
+
userId: this.identity.userId,
|
|
8451
|
+
agentId: this.identity.agentId
|
|
8452
|
+
},
|
|
8453
|
+
sink: this.cliRunner,
|
|
8454
|
+
isGivenUp: (cardId) => (this.stateStore.getCard(cardId)?.attempts ?? 0) >= this.config.budget.maxAttemptsPerCard
|
|
8455
|
+
});
|
|
8456
|
+
} catch (err) {
|
|
8457
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
8458
|
+
log24.warn(this.tag, `fan-out tick failed on #${card.short_id}: ${detail}`);
|
|
8459
|
+
await this.holdStageCard(card, `Fan-out stage "${ctx.stage.name}" could not run: ${detail}`);
|
|
8460
|
+
return;
|
|
8461
|
+
}
|
|
8462
|
+
switch (outcome.kind) {
|
|
8463
|
+
case "dispatched":
|
|
8464
|
+
case "waiting": {
|
|
8465
|
+
const total = outcome.kind === "dispatched" ? outcome.total : outcome.total;
|
|
8466
|
+
const note = outcome.kind === "dispatched" ? `Fan-out stage "${ctx.stage.name}": dispatched ${outcome.created} of ${total} item(s); ${outcome.inFlight} in flight.` : `Fan-out stage "${ctx.stage.name}": ${outcome.settled} of ${total} item(s) settled; waiting on the rest.`;
|
|
8467
|
+
log24.info(this.tag, `#${card.short_id} ${note}`);
|
|
8468
|
+
await this.client.updateAgentProgress(card.id, {
|
|
8469
|
+
agentIdentifier: "claude-code-stage",
|
|
8470
|
+
agentName: "Harmony Agent",
|
|
8471
|
+
status: "waiting",
|
|
8472
|
+
currentTask: note
|
|
8473
|
+
}).catch(() => {});
|
|
8474
|
+
await this.holdStageCard(card, note);
|
|
8475
|
+
return;
|
|
8476
|
+
}
|
|
8477
|
+
case "complete": {
|
|
8478
|
+
await this.stateStore.resetFanoutSettled(card.id).catch(() => {});
|
|
8479
|
+
const summary = `Fan-out stage "${ctx.stage.name}" complete: ${outcome.passed} item(s) passed${outcome.failed > 0 ? `, ${outcome.failed} failed (continuing per on_item_fail)` : ""}.`;
|
|
8480
|
+
this.cliRunner?.recordLoopCompleted({
|
|
8481
|
+
stageId: ctx.stage.id,
|
|
8482
|
+
iterations: outcome.passed + outcome.failed,
|
|
8483
|
+
maxIterations: Math.max(1, Math.floor(ctx.loop.max_iterations) || 1),
|
|
8484
|
+
reason: summary
|
|
8485
|
+
});
|
|
8486
|
+
const exitEval = {
|
|
8487
|
+
passed: true,
|
|
8488
|
+
findings: [{ level: "info", message: summary }],
|
|
8489
|
+
structured: {}
|
|
8490
|
+
};
|
|
8491
|
+
const advance = await this.advanceFromGateEvaluation(card, ctx.stage, ctx.index, ctx.def, exitEval);
|
|
8492
|
+
if (advance.kind === "advanced" || advance.kind === "completed_terminal") {
|
|
8493
|
+
this.held = false;
|
|
8494
|
+
}
|
|
8495
|
+
log24.info(this.tag, `#${card.short_id} ${summary} → ${advance.kind}`);
|
|
8496
|
+
return;
|
|
8497
|
+
}
|
|
8498
|
+
case "halted": {
|
|
8499
|
+
await this.client.updateAgentProgress(card.id, {
|
|
8500
|
+
agentIdentifier: "claude-code-stage",
|
|
8501
|
+
agentName: "Harmony Agent",
|
|
8502
|
+
status: "waiting",
|
|
8503
|
+
currentTask: outcome.reason
|
|
8504
|
+
}).catch(() => {});
|
|
8505
|
+
await this.holdStageCard(card, outcome.reason, true);
|
|
8506
|
+
return;
|
|
8507
|
+
}
|
|
8508
|
+
case "held": {
|
|
8509
|
+
await this.holdStageCard(card, outcome.reason, true);
|
|
8510
|
+
return;
|
|
8511
|
+
}
|
|
8512
|
+
}
|
|
8513
|
+
}
|
|
7911
8514
|
async holdStageCard(card, reason, wait = false) {
|
|
7912
|
-
|
|
8515
|
+
log24.info(this.tag, `Holding #${card.short_id}: ${reason}`);
|
|
7913
8516
|
await this.stateStore.decrementAttempt(card.id);
|
|
7914
8517
|
try {
|
|
7915
8518
|
await this.client.addComment(card.id, reason, { commentType: "blocker" });
|
|
@@ -7925,7 +8528,7 @@ ${prompt}`;
|
|
|
7925
8528
|
}
|
|
7926
8529
|
});
|
|
7927
8530
|
} catch (tErr) {
|
|
7928
|
-
|
|
8531
|
+
log24.warn(this.tag, `hold transition failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
|
|
7929
8532
|
}
|
|
7930
8533
|
if (this.runId) {
|
|
7931
8534
|
try {
|
|
@@ -7943,7 +8546,7 @@ ${prompt}`;
|
|
|
7943
8546
|
const holderMessage = err instanceof Error ? err.message : String(err);
|
|
7944
8547
|
const waitHours = this.config.budget.pause.waitHours;
|
|
7945
8548
|
const until = computeDecisionDeadline(waitHours);
|
|
7946
|
-
|
|
8549
|
+
log24.warn(this.tag, `#${card.short_id} stays parked — the resume could not reclaim its session: ${holderMessage}`);
|
|
7947
8550
|
try {
|
|
7948
8551
|
await this.client.addComment(card.id, formatResumeConflictComment({
|
|
7949
8552
|
holderMessage,
|
|
@@ -7955,7 +8558,7 @@ ${prompt}`;
|
|
|
7955
8558
|
agentSessionId: this.sessionId ?? undefined
|
|
7956
8559
|
});
|
|
7957
8560
|
} catch (commentErr) {
|
|
7958
|
-
|
|
8561
|
+
log24.warn(this.tag, `Failed to post the resume-conflict note for #${card.short_id}: ${commentErr}`);
|
|
7959
8562
|
}
|
|
7960
8563
|
if (this.runId) {
|
|
7961
8564
|
const run = this.stateStore.getRun(this.runId);
|
|
@@ -7966,7 +8569,7 @@ ${prompt}`;
|
|
|
7966
8569
|
awaitingDecisionUntil: until
|
|
7967
8570
|
});
|
|
7968
8571
|
} catch (storeErr) {
|
|
7969
|
-
|
|
8572
|
+
log24.error(this.tag, `#${card.short_id} could not be re-parked after a resume conflict — the daemon has no hold on it: ${storeErr}`);
|
|
7970
8573
|
}
|
|
7971
8574
|
}
|
|
7972
8575
|
}
|
|
@@ -7977,7 +8580,7 @@ ${prompt}`;
|
|
|
7977
8580
|
this.progressTracker = null;
|
|
7978
8581
|
const waitHours = this.config.budget.pause.waitHours;
|
|
7979
8582
|
const until = computeDecisionDeadline(waitHours);
|
|
7980
|
-
|
|
8583
|
+
log24.warn(this.tag, `#${card.short_id} parked (${trigger}) — awaiting a human decision for ${waitHours}h`);
|
|
7981
8584
|
const body = formatBudgetComment({
|
|
7982
8585
|
trigger,
|
|
7983
8586
|
numTurns: stats?.cost?.numTurns ?? 0,
|
|
@@ -7996,18 +8599,18 @@ ${prompt}`;
|
|
|
7996
8599
|
});
|
|
7997
8600
|
commentId = res?.comment?.id ?? null;
|
|
7998
8601
|
} catch (err) {
|
|
7999
|
-
|
|
8602
|
+
log24.warn(this.tag, `Failed to post the budget-pause comment: ${err}`);
|
|
8000
8603
|
}
|
|
8001
8604
|
try {
|
|
8002
8605
|
await this.client.updateAgentProgress(card.id, {
|
|
8003
|
-
agentIdentifier:
|
|
8606
|
+
agentIdentifier: this.sessionIdentifier,
|
|
8004
8607
|
agentName: AGENT_NAME,
|
|
8005
8608
|
status: "blocked",
|
|
8006
8609
|
currentTask: "Waiting for your decision on the turn budget",
|
|
8007
8610
|
awaitingDecisionUntil: new Date(until).toISOString()
|
|
8008
8611
|
});
|
|
8009
8612
|
} catch (err) {
|
|
8010
|
-
|
|
8613
|
+
log24.warn(this.tag, `Failed to mark the session blocked: ${err}`);
|
|
8011
8614
|
}
|
|
8012
8615
|
if (this.runId) {
|
|
8013
8616
|
try {
|
|
@@ -8019,7 +8622,7 @@ ${prompt}`;
|
|
|
8019
8622
|
numTurns: stats?.cost?.numTurns ?? 0
|
|
8020
8623
|
});
|
|
8021
8624
|
} catch (err) {
|
|
8022
|
-
|
|
8625
|
+
log24.error(this.tag, `#${card.short_id} parkRun failed after the blocker comment + blocked session were already written — the daemon has no local record of this park: ${err}`);
|
|
8023
8626
|
}
|
|
8024
8627
|
}
|
|
8025
8628
|
}
|
|
@@ -8042,7 +8645,7 @@ ${prompt}`;
|
|
|
8042
8645
|
});
|
|
8043
8646
|
const motorTask = `Running stage "${ctx.stage.name}" under the harness motor`;
|
|
8044
8647
|
await this.client.updateAgentProgress(card.id, {
|
|
8045
|
-
agentIdentifier:
|
|
8648
|
+
agentIdentifier: this.sessionIdentifier,
|
|
8046
8649
|
agentName: AGENT_NAME,
|
|
8047
8650
|
status: "working",
|
|
8048
8651
|
currentTask: motorTask,
|
|
@@ -8050,27 +8653,27 @@ ${prompt}`;
|
|
|
8050
8653
|
});
|
|
8051
8654
|
const gate = normalizeGateSpec(ctx.stage.gate);
|
|
8052
8655
|
const metricsPath = gate?.kind === "custom" ? writeMetricsFile(this.config.playbooks.metrics ?? {}) : null;
|
|
8053
|
-
|
|
8656
|
+
log24.info(this.tag, `Running stage "${ctx.stage.name}" (role ${ctx.role}) for #${card.short_id} under the harness motor`);
|
|
8054
8657
|
const motorAbort = new AbortController;
|
|
8055
8658
|
this.motorAbort = motorAbort;
|
|
8056
8659
|
this.timeoutTimer = setTimeout(() => {
|
|
8057
|
-
|
|
8660
|
+
log24.warn(this.tag, `Timeout reached (${this.config.maxTimeout}ms) during the motor stage run, cancelling`);
|
|
8058
8661
|
this.timedOut = true;
|
|
8059
8662
|
this.cancel("timeout");
|
|
8060
8663
|
}, this.config.maxTimeout);
|
|
8061
|
-
const motorTracker = new ProgressTracker(this.client, card.id, this.
|
|
8664
|
+
const motorTracker = new ProgressTracker(this.client, card.id, this.sessionIdentifier, subtasks, "exploring");
|
|
8062
8665
|
if (this.cliRunner)
|
|
8063
8666
|
motorTracker.setRunEventSink(this.cliRunner);
|
|
8064
8667
|
let motorTrackerLive = false;
|
|
8065
8668
|
let motorRunSettled = false;
|
|
8066
8669
|
const onMotorLine = (line) => {
|
|
8067
8670
|
if (line.type !== "agent_event") {
|
|
8068
|
-
|
|
8671
|
+
log24.info(this.tag, `motor: ${line.type}`);
|
|
8069
8672
|
return;
|
|
8070
8673
|
}
|
|
8071
8674
|
if (motorRunSettled)
|
|
8072
8675
|
return;
|
|
8073
|
-
|
|
8676
|
+
log24.debug(this.tag, `motor: agent_event ${line.event.kind}`);
|
|
8074
8677
|
if (line.event.kind === "tool_started" && STAGE_DAEMON_OWNED_TOOLS.includes(line.event.payload.toolName)) {
|
|
8075
8678
|
return;
|
|
8076
8679
|
}
|
|
@@ -8084,13 +8687,13 @@ ${prompt}`;
|
|
|
8084
8687
|
if (motorTrackerLive && !motorTracker.isStopped)
|
|
8085
8688
|
return;
|
|
8086
8689
|
this.client.updateAgentProgress(card.id, {
|
|
8087
|
-
agentIdentifier:
|
|
8690
|
+
agentIdentifier: this.sessionIdentifier,
|
|
8088
8691
|
agentName: AGENT_NAME,
|
|
8089
8692
|
status: "working",
|
|
8090
8693
|
currentTask: motorTask,
|
|
8091
8694
|
progressPercent: MOTOR_RUN_PROGRESS_PERCENT
|
|
8092
8695
|
}).catch((err) => {
|
|
8093
|
-
|
|
8696
|
+
log24.warn(this.tag, `motor session heartbeat failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
8094
8697
|
});
|
|
8095
8698
|
}, MOTOR_SESSION_HEARTBEAT_MS);
|
|
8096
8699
|
heartbeat.unref?.();
|
|
@@ -8167,9 +8770,7 @@ ${prompt}`;
|
|
|
8167
8770
|
}
|
|
8168
8771
|
}
|
|
8169
8772
|
}
|
|
8170
|
-
async finishMotorStageRun(card, ctx, disposition = {
|
|
8171
|
-
status: "completed"
|
|
8172
|
-
}) {
|
|
8773
|
+
async finishMotorStageRun(card, ctx, disposition = { status: "completed" }) {
|
|
8173
8774
|
const worktreePath = this.worktreePath;
|
|
8174
8775
|
if (worktreePath) {
|
|
8175
8776
|
commitUncommittedChanges(worktreePath, card);
|
|
@@ -8177,36 +8778,17 @@ ${prompt}`;
|
|
|
8177
8778
|
try {
|
|
8178
8779
|
pushBranch3(this.branchName, worktreePath);
|
|
8179
8780
|
} catch (err) {
|
|
8180
|
-
|
|
8781
|
+
log24.error(this.tag, `push after the motor stage "${ctx.stage.name}" failed for ${this.branchName}: ${err instanceof Error ? err.message : err}`);
|
|
8181
8782
|
}
|
|
8182
8783
|
}
|
|
8183
8784
|
}
|
|
8184
8785
|
const completionColumn = this.config.completion.moveToColumn;
|
|
8185
8786
|
if (completionColumn) {
|
|
8186
|
-
await
|
|
8187
|
-
try {
|
|
8188
|
-
await releaseAssignedAgent(this.client, card.id);
|
|
8189
|
-
} catch (err) {
|
|
8190
|
-
log22.warn(this.tag, `assignment release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
8191
|
-
}
|
|
8192
|
-
if (this.onCardCompleted) {
|
|
8193
|
-
try {
|
|
8194
|
-
await this.onCardCompleted(card);
|
|
8195
|
-
} catch (err) {
|
|
8196
|
-
log22.warn(this.tag, `successor promotion failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
8197
|
-
}
|
|
8198
|
-
}
|
|
8787
|
+
await transferCardToCompletion({ client: this.client, tag: this.tag }, card, completionColumn, this.onCardCompleted);
|
|
8199
8788
|
} else {
|
|
8200
|
-
|
|
8201
|
-
}
|
|
8202
|
-
try {
|
|
8203
|
-
await this.client.endAgentSession(card.id, {
|
|
8204
|
-
...disposition,
|
|
8205
|
-
progressPercent: 100
|
|
8206
|
-
});
|
|
8207
|
-
} catch (err) {
|
|
8208
|
-
log22.error(this.tag, `endAgentSession after the motor stage run failed on #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
8789
|
+
log24.warn(this.tag, `completion.moveToColumn is empty — #${card.short_id} stays in its current column after the motor stage run`);
|
|
8209
8790
|
}
|
|
8791
|
+
await endRunSession({ client: this.client, tag: this.tag }, card, disposition, {}, "log");
|
|
8210
8792
|
await this.closeoutMotorWorktree(card);
|
|
8211
8793
|
}
|
|
8212
8794
|
async closeoutMotorWorktree(card) {
|
|
@@ -8216,7 +8798,7 @@ ${prompt}`;
|
|
|
8216
8798
|
try {
|
|
8217
8799
|
await teardownWorktree2(this.client, card.id, worktreePath, this.branchName ?? undefined);
|
|
8218
8800
|
} catch {
|
|
8219
|
-
|
|
8801
|
+
log24.warn(this.tag, "Failed to cleanup worktree after the motor stage run");
|
|
8220
8802
|
}
|
|
8221
8803
|
this.worktreePath = null;
|
|
8222
8804
|
}
|
|
@@ -8230,7 +8812,7 @@ ${prompt}`;
|
|
|
8230
8812
|
});
|
|
8231
8813
|
return handoff ? renderInheritedHandoffSection(handoff) : "";
|
|
8232
8814
|
} catch (err) {
|
|
8233
|
-
|
|
8815
|
+
log24.warn(this.tag, `inherited-handoff fetch failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
8234
8816
|
return "";
|
|
8235
8817
|
}
|
|
8236
8818
|
}
|
|
@@ -8247,9 +8829,9 @@ ${prompt}`;
|
|
|
8247
8829
|
nextStageNeeds: "Pick up from the produced artifact above; treat the recorded decisions as settled."
|
|
8248
8830
|
});
|
|
8249
8831
|
await this.client.addComment(card.id, body, { commentType: "decision" });
|
|
8250
|
-
|
|
8832
|
+
log24.info(this.tag, `Wrote stage handoff for #${card.short_id} (stage "${stage.name}")`);
|
|
8251
8833
|
} catch (err) {
|
|
8252
|
-
|
|
8834
|
+
log24.warn(this.tag, `stage-handoff write failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
8253
8835
|
}
|
|
8254
8836
|
}
|
|
8255
8837
|
async collectStageGateEvidence(card, stage, worktreePath, subtasks) {
|
|
@@ -8261,12 +8843,12 @@ ${prompt}`;
|
|
|
8261
8843
|
return null;
|
|
8262
8844
|
}
|
|
8263
8845
|
if (gate.pendingEngine === true) {
|
|
8264
|
-
|
|
8846
|
+
log24.info(this.tag, `Stage "${stage.name}" gate "${gate.kind}" is advisory — skipping enforcement`);
|
|
8265
8847
|
return null;
|
|
8266
8848
|
}
|
|
8267
8849
|
const review = gate.kind === "review_passed" ? parseReviewOutput(this.lastRunText) : undefined;
|
|
8268
8850
|
if (review) {
|
|
8269
|
-
|
|
8851
|
+
log24.info(this.tag, `Review-gated stage "${stage.name}" verdict: ${review.verdict} (${review.findings.length} finding(s))`);
|
|
8270
8852
|
}
|
|
8271
8853
|
const registry = buildGateCollectorRegistry2({
|
|
8272
8854
|
build: {
|
|
@@ -8295,10 +8877,10 @@ ${prompt}`;
|
|
|
8295
8877
|
const evaluation = gateEvaluate(gate, evidence);
|
|
8296
8878
|
const insert = toStageGateEvidenceInsert(context, evidence);
|
|
8297
8879
|
await this.client.request("POST", `/cards/${encodeURIComponent(card.id)}/stage-gate-evidence`, insert);
|
|
8298
|
-
|
|
8880
|
+
log24.info(this.tag, `Recorded ${gate.kind} gate evidence for #${card.short_id} stage "${stage.name}": result=${evidence.result} passed=${evaluation.passed}`);
|
|
8299
8881
|
return evaluation;
|
|
8300
8882
|
} catch (err) {
|
|
8301
|
-
|
|
8883
|
+
log24.warn(this.tag, `stage-gate evidence collection failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
8302
8884
|
return null;
|
|
8303
8885
|
}
|
|
8304
8886
|
}
|
|
@@ -8314,7 +8896,7 @@ ${prompt}`;
|
|
|
8314
8896
|
runId: this.runId ?? undefined
|
|
8315
8897
|
});
|
|
8316
8898
|
} catch (err) {
|
|
8317
|
-
|
|
8899
|
+
log24.warn(this.tag, `stage advancement failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
8318
8900
|
return { kind: "no_advance" };
|
|
8319
8901
|
}
|
|
8320
8902
|
}
|
|
@@ -8324,7 +8906,7 @@ ${prompt}`;
|
|
|
8324
8906
|
this.modelChoice = choice;
|
|
8325
8907
|
const { model, escalated, source } = choice;
|
|
8326
8908
|
if (source !== "policy" || escalated) {
|
|
8327
|
-
|
|
8909
|
+
log24.info(this.tag, `Implement model "${model}" (source=${source}, escalated=${escalated}, attempts=${attempts}, priority=${card.priority ?? "none"}, tier=${this.sizing?.tier ?? "none"})`);
|
|
8328
8910
|
}
|
|
8329
8911
|
return model;
|
|
8330
8912
|
}
|
|
@@ -8338,7 +8920,7 @@ ${prompt}`;
|
|
|
8338
8920
|
encoding: "utf-8"
|
|
8339
8921
|
}).trim();
|
|
8340
8922
|
} catch (err) {
|
|
8341
|
-
|
|
8923
|
+
log24.warn(this.tag, `Sizing #${card.short_id}: could not resolve the repo root (${err instanceof Error ? err.message : String(err)}) — using the policy fallback`);
|
|
8342
8924
|
return null;
|
|
8343
8925
|
}
|
|
8344
8926
|
const sized = await sizeRun({
|
|
@@ -8350,7 +8932,7 @@ ${prompt}`;
|
|
|
8350
8932
|
description: card.description,
|
|
8351
8933
|
model
|
|
8352
8934
|
});
|
|
8353
|
-
|
|
8935
|
+
log24.info(this.tag, sized ? `Sized #${card.short_id}: complexity ${sized.complexity}/10 -> ${sized.tier}` : `Sizing #${card.short_id} produced no verdict — using the policy fallback`);
|
|
8354
8936
|
return sized;
|
|
8355
8937
|
}
|
|
8356
8938
|
recordRunSized() {
|
|
@@ -8392,44 +8974,44 @@ ${prompt}`;
|
|
|
8392
8974
|
commentType: "blocker"
|
|
8393
8975
|
});
|
|
8394
8976
|
giveUpCommentId = res?.comment?.id ?? null;
|
|
8395
|
-
|
|
8977
|
+
log24.warn(this.tag, `gave up on ${cardId} after ${attempts} attempts`);
|
|
8396
8978
|
} catch (err) {
|
|
8397
|
-
|
|
8979
|
+
log24.warn(this.tag, `failed to post give-up comment for ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
8398
8980
|
}
|
|
8399
8981
|
if (this.config.budget.pause.enabled) {
|
|
8400
8982
|
const waitHours = this.config.budget.pause.waitHours;
|
|
8401
8983
|
const until = computeDecisionDeadline(waitHours);
|
|
8402
8984
|
try {
|
|
8403
8985
|
await this.client.updateAgentProgress(cardId, {
|
|
8404
|
-
agentIdentifier:
|
|
8986
|
+
agentIdentifier: this.sessionIdentifier,
|
|
8405
8987
|
agentName: AGENT_NAME,
|
|
8406
8988
|
status: "blocked",
|
|
8407
8989
|
currentTask: "Waiting for your decision on the attempt budget",
|
|
8408
8990
|
awaitingDecisionUntil: new Date(until).toISOString()
|
|
8409
8991
|
});
|
|
8410
8992
|
} catch (err) {
|
|
8411
|
-
|
|
8993
|
+
log24.warn(this.tag, `Failed to mark the attempt cap pending for ${cardId}: ${err}`);
|
|
8412
8994
|
}
|
|
8413
8995
|
try {
|
|
8414
8996
|
await this.stateStore.markAwaitingDecision(cardId, {
|
|
8415
8997
|
until,
|
|
8416
8998
|
blockerCommentId: giveUpCommentId,
|
|
8417
|
-
agentIdentifier:
|
|
8999
|
+
agentIdentifier: this.sessionIdentifier
|
|
8418
9000
|
});
|
|
8419
9001
|
} catch (err) {
|
|
8420
|
-
|
|
9002
|
+
log24.warn(this.tag, `Failed to record the attempt-cap decision locally for ${cardId}: ${err}`);
|
|
8421
9003
|
}
|
|
8422
9004
|
}
|
|
8423
9005
|
}
|
|
8424
9006
|
}
|
|
8425
9007
|
} catch (err) {
|
|
8426
|
-
|
|
9008
|
+
log24.warn(this.tag, `recordOutcome(${outcome}) failed: ${err instanceof Error ? err.message : err}`);
|
|
8427
9009
|
}
|
|
8428
9010
|
}
|
|
8429
9011
|
async pause() {
|
|
8430
9012
|
if (!this.isActive || !this.process || this.process.killed)
|
|
8431
9013
|
return;
|
|
8432
|
-
|
|
9014
|
+
log24.info(this.tag, `Pausing work on ${this.cardId}`);
|
|
8433
9015
|
signalGroup2(this.process, "SIGSTOP");
|
|
8434
9016
|
if (this.timeoutTimer) {
|
|
8435
9017
|
clearTimeout(this.timeoutTimer);
|
|
@@ -8438,34 +9020,34 @@ ${prompt}`;
|
|
|
8438
9020
|
if (this.cardId) {
|
|
8439
9021
|
try {
|
|
8440
9022
|
await this.client.updateAgentProgress(this.cardId, {
|
|
8441
|
-
agentIdentifier:
|
|
9023
|
+
agentIdentifier: this.sessionIdentifier,
|
|
8442
9024
|
agentName: AGENT_NAME,
|
|
8443
9025
|
status: "paused"
|
|
8444
9026
|
});
|
|
8445
9027
|
} catch {
|
|
8446
|
-
|
|
9028
|
+
log24.warn(this.tag, "Failed to update agent session to paused");
|
|
8447
9029
|
}
|
|
8448
9030
|
}
|
|
8449
9031
|
}
|
|
8450
9032
|
async resume() {
|
|
8451
9033
|
if (!this.isActive || !this.process || this.process.killed)
|
|
8452
9034
|
return;
|
|
8453
|
-
|
|
9035
|
+
log24.info(this.tag, `Resuming work on ${this.cardId}`);
|
|
8454
9036
|
signalGroup2(this.process, "SIGCONT");
|
|
8455
9037
|
this.timeoutTimer = setTimeout(() => {
|
|
8456
|
-
|
|
9038
|
+
log24.warn(this.tag, `Timeout reached (${this.config.maxTimeout}ms), cancelling`);
|
|
8457
9039
|
this.timedOut = true;
|
|
8458
9040
|
this.cancel("timeout");
|
|
8459
9041
|
}, this.config.maxTimeout);
|
|
8460
9042
|
if (this.cardId) {
|
|
8461
9043
|
try {
|
|
8462
9044
|
await this.client.updateAgentProgress(this.cardId, {
|
|
8463
|
-
agentIdentifier:
|
|
9045
|
+
agentIdentifier: this.sessionIdentifier,
|
|
8464
9046
|
agentName: AGENT_NAME,
|
|
8465
9047
|
status: "working"
|
|
8466
9048
|
});
|
|
8467
9049
|
} catch {
|
|
8468
|
-
|
|
9050
|
+
log24.warn(this.tag, "Failed to update agent session to working");
|
|
8469
9051
|
}
|
|
8470
9052
|
}
|
|
8471
9053
|
}
|
|
@@ -8474,7 +9056,7 @@ ${prompt}`;
|
|
|
8474
9056
|
return;
|
|
8475
9057
|
this.aborted = true;
|
|
8476
9058
|
this.state = "cancelling";
|
|
8477
|
-
|
|
9059
|
+
log24.info(this.tag, `Cancelling work on ${this.cardId}`);
|
|
8478
9060
|
this.motorAbort?.abort();
|
|
8479
9061
|
if (this.sdkRunner) {
|
|
8480
9062
|
await this.sdkRunner.stop(this.timedOut ? "timeout" : "user_requested");
|
|
@@ -8492,16 +9074,16 @@ ${prompt}`;
|
|
|
8492
9074
|
...buildTokenPayload(stats)
|
|
8493
9075
|
});
|
|
8494
9076
|
} catch (err) {
|
|
8495
|
-
|
|
9077
|
+
log24.warn(this.tag, `endAgentSession after cancel failed: ${err instanceof Error ? err.message : err}`);
|
|
8496
9078
|
}
|
|
8497
9079
|
}
|
|
8498
9080
|
}
|
|
8499
9081
|
async runPlanningPhase(enriched) {
|
|
8500
9082
|
const planning = this.config.planning;
|
|
8501
9083
|
const { card } = enriched;
|
|
8502
|
-
|
|
9084
|
+
log24.info(this.tag, `Planning pass for #${card.short_id} (mode=${planning.mode}, model=${planning.model})`);
|
|
8503
9085
|
await this.client.updateAgentProgress(card.id, {
|
|
8504
|
-
agentIdentifier:
|
|
9086
|
+
agentIdentifier: this.sessionIdentifier,
|
|
8505
9087
|
agentName: AGENT_NAME,
|
|
8506
9088
|
status: "working",
|
|
8507
9089
|
currentTask: "Planning approach (read-only)",
|
|
@@ -8512,7 +9094,7 @@ ${prompt}`;
|
|
|
8512
9094
|
let planTimedOut = false;
|
|
8513
9095
|
const planTimeout = setTimeout(() => {
|
|
8514
9096
|
planTimedOut = true;
|
|
8515
|
-
|
|
9097
|
+
log24.warn(this.tag, "Planning pass exceeded timeout — abandoning, implementing directly");
|
|
8516
9098
|
if (this.sdkRunner) {
|
|
8517
9099
|
this.sdkRunner.stop("timeout").catch(() => {});
|
|
8518
9100
|
} else if (this.process && !this.process.killed) {
|
|
@@ -8530,7 +9112,7 @@ ${prompt}`;
|
|
|
8530
9112
|
initialPhase: "planning"
|
|
8531
9113
|
});
|
|
8532
9114
|
} catch (err) {
|
|
8533
|
-
|
|
9115
|
+
log24.warn(this.tag, `Planning pass failed (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
8534
9116
|
return false;
|
|
8535
9117
|
} finally {
|
|
8536
9118
|
clearTimeout(planTimeout);
|
|
@@ -8548,7 +9130,7 @@ ${prompt}`;
|
|
|
8548
9130
|
}
|
|
8549
9131
|
const planText = stats?.lastAssistantText ?? "";
|
|
8550
9132
|
if (!planText.trim()) {
|
|
8551
|
-
|
|
9133
|
+
log24.warn(this.tag, `Planning pass for #${card.short_id} produced no text — implementing directly`);
|
|
8552
9134
|
return false;
|
|
8553
9135
|
}
|
|
8554
9136
|
const artifact = extractPlanArtifact(planText, card.title);
|
|
@@ -8569,9 +9151,9 @@ ${prompt}`;
|
|
|
8569
9151
|
});
|
|
8570
9152
|
planId = createdId;
|
|
8571
9153
|
}
|
|
8572
|
-
|
|
9154
|
+
log24.info(this.tag, `Stored plan ${planId ?? "(unlinked)"} for #${card.short_id} (${artifact.tasks.length} tasks)`);
|
|
8573
9155
|
} catch (err) {
|
|
8574
|
-
|
|
9156
|
+
log24.warn(this.tag, `Failed to store/link plan (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
8575
9157
|
}
|
|
8576
9158
|
if (planning.mode === "gated" && planId) {
|
|
8577
9159
|
try {
|
|
@@ -8590,11 +9172,11 @@ ${prompt}`;
|
|
|
8590
9172
|
...buildTokenPayload(stats)
|
|
8591
9173
|
}
|
|
8592
9174
|
}, { store: this.stateStore, runId: this.runId ?? undefined });
|
|
8593
|
-
|
|
9175
|
+
log24.info(this.tag, `#${card.short_id} parked in "${planning.awaitingApprovalColumn}" for plan approval`);
|
|
8594
9176
|
this.lastSessionStats = undefined;
|
|
8595
9177
|
return true;
|
|
8596
9178
|
} catch (err) {
|
|
8597
|
-
|
|
9179
|
+
log24.warn(this.tag, `Gated park failed for #${card.short_id} (non-fatal, implementing directly): ${err instanceof TransitionError ? err.detail : err instanceof Error ? err.message : err}`);
|
|
8598
9180
|
}
|
|
8599
9181
|
}
|
|
8600
9182
|
if (planId && planning.postComment) {
|
|
@@ -8604,7 +9186,7 @@ ${prompt}`;
|
|
|
8604
9186
|
agentSessionId: this.sessionId ?? undefined
|
|
8605
9187
|
});
|
|
8606
9188
|
} catch (err) {
|
|
8607
|
-
|
|
9189
|
+
log24.warn(this.tag, `Failed to post advisory plan comment (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
8608
9190
|
}
|
|
8609
9191
|
}
|
|
8610
9192
|
return false;
|
|
@@ -8614,12 +9196,12 @@ ${prompt}`;
|
|
|
8614
9196
|
const { card } = enriched;
|
|
8615
9197
|
const existing = await this.loadPinnedContract(card.id);
|
|
8616
9198
|
if (existing) {
|
|
8617
|
-
|
|
9199
|
+
log24.info(this.tag, `Contract already pinned for #${card.short_id} (${existing.assertions.length} assertions) — reusing`);
|
|
8618
9200
|
return;
|
|
8619
9201
|
}
|
|
8620
|
-
|
|
9202
|
+
log24.info(this.tag, `Contract pass for #${card.short_id} (model=${contractCfg.model})`);
|
|
8621
9203
|
await this.client.updateAgentProgress(card.id, {
|
|
8622
|
-
agentIdentifier:
|
|
9204
|
+
agentIdentifier: this.sessionIdentifier,
|
|
8623
9205
|
agentName: AGENT_NAME,
|
|
8624
9206
|
status: "working",
|
|
8625
9207
|
currentTask: "Writing acceptance contract (read-only)",
|
|
@@ -8630,7 +9212,7 @@ ${prompt}`;
|
|
|
8630
9212
|
let contractTimedOut = false;
|
|
8631
9213
|
const contractTimeout = setTimeout(() => {
|
|
8632
9214
|
contractTimedOut = true;
|
|
8633
|
-
|
|
9215
|
+
log24.warn(this.tag, "Contract pass exceeded timeout — abandoning, implementing directly");
|
|
8634
9216
|
if (this.sdkRunner) {
|
|
8635
9217
|
this.sdkRunner.stop("timeout").catch(() => {});
|
|
8636
9218
|
} else if (this.process && !this.process.killed) {
|
|
@@ -8648,7 +9230,7 @@ ${prompt}`;
|
|
|
8648
9230
|
initialPhase: "planning"
|
|
8649
9231
|
});
|
|
8650
9232
|
} catch (err) {
|
|
8651
|
-
|
|
9233
|
+
log24.warn(this.tag, `Contract pass failed (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
8652
9234
|
return;
|
|
8653
9235
|
} finally {
|
|
8654
9236
|
clearTimeout(contractTimeout);
|
|
@@ -8666,12 +9248,12 @@ ${prompt}`;
|
|
|
8666
9248
|
}
|
|
8667
9249
|
const contractText = stats?.lastAssistantText ?? "";
|
|
8668
9250
|
if (!contractText.trim()) {
|
|
8669
|
-
|
|
9251
|
+
log24.warn(this.tag, `Contract pass for #${card.short_id} produced no text — implementing directly`);
|
|
8670
9252
|
return;
|
|
8671
9253
|
}
|
|
8672
9254
|
const contract = extractContract(contractText, card);
|
|
8673
9255
|
if (contract.assertions.length < contractCfg.minAssertions) {
|
|
8674
|
-
|
|
9256
|
+
log24.warn(this.tag, `Contract for #${card.short_id} had ${contract.assertions.length} assertion(s) (< ${contractCfg.minAssertions}) — not pinning, implementing directly`);
|
|
8675
9257
|
return;
|
|
8676
9258
|
}
|
|
8677
9259
|
try {
|
|
@@ -8679,9 +9261,9 @@ ${prompt}`;
|
|
|
8679
9261
|
commentType: "decision",
|
|
8680
9262
|
agentSessionId: this.sessionId ?? undefined
|
|
8681
9263
|
});
|
|
8682
|
-
|
|
9264
|
+
log24.info(this.tag, `Pinned acceptance contract for #${card.short_id} (${contract.assertions.length} assertions)`);
|
|
8683
9265
|
} catch (err) {
|
|
8684
|
-
|
|
9266
|
+
log24.warn(this.tag, `Failed to pin contract (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
8685
9267
|
}
|
|
8686
9268
|
}
|
|
8687
9269
|
async loadPinnedContract(cardId) {
|
|
@@ -8691,7 +9273,7 @@ ${prompt}`;
|
|
|
8691
9273
|
return null;
|
|
8692
9274
|
return extractPinnedContract(comments, this.identity);
|
|
8693
9275
|
} catch (err) {
|
|
8694
|
-
|
|
9276
|
+
log24.warn(this.tag, `pinned-contract fetch failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
8695
9277
|
return null;
|
|
8696
9278
|
}
|
|
8697
9279
|
}
|
|
@@ -8704,13 +9286,13 @@ ${prompt}`;
|
|
|
8704
9286
|
const res = await this.client.getPendingUserMessages(this.cardId, this.sessionId, this.lastDrainedSeq);
|
|
8705
9287
|
messages = res.messages ?? [];
|
|
8706
9288
|
} catch (err) {
|
|
8707
|
-
|
|
9289
|
+
log24.warn(this.tag, `Failed to fetch steering messages (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
8708
9290
|
return;
|
|
8709
9291
|
}
|
|
8710
9292
|
if (messages.length === 0)
|
|
8711
9293
|
return;
|
|
8712
9294
|
this.lastDrainedSeq = Math.max(this.lastDrainedSeq, ...messages.map((m) => m.seq));
|
|
8713
|
-
|
|
9295
|
+
log24.info(this.tag, `Steering #${card.short_id}: resuming with ${messages.length} queued message(s)`);
|
|
8714
9296
|
this.state = "running";
|
|
8715
9297
|
await this.recordPhase("running");
|
|
8716
9298
|
try {
|
|
@@ -8721,7 +9303,7 @@ ${prompt}`;
|
|
|
8721
9303
|
...this.activeRunSpawnOpts ?? {}
|
|
8722
9304
|
});
|
|
8723
9305
|
} catch (err) {
|
|
8724
|
-
|
|
9306
|
+
log24.warn(this.tag, `Steering resume failed (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
8725
9307
|
return;
|
|
8726
9308
|
}
|
|
8727
9309
|
}
|
|
@@ -8752,10 +9334,10 @@ ${prompt}`;
|
|
|
8752
9334
|
"--",
|
|
8753
9335
|
prompt
|
|
8754
9336
|
];
|
|
8755
|
-
|
|
9337
|
+
log24.info(this.tag, `Spawning: claude ${args.slice(0, 4).join(" ")} ...`);
|
|
8756
9338
|
const runLog = openRunLog(this.tag, this.runId, card.short_id);
|
|
8757
9339
|
if (runLog) {
|
|
8758
|
-
|
|
9340
|
+
log24.info(this.tag, `Run log: ${runLog.path}`);
|
|
8759
9341
|
runLog.stream.write(`# run=${this.runId} card=#${card.short_id} started=${new Date().toISOString()}
|
|
8760
9342
|
` + `# args: ${args.slice(0, -2).join(" ")} -- <prompt:${prompt.length} chars>
|
|
8761
9343
|
|
|
@@ -8766,7 +9348,7 @@ ${prompt}`;
|
|
|
8766
9348
|
stdio: ["ignore", "pipe", "pipe"]
|
|
8767
9349
|
});
|
|
8768
9350
|
const parser = new StreamParser;
|
|
8769
|
-
this.progressTracker = new ProgressTracker(this.client, card.id, this.
|
|
9351
|
+
this.progressTracker = new ProgressTracker(this.client, card.id, this.sessionIdentifier, subtasks, initialPhase);
|
|
8770
9352
|
this.progressTracker.setRequestedModel(model);
|
|
8771
9353
|
this.progressTracker.attach(parser);
|
|
8772
9354
|
this.cliRunner?.attach(parser);
|
|
@@ -8786,7 +9368,7 @@ ${prompt}`;
|
|
|
8786
9368
|
this.captureCliSessionId(parser.sessionId);
|
|
8787
9369
|
});
|
|
8788
9370
|
parser.on("parse_error", (msg) => {
|
|
8789
|
-
|
|
9371
|
+
log24.debug(this.tag, `Stream parse error (non-fatal): ${msg}`);
|
|
8790
9372
|
runLog?.stream.write(`
|
|
8791
9373
|
[parse_error] ${msg}
|
|
8792
9374
|
`);
|
|
@@ -8853,16 +9435,16 @@ ${prompt}`;
|
|
|
8853
9435
|
const disallowedTools = opts.disallowedTools ? opts.disallowedTools.split(",").map((t) => t.trim()).filter(Boolean) : undefined;
|
|
8854
9436
|
const initialPhase = opts.initialPhase ?? "exploring";
|
|
8855
9437
|
const sdkCfg = this.config.sdk;
|
|
8856
|
-
|
|
9438
|
+
log24.info(this.tag, `Spawning Agent SDK runner (model=${model}, maxTurns=${maxTurns}${opts.resumeSessionId ? ", resume" : ""})`);
|
|
8857
9439
|
const runLog = openRunLog(this.tag, this.runId, card.short_id);
|
|
8858
9440
|
if (runLog) {
|
|
8859
|
-
|
|
9441
|
+
log24.info(this.tag, `Run log: ${runLog.path}`);
|
|
8860
9442
|
runLog.stream.write(`# run=${this.runId} card=#${card.short_id} runner=sdk started=${new Date().toISOString()}
|
|
8861
9443
|
` + `# model=${model} maxTurns=${maxTurns} <prompt:${prompt.length} chars>
|
|
8862
9444
|
|
|
8863
9445
|
`);
|
|
8864
9446
|
}
|
|
8865
|
-
this.progressTracker = new ProgressTracker(this.client, card.id, this.
|
|
9447
|
+
this.progressTracker = new ProgressTracker(this.client, card.id, this.sessionIdentifier, subtasks, initialPhase);
|
|
8866
9448
|
this.progressTracker.setRequestedModel(model);
|
|
8867
9449
|
if (this.cliRunner) {
|
|
8868
9450
|
this.progressTracker.setRunEventSink(this.cliRunner);
|
|
@@ -8980,7 +9562,7 @@ ${prompt}`;
|
|
|
8980
9562
|
try {
|
|
8981
9563
|
await teardownWorktree2(this.client, this.cardId, this.worktreePath, this.branchName ?? undefined);
|
|
8982
9564
|
} catch {
|
|
8983
|
-
|
|
9565
|
+
log24.warn(this.tag, "Failed to cleanup worktree");
|
|
8984
9566
|
}
|
|
8985
9567
|
}
|
|
8986
9568
|
this.process = null;
|
|
@@ -8995,7 +9577,7 @@ ${prompt}`;
|
|
|
8995
9577
|
this.runTurns = 0;
|
|
8996
9578
|
}
|
|
8997
9579
|
}
|
|
8998
|
-
var
|
|
9580
|
+
var TAG22 = "worker", CANCEL_SIGINT_TIMEOUT2 = 30000, CANCEL_SIGTERM_TIMEOUT2 = 1e4, MOTOR_SESSION_HEARTBEAT_MS = 60000, MOTOR_RUN_PROGRESS_PERCENT = 10, 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, MAX_STAGE_NAME_CHARS = 80, MAX_HANDOFF_CHARS = 400;
|
|
8999
9581
|
var init_worker = __esm(() => {
|
|
9000
9582
|
init_dist();
|
|
9001
9583
|
init_board_helpers();
|
|
@@ -9003,12 +9585,14 @@ var init_worker = __esm(() => {
|
|
|
9003
9585
|
init_cli_agent_runner();
|
|
9004
9586
|
init_completion();
|
|
9005
9587
|
init_contract_phase();
|
|
9588
|
+
init_fanout();
|
|
9006
9589
|
init_motor_driver();
|
|
9007
9590
|
init_plan_phase();
|
|
9008
9591
|
init_progress_tracker();
|
|
9009
9592
|
init_prompt();
|
|
9010
9593
|
init_review_completion();
|
|
9011
9594
|
init_review_knowledge();
|
|
9595
|
+
init_run_closeout();
|
|
9012
9596
|
init_run_log();
|
|
9013
9597
|
init_stage_advance();
|
|
9014
9598
|
init_state_store();
|
|
@@ -9022,7 +9606,7 @@ var init_worker = __esm(() => {
|
|
|
9022
9606
|
import {
|
|
9023
9607
|
cooldownMsFor,
|
|
9024
9608
|
describeApiError as describeApiError2,
|
|
9025
|
-
log as
|
|
9609
|
+
log as log25
|
|
9026
9610
|
} from "@gethmy/harness";
|
|
9027
9611
|
async function routeBudgetDecision(d, run, actions, cardId) {
|
|
9028
9612
|
if (!run) {
|
|
@@ -9098,41 +9682,41 @@ class Pool {
|
|
|
9098
9682
|
}
|
|
9099
9683
|
async enqueue(card, column, labels, subtasks, mode = "implement") {
|
|
9100
9684
|
if (this.isCardKnown(card.id) || this.reservations.has(card.id)) {
|
|
9101
|
-
|
|
9685
|
+
log25.debug(TAG23, `Card ${card.id} already queued, active, or reserved, skipping`);
|
|
9102
9686
|
return;
|
|
9103
9687
|
}
|
|
9104
9688
|
this.reservations.add(card.id);
|
|
9105
9689
|
try {
|
|
9106
9690
|
if (mode === "implement") {
|
|
9107
9691
|
if (this.authPaused) {
|
|
9108
|
-
|
|
9692
|
+
log25.debug(TAG23, `#${card.short_id} held — agent paused (auth error)`);
|
|
9109
9693
|
await this.emitWaiting(card.id, "Agent paused — Anthropic auth error, check API credentials");
|
|
9110
9694
|
return;
|
|
9111
9695
|
}
|
|
9112
9696
|
const cooldownMs = this.apiCooldownRemainingMs();
|
|
9113
9697
|
if (cooldownMs > 0) {
|
|
9114
|
-
|
|
9698
|
+
log25.debug(TAG23, `#${card.short_id} held — API cooldown ${Math.round(cooldownMs / 1000)}s remaining`);
|
|
9115
9699
|
await this.emitWaiting(card.id, `Paused — Anthropic API limit, retrying in ~${Math.round(cooldownMs / 1000)}s`);
|
|
9116
9700
|
return;
|
|
9117
9701
|
}
|
|
9118
9702
|
const decision = this.budget.check(card.id);
|
|
9119
9703
|
if (!decision.allow) {
|
|
9120
9704
|
if (decision.reason === "daily_budget") {
|
|
9121
|
-
|
|
9705
|
+
log25.warn(TAG23, `#${card.short_id} skipped (daily_budget): ${decision.detail}`);
|
|
9122
9706
|
await this.emitWaiting(card.id, `Daily budget reached — waiting for reset (${decision.detail})`);
|
|
9123
9707
|
} else {
|
|
9124
|
-
|
|
9708
|
+
log25.debug(TAG23, `#${card.short_id} gave up: ${decision.detail}`);
|
|
9125
9709
|
}
|
|
9126
9710
|
return;
|
|
9127
9711
|
}
|
|
9128
9712
|
const blockers = await getUnresolvedBlockers(this.client, card, this.projectId);
|
|
9129
9713
|
if (blockers === null) {
|
|
9130
|
-
|
|
9714
|
+
log25.warn(TAG23, `#${card.short_id} blocker check failed — deferring to next tick`);
|
|
9131
9715
|
return;
|
|
9132
9716
|
}
|
|
9133
9717
|
if (blockers.length > 0) {
|
|
9134
9718
|
const list = blockers.map((b) => `#${b.shortId}`).join(", ");
|
|
9135
|
-
|
|
9719
|
+
log25.info(TAG23, `#${card.short_id} blocked by ${list} — waiting`);
|
|
9136
9720
|
await this.emitWaiting(card.id, `Blocked by ${list} — waiting for chain`);
|
|
9137
9721
|
return;
|
|
9138
9722
|
}
|
|
@@ -9164,7 +9748,7 @@ class Pool {
|
|
|
9164
9748
|
});
|
|
9165
9749
|
this.lastWaitingEmit.set(cardId, currentTask);
|
|
9166
9750
|
} catch (err) {
|
|
9167
|
-
|
|
9751
|
+
log25.debug(TAG23, `waiting emit failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
9168
9752
|
}
|
|
9169
9753
|
}
|
|
9170
9754
|
noteApiError(err) {
|
|
@@ -9172,7 +9756,7 @@ class Pool {
|
|
|
9172
9756
|
return;
|
|
9173
9757
|
if (err.kind === "auth") {
|
|
9174
9758
|
if (!this.authPaused) {
|
|
9175
|
-
|
|
9759
|
+
log25.error(TAG23, "Auth error from Claude CLI — pausing implement pickups until the daemon is restarted with valid credentials");
|
|
9176
9760
|
}
|
|
9177
9761
|
this.authPaused = true;
|
|
9178
9762
|
return;
|
|
@@ -9181,7 +9765,7 @@ class Pool {
|
|
|
9181
9765
|
const until = Date.now() + cooldownMs;
|
|
9182
9766
|
if (until > this.apiCooldownUntil) {
|
|
9183
9767
|
this.apiCooldownUntil = until;
|
|
9184
|
-
|
|
9768
|
+
log25.warn(TAG23, `${describeApiError2(err.kind)} — pausing implement pickups for ${Math.round(cooldownMs / 1000)}s`);
|
|
9185
9769
|
}
|
|
9186
9770
|
}
|
|
9187
9771
|
apiCooldownRemainingMs() {
|
|
@@ -9195,13 +9779,13 @@ class Pool {
|
|
|
9195
9779
|
const removed = queue.remove(cardId);
|
|
9196
9780
|
if (removed) {
|
|
9197
9781
|
this.cardDataCache.delete(cardId);
|
|
9198
|
-
|
|
9782
|
+
log25.info(TAG23, `Removed #${removed.shortId} from ${removed.mode} queue`);
|
|
9199
9783
|
return;
|
|
9200
9784
|
}
|
|
9201
9785
|
}
|
|
9202
9786
|
const worker = this.implWorkers.find((w) => w.cardId === cardId) ?? this.reviewWorkers.find((w) => w.cardId === cardId);
|
|
9203
9787
|
if (worker) {
|
|
9204
|
-
|
|
9788
|
+
log25.info(TAG23, `Cancelling worker ${worker.id} for card ${cardId}`);
|
|
9205
9789
|
await worker.cancel("unassigned");
|
|
9206
9790
|
}
|
|
9207
9791
|
}
|
|
@@ -9238,10 +9822,10 @@ class Pool {
|
|
|
9238
9822
|
}
|
|
9239
9823
|
const worker = this.implWorkers.find((w) => w.cardId === cardId && w.isActive) ?? this.reviewWorkers.find((w) => w.cardId === cardId && w.isActive);
|
|
9240
9824
|
if (!worker) {
|
|
9241
|
-
|
|
9825
|
+
log25.debug(TAG23, `No active worker for card ${cardId}, ignoring ${command}`);
|
|
9242
9826
|
return;
|
|
9243
9827
|
}
|
|
9244
|
-
|
|
9828
|
+
log25.info(TAG23, `Agent command: ${command} → worker ${worker.id} (card ${cardId})`);
|
|
9245
9829
|
switch (command) {
|
|
9246
9830
|
case "pause":
|
|
9247
9831
|
await worker.pause();
|
|
@@ -9289,7 +9873,7 @@ class Pool {
|
|
|
9289
9873
|
};
|
|
9290
9874
|
}
|
|
9291
9875
|
async shutdown() {
|
|
9292
|
-
|
|
9876
|
+
log25.info(TAG23, "Shutting down pool...");
|
|
9293
9877
|
this.shuttingDown = true;
|
|
9294
9878
|
const active = [
|
|
9295
9879
|
...this.implWorkers.filter((w) => w.isActive),
|
|
@@ -9297,7 +9881,7 @@ class Pool {
|
|
|
9297
9881
|
];
|
|
9298
9882
|
await Promise.all(active.map((w) => w.cancel("shutdown")));
|
|
9299
9883
|
this.sleepGuard.stop();
|
|
9300
|
-
|
|
9884
|
+
log25.info(TAG23, "Pool shutdown complete");
|
|
9301
9885
|
}
|
|
9302
9886
|
async drainBudgetDecisions(cardId) {
|
|
9303
9887
|
const targets = cardId ? [cardId] : [
|
|
@@ -9328,7 +9912,7 @@ class Pool {
|
|
|
9328
9912
|
try {
|
|
9329
9913
|
({ decisions } = await this.client.getBudgetDecisions(cardId, new Date(sinceMs).toISOString()));
|
|
9330
9914
|
} catch (err) {
|
|
9331
|
-
|
|
9915
|
+
log25.warn(TAG23, `getBudgetDecisions failed for ${cardId}: ${err}`);
|
|
9332
9916
|
return;
|
|
9333
9917
|
}
|
|
9334
9918
|
if (decisions.length > 0) {
|
|
@@ -9363,7 +9947,7 @@ class Pool {
|
|
|
9363
9947
|
...run.blockerCommentId ? { replyToId: run.blockerCommentId } : {}
|
|
9364
9948
|
});
|
|
9365
9949
|
} catch (err) {
|
|
9366
|
-
|
|
9950
|
+
log25.warn(TAG23, `Failed to post the expired-park closing comment for ${run.cardId}: ${err}`);
|
|
9367
9951
|
}
|
|
9368
9952
|
try {
|
|
9369
9953
|
const { card } = await this.client.getCard(run.cardId);
|
|
@@ -9374,14 +9958,14 @@ class Pool {
|
|
|
9374
9958
|
});
|
|
9375
9959
|
}
|
|
9376
9960
|
} catch (err) {
|
|
9377
|
-
|
|
9961
|
+
log25.error(TAG23, `Failed to move #${run.cardShortId} after an expired park: ${err}`);
|
|
9378
9962
|
}
|
|
9379
9963
|
try {
|
|
9380
9964
|
await this.stateStore.endRun(run.runId, "failed", {
|
|
9381
9965
|
errorMessage: "budget decision expired"
|
|
9382
9966
|
});
|
|
9383
9967
|
} catch (err) {
|
|
9384
|
-
|
|
9968
|
+
log25.warn(TAG23, `Failed to release the expired park for ${run.cardId}: ${err}`);
|
|
9385
9969
|
}
|
|
9386
9970
|
}
|
|
9387
9971
|
async releaseExpiredAttemptCap(cardId, blockerCommentId) {
|
|
@@ -9391,7 +9975,7 @@ class Pool {
|
|
|
9391
9975
|
...blockerCommentId ? { replyToId: blockerCommentId } : {}
|
|
9392
9976
|
});
|
|
9393
9977
|
} catch (err) {
|
|
9394
|
-
|
|
9978
|
+
log25.warn(TAG23, `Failed to post the expired attempt-cap note for ${cardId}: ${err}`);
|
|
9395
9979
|
}
|
|
9396
9980
|
try {
|
|
9397
9981
|
await this.client.endAgentSession(cardId, {
|
|
@@ -9400,14 +9984,14 @@ class Pool {
|
|
|
9400
9984
|
failureSummary: "The attempt-budget decision expired with no answer. Reassign the card to grant a fresh attempt."
|
|
9401
9985
|
});
|
|
9402
9986
|
} catch (err) {
|
|
9403
|
-
|
|
9987
|
+
log25.warn(TAG23, `Failed to end the expired attempt-cap session for ${cardId}: ${err}`);
|
|
9404
9988
|
}
|
|
9405
9989
|
await this.stateStore.clearAwaitingDecision(cardId);
|
|
9406
9990
|
}
|
|
9407
9991
|
async adoptGrantedRun(run) {
|
|
9408
9992
|
if (this.isCardKnown(run.cardId))
|
|
9409
9993
|
return;
|
|
9410
|
-
|
|
9994
|
+
log25.warn(TAG23, `#${run.cardShortId}: granted continue never reached a worker — re-enqueueing (${run.pipeline})`);
|
|
9411
9995
|
await this.enqueueCard(run.cardId, run.pipeline);
|
|
9412
9996
|
}
|
|
9413
9997
|
sessionIdentityFor(run) {
|
|
@@ -9439,7 +10023,7 @@ class Pool {
|
|
|
9439
10023
|
awaitingDecisionUntil: null
|
|
9440
10024
|
});
|
|
9441
10025
|
} catch (err) {
|
|
9442
|
-
|
|
10026
|
+
log25.warn(TAG23, `Failed to clear the decision deadline for ${run.cardId}: ${err}`);
|
|
9443
10027
|
}
|
|
9444
10028
|
if (run.blockerCommentId) {
|
|
9445
10029
|
try {
|
|
@@ -9447,7 +10031,7 @@ class Pool {
|
|
|
9447
10031
|
resolve: true
|
|
9448
10032
|
});
|
|
9449
10033
|
} catch (err) {
|
|
9450
|
-
|
|
10034
|
+
log25.warn(TAG23, `Failed to resolve the blocker comment: ${err}`);
|
|
9451
10035
|
}
|
|
9452
10036
|
}
|
|
9453
10037
|
await this.enqueueCard(run.cardId, run.pipeline);
|
|
@@ -9459,7 +10043,7 @@ class Pool {
|
|
|
9459
10043
|
resolve: true
|
|
9460
10044
|
});
|
|
9461
10045
|
} catch (err) {
|
|
9462
|
-
|
|
10046
|
+
log25.warn(TAG23, `Failed to resolve the blocker comment: ${err}`);
|
|
9463
10047
|
}
|
|
9464
10048
|
}
|
|
9465
10049
|
try {
|
|
@@ -9468,7 +10052,7 @@ class Pool {
|
|
|
9468
10052
|
awaitingDecisionUntil: null
|
|
9469
10053
|
});
|
|
9470
10054
|
} catch (err) {
|
|
9471
|
-
|
|
10055
|
+
log25.warn(TAG23, `Failed to clear the decision deadline for ${run.cardId}: ${err}`);
|
|
9472
10056
|
}
|
|
9473
10057
|
try {
|
|
9474
10058
|
const { card } = await this.client.getCard(run.cardId);
|
|
@@ -9482,14 +10066,14 @@ class Pool {
|
|
|
9482
10066
|
}
|
|
9483
10067
|
});
|
|
9484
10068
|
} catch (err) {
|
|
9485
|
-
|
|
10069
|
+
log25.error(TAG23, `Failed to hand #${run.cardShortId} back after a stop: ${err}`);
|
|
9486
10070
|
}
|
|
9487
10071
|
try {
|
|
9488
10072
|
await this.stateStore.endRun(run.runId, "failed", {
|
|
9489
10073
|
errorMessage: "budget_decision_stop"
|
|
9490
10074
|
});
|
|
9491
10075
|
} catch (err) {
|
|
9492
|
-
|
|
10076
|
+
log25.warn(TAG23, `Failed to end the local run record for ${run.cardId}: ${err}`);
|
|
9493
10077
|
}
|
|
9494
10078
|
}
|
|
9495
10079
|
async grantAttempt(cardId) {
|
|
@@ -9502,7 +10086,7 @@ class Pool {
|
|
|
9502
10086
|
awaitingDecisionUntil: null
|
|
9503
10087
|
});
|
|
9504
10088
|
} catch (err) {
|
|
9505
|
-
|
|
10089
|
+
log25.warn(TAG23, `Failed to clear the attempt-cap decision deadline for ${cardId}: ${err}`);
|
|
9506
10090
|
}
|
|
9507
10091
|
await this.enqueueCard(cardId, "implement");
|
|
9508
10092
|
}
|
|
@@ -9513,7 +10097,7 @@ class Pool {
|
|
|
9513
10097
|
try {
|
|
9514
10098
|
await this.client.updateComment(blockerCommentId, { resolve: true });
|
|
9515
10099
|
} catch (err) {
|
|
9516
|
-
|
|
10100
|
+
log25.warn(TAG23, `Failed to resolve the blocker comment: ${err}`);
|
|
9517
10101
|
}
|
|
9518
10102
|
}
|
|
9519
10103
|
try {
|
|
@@ -9522,7 +10106,7 @@ class Pool {
|
|
|
9522
10106
|
awaitingDecisionUntil: null
|
|
9523
10107
|
});
|
|
9524
10108
|
} catch (err) {
|
|
9525
|
-
|
|
10109
|
+
log25.warn(TAG23, `Failed to clear the decision deadline for ${cardId}: ${err}`);
|
|
9526
10110
|
}
|
|
9527
10111
|
try {
|
|
9528
10112
|
await this.client.endAgentSession(cardId, {
|
|
@@ -9531,7 +10115,7 @@ class Pool {
|
|
|
9531
10115
|
failureSummary: "Stopped by a human decision on the attempt budget."
|
|
9532
10116
|
});
|
|
9533
10117
|
} catch (err) {
|
|
9534
|
-
|
|
10118
|
+
log25.warn(TAG23, `Failed to end the attempt-cap session for ${cardId}: ${err}`);
|
|
9535
10119
|
}
|
|
9536
10120
|
await this.stateStore.clearAwaitingDecision(cardId);
|
|
9537
10121
|
}
|
|
@@ -9550,7 +10134,7 @@ class Pool {
|
|
|
9550
10134
|
const columns = board.columns ?? [];
|
|
9551
10135
|
const column = columns.find((c) => c.id === card.column_id);
|
|
9552
10136
|
if (!column) {
|
|
9553
|
-
|
|
10137
|
+
log25.warn(TAG23, `#${card.short_id}: column not found — cannot re-enqueue`);
|
|
9554
10138
|
return;
|
|
9555
10139
|
}
|
|
9556
10140
|
const labelMap = buildLabelMap(board.labels ?? []);
|
|
@@ -9558,7 +10142,7 @@ class Pool {
|
|
|
9558
10142
|
const subtasks = card.subtasks ?? [];
|
|
9559
10143
|
await this.enqueue(card, column, cardLabels, subtasks, mode);
|
|
9560
10144
|
} catch (err) {
|
|
9561
|
-
|
|
10145
|
+
log25.error(TAG23, `Failed to re-enqueue ${cardId} after a budget decision: ${err}`);
|
|
9562
10146
|
}
|
|
9563
10147
|
}
|
|
9564
10148
|
reservations = new Set;
|
|
@@ -9568,7 +10152,7 @@ class Pool {
|
|
|
9568
10152
|
return false;
|
|
9569
10153
|
const idle = workers.find((w) => w.isIdle);
|
|
9570
10154
|
if (!idle) {
|
|
9571
|
-
|
|
10155
|
+
log25.debug(TAG23, `No idle ${label} workers (queue: ${queue.length})`);
|
|
9572
10156
|
return false;
|
|
9573
10157
|
}
|
|
9574
10158
|
const next = queue.dequeue();
|
|
@@ -9576,18 +10160,18 @@ class Pool {
|
|
|
9576
10160
|
return false;
|
|
9577
10161
|
const data = this.cardDataCache.get(next.cardId);
|
|
9578
10162
|
if (!data) {
|
|
9579
|
-
|
|
10163
|
+
log25.warn(TAG23, `No cached data for card ${next.cardId}, skipping`);
|
|
9580
10164
|
return false;
|
|
9581
10165
|
}
|
|
9582
10166
|
this.cardDataCache.delete(next.cardId);
|
|
9583
10167
|
this.lastWaitingEmit.delete(next.cardId);
|
|
9584
|
-
|
|
10168
|
+
log25.info(TAG23, `Dispatching #${next.shortId} to ${label} worker ${idle.id}`);
|
|
9585
10169
|
this.sleepGuard.acquire();
|
|
9586
10170
|
idle.run(data.card, data.column, data.labels, data.subtasks);
|
|
9587
10171
|
return true;
|
|
9588
10172
|
}
|
|
9589
10173
|
}
|
|
9590
|
-
var
|
|
10174
|
+
var TAG23 = "pool";
|
|
9591
10175
|
var init_pool = __esm(() => {
|
|
9592
10176
|
init_board_helpers();
|
|
9593
10177
|
init_budget_pause();
|
|
@@ -9617,7 +10201,7 @@ import {
|
|
|
9617
10201
|
} from "node:fs";
|
|
9618
10202
|
import { homedir as homedir4 } from "node:os";
|
|
9619
10203
|
import { dirname as dirname4, join as join5 } from "node:path";
|
|
9620
|
-
import { log as
|
|
10204
|
+
import { log as log26 } from "@gethmy/harness";
|
|
9621
10205
|
function defaultRegistryPath() {
|
|
9622
10206
|
return join5(homedir4(), ".harmony-mcp", "agent-ports.json");
|
|
9623
10207
|
}
|
|
@@ -9631,7 +10215,7 @@ function load(path) {
|
|
|
9631
10215
|
return parsed;
|
|
9632
10216
|
return {};
|
|
9633
10217
|
} catch (err) {
|
|
9634
|
-
|
|
10218
|
+
log26.warn(TAG24, `failed to read ${path}: ${err instanceof Error ? err.message : err}`);
|
|
9635
10219
|
return {};
|
|
9636
10220
|
}
|
|
9637
10221
|
}
|
|
@@ -9649,7 +10233,7 @@ function recordDaemonPort(projectId, entry, path = defaultRegistryPath()) {
|
|
|
9649
10233
|
registry[projectId] = { ...entry, updatedAt: Date.now() };
|
|
9650
10234
|
save(path, registry);
|
|
9651
10235
|
} catch (err) {
|
|
9652
|
-
|
|
10236
|
+
log26.warn(TAG24, `failed to record port for ${projectId}: ${err instanceof Error ? err.message : err}`);
|
|
9653
10237
|
}
|
|
9654
10238
|
}
|
|
9655
10239
|
function lookupDaemonPort(projectId, path = defaultRegistryPath()) {
|
|
@@ -9665,14 +10249,14 @@ function clearDaemonPort(projectId, pid, path = defaultRegistryPath()) {
|
|
|
9665
10249
|
delete registry[projectId];
|
|
9666
10250
|
save(path, registry);
|
|
9667
10251
|
} catch (err) {
|
|
9668
|
-
|
|
10252
|
+
log26.warn(TAG24, `failed to clear port for ${projectId}: ${err instanceof Error ? err.message : err}`);
|
|
9669
10253
|
}
|
|
9670
10254
|
}
|
|
9671
|
-
var
|
|
10255
|
+
var TAG24 = "port-registry";
|
|
9672
10256
|
var init_port_registry = () => {};
|
|
9673
10257
|
|
|
9674
10258
|
// src/recovery.ts
|
|
9675
|
-
import { log as
|
|
10259
|
+
import { log as log27, teardownWorktree as teardownWorktree3 } from "@gethmy/harness";
|
|
9676
10260
|
function isProcessAlive(pid, currentPid) {
|
|
9677
10261
|
if (pid === currentPid)
|
|
9678
10262
|
return true;
|
|
@@ -9688,7 +10272,7 @@ async function fetchCardSafely(client, cardId) {
|
|
|
9688
10272
|
const { card } = await client.getCard(cardId);
|
|
9689
10273
|
return card;
|
|
9690
10274
|
} catch (err) {
|
|
9691
|
-
|
|
10275
|
+
log27.warn(TAG25, `cannot fetch card ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
9692
10276
|
return null;
|
|
9693
10277
|
}
|
|
9694
10278
|
}
|
|
@@ -9698,7 +10282,7 @@ async function recoverOrphans(store, client, config) {
|
|
|
9698
10282
|
return [];
|
|
9699
10283
|
}
|
|
9700
10284
|
const outcomes = [];
|
|
9701
|
-
|
|
10285
|
+
log27.info(TAG25, `recovering ${active.length} orphan run(s) from prior daemon`);
|
|
9702
10286
|
for (const run of active) {
|
|
9703
10287
|
const outcome = {
|
|
9704
10288
|
runId: run.runId,
|
|
@@ -9710,16 +10294,16 @@ async function recoverOrphans(store, client, config) {
|
|
|
9710
10294
|
};
|
|
9711
10295
|
outcomes.push(outcome);
|
|
9712
10296
|
if (isBudgetHeldRun(run)) {
|
|
9713
|
-
|
|
10297
|
+
log27.info(TAG25, `run ${run.runId} (#${run.cardShortId}) is held for a human budget decision — leaving it`);
|
|
9714
10298
|
outcome.actions.push("skipped: held for a human budget decision");
|
|
9715
10299
|
continue;
|
|
9716
10300
|
}
|
|
9717
10301
|
if (isProcessAlive(run.daemonPid, process.pid)) {
|
|
9718
|
-
|
|
10302
|
+
log27.warn(TAG25, `run ${run.runId} claims live daemon pid ${run.daemonPid} — skipping`);
|
|
9719
10303
|
outcome.actions.push("skipped: daemon pid still alive");
|
|
9720
10304
|
continue;
|
|
9721
10305
|
}
|
|
9722
|
-
|
|
10306
|
+
log27.info(TAG25, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
|
|
9723
10307
|
await recoverRun(run, store, client, config, outcome, {
|
|
9724
10308
|
rollbackAttempt: true
|
|
9725
10309
|
});
|
|
@@ -9739,7 +10323,7 @@ async function recoverRun(run, store, client, config, outcome, opts = {}) {
|
|
|
9739
10323
|
} catch (err) {
|
|
9740
10324
|
const msg = err instanceof Error ? err.message : String(err);
|
|
9741
10325
|
outcome.errors.push(`endAgentSession: ${msg}`);
|
|
9742
|
-
|
|
10326
|
+
log27.warn(TAG25, `endAgentSession failed for ${run.cardId}: ${msg}`);
|
|
9743
10327
|
}
|
|
9744
10328
|
const card = await fetchCardSafely(client, run.cardId);
|
|
9745
10329
|
if (card) {
|
|
@@ -9791,27 +10375,27 @@ async function recoverRun(run, store, client, config, outcome, opts = {}) {
|
|
|
9791
10375
|
outcome.errors.push(`decrementAttempt: ${msg}`);
|
|
9792
10376
|
}
|
|
9793
10377
|
}
|
|
9794
|
-
|
|
10378
|
+
log27.info(TAG25, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
|
|
9795
10379
|
}
|
|
9796
|
-
var
|
|
10380
|
+
var TAG25 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
|
|
9797
10381
|
var init_recovery = __esm(() => {
|
|
9798
10382
|
init_board_helpers();
|
|
9799
10383
|
init_state_store();
|
|
9800
10384
|
});
|
|
9801
10385
|
|
|
9802
10386
|
// src/claim.ts
|
|
9803
|
-
import { log as
|
|
10387
|
+
import { log as log28 } from "@gethmy/harness";
|
|
9804
10388
|
async function claimReviewCard(client, cardId, agentId) {
|
|
9805
10389
|
try {
|
|
9806
10390
|
const { claimed } = await client.claimCard(cardId, agentId);
|
|
9807
|
-
|
|
10391
|
+
log28.debug(TAG26, `claim ${cardId} → ${claimed ? "won" : "lost"}`);
|
|
9808
10392
|
return claimed;
|
|
9809
10393
|
} catch (err) {
|
|
9810
|
-
|
|
10394
|
+
log28.error(TAG26, `claim ${cardId} failed: ${err instanceof Error ? err.message : err}`);
|
|
9811
10395
|
return false;
|
|
9812
10396
|
}
|
|
9813
10397
|
}
|
|
9814
|
-
var
|
|
10398
|
+
var TAG26 = "claim";
|
|
9815
10399
|
var init_claim = () => {};
|
|
9816
10400
|
|
|
9817
10401
|
// src/strand-recovery.ts
|
|
@@ -9819,7 +10403,7 @@ var exports_strand_recovery = {};
|
|
|
9819
10403
|
__export(exports_strand_recovery, {
|
|
9820
10404
|
reclaimPreReviewStrands: () => reclaimPreReviewStrands
|
|
9821
10405
|
});
|
|
9822
|
-
import { log as
|
|
10406
|
+
import { log as log29, resolvePrUrl as resolvePrUrl2 } from "@gethmy/harness";
|
|
9823
10407
|
async function reclaimPreReviewStrands(opts) {
|
|
9824
10408
|
const {
|
|
9825
10409
|
client,
|
|
@@ -9863,22 +10447,22 @@ async function reclaimPreReviewStrands(opts) {
|
|
|
9863
10447
|
continue;
|
|
9864
10448
|
const won = await claimReviewCard(client, card.id, agentId);
|
|
9865
10449
|
if (!won) {
|
|
9866
|
-
|
|
10450
|
+
log29.debug(TAG27, `#${card.short_id} — lost the review claim race, skipping`);
|
|
9867
10451
|
continue;
|
|
9868
10452
|
}
|
|
9869
|
-
|
|
10453
|
+
log29.warn(TAG27, `#${card.short_id} claimed for review (branch pushed, no PR, unowned)`);
|
|
9870
10454
|
reclaimed.push(card.id);
|
|
9871
10455
|
if (opts.onClaimed) {
|
|
9872
10456
|
try {
|
|
9873
10457
|
await opts.onClaimed(card);
|
|
9874
10458
|
} catch (err) {
|
|
9875
|
-
|
|
10459
|
+
log29.error(TAG27, `onClaimed for #${card.short_id} failed: ${err instanceof Error ? err.message : err}`);
|
|
9876
10460
|
}
|
|
9877
10461
|
}
|
|
9878
10462
|
}
|
|
9879
10463
|
return reclaimed;
|
|
9880
10464
|
}
|
|
9881
|
-
var
|
|
10465
|
+
var TAG27 = "strand-recovery";
|
|
9882
10466
|
var init_strand_recovery = __esm(() => {
|
|
9883
10467
|
init_board_helpers();
|
|
9884
10468
|
init_claim();
|
|
@@ -9887,7 +10471,7 @@ var init_strand_recovery = __esm(() => {
|
|
|
9887
10471
|
});
|
|
9888
10472
|
|
|
9889
10473
|
// src/reconcile.ts
|
|
9890
|
-
import { detectGitProvider as detectGitProvider5, log as
|
|
10474
|
+
import { detectGitProvider as detectGitProvider5, log as log30 } from "@gethmy/harness";
|
|
9891
10475
|
|
|
9892
10476
|
class Reconciler {
|
|
9893
10477
|
client;
|
|
@@ -9930,7 +10514,7 @@ class Reconciler {
|
|
|
9930
10514
|
clearInterval(this.timer);
|
|
9931
10515
|
this.timer = null;
|
|
9932
10516
|
}
|
|
9933
|
-
|
|
10517
|
+
log30.info(TAG28, "Heartbeat stopped");
|
|
9934
10518
|
}
|
|
9935
10519
|
async recoverStaleRuns() {
|
|
9936
10520
|
if (!this.stateStore || !this.agentConfig)
|
|
@@ -9941,7 +10525,7 @@ class Reconciler {
|
|
|
9941
10525
|
const pool = this.pool;
|
|
9942
10526
|
for (const run of active) {
|
|
9943
10527
|
if (isBudgetHeldRun(run)) {
|
|
9944
|
-
|
|
10528
|
+
log30.info(TAG28, `run ${run.runId} (#${run.cardShortId}) is held for a human budget decision — leaving it`);
|
|
9945
10529
|
continue;
|
|
9946
10530
|
}
|
|
9947
10531
|
const foreignDaemon = run.daemonPid !== process.pid;
|
|
@@ -9951,7 +10535,7 @@ class Reconciler {
|
|
|
9951
10535
|
if (!daemonDead && !(heartbeatStale && ourZombie))
|
|
9952
10536
|
continue;
|
|
9953
10537
|
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`;
|
|
9954
|
-
|
|
10538
|
+
log30.warn(TAG28, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
|
|
9955
10539
|
await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
|
|
9956
10540
|
runId: run.runId,
|
|
9957
10541
|
cardId: run.cardId,
|
|
@@ -9978,11 +10562,11 @@ class Reconciler {
|
|
|
9978
10562
|
const stalledAt = Date.parse(card.updated_at ?? "");
|
|
9979
10563
|
if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
|
|
9980
10564
|
continue;
|
|
9981
|
-
|
|
10565
|
+
log30.warn(TAG28, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
|
|
9982
10566
|
try {
|
|
9983
10567
|
await this.client.moveCard(card.id, pickupCol.id);
|
|
9984
10568
|
} catch (err) {
|
|
9985
|
-
|
|
10569
|
+
log30.error(TAG28, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
9986
10570
|
}
|
|
9987
10571
|
}
|
|
9988
10572
|
}
|
|
@@ -10014,7 +10598,7 @@ class Reconciler {
|
|
|
10014
10598
|
return;
|
|
10015
10599
|
const cardLabels = resolveCardLabels(card, labelMap);
|
|
10016
10600
|
const subtasks = card.subtasks ?? [];
|
|
10017
|
-
|
|
10601
|
+
log30.info(TAG28, `Enqueuing claimed review card #${card.short_id} (agent-agnostic pickup)`);
|
|
10018
10602
|
await this.pool.enqueue(card, column, cardLabels, subtasks, "review");
|
|
10019
10603
|
}
|
|
10020
10604
|
});
|
|
@@ -10038,11 +10622,11 @@ class Reconciler {
|
|
|
10038
10622
|
const parkedAt = Date.parse(card.updated_at ?? "");
|
|
10039
10623
|
if (!Number.isFinite(parkedAt) || now - parkedAt < ttlMs)
|
|
10040
10624
|
continue;
|
|
10041
|
-
|
|
10625
|
+
log30.warn(TAG28, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
|
|
10042
10626
|
try {
|
|
10043
10627
|
await this.client.moveCard(card.id, pickupCol.id);
|
|
10044
10628
|
} catch (err) {
|
|
10045
|
-
|
|
10629
|
+
log30.error(TAG28, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
10046
10630
|
}
|
|
10047
10631
|
}
|
|
10048
10632
|
}
|
|
@@ -10086,21 +10670,21 @@ class Reconciler {
|
|
|
10086
10670
|
const subtasks = card.subtasks ?? [];
|
|
10087
10671
|
const mode = route.mode;
|
|
10088
10672
|
if (route.stage) {
|
|
10089
|
-
|
|
10673
|
+
log30.info(TAG28, `Stage card #${card.short_id} (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement) regardless of column`);
|
|
10090
10674
|
}
|
|
10091
10675
|
if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
|
|
10092
|
-
|
|
10676
|
+
log30.debug(TAG28, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
|
|
10093
10677
|
continue;
|
|
10094
10678
|
}
|
|
10095
10679
|
if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
|
|
10096
|
-
|
|
10680
|
+
log30.debug(TAG28, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
|
|
10097
10681
|
continue;
|
|
10098
10682
|
}
|
|
10099
10683
|
if (mode === "review" && !qualifiesForAutoReview(card.description)) {
|
|
10100
|
-
|
|
10684
|
+
log30.debug(TAG28, `Skipping #${card.short_id} — no branch or PR reference (not qualified for auto-review)`);
|
|
10101
10685
|
continue;
|
|
10102
10686
|
}
|
|
10103
|
-
|
|
10687
|
+
log30.info(TAG28, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
|
|
10104
10688
|
await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
10105
10689
|
}
|
|
10106
10690
|
}
|
|
@@ -10110,24 +10694,24 @@ class Reconciler {
|
|
|
10110
10694
|
try {
|
|
10111
10695
|
await this.pool.drainBudgetDecisions();
|
|
10112
10696
|
} catch (err) {
|
|
10113
|
-
|
|
10697
|
+
log30.error(TAG28, `budget decisions were not drained this tick: ${err instanceof Error ? err.message : err}`);
|
|
10114
10698
|
}
|
|
10115
10699
|
await this.recoverStrandedInProgress(cards, columns, knownCardIds);
|
|
10116
10700
|
await this.recoverStrandedReview(cards, columns, labelMap, knownCardIds);
|
|
10117
10701
|
for (const knownId of knownCardIds) {
|
|
10118
10702
|
if (!allAgentCardIds.has(knownId)) {
|
|
10119
|
-
|
|
10703
|
+
log30.info(TAG28, `Missed unassign: ${knownId} — removing`);
|
|
10120
10704
|
await this.pool.removeCard(knownId);
|
|
10121
10705
|
}
|
|
10122
10706
|
}
|
|
10123
10707
|
await this.releaseStalledApprovals(cards, columns, knownCardIds);
|
|
10124
|
-
|
|
10708
|
+
log30.debug(TAG28, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
|
|
10125
10709
|
} catch (err) {
|
|
10126
|
-
|
|
10710
|
+
log30.error(TAG28, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
|
|
10127
10711
|
}
|
|
10128
10712
|
}
|
|
10129
10713
|
}
|
|
10130
|
-
var
|
|
10714
|
+
var TAG28 = "reconcile";
|
|
10131
10715
|
var init_reconcile = __esm(() => {
|
|
10132
10716
|
init_board_helpers();
|
|
10133
10717
|
init_recovery();
|
|
@@ -10142,7 +10726,7 @@ var exports_startup_banner = {};
|
|
|
10142
10726
|
__export(exports_startup_banner, {
|
|
10143
10727
|
createStartupBanner: () => createStartupBanner
|
|
10144
10728
|
});
|
|
10145
|
-
import { isPretty, log as
|
|
10729
|
+
import { isPretty, log as log31 } from "@gethmy/harness";
|
|
10146
10730
|
function createStartupBanner(config, version) {
|
|
10147
10731
|
return isPretty() ? prettyBanner(config, version) : jsonBanner(config, version);
|
|
10148
10732
|
}
|
|
@@ -10167,7 +10751,7 @@ function prettyBanner(config, version) {
|
|
|
10167
10751
|
checks.push({ kind: "ok", message });
|
|
10168
10752
|
},
|
|
10169
10753
|
warn(message) {
|
|
10170
|
-
|
|
10754
|
+
log31.warn(TAG29, message);
|
|
10171
10755
|
checks.push({ kind: "warn", message: message.split(`
|
|
10172
10756
|
`, 1)[0] });
|
|
10173
10757
|
},
|
|
@@ -10192,25 +10776,25 @@ function prettyBanner(config, version) {
|
|
|
10192
10776
|
};
|
|
10193
10777
|
}
|
|
10194
10778
|
function jsonBanner(config, version) {
|
|
10195
|
-
|
|
10196
|
-
|
|
10779
|
+
log31.info(TAG29, `Harmony Agent Daemon v${version} starting...`);
|
|
10780
|
+
log31.info(TAG29, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Runner: ${config.agent.runner} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
|
|
10197
10781
|
if (config.agent.review.enabled) {
|
|
10198
|
-
|
|
10782
|
+
log31.info(TAG29, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
|
|
10199
10783
|
}
|
|
10200
10784
|
let failed = false;
|
|
10201
10785
|
return {
|
|
10202
10786
|
setProjectName(_name) {},
|
|
10203
10787
|
setGitProvider(provider) {
|
|
10204
|
-
|
|
10788
|
+
log31.info(TAG29, `Git provider: ${provider}`);
|
|
10205
10789
|
},
|
|
10206
10790
|
setHttpPort(port) {
|
|
10207
|
-
|
|
10791
|
+
log31.info(TAG29, `HTTP server on port ${port}`);
|
|
10208
10792
|
},
|
|
10209
10793
|
check(message) {
|
|
10210
|
-
|
|
10794
|
+
log31.info(TAG29, message);
|
|
10211
10795
|
},
|
|
10212
10796
|
warn(message) {
|
|
10213
|
-
|
|
10797
|
+
log31.warn(TAG29, message);
|
|
10214
10798
|
},
|
|
10215
10799
|
fail() {
|
|
10216
10800
|
failed = true;
|
|
@@ -10218,7 +10802,7 @@ function jsonBanner(config, version) {
|
|
|
10218
10802
|
async ready(message) {
|
|
10219
10803
|
if (failed)
|
|
10220
10804
|
return;
|
|
10221
|
-
|
|
10805
|
+
log31.info(TAG29, message);
|
|
10222
10806
|
}
|
|
10223
10807
|
};
|
|
10224
10808
|
}
|
|
@@ -10299,7 +10883,7 @@ function cyan(s) {
|
|
|
10299
10883
|
function yellow(s) {
|
|
10300
10884
|
return `${ANSI.yellow}${s}${ANSI.reset}`;
|
|
10301
10885
|
}
|
|
10302
|
-
var
|
|
10886
|
+
var TAG29 = "daemon", RULE_WIDTH = 70, ANSI;
|
|
10303
10887
|
var init_startup_banner = __esm(() => {
|
|
10304
10888
|
ANSI = {
|
|
10305
10889
|
reset: "\x1B[0m",
|
|
@@ -10402,7 +10986,7 @@ var init_stream_parser_selftest = __esm(() => {
|
|
|
10402
10986
|
|
|
10403
10987
|
// src/watcher.ts
|
|
10404
10988
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
10405
|
-
import { isPretty as isPretty2, log as
|
|
10989
|
+
import { isPretty as isPretty2, log as log32 } from "@gethmy/harness";
|
|
10406
10990
|
import { createClient } from "@supabase/supabase-js";
|
|
10407
10991
|
|
|
10408
10992
|
class Watcher {
|
|
@@ -10453,7 +11037,7 @@ class Watcher {
|
|
|
10453
11037
|
}
|
|
10454
11038
|
async start() {
|
|
10455
11039
|
if (!isPretty2()) {
|
|
10456
|
-
|
|
11040
|
+
log32.info(TAG30, "Connecting to Supabase realtime (broadcast)...");
|
|
10457
11041
|
}
|
|
10458
11042
|
this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
|
|
10459
11043
|
this.subscribeBroadcast();
|
|
@@ -10466,7 +11050,7 @@ class Watcher {
|
|
|
10466
11050
|
const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
|
|
10467
11051
|
this.presenceChannel = presenceChannel;
|
|
10468
11052
|
presenceChannel.on("presence", { event: "sync" }, () => {
|
|
10469
|
-
|
|
11053
|
+
log32.debug(TAG30, "Presence sync");
|
|
10470
11054
|
}).subscribe(async (status) => {
|
|
10471
11055
|
if (gen !== this.presenceGen)
|
|
10472
11056
|
return;
|
|
@@ -10490,13 +11074,13 @@ class Watcher {
|
|
|
10490
11074
|
if (trackStatus !== "ok") {
|
|
10491
11075
|
this.presenceTracked = false;
|
|
10492
11076
|
if (!this.stopping) {
|
|
10493
|
-
|
|
11077
|
+
log32.warn(TAG30, `Presence track returned "${trackStatus}" — scheduling reconnect`);
|
|
10494
11078
|
this.schedulePresenceReconnect();
|
|
10495
11079
|
}
|
|
10496
11080
|
return;
|
|
10497
11081
|
}
|
|
10498
11082
|
if (!isPretty2() || !this.suppressStartupLogs) {
|
|
10499
|
-
|
|
11083
|
+
log32.info(TAG30, "Presence tracked on board-presence channel");
|
|
10500
11084
|
}
|
|
10501
11085
|
this.presenceTracked = true;
|
|
10502
11086
|
this.presenceReconnectAttempts = 0;
|
|
@@ -10504,7 +11088,7 @@ class Watcher {
|
|
|
10504
11088
|
} else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
|
|
10505
11089
|
this.presenceTracked = false;
|
|
10506
11090
|
if (!this.stopping) {
|
|
10507
|
-
|
|
11091
|
+
log32.warn(TAG30, `Presence subscription ${status} — scheduling reconnect`);
|
|
10508
11092
|
this.schedulePresenceReconnect();
|
|
10509
11093
|
}
|
|
10510
11094
|
}
|
|
@@ -10523,7 +11107,7 @@ class Watcher {
|
|
|
10523
11107
|
async reconnectPresence() {
|
|
10524
11108
|
if (this.stopping || !this.supabase)
|
|
10525
11109
|
return;
|
|
10526
|
-
|
|
11110
|
+
log32.warn(TAG30, `Reconnecting presence subscription (attempt ${this.presenceReconnectAttempts})`);
|
|
10527
11111
|
if (this.presenceChannel) {
|
|
10528
11112
|
const old = this.presenceChannel;
|
|
10529
11113
|
this.presenceChannel = null;
|
|
@@ -10541,13 +11125,13 @@ class Watcher {
|
|
|
10541
11125
|
return;
|
|
10542
11126
|
const gen = ++this.broadcastGen;
|
|
10543
11127
|
this.channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
|
|
10544
|
-
|
|
11128
|
+
log32.debug(TAG30, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
|
|
10545
11129
|
this.onCardBroadcast({
|
|
10546
11130
|
event: "card_update",
|
|
10547
11131
|
payload: msg.payload ?? {}
|
|
10548
11132
|
});
|
|
10549
11133
|
}).on("broadcast", { event: "card_created" }, (msg) => {
|
|
10550
|
-
|
|
11134
|
+
log32.debug(TAG30, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
|
|
10551
11135
|
this.onCardBroadcast({
|
|
10552
11136
|
event: "card_created",
|
|
10553
11137
|
payload: msg.payload ?? {}
|
|
@@ -10557,7 +11141,7 @@ class Watcher {
|
|
|
10557
11141
|
const cardId = payload.card_id;
|
|
10558
11142
|
const command = payload.command;
|
|
10559
11143
|
if (cardId && command) {
|
|
10560
|
-
|
|
11144
|
+
log32.info(TAG30, `Broadcast: agent_command ${command} for ${cardId}`);
|
|
10561
11145
|
this.onAgentCommand?.({ cardId, command });
|
|
10562
11146
|
}
|
|
10563
11147
|
}).subscribe((status) => {
|
|
@@ -10567,13 +11151,13 @@ class Watcher {
|
|
|
10567
11151
|
this.connected = true;
|
|
10568
11152
|
this.reconnectAttempts = 0;
|
|
10569
11153
|
if (!isPretty2() || !this.suppressStartupLogs) {
|
|
10570
|
-
|
|
11154
|
+
log32.info(TAG30, "Broadcast subscription active");
|
|
10571
11155
|
}
|
|
10572
11156
|
this.maybeResolveReady();
|
|
10573
11157
|
} else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
|
|
10574
11158
|
this.connected = false;
|
|
10575
11159
|
if (!this.stopping) {
|
|
10576
|
-
|
|
11160
|
+
log32.warn(TAG30, `Broadcast subscription ${status} — scheduling reconnect`);
|
|
10577
11161
|
this.scheduleReconnect();
|
|
10578
11162
|
}
|
|
10579
11163
|
}
|
|
@@ -10592,7 +11176,7 @@ class Watcher {
|
|
|
10592
11176
|
async reconnectBroadcast() {
|
|
10593
11177
|
if (this.stopping || !this.supabase)
|
|
10594
11178
|
return;
|
|
10595
|
-
|
|
11179
|
+
log32.warn(TAG30, `Reconnecting broadcast subscription (attempt ${this.reconnectAttempts})`);
|
|
10596
11180
|
if (this.channel) {
|
|
10597
11181
|
const old = this.channel;
|
|
10598
11182
|
this.channel = null;
|
|
@@ -10629,10 +11213,10 @@ class Watcher {
|
|
|
10629
11213
|
}
|
|
10630
11214
|
this.connected = false;
|
|
10631
11215
|
this.presenceTracked = false;
|
|
10632
|
-
|
|
11216
|
+
log32.info(TAG30, "Broadcast subscription stopped");
|
|
10633
11217
|
}
|
|
10634
11218
|
}
|
|
10635
|
-
var
|
|
11219
|
+
var TAG30 = "watcher";
|
|
10636
11220
|
var init_watcher = () => {};
|
|
10637
11221
|
|
|
10638
11222
|
// src/worktree-gc.ts
|
|
@@ -10644,9 +11228,9 @@ __export(exports_worktree_gc, {
|
|
|
10644
11228
|
WorktreeGc: () => WorktreeGc
|
|
10645
11229
|
});
|
|
10646
11230
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
10647
|
-
import { readdirSync, statSync as statSync2 } from "node:fs";
|
|
11231
|
+
import { existsSync as existsSync4, readdirSync, statSync as statSync2 } from "node:fs";
|
|
10648
11232
|
import { resolve as resolve2 } from "node:path";
|
|
10649
|
-
import { cleanupWorktree as cleanupWorktree4, log as
|
|
11233
|
+
import { cleanupWorktree as cleanupWorktree4, log as log33 } from "@gethmy/harness";
|
|
10650
11234
|
function isTransientGitNetworkError(message) {
|
|
10651
11235
|
return TRANSIENT_GIT_NETWORK_ERROR.test(message);
|
|
10652
11236
|
}
|
|
@@ -10675,17 +11259,58 @@ function runWorktreeGc(basePath, store, opts = {}) {
|
|
|
10675
11259
|
return result;
|
|
10676
11260
|
}
|
|
10677
11261
|
const activePaths = new Set(store.getActiveRuns().map((r) => r.worktreePath).filter((p) => !!p));
|
|
11262
|
+
const candidates = [];
|
|
10678
11263
|
for (const entry of entries) {
|
|
10679
11264
|
const full = resolve2(baseAbs, entry);
|
|
11265
|
+
let isDirectory;
|
|
11266
|
+
try {
|
|
11267
|
+
isDirectory = statSync2(full).isDirectory();
|
|
11268
|
+
} catch (err) {
|
|
11269
|
+
result.errors.push({
|
|
11270
|
+
path: full,
|
|
11271
|
+
error: err instanceof Error ? err.message : String(err)
|
|
11272
|
+
});
|
|
11273
|
+
continue;
|
|
11274
|
+
}
|
|
11275
|
+
if (!isDirectory) {
|
|
11276
|
+
result.checked++;
|
|
11277
|
+
result.skipped.push(full);
|
|
11278
|
+
continue;
|
|
11279
|
+
}
|
|
11280
|
+
if (existsSync4(resolve2(full, ".git"))) {
|
|
11281
|
+
candidates.push(full);
|
|
11282
|
+
continue;
|
|
11283
|
+
}
|
|
11284
|
+
let children;
|
|
11285
|
+
try {
|
|
11286
|
+
children = readdirSync(full);
|
|
11287
|
+
} catch (err) {
|
|
11288
|
+
result.errors.push({
|
|
11289
|
+
path: full,
|
|
11290
|
+
error: err instanceof Error ? err.message : String(err)
|
|
11291
|
+
});
|
|
11292
|
+
continue;
|
|
11293
|
+
}
|
|
11294
|
+
const childDirs = children.filter((child) => {
|
|
11295
|
+
try {
|
|
11296
|
+
return statSync2(resolve2(full, child)).isDirectory();
|
|
11297
|
+
} catch {
|
|
11298
|
+
return false;
|
|
11299
|
+
}
|
|
11300
|
+
});
|
|
11301
|
+
if (childDirs.length === 0) {
|
|
11302
|
+
candidates.push(full);
|
|
11303
|
+
continue;
|
|
11304
|
+
}
|
|
11305
|
+
for (const child of childDirs) {
|
|
11306
|
+
candidates.push(resolve2(full, child));
|
|
11307
|
+
}
|
|
11308
|
+
}
|
|
11309
|
+
for (const full of candidates) {
|
|
10680
11310
|
result.checked++;
|
|
10681
11311
|
let mtimeMs;
|
|
10682
11312
|
try {
|
|
10683
|
-
|
|
10684
|
-
if (!stat.isDirectory()) {
|
|
10685
|
-
result.skipped.push(full);
|
|
10686
|
-
continue;
|
|
10687
|
-
}
|
|
10688
|
-
mtimeMs = stat.mtimeMs;
|
|
11313
|
+
mtimeMs = statSync2(full).mtimeMs;
|
|
10689
11314
|
} catch (err) {
|
|
10690
11315
|
result.errors.push({
|
|
10691
11316
|
path: full,
|
|
@@ -10718,10 +11343,10 @@ function runWorktreeGc(basePath, store, opts = {}) {
|
|
|
10718
11343
|
});
|
|
10719
11344
|
} catch {}
|
|
10720
11345
|
if (result.removed.length > 0) {
|
|
10721
|
-
|
|
11346
|
+
log33.info(TAG31, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
|
|
10722
11347
|
}
|
|
10723
11348
|
if (result.errors.length > 0) {
|
|
10724
|
-
|
|
11349
|
+
log33.warn(TAG31, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
|
|
10725
11350
|
}
|
|
10726
11351
|
return result;
|
|
10727
11352
|
}
|
|
@@ -10751,7 +11376,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
10751
11376
|
} catch (err) {
|
|
10752
11377
|
const detail = gitErrorDetail2(err);
|
|
10753
11378
|
if (isTransientGitNetworkError(detail)) {
|
|
10754
|
-
|
|
11379
|
+
log33.debug(TAG31, `Remote branch GC skipped — remote unreachable: ${detail}`);
|
|
10755
11380
|
return result;
|
|
10756
11381
|
}
|
|
10757
11382
|
result.errors.push({ ref: "fetch", error: detail });
|
|
@@ -10790,7 +11415,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
10790
11415
|
continue;
|
|
10791
11416
|
}
|
|
10792
11417
|
if (clock() > sweepDeadline) {
|
|
10793
|
-
|
|
11418
|
+
log33.debug(TAG31, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
|
|
10794
11419
|
break;
|
|
10795
11420
|
}
|
|
10796
11421
|
try {
|
|
@@ -10803,17 +11428,17 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
10803
11428
|
} catch (err) {
|
|
10804
11429
|
const detail = gitErrorDetail2(err);
|
|
10805
11430
|
if (isTransientGitNetworkError(detail)) {
|
|
10806
|
-
|
|
11431
|
+
log33.debug(TAG31, `Remote branch GC interrupted — remote unreachable: ${detail}`);
|
|
10807
11432
|
break;
|
|
10808
11433
|
}
|
|
10809
11434
|
result.errors.push({ ref, error: detail });
|
|
10810
11435
|
}
|
|
10811
11436
|
}
|
|
10812
11437
|
if (result.removed.length > 0) {
|
|
10813
|
-
|
|
11438
|
+
log33.info(TAG31, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
|
|
10814
11439
|
}
|
|
10815
11440
|
if (result.errors.length > 0) {
|
|
10816
|
-
|
|
11441
|
+
log33.warn(TAG31, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
|
|
10817
11442
|
}
|
|
10818
11443
|
return result;
|
|
10819
11444
|
}
|
|
@@ -10844,13 +11469,13 @@ class WorktreeGc {
|
|
|
10844
11469
|
try {
|
|
10845
11470
|
runWorktreeGc(this.basePath, this.store);
|
|
10846
11471
|
} catch (err) {
|
|
10847
|
-
|
|
11472
|
+
log33.warn(TAG31, `GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
10848
11473
|
}
|
|
10849
11474
|
if (this.remoteOpts) {
|
|
10850
11475
|
try {
|
|
10851
11476
|
pruneFailedRemoteBranches(this.remoteOpts);
|
|
10852
11477
|
} catch (err) {
|
|
10853
|
-
|
|
11478
|
+
log33.warn(TAG31, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
10854
11479
|
}
|
|
10855
11480
|
}
|
|
10856
11481
|
}
|
|
@@ -10864,7 +11489,7 @@ function getRepoRoot2() {
|
|
|
10864
11489
|
return null;
|
|
10865
11490
|
}
|
|
10866
11491
|
}
|
|
10867
|
-
var
|
|
11492
|
+
var TAG31 = "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;
|
|
10868
11493
|
var init_worktree_gc = __esm(() => {
|
|
10869
11494
|
GIT_NETWORK_EXEC = {
|
|
10870
11495
|
timeout: GIT_NETWORK_TIMEOUT_MS,
|
|
@@ -10901,7 +11526,7 @@ import { randomUUID as randomUUID3 } from "node:crypto";
|
|
|
10901
11526
|
import { createRequire as createRequire3 } from "node:module";
|
|
10902
11527
|
import {
|
|
10903
11528
|
detectGitProvider as detectGitProvider6,
|
|
10904
|
-
log as
|
|
11529
|
+
log as log34,
|
|
10905
11530
|
validateGitProviderCli
|
|
10906
11531
|
} from "@gethmy/harness";
|
|
10907
11532
|
async function validatePrerequisites(config, banner) {
|
|
@@ -10975,7 +11600,7 @@ async function main() {
|
|
|
10975
11600
|
} catch (err) {
|
|
10976
11601
|
if (err instanceof ConfigValidationError) {
|
|
10977
11602
|
banner.fail();
|
|
10978
|
-
|
|
11603
|
+
log34.error(TAG32, err.message);
|
|
10979
11604
|
process.exit(1);
|
|
10980
11605
|
}
|
|
10981
11606
|
throw err;
|
|
@@ -10985,7 +11610,7 @@ async function main() {
|
|
|
10985
11610
|
} catch (err) {
|
|
10986
11611
|
if (err instanceof ConfigValidationError) {
|
|
10987
11612
|
banner.fail();
|
|
10988
|
-
|
|
11613
|
+
log34.error(TAG32, err.message);
|
|
10989
11614
|
process.exit(1);
|
|
10990
11615
|
}
|
|
10991
11616
|
throw err;
|
|
@@ -11107,7 +11732,7 @@ async function main() {
|
|
|
11107
11732
|
if (shuttingDown)
|
|
11108
11733
|
return;
|
|
11109
11734
|
shuttingDown = true;
|
|
11110
|
-
|
|
11735
|
+
log34.info(TAG32, `Received ${signal}, shutting down gracefully...`);
|
|
11111
11736
|
reconciler.stop();
|
|
11112
11737
|
mergeMonitor?.stop();
|
|
11113
11738
|
worktreeGc.stop();
|
|
@@ -11118,18 +11743,18 @@ async function main() {
|
|
|
11118
11743
|
}
|
|
11119
11744
|
await watcher.stop();
|
|
11120
11745
|
await pool.shutdown();
|
|
11121
|
-
|
|
11746
|
+
log34.info(TAG32, "Daemon stopped.");
|
|
11122
11747
|
process.exit(exitCode);
|
|
11123
11748
|
};
|
|
11124
11749
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
11125
11750
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
11126
11751
|
process.on("uncaughtException", (err) => {
|
|
11127
|
-
|
|
11752
|
+
log34.error(TAG32, `Uncaught exception: ${err.message}`);
|
|
11128
11753
|
exitCode = 1;
|
|
11129
11754
|
shutdown("uncaughtException");
|
|
11130
11755
|
});
|
|
11131
11756
|
process.on("unhandledRejection", (reason) => {
|
|
11132
|
-
|
|
11757
|
+
log34.error(TAG32, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
|
|
11133
11758
|
exitCode = 1;
|
|
11134
11759
|
shutdown("unhandledRejection");
|
|
11135
11760
|
});
|
|
@@ -11188,29 +11813,29 @@ async function handleBroadcast(event, client, pool, config, agentId) {
|
|
|
11188
11813
|
if (assignedAgentId === undefined)
|
|
11189
11814
|
return;
|
|
11190
11815
|
if (assignedAgentId === agentId) {
|
|
11191
|
-
|
|
11816
|
+
log34.info(TAG32, `Broadcast: card ${cardId} assigned to agent`);
|
|
11192
11817
|
try {
|
|
11193
11818
|
await pool.resetAttemptsForReassign(cardId);
|
|
11194
11819
|
await tryEnqueueCard(cardId, client, pool, config, agentId);
|
|
11195
11820
|
} catch (err) {
|
|
11196
|
-
|
|
11821
|
+
log34.error(TAG32, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
|
|
11197
11822
|
}
|
|
11198
11823
|
} else if (pool.isCardKnown(cardId)) {
|
|
11199
|
-
|
|
11824
|
+
log34.info(TAG32, `Broadcast: card ${cardId} unassigned from agent`);
|
|
11200
11825
|
await pool.removeCard(cardId);
|
|
11201
11826
|
}
|
|
11202
11827
|
}
|
|
11203
11828
|
async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
11204
11829
|
const { card } = await client.getCard(cardId);
|
|
11205
11830
|
if (card.assigned_agent_id !== agentId) {
|
|
11206
|
-
|
|
11831
|
+
log34.debug(TAG32, `Card ${cardId} no longer assigned to agent — skipping`);
|
|
11207
11832
|
return;
|
|
11208
11833
|
}
|
|
11209
11834
|
const board = await client.getBoard(config.projectId, { summary: true });
|
|
11210
11835
|
const columns = board.columns;
|
|
11211
11836
|
const column = columns.find((c) => c.id === card.column_id);
|
|
11212
11837
|
if (!column) {
|
|
11213
|
-
|
|
11838
|
+
log34.warn(TAG32, `Column not found for card ${cardId}`);
|
|
11214
11839
|
return;
|
|
11215
11840
|
}
|
|
11216
11841
|
const route = classifyPickup(card, column.name, {
|
|
@@ -11219,31 +11844,31 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
|
11219
11844
|
playbooks: config.agent.playbooks
|
|
11220
11845
|
});
|
|
11221
11846
|
if (!route) {
|
|
11222
|
-
|
|
11847
|
+
log34.info(TAG32, `Card #${card.short_id} is in "${column.name}", not a pickup/review/stage column — skipping`);
|
|
11223
11848
|
return;
|
|
11224
11849
|
}
|
|
11225
11850
|
if (route.stage) {
|
|
11226
|
-
|
|
11851
|
+
log34.info(TAG32, `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`);
|
|
11227
11852
|
}
|
|
11228
11853
|
const mode = route.mode;
|
|
11229
11854
|
const labelMap = buildLabelMap(board.labels ?? []);
|
|
11230
11855
|
const cardLabels = resolveCardLabels(card, labelMap);
|
|
11231
11856
|
const subtasks = card.subtasks ?? [];
|
|
11232
11857
|
if (mode === "review" && config.agent.review.approvedLabel && hasLabel(cardLabels, config.agent.review.approvedLabel)) {
|
|
11233
|
-
|
|
11858
|
+
log34.debug(TAG32, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
|
|
11234
11859
|
return;
|
|
11235
11860
|
}
|
|
11236
11861
|
if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
|
|
11237
|
-
|
|
11862
|
+
log34.debug(TAG32, `Card #${card.short_id} has "${NEED_REVIEW_LABEL}" label (needs human) — skipping review`);
|
|
11238
11863
|
return;
|
|
11239
11864
|
}
|
|
11240
11865
|
if (mode === "review" && !qualifiesForAutoReview(card.description)) {
|
|
11241
|
-
|
|
11866
|
+
log34.info(TAG32, `Card #${card.short_id} has no branch or PR reference — skipping auto-review`);
|
|
11242
11867
|
return;
|
|
11243
11868
|
}
|
|
11244
11869
|
await pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
11245
11870
|
}
|
|
11246
|
-
var
|
|
11871
|
+
var TAG32 = "daemon", BASE_REMOTE = "origin", PKG_VERSION;
|
|
11247
11872
|
var init_src = __esm(() => {
|
|
11248
11873
|
init_base_branch();
|
|
11249
11874
|
init_board_helpers();
|