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