@gethmy/agent 1.27.0 → 1.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1896 -567
- package/dist/index.js +1889 -563
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -17,6 +17,104 @@ var __export = (target, all) => {
|
|
|
17
17
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
18
18
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
19
19
|
|
|
20
|
+
// src/base-branch.ts
|
|
21
|
+
import { execFileSync } from "node:child_process";
|
|
22
|
+
function resolveBaseBranch(baseBranch, remote, probe) {
|
|
23
|
+
const ref = `${remote}/${baseBranch}`;
|
|
24
|
+
if (probe.hasRef(ref))
|
|
25
|
+
return { ok: true, fetched: false };
|
|
26
|
+
const remotes = probe.remotes();
|
|
27
|
+
if (!remotes.includes(remote)) {
|
|
28
|
+
return {
|
|
29
|
+
ok: false,
|
|
30
|
+
message: remotes.length ? `This checkout has no "${remote}" remote — it has ${remotes.map((r) => `"${r}"`).join(", ")}. Every worktree branches from "${ref}", so the daemon needs that remote. Add it, or run the daemon from a clone that has it.` : `This checkout has no git remote, so "${ref}" can never resolve. Every worktree branches from the remote base branch. Add a remote, or run the daemon from a clone of the repository.`
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const fetchError = probe.fetch(remote, baseBranch);
|
|
34
|
+
if (fetchError === null && probe.hasRef(ref)) {
|
|
35
|
+
return { ok: true, fetched: true };
|
|
36
|
+
}
|
|
37
|
+
const upstream = probe.remoteHasBranch(remote, baseBranch);
|
|
38
|
+
if (upstream === false) {
|
|
39
|
+
const actual = probe.remoteDefaultBranch(remote);
|
|
40
|
+
const suggestion = actual && actual !== baseBranch ? ` "${remote}" reports "${actual}" as its default branch.` : "";
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
message: `Branch "${baseBranch}" does not exist on "${remote}".${suggestion} ${BASE_BRANCH_CONFIG_HINT}`
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const detail = fetchError ? `: ${fetchError}` : " for an unknown reason";
|
|
47
|
+
if (upstream === null) {
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
message: `Could not reach "${remote}" to resolve "${ref}"${detail}. Fix the git access from this checkout, then start the daemon again.`
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
message: `"${baseBranch}" exists on "${remote}", but fetching it into "${ref}" failed${detail}. Fix the git access from this checkout, then start the daemon again.`
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function createGitProbe(cwd) {
|
|
59
|
+
const git = (args) => execFileSync("git", args, {
|
|
60
|
+
cwd,
|
|
61
|
+
encoding: "utf-8",
|
|
62
|
+
stdio: "pipe"
|
|
63
|
+
}).trim();
|
|
64
|
+
return {
|
|
65
|
+
remotes() {
|
|
66
|
+
try {
|
|
67
|
+
return git(["remote"]).split(`
|
|
68
|
+
`).filter(Boolean);
|
|
69
|
+
} catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
hasRef(ref) {
|
|
74
|
+
try {
|
|
75
|
+
git(["rev-parse", "--verify", ref]);
|
|
76
|
+
return true;
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
fetch(remote, branch) {
|
|
82
|
+
try {
|
|
83
|
+
git([
|
|
84
|
+
"fetch",
|
|
85
|
+
remote,
|
|
86
|
+
`+refs/heads/${branch}:refs/remotes/${remote}/${branch}`
|
|
87
|
+
]);
|
|
88
|
+
return null;
|
|
89
|
+
} catch (err) {
|
|
90
|
+
const e = err;
|
|
91
|
+
const text = String(e?.stderr ?? e?.message ?? "").trim();
|
|
92
|
+
return text.split(`
|
|
93
|
+
`).filter(Boolean).slice(-1)[0] ?? "git fetch failed";
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
remoteHasBranch(remote, branch) {
|
|
97
|
+
try {
|
|
98
|
+
return git(["ls-remote", "--heads", remote, branch]).length > 0;
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
remoteDefaultBranch(remote) {
|
|
104
|
+
try {
|
|
105
|
+
const line = git(["ls-remote", "--symref", remote, "HEAD"]).split(`
|
|
106
|
+
`).find((l) => l.startsWith("ref:"));
|
|
107
|
+
const match = line?.match(/refs\/heads\/(\S+)/);
|
|
108
|
+
return match?.[1] ?? null;
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
var BASE_BRANCH_CONFIG_HINT = "Set `agent.worktree.baseBranch` in ~/.harmony-mcp/config.json to the branch worktrees should branch from.";
|
|
116
|
+
var init_base_branch = () => {};
|
|
117
|
+
|
|
20
118
|
// src/board-helpers.ts
|
|
21
119
|
var exports_board_helpers = {};
|
|
22
120
|
__export(exports_board_helpers, {
|
|
@@ -547,6 +645,16 @@ function hasUnsafeDaemonBranchLine(description) {
|
|
|
547
645
|
}
|
|
548
646
|
return false;
|
|
549
647
|
}
|
|
648
|
+
function recordsPushedWorkOn(description, branchName) {
|
|
649
|
+
if (!description || !branchName)
|
|
650
|
+
return false;
|
|
651
|
+
for (const match of description.matchAll(DAEMON_BRANCH_LINE_PATTERN)) {
|
|
652
|
+
const ref = match[1];
|
|
653
|
+
if (ref && SAFE_GIT_REF_PATTERN.test(ref) && ref === branchName)
|
|
654
|
+
return true;
|
|
655
|
+
}
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
550
658
|
var BRANCH_REF_PATTERN, DAEMON_BRANCH_LINE_PATTERN, SAFE_GIT_REF_PATTERN, PR_LINK_PATTERN;
|
|
551
659
|
var init_branchRef = __esm(() => {
|
|
552
660
|
BRANCH_REF_PATTERN = /Branch:\s*`([^`]+)`/g;
|
|
@@ -676,6 +784,12 @@ var init_constants = __esm(() => {
|
|
|
676
784
|
QUERY_GC_TIME: 1000 * 60 * 60 * 24
|
|
677
785
|
};
|
|
678
786
|
});
|
|
787
|
+
// ../harmony-shared/dist/gateConfigError.js
|
|
788
|
+
var GATE_CONFIG_ERROR_KEY = "configError", GATE_CONFIG_ERROR_MARK;
|
|
789
|
+
var init_gateConfigError = __esm(() => {
|
|
790
|
+
GATE_CONFIG_ERROR_MARK = Object.freeze({ [GATE_CONFIG_ERROR_KEY]: true });
|
|
791
|
+
});
|
|
792
|
+
|
|
679
793
|
// ../harmony-shared/dist/gateEvaluate.js
|
|
680
794
|
function isGateKind(value) {
|
|
681
795
|
return typeof value === "string" && GATE_KINDS.includes(value);
|
|
@@ -1140,6 +1254,42 @@ function entryActionAllowlist(entryAction) {
|
|
|
1140
1254
|
function stageDisallowedTools() {
|
|
1141
1255
|
return STAGE_DAEMON_OWNED_TOOLS.length > 0 ? STAGE_DAEMON_OWNED_TOOLS.join(",") : null;
|
|
1142
1256
|
}
|
|
1257
|
+
function customGateMetric(gate) {
|
|
1258
|
+
if (gate === null || typeof gate !== "object" || Array.isArray(gate)) {
|
|
1259
|
+
return null;
|
|
1260
|
+
}
|
|
1261
|
+
const record = gate;
|
|
1262
|
+
if (record.kind !== "custom")
|
|
1263
|
+
return null;
|
|
1264
|
+
if (record.pendingEngine === true)
|
|
1265
|
+
return null;
|
|
1266
|
+
const metric = typeof record.metric === "string" ? record.metric.trim() : "";
|
|
1267
|
+
return metric ? metric : null;
|
|
1268
|
+
}
|
|
1269
|
+
function referencedGateMetrics(def) {
|
|
1270
|
+
const out = [];
|
|
1271
|
+
for (const stage of readStageDefs(def)) {
|
|
1272
|
+
if (!stage || typeof stage !== "object")
|
|
1273
|
+
continue;
|
|
1274
|
+
const stageId = typeof stage.id === "string" ? stage.id : "";
|
|
1275
|
+
const stageName = typeof stage.name === "string" ? stage.name : stageId;
|
|
1276
|
+
const gateMetric = customGateMetric(stage.gate);
|
|
1277
|
+
if (gateMetric) {
|
|
1278
|
+
out.push({ stageId, stageName, metric: gateMetric, source: "gate" });
|
|
1279
|
+
}
|
|
1280
|
+
const loop = normalizeLoopDef(stage.loop);
|
|
1281
|
+
const loopMetric = loop?.exit_gate ? customGateMetric(loop.exit_gate) : null;
|
|
1282
|
+
if (loopMetric) {
|
|
1283
|
+
out.push({
|
|
1284
|
+
stageId,
|
|
1285
|
+
stageName,
|
|
1286
|
+
metric: loopMetric,
|
|
1287
|
+
source: "loop_exit_gate"
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
return out;
|
|
1292
|
+
}
|
|
1143
1293
|
var DEFAULT_LOOP_MAX_ITERATIONS = 5, PLAYBOOK_STAGE_ROLES, SKILL_TOOL_ALLOWLIST, HARMONY_TOOL_RE, STAGE_DAEMON_OWNED_TOOLS;
|
|
1144
1294
|
var init_playbookStage = __esm(() => {
|
|
1145
1295
|
PLAYBOOK_STAGE_ROLES = [
|
|
@@ -1430,6 +1580,7 @@ var init_dist = __esm(() => {
|
|
|
1430
1580
|
init_columnSort();
|
|
1431
1581
|
init_commentSerializer();
|
|
1432
1582
|
init_constants();
|
|
1583
|
+
init_gateConfigError();
|
|
1433
1584
|
init_gateEvaluate();
|
|
1434
1585
|
init_logger();
|
|
1435
1586
|
init_playbookAutoBind();
|
|
@@ -1775,6 +1926,7 @@ var init_types2 = __esm(() => {
|
|
|
1775
1926
|
advanced: "claude-opus-5",
|
|
1776
1927
|
research: "claude-fable-5"
|
|
1777
1928
|
},
|
|
1929
|
+
sizingModel: "",
|
|
1778
1930
|
reviewModel: "sonnet",
|
|
1779
1931
|
maxTurns: 80,
|
|
1780
1932
|
reviewMaxTurns: 60,
|
|
@@ -1828,7 +1980,8 @@ var init_types2 = __esm(() => {
|
|
|
1828
1980
|
},
|
|
1829
1981
|
budget: {
|
|
1830
1982
|
maxAttemptsPerCard: 3,
|
|
1831
|
-
dailyBudgetCents: 5000
|
|
1983
|
+
dailyBudgetCents: 5000,
|
|
1984
|
+
pause: { enabled: true, waitHours: 24, extraTurns: null }
|
|
1832
1985
|
},
|
|
1833
1986
|
http: {
|
|
1834
1987
|
enabled: true,
|
|
@@ -2105,6 +2258,15 @@ var init_config_validation = __esm(() => {
|
|
|
2105
2258
|
};
|
|
2106
2259
|
});
|
|
2107
2260
|
|
|
2261
|
+
// src/declared-metrics.ts
|
|
2262
|
+
var exports_declared_metrics = {};
|
|
2263
|
+
__export(exports_declared_metrics, {
|
|
2264
|
+
declaredMetricNames: () => declaredMetricNames
|
|
2265
|
+
});
|
|
2266
|
+
function declaredMetricNames(metrics) {
|
|
2267
|
+
return Object.keys(metrics ?? {}).map((name) => name.trim()).filter((name) => name.length > 0).sort();
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2108
2270
|
// src/http-server.ts
|
|
2109
2271
|
import {
|
|
2110
2272
|
createServer
|
|
@@ -2231,7 +2393,7 @@ function isAddrInUse(err) {
|
|
|
2231
2393
|
return typeof err === "object" && err !== null && err.code === "EADDRINUSE";
|
|
2232
2394
|
}
|
|
2233
2395
|
function parseCommand(path) {
|
|
2234
|
-
const match = path.match(/^\/(pause|resume|stop)\/([^/]+)$/);
|
|
2396
|
+
const match = path.match(/^\/(pause|resume|stop|continue)\/([^/]+)$/);
|
|
2235
2397
|
if (!match)
|
|
2236
2398
|
return null;
|
|
2237
2399
|
return { command: match[1], cardId: decodeURIComponent(match[2]) };
|
|
@@ -2273,8 +2435,11 @@ function decideAutoMergeAction(input) {
|
|
|
2273
2435
|
if (ciStatus !== "success")
|
|
2274
2436
|
return "wait";
|
|
2275
2437
|
}
|
|
2276
|
-
if (config.reReviewOnBranchChange
|
|
2277
|
-
|
|
2438
|
+
if (config.reReviewOnBranchChange) {
|
|
2439
|
+
if (!reviewedSha)
|
|
2440
|
+
return "wait";
|
|
2441
|
+
if (headSha && reviewedSha !== headSha)
|
|
2442
|
+
return "rereview";
|
|
2278
2443
|
}
|
|
2279
2444
|
return "merge";
|
|
2280
2445
|
}
|
|
@@ -2310,7 +2475,11 @@ async function attemptAutoMerge(deps) {
|
|
|
2310
2475
|
});
|
|
2311
2476
|
switch (action) {
|
|
2312
2477
|
case "wait":
|
|
2313
|
-
|
|
2478
|
+
if (autoMerge.reReviewOnBranchChange && !reviewedSha) {
|
|
2479
|
+
log4.info(TAG4, `#${card.short_id} holding — no Reviewed-SHA on the card, so nothing has reviewed this head. Merge it yourself, or let the review pipeline run.`);
|
|
2480
|
+
} else {
|
|
2481
|
+
log4.debug(TAG4, `#${card.short_id} waiting (ci=${ciStatus})`);
|
|
2482
|
+
}
|
|
2314
2483
|
return;
|
|
2315
2484
|
case "stamp-failure":
|
|
2316
2485
|
log4.info(TAG4, `#${card.short_id} CI failed — flagging for human`);
|
|
@@ -2330,7 +2499,7 @@ var TAG4 = "auto-merge";
|
|
|
2330
2499
|
var init_auto_merge = () => {};
|
|
2331
2500
|
|
|
2332
2501
|
// src/review-worktree.ts
|
|
2333
|
-
import { execFileSync, execSync as execSync2 } from "node:child_process";
|
|
2502
|
+
import { execFileSync as execFileSync2, execSync as execSync2 } from "node:child_process";
|
|
2334
2503
|
import { existsSync } from "node:fs";
|
|
2335
2504
|
import { resolve } from "node:path";
|
|
2336
2505
|
import {
|
|
@@ -2353,7 +2522,7 @@ function gitErrorDetail(err) {
|
|
|
2353
2522
|
return err instanceof Error ? err.message : String(err);
|
|
2354
2523
|
}
|
|
2355
2524
|
function checkoutExistingBranch(basePath, branchName) {
|
|
2356
|
-
const repoRoot =
|
|
2525
|
+
const repoRoot = execFileSync2("git", ["rev-parse", "--show-toplevel"], {
|
|
2357
2526
|
encoding: "utf-8"
|
|
2358
2527
|
}).trim();
|
|
2359
2528
|
const worktreeDir = resolve(repoRoot, basePath, `review-${branchName}`);
|
|
@@ -2362,13 +2531,13 @@ function checkoutExistingBranch(basePath, branchName) {
|
|
|
2362
2531
|
cleanupWorktree(worktreeDir);
|
|
2363
2532
|
}
|
|
2364
2533
|
try {
|
|
2365
|
-
|
|
2534
|
+
execFileSync2("git", ["worktree", "prune", "--expire=now"], {
|
|
2366
2535
|
cwd: repoRoot,
|
|
2367
2536
|
stdio: "pipe"
|
|
2368
2537
|
});
|
|
2369
2538
|
} catch {}
|
|
2370
2539
|
try {
|
|
2371
|
-
|
|
2540
|
+
execFileSync2("git", ["fetch", "origin", branchName], {
|
|
2372
2541
|
cwd: repoRoot,
|
|
2373
2542
|
stdio: "pipe"
|
|
2374
2543
|
});
|
|
@@ -2377,14 +2546,14 @@ function checkoutExistingBranch(basePath, branchName) {
|
|
|
2377
2546
|
}
|
|
2378
2547
|
removeWorktreeHoldingBranch(repoRoot, branchName, worktreeDir);
|
|
2379
2548
|
try {
|
|
2380
|
-
|
|
2549
|
+
execFileSync2("git", ["branch", "-D", branchName], {
|
|
2381
2550
|
cwd: repoRoot,
|
|
2382
2551
|
stdio: "pipe"
|
|
2383
2552
|
});
|
|
2384
2553
|
} catch {}
|
|
2385
2554
|
log5.info(TAG5, `Creating review worktree: ${worktreeDir} (branch: ${branchName})`);
|
|
2386
2555
|
try {
|
|
2387
|
-
|
|
2556
|
+
execFileSync2("git", [
|
|
2388
2557
|
"worktree",
|
|
2389
2558
|
"add",
|
|
2390
2559
|
"--track",
|
|
@@ -2621,6 +2790,57 @@ var init_merge_monitor = __esm(() => {
|
|
|
2621
2790
|
execFileAsync = promisify(execFile);
|
|
2622
2791
|
});
|
|
2623
2792
|
|
|
2793
|
+
// src/metric-validation.ts
|
|
2794
|
+
function formatUndeclaredMetricWarning(finding) {
|
|
2795
|
+
const card = finding.cardShortId != null ? `#${finding.cardShortId} "${finding.cardTitle}"` : `"${finding.cardTitle}"`;
|
|
2796
|
+
return `Card ${card} is bound to a playbook whose stage "${finding.stageName}" ` + `gates on metric "${finding.metric}", which this daemon does not declare — ` + `its stage run would hold as misconfigured. Add \`agent.playbooks.metrics.${finding.metric}\` to permit it.`;
|
|
2797
|
+
}
|
|
2798
|
+
async function findUndeclaredGateMetrics(client, projectId, config) {
|
|
2799
|
+
if (!config.playbooks.enabled)
|
|
2800
|
+
return [];
|
|
2801
|
+
const declared = new Set(Object.keys(config.playbooks.metrics ?? {}));
|
|
2802
|
+
const board = await client.getFullBoard(projectId);
|
|
2803
|
+
const bound = board.cards.filter((card) => card && typeof card === "object" && card.playbook_id && card.playbook_version != null && card.current_stage);
|
|
2804
|
+
if (bound.length === 0)
|
|
2805
|
+
return [];
|
|
2806
|
+
const request = client.request?.bind(client);
|
|
2807
|
+
if (typeof request !== "function")
|
|
2808
|
+
return [];
|
|
2809
|
+
const versionCache = new Map;
|
|
2810
|
+
const findings = [];
|
|
2811
|
+
for (const card of bound) {
|
|
2812
|
+
const cacheKey = `${card.playbook_id}@${card.playbook_version}`;
|
|
2813
|
+
let def = versionCache.get(cacheKey);
|
|
2814
|
+
if (def === undefined) {
|
|
2815
|
+
try {
|
|
2816
|
+
const res = await request("GET", `/playbooks/${encodeURIComponent(String(card.playbook_id))}/versions/${card.playbook_version}`);
|
|
2817
|
+
def = res?.version ?? null;
|
|
2818
|
+
} catch {
|
|
2819
|
+
def = null;
|
|
2820
|
+
}
|
|
2821
|
+
versionCache.set(cacheKey, def);
|
|
2822
|
+
}
|
|
2823
|
+
if (!def)
|
|
2824
|
+
continue;
|
|
2825
|
+
const warned = new Set;
|
|
2826
|
+
for (const ref of referencedGateMetrics(def)) {
|
|
2827
|
+
if (declared.has(ref.metric) || warned.has(ref.metric))
|
|
2828
|
+
continue;
|
|
2829
|
+
warned.add(ref.metric);
|
|
2830
|
+
findings.push({
|
|
2831
|
+
cardShortId: typeof card.short_id === "number" ? card.short_id : null,
|
|
2832
|
+
cardTitle: card.title ?? "",
|
|
2833
|
+
stageName: ref.stageName || ref.stageId,
|
|
2834
|
+
metric: ref.metric
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
return findings;
|
|
2839
|
+
}
|
|
2840
|
+
var init_metric_validation = __esm(() => {
|
|
2841
|
+
init_dist();
|
|
2842
|
+
});
|
|
2843
|
+
|
|
2624
2844
|
// src/pickup-router.ts
|
|
2625
2845
|
function isStageCard(card, columnName, playbooks) {
|
|
2626
2846
|
if (!playbooks.enabled)
|
|
@@ -2668,10 +2888,11 @@ class BudgetGuard {
|
|
|
2668
2888
|
return { allow: true };
|
|
2669
2889
|
}
|
|
2670
2890
|
}
|
|
2671
|
-
function buildGaveUpComment(maxAttempts, failures) {
|
|
2891
|
+
function buildGaveUpComment(maxAttempts, failures, pauseEnabled) {
|
|
2892
|
+
const wayBackIn = pauseEnabled ? "Continue to grant a fresh attempt, or reassign the card." : "Reassign the card to grant a fresh attempt.";
|
|
2672
2893
|
const lines = [
|
|
2673
|
-
"**
|
|
2674
|
-
`Stopped after ${maxAttempts} failed attempt${maxAttempts === 1 ? "" : "s"}.
|
|
2894
|
+
"**Out of attempts — over to you.**",
|
|
2895
|
+
`Stopped after ${maxAttempts} failed attempt${maxAttempts === 1 ? "" : "s"}. ${wayBackIn}`
|
|
2675
2896
|
];
|
|
2676
2897
|
if (failures.length > 0) {
|
|
2677
2898
|
lines.push("", "Recent failures:");
|
|
@@ -2689,6 +2910,89 @@ function buildGaveUpComment(maxAttempts, failures) {
|
|
|
2689
2910
|
`);
|
|
2690
2911
|
}
|
|
2691
2912
|
|
|
2913
|
+
// src/budget-pause.ts
|
|
2914
|
+
function classifyRunExit(x) {
|
|
2915
|
+
if (x.exitCode === 0)
|
|
2916
|
+
return null;
|
|
2917
|
+
if (x.timedOut)
|
|
2918
|
+
return "timeout";
|
|
2919
|
+
if (x.maxTurns <= 0)
|
|
2920
|
+
return null;
|
|
2921
|
+
if (x.stopReason === "error_max_turns")
|
|
2922
|
+
return "max_turns";
|
|
2923
|
+
if (x.numTurns >= x.maxTurns)
|
|
2924
|
+
return "max_turns";
|
|
2925
|
+
return null;
|
|
2926
|
+
}
|
|
2927
|
+
function computeDecisionDeadline(waitHours, now = Date.now()) {
|
|
2928
|
+
return now + waitHours * 60 * 60 * 1000;
|
|
2929
|
+
}
|
|
2930
|
+
function humanDuration(ms) {
|
|
2931
|
+
const total = Math.round(ms / 1000);
|
|
2932
|
+
const m = Math.floor(total / 60);
|
|
2933
|
+
const s = total % 60;
|
|
2934
|
+
return m > 0 ? `${m}m ${s}s` : `${s}s`;
|
|
2935
|
+
}
|
|
2936
|
+
function formatBudgetComment(i) {
|
|
2937
|
+
const lines = [HEADLINE[i.trigger](i)];
|
|
2938
|
+
if (i.trigger !== "max_attempts") {
|
|
2939
|
+
lines.push(`${i.toolCalls} tool calls · ${humanDuration(i.durationMs)} · $${i.costUsd.toFixed(2)}.`);
|
|
2940
|
+
if (i.lastAction)
|
|
2941
|
+
lines.push(`Last action: ${i.lastAction}.`);
|
|
2942
|
+
}
|
|
2943
|
+
lines.push(i.branchName ? `The work is parked, not lost: branch \`${i.branchName}\`, worktree kept.` : "The work is parked, not lost.");
|
|
2944
|
+
lines.push(i.trigger === "max_attempts" ? `Continue to start a fresh attempt, or stop and I'll hand the card back.` : `Continue to pick up from where I stopped with a fresh turn budget, or stop and I'll hand the card back.`);
|
|
2945
|
+
lines.push(`This decision expires in ${i.waitHours}h.`);
|
|
2946
|
+
return lines.join(`
|
|
2947
|
+
`);
|
|
2948
|
+
}
|
|
2949
|
+
function formatExpiredAttemptCapComment() {
|
|
2950
|
+
return [
|
|
2951
|
+
"The decision window closed with no answer.",
|
|
2952
|
+
"Nothing was running, so there is nothing to resume — reassign the card when you want me to try again."
|
|
2953
|
+
].join(`
|
|
2954
|
+
`);
|
|
2955
|
+
}
|
|
2956
|
+
function formatExpiredParkComment(i) {
|
|
2957
|
+
const lines = ["The decision window closed with no answer."];
|
|
2958
|
+
lines.push(i.branchName ? `The work is not lost: branch \`${i.branchName}\` is on origin.` : "The work is not lost, though no branch was recorded for this run.");
|
|
2959
|
+
if (i.cliSessionId) {
|
|
2960
|
+
lines.push(`CLI session \`${i.cliSessionId}\` is still available for a manual \`claude --resume\`.`);
|
|
2961
|
+
}
|
|
2962
|
+
return lines.join(`
|
|
2963
|
+
`);
|
|
2964
|
+
}
|
|
2965
|
+
function formatResumeConflictComment(i) {
|
|
2966
|
+
const lines = [
|
|
2967
|
+
"I could not pick this back up: another driver holds the agent session on this card.",
|
|
2968
|
+
`The server said: ${i.holderMessage}`,
|
|
2969
|
+
"Nothing was thrown away — the run is still parked exactly where it stopped."
|
|
2970
|
+
];
|
|
2971
|
+
lines.push(i.branchName ? `Branch \`${i.branchName}\` and its worktree are kept.` : "The worktree is kept, though no branch was recorded for this run.");
|
|
2972
|
+
if (i.cliSessionId) {
|
|
2973
|
+
lines.push(`CLI session \`${i.cliSessionId}\` is still resumable, here or by hand.`);
|
|
2974
|
+
}
|
|
2975
|
+
lines.push(`End the other session and press Continue again. I'll hold for ${i.waitHours}h, then hand the card back.`);
|
|
2976
|
+
return lines.join(`
|
|
2977
|
+
`);
|
|
2978
|
+
}
|
|
2979
|
+
var BudgetPauseError, MAX_GRANTED_TURNS = 1000, HEADLINE;
|
|
2980
|
+
var init_budget_pause = __esm(() => {
|
|
2981
|
+
BudgetPauseError = class BudgetPauseError extends Error {
|
|
2982
|
+
trigger;
|
|
2983
|
+
constructor(trigger) {
|
|
2984
|
+
super(`budget limit reached: ${trigger}`);
|
|
2985
|
+
this.trigger = trigger;
|
|
2986
|
+
this.name = "BudgetPauseError";
|
|
2987
|
+
}
|
|
2988
|
+
};
|
|
2989
|
+
HEADLINE = {
|
|
2990
|
+
max_turns: (i) => `Turn budget exhausted — ${i.maxTurns} of ${i.maxTurns} turns.`,
|
|
2991
|
+
timeout: (i) => `Wall-clock budget exhausted after ${humanDuration(i.durationMs)}.`,
|
|
2992
|
+
max_attempts: () => "Attempt budget exhausted — I have used every attempt on this card."
|
|
2993
|
+
};
|
|
2994
|
+
});
|
|
2995
|
+
|
|
2692
2996
|
// src/queue.ts
|
|
2693
2997
|
import { log as log7 } from "@gethmy/harness";
|
|
2694
2998
|
|
|
@@ -3078,7 +3382,7 @@ var init_episode_writer = __esm(() => {
|
|
|
3078
3382
|
});
|
|
3079
3383
|
|
|
3080
3384
|
// src/completion.ts
|
|
3081
|
-
import { execFileSync as
|
|
3385
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
3082
3386
|
import {
|
|
3083
3387
|
attemptAutoFix,
|
|
3084
3388
|
captureDiffStat,
|
|
@@ -3106,6 +3410,9 @@ function describeNoCommitFailure(numTurns, maxTurns) {
|
|
|
3106
3410
|
failureSummary: maxTurnsExhausted ? `Agent exhausted its ${maxTurns}-turn budget without committing any changes` : "Agent finished without making any changes to commit"
|
|
3107
3411
|
};
|
|
3108
3412
|
}
|
|
3413
|
+
function noCommitOutcome(maxTurnsExhausted, pauseEnabled) {
|
|
3414
|
+
return maxTurnsExhausted && pauseEnabled ? "park" : "fail";
|
|
3415
|
+
}
|
|
3109
3416
|
function buildTokenPayload(stats) {
|
|
3110
3417
|
if (!stats?.cost)
|
|
3111
3418
|
return {};
|
|
@@ -3119,7 +3426,7 @@ function buildTokenPayload(stats) {
|
|
|
3119
3426
|
numTurns: stats.cost.numTurns
|
|
3120
3427
|
};
|
|
3121
3428
|
}
|
|
3122
|
-
async function runCompletion(client, card, branchName, worktreePath, config, workerId, sessionStats, workspaceId, agentSessionId, stateStore, onMovedToCompletion, onBeforeWorktreeCleanup) {
|
|
3429
|
+
async function runCompletion(client, card, branchName, worktreePath, config, workerId, sessionStats, workspaceId, agentSessionId, stateStore, onMovedToCompletion, onBeforeWorktreeCleanup, runBaselineSha, effectiveMaxTurns) {
|
|
3123
3430
|
let verificationResult = {
|
|
3124
3431
|
passed: true,
|
|
3125
3432
|
buildErrors: [],
|
|
@@ -3132,9 +3439,13 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
3132
3439
|
runFormatFix(worktreePath, config.verification.timeout, workerId);
|
|
3133
3440
|
}
|
|
3134
3441
|
commitUncommittedChanges(worktreePath, card);
|
|
3135
|
-
const hasCommits = checkHasCommits(worktreePath, config.worktree.baseBranch);
|
|
3442
|
+
const hasCommits = checkHasCommits(worktreePath, config.worktree.baseBranch, runBaselineSha);
|
|
3136
3443
|
if (!hasCommits) {
|
|
3137
|
-
const { maxTurnsExhausted, failureSummary } = describeNoCommitFailure(sessionStats?.cost?.numTurns ?? 0, config.claude.maxTurns);
|
|
3444
|
+
const { maxTurnsExhausted, failureSummary } = describeNoCommitFailure(sessionStats?.cost?.numTurns ?? 0, effectiveMaxTurns ?? config.claude.maxTurns);
|
|
3445
|
+
if (noCommitOutcome(maxTurnsExhausted, config.budget.pause.enabled) === "park") {
|
|
3446
|
+
log9.warn(TAG9, `No commits on branch ${branchName} — ${failureSummary}; parking for a decision`);
|
|
3447
|
+
return "park";
|
|
3448
|
+
}
|
|
3138
3449
|
log9.warn(TAG9, `No commits on branch ${branchName} — ${failureSummary}; counting as a failed attempt`);
|
|
3139
3450
|
await moveCardToColumn(client, card, config.pickupColumns[0] ?? "To Do");
|
|
3140
3451
|
await client.endAgentSession(card.id, {
|
|
@@ -3320,7 +3631,7 @@ function buildVerificationFailureSummary(result, autoFixAttempts) {
|
|
|
3320
3631
|
}
|
|
3321
3632
|
function readHeadSha(worktreePath) {
|
|
3322
3633
|
try {
|
|
3323
|
-
return
|
|
3634
|
+
return execFileSync3("git", ["rev-parse", "HEAD"], {
|
|
3324
3635
|
cwd: worktreePath,
|
|
3325
3636
|
encoding: "utf-8"
|
|
3326
3637
|
}).trim();
|
|
@@ -3331,7 +3642,7 @@ function readHeadSha(worktreePath) {
|
|
|
3331
3642
|
function commitUncommittedChanges(worktreePath, card) {
|
|
3332
3643
|
let status = "";
|
|
3333
3644
|
try {
|
|
3334
|
-
status =
|
|
3645
|
+
status = execFileSync3("git", ["status", "--porcelain"], {
|
|
3335
3646
|
cwd: worktreePath,
|
|
3336
3647
|
encoding: "utf-8"
|
|
3337
3648
|
}).trim();
|
|
@@ -3344,11 +3655,11 @@ function commitUncommittedChanges(worktreePath, card) {
|
|
|
3344
3655
|
const title = card.title?.trim() || "agent changes";
|
|
3345
3656
|
const message = `#${card.short_id} ${title}`;
|
|
3346
3657
|
try {
|
|
3347
|
-
|
|
3658
|
+
execFileSync3("git", ["add", "-A"], {
|
|
3348
3659
|
cwd: worktreePath,
|
|
3349
3660
|
encoding: "utf-8"
|
|
3350
3661
|
});
|
|
3351
|
-
|
|
3662
|
+
execFileSync3("git", ["commit", "-m", message], {
|
|
3352
3663
|
cwd: worktreePath,
|
|
3353
3664
|
encoding: "utf-8"
|
|
3354
3665
|
});
|
|
@@ -3359,9 +3670,17 @@ function commitUncommittedChanges(worktreePath, card) {
|
|
|
3359
3670
|
return false;
|
|
3360
3671
|
}
|
|
3361
3672
|
}
|
|
3362
|
-
function checkHasCommits(worktreePath, baseBranch) {
|
|
3673
|
+
function checkHasCommits(worktreePath, baseBranch, baselineSha, gitImpl = (args, cwd) => execFileSync3("git", args, { cwd, encoding: "utf-8" })) {
|
|
3674
|
+
if (baselineSha) {
|
|
3675
|
+
try {
|
|
3676
|
+
gitImpl(["merge-base", "--is-ancestor", baselineSha, "HEAD"], worktreePath);
|
|
3677
|
+
} catch {
|
|
3678
|
+
return false;
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
const range = baselineSha ? `${baselineSha}..HEAD` : `origin/${baseBranch}..HEAD`;
|
|
3363
3682
|
try {
|
|
3364
|
-
const count =
|
|
3683
|
+
const count = gitImpl(["rev-list", "--count", range], worktreePath).trim();
|
|
3365
3684
|
return parseInt(count, 10) > 0;
|
|
3366
3685
|
} catch {
|
|
3367
3686
|
return false;
|
|
@@ -3370,7 +3689,7 @@ function checkHasCommits(worktreePath, baseBranch) {
|
|
|
3370
3689
|
async function postSummary(client, card, branchName, worktreePath, prUrl, baseBranch, sessionStats) {
|
|
3371
3690
|
let commitLog = "";
|
|
3372
3691
|
try {
|
|
3373
|
-
commitLog =
|
|
3692
|
+
commitLog = execFileSync3("git", ["log", "--oneline", `origin/${baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
3374
3693
|
} catch {}
|
|
3375
3694
|
const SUMMARY_MARKER = `---
|
|
3376
3695
|
**Agent completed**`;
|
|
@@ -3531,6 +3850,9 @@ class ProgressTracker {
|
|
|
3531
3850
|
this.heartbeatTimer = null;
|
|
3532
3851
|
}
|
|
3533
3852
|
}
|
|
3853
|
+
get isStopped() {
|
|
3854
|
+
return this.stopped;
|
|
3855
|
+
}
|
|
3534
3856
|
get stats() {
|
|
3535
3857
|
return {
|
|
3536
3858
|
filesEdited: this.filesEdited.size,
|
|
@@ -3641,6 +3963,9 @@ class ProgressTracker {
|
|
|
3641
3963
|
this.scheduleUpdate(this.currentTaskLabel());
|
|
3642
3964
|
}
|
|
3643
3965
|
}
|
|
3966
|
+
get lastActionSummary() {
|
|
3967
|
+
return this.lastAction || null;
|
|
3968
|
+
}
|
|
3644
3969
|
currentTaskLabel() {
|
|
3645
3970
|
if (this.lastAction)
|
|
3646
3971
|
return this.lastAction;
|
|
@@ -3798,89 +4123,280 @@ var init_progress_tracker = __esm(() => {
|
|
|
3798
4123
|
};
|
|
3799
4124
|
});
|
|
3800
4125
|
|
|
3801
|
-
// src/
|
|
3802
|
-
import {
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
getBranchWebUrl as getBranchWebUrl2,
|
|
3809
|
-
getHeadSha,
|
|
3810
|
-
log as log11,
|
|
3811
|
-
pushBranch as pushBranch2,
|
|
3812
|
-
renameRemoteBranch,
|
|
3813
|
-
upsertReviewedSha
|
|
3814
|
-
} from "@gethmy/harness";
|
|
3815
|
-
function clampSubtaskTitle(title) {
|
|
3816
|
-
return title.length > MAX_SUBTASK_TITLE ? `${title.slice(0, MAX_SUBTASK_TITLE - 3)}...` : title;
|
|
4126
|
+
// src/prompt.ts
|
|
4127
|
+
import { log as log11 } from "@gethmy/harness";
|
|
4128
|
+
function buildSteeringPrompt(messages) {
|
|
4129
|
+
if (messages.length === 1)
|
|
4130
|
+
return messages[0];
|
|
4131
|
+
return messages.map((m, i) => `${i + 1}. ${m}`).join(`
|
|
4132
|
+
`);
|
|
3817
4133
|
}
|
|
3818
|
-
function
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
${f.
|
|
4134
|
+
function renderPreviousAttemptsSection(failures) {
|
|
4135
|
+
if (failures.length === 0)
|
|
4136
|
+
return "";
|
|
4137
|
+
const lines = failures.map((f) => {
|
|
4138
|
+
const tag = f.reason ? `[${f.reason}] ` : "";
|
|
4139
|
+
return `- ${tag}${f.summary}`;
|
|
4140
|
+
});
|
|
4141
|
+
return [
|
|
4142
|
+
"## Previous attempt feedback",
|
|
4143
|
+
"This is a re-attempt on the branch your last run already pushed — build on that existing work and FIX the issues below. Do NOT reimplement from scratch or revert the prior commits.",
|
|
4144
|
+
...lines
|
|
4145
|
+
].join(`
|
|
4146
|
+
`);
|
|
3823
4147
|
}
|
|
3824
|
-
function
|
|
3825
|
-
const
|
|
3826
|
-
const
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
}
|
|
4148
|
+
async function buildPrompt(enriched, branchName, worktreePath, client, workspaceId, projectId) {
|
|
4149
|
+
const { card } = enriched;
|
|
4150
|
+
const [pastEpisodesSection, referenceSection] = await Promise.all([
|
|
4151
|
+
renderPastEpisodesSection(client, card.title, card.description ?? "", workspaceId, projectId),
|
|
4152
|
+
renderReferenceSection(client, card.title, card.description ?? "", workspaceId, projectId)
|
|
4153
|
+
]);
|
|
4154
|
+
try {
|
|
4155
|
+
const result = await client.generateCardPrompt({
|
|
4156
|
+
cardId: card.id,
|
|
4157
|
+
workspaceId,
|
|
4158
|
+
projectId,
|
|
4159
|
+
variant: "execute",
|
|
4160
|
+
customConstraints: `You are working in a git worktree at \`${worktreePath}\` on branch \`${branchName}\`.
|
|
4161
|
+
Do NOT push to main. All your work stays on \`${branchName}\`.
|
|
4162
|
+
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
|
+
});
|
|
4164
|
+
log11.info(TAG11, `Generated prompt for #${card.short_id} — ${result.contextSummary.memoryCount} memories, ${result.tokenEstimate} tokens`);
|
|
4165
|
+
return result.prompt + pastEpisodesSection + referenceSection;
|
|
4166
|
+
} catch (err) {
|
|
4167
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4168
|
+
log11.warn(TAG11, `Failed to generate prompt via API, using fallback: ${msg}`);
|
|
4169
|
+
const commentsSection = await renderCommentsSection(client, card.id);
|
|
4170
|
+
return buildFallbackPrompt(enriched, branchName, worktreePath) + commentsSection + pastEpisodesSection + referenceSection;
|
|
3845
4171
|
}
|
|
3846
|
-
if (current.length > header.length)
|
|
3847
|
-
bodies.push(current);
|
|
3848
|
-
return bodies;
|
|
3849
4172
|
}
|
|
3850
|
-
function
|
|
3851
|
-
|
|
4173
|
+
async function renderCommentsSection(client, cardId) {
|
|
4174
|
+
try {
|
|
4175
|
+
const { comments } = await client.request("GET", `/cards/${encodeURIComponent(cardId)}/comments?limit=200&order=desc`);
|
|
4176
|
+
if (!Array.isArray(comments) || comments.length === 0)
|
|
4177
|
+
return "";
|
|
4178
|
+
const section = serializeCommentThread(comments, {
|
|
4179
|
+
heading: "Comments",
|
|
4180
|
+
maxComments: 40
|
|
4181
|
+
});
|
|
4182
|
+
return section ? `
|
|
4183
|
+
|
|
4184
|
+
${section}` : "";
|
|
4185
|
+
} catch (err) {
|
|
4186
|
+
log11.warn(TAG11, "comment-thread fetch failed", {
|
|
4187
|
+
event: "comment_fetch_failed",
|
|
4188
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4189
|
+
});
|
|
3852
4190
|
return "";
|
|
3853
|
-
const counts = { pass: 0, partial: 0, fail: 0, unverifiable: 0 };
|
|
3854
|
-
for (const c of checks)
|
|
3855
|
-
counts[c.status]++;
|
|
3856
|
-
const flagged = [];
|
|
3857
|
-
if (counts.fail)
|
|
3858
|
-
flagged.push(`${counts.fail} fail`);
|
|
3859
|
-
if (counts.partial)
|
|
3860
|
-
flagged.push(`${counts.partial} partial`);
|
|
3861
|
-
if (counts.unverifiable)
|
|
3862
|
-
flagged.push(`${counts.unverifiable} unverifiable`);
|
|
3863
|
-
const detail = flagged.length ? ` (${flagged.join(", ")})` : "";
|
|
3864
|
-
return `Acceptance: ${counts.pass}/${checks.length} pass${detail}`;
|
|
3865
|
-
}
|
|
3866
|
-
async function persistReviewedSha(client, card, worktreePath) {
|
|
3867
|
-
const headSha = getHeadSha(worktreePath);
|
|
3868
|
-
if (!headSha)
|
|
3869
|
-
return;
|
|
3870
|
-
const { card: latest } = await client.getCard(card.id);
|
|
3871
|
-
const desc = latest.description || "";
|
|
3872
|
-
const next = upsertReviewedSha(desc, headSha);
|
|
3873
|
-
if (next !== desc) {
|
|
3874
|
-
await client.updateCard(card.id, { description: next });
|
|
3875
4191
|
}
|
|
3876
4192
|
}
|
|
3877
|
-
function
|
|
4193
|
+
async function renderPastEpisodesSection(client, title, description, workspaceId, projectId) {
|
|
4194
|
+
if (!projectId)
|
|
4195
|
+
return "";
|
|
3878
4196
|
try {
|
|
3879
|
-
const
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
4197
|
+
const query = `${title}
|
|
4198
|
+
${description}`.trim();
|
|
4199
|
+
const { entities } = await client.harmonyRecall({
|
|
4200
|
+
workspaceId,
|
|
4201
|
+
projectId,
|
|
4202
|
+
query,
|
|
4203
|
+
type: ["solution", "error"],
|
|
4204
|
+
memory_tier: "episode",
|
|
4205
|
+
scope: "project",
|
|
4206
|
+
topK: 3,
|
|
4207
|
+
includeEpisodes: true,
|
|
4208
|
+
consumer: "agent-prompt"
|
|
4209
|
+
});
|
|
4210
|
+
if (entities.length === 0)
|
|
4211
|
+
return "";
|
|
4212
|
+
const bullets = entities.map((entity) => {
|
|
4213
|
+
const e = entity;
|
|
4214
|
+
const meta = e.metadata ?? {};
|
|
4215
|
+
const outcomeTag = meta.outcome ? `[${meta.outcome}]` : "[?]";
|
|
4216
|
+
const approach = meta.approach_summary ?? "";
|
|
4217
|
+
const lines = [
|
|
4218
|
+
`- ${outcomeTag} ${e.title ?? "(untitled episode)"}`,
|
|
4219
|
+
` Approach: ${approach}`
|
|
4220
|
+
];
|
|
4221
|
+
if (meta.key_insight)
|
|
4222
|
+
lines.push(` Key insight: ${meta.key_insight}`);
|
|
4223
|
+
if (meta.changed_files && meta.changed_files.length > 0) {
|
|
4224
|
+
const shown = meta.changed_files.slice(0, 8);
|
|
4225
|
+
const extra = meta.changed_files.length - shown.length;
|
|
4226
|
+
const suffix = extra > 0 ? ` (+${extra} more)` : "";
|
|
4227
|
+
lines.push(` Changed files: ${shown.join(", ")}${suffix}`);
|
|
4228
|
+
}
|
|
4229
|
+
return lines.join(`
|
|
4230
|
+
`);
|
|
4231
|
+
}).join(`
|
|
4232
|
+
`);
|
|
4233
|
+
return `
|
|
4234
|
+
|
|
4235
|
+
## Similar past tasks
|
|
4236
|
+
${bullets}`;
|
|
4237
|
+
} catch (err) {
|
|
4238
|
+
log11.warn(TAG11, "past-episodes recall failed", {
|
|
4239
|
+
event: "episode_recall_failed",
|
|
4240
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4241
|
+
});
|
|
4242
|
+
return "";
|
|
4243
|
+
}
|
|
4244
|
+
}
|
|
4245
|
+
async function renderReferenceSection(client, title, description, workspaceId, projectId) {
|
|
4246
|
+
try {
|
|
4247
|
+
const query = `${title}
|
|
4248
|
+
${description}`.trim();
|
|
4249
|
+
const { entities } = await client.harmonyRecall({
|
|
4250
|
+
workspaceId,
|
|
4251
|
+
projectId,
|
|
4252
|
+
query,
|
|
4253
|
+
memory_tier: "reference",
|
|
4254
|
+
topK: 5,
|
|
4255
|
+
consumer: "agent-prompt"
|
|
4256
|
+
});
|
|
4257
|
+
if (entities.length === 0)
|
|
4258
|
+
return "";
|
|
4259
|
+
const bullets = entities.map((entity) => {
|
|
4260
|
+
const e = entity;
|
|
4261
|
+
const content = (e.content ?? "").slice(0, 300);
|
|
4262
|
+
return `- ${e.title ?? "(untitled)"}
|
|
4263
|
+
${content}`;
|
|
4264
|
+
}).join(`
|
|
4265
|
+
`);
|
|
4266
|
+
return `
|
|
4267
|
+
|
|
4268
|
+
## How we work here
|
|
4269
|
+
${bullets}`;
|
|
4270
|
+
} catch (err) {
|
|
4271
|
+
log11.warn(TAG11, "reference recall failed", {
|
|
4272
|
+
event: "reference_recall_failed",
|
|
4273
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4274
|
+
});
|
|
4275
|
+
return "";
|
|
4276
|
+
}
|
|
4277
|
+
}
|
|
4278
|
+
function buildFallbackPrompt(enriched, branchName, worktreePath) {
|
|
4279
|
+
const { card, column, labels, subtasks } = enriched;
|
|
4280
|
+
const labelStr = labels.length > 0 ? labels.map((l) => l.name).join(", ") : "none";
|
|
4281
|
+
const subtaskStr = subtasks.length > 0 ? subtasks.map((s) => `- [${s.completed ? "x" : " "}] ${s.title}`).join(`
|
|
4282
|
+
`) : "No subtasks defined.";
|
|
4283
|
+
const description = card.description?.trim() || "No description provided.";
|
|
4284
|
+
return `You are an AI agent working on a task from the Harmony project board.
|
|
4285
|
+
|
|
4286
|
+
## Card: #${card.short_id} - ${card.title}
|
|
4287
|
+
**Labels**: ${labelStr}
|
|
4288
|
+
**Column**: ${column.name}
|
|
4289
|
+
**Priority**: ${card.priority}
|
|
4290
|
+
|
|
4291
|
+
## Description
|
|
4292
|
+
${description}
|
|
4293
|
+
|
|
4294
|
+
## Subtasks
|
|
4295
|
+
${subtaskStr}
|
|
4296
|
+
|
|
4297
|
+
## Instructions
|
|
4298
|
+
1. Read the codebase and understand the context needed for this task
|
|
4299
|
+
2. Report progress via harmony_update_agent_progress at key milestones:
|
|
4300
|
+
- After reading codebase and forming a plan (~20%)
|
|
4301
|
+
- After each major implementation step (~30-60%)
|
|
4302
|
+
- After completing each subtask (also toggle via harmony_toggle_subtask)
|
|
4303
|
+
- Before committing (~65%)
|
|
4304
|
+
Include a brief currentTask description.
|
|
4305
|
+
3. Implement the changes on branch \`${branchName}\`
|
|
4306
|
+
4. Commit your work with clear, descriptive commit messages
|
|
4307
|
+
5. When the work is committed, STOP. The daemon owns the run lifecycle: 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 — those tools are disabled for this run.
|
|
4308
|
+
|
|
4309
|
+
You are working in a git worktree at \`${worktreePath}\` on branch \`${branchName}\`.
|
|
4310
|
+
Do NOT push to main. All your work stays on \`${branchName}\`.`;
|
|
4311
|
+
}
|
|
4312
|
+
var TAG11 = "prompt";
|
|
4313
|
+
var init_prompt = __esm(() => {
|
|
4314
|
+
init_dist();
|
|
4315
|
+
});
|
|
4316
|
+
|
|
4317
|
+
// src/review-completion.ts
|
|
4318
|
+
import { readFileSync as readFileSync2, statSync } from "node:fs";
|
|
4319
|
+
import {
|
|
4320
|
+
cleanupWorktree as cleanupWorktree2,
|
|
4321
|
+
createPullRequest as createPullRequest2,
|
|
4322
|
+
detectGitProvider as detectGitProvider4,
|
|
4323
|
+
extractPrUrl as extractPrUrl2,
|
|
4324
|
+
getBranchWebUrl as getBranchWebUrl2,
|
|
4325
|
+
getHeadSha,
|
|
4326
|
+
log as log12,
|
|
4327
|
+
pushBranch as pushBranch2,
|
|
4328
|
+
renameRemoteBranch,
|
|
4329
|
+
upsertReviewedSha
|
|
4330
|
+
} from "@gethmy/harness";
|
|
4331
|
+
function clampSubtaskTitle(title) {
|
|
4332
|
+
return title.length > MAX_SUBTASK_TITLE ? `${title.slice(0, MAX_SUBTASK_TITLE - 3)}...` : title;
|
|
4333
|
+
}
|
|
4334
|
+
function renderFindingBlock(f) {
|
|
4335
|
+
const locationLine = f.location ? `
|
|
4336
|
+
Location: ${f.location}` : "";
|
|
4337
|
+
return `**[${f.severity}] ${f.title}**
|
|
4338
|
+
${f.description}${locationLine}`;
|
|
4339
|
+
}
|
|
4340
|
+
function buildFindingComments(findings) {
|
|
4341
|
+
const header = `**Review findings — ${findings.length} blocking issue(s) to resolve.**`;
|
|
4342
|
+
const sep = `
|
|
4343
|
+
|
|
4344
|
+
`;
|
|
4345
|
+
const bodies = [];
|
|
4346
|
+
let current = header;
|
|
4347
|
+
for (const f of findings) {
|
|
4348
|
+
let block = renderFindingBlock(f);
|
|
4349
|
+
const maxBlock = COMMENT_BODY_BUDGET - header.length - sep.length;
|
|
4350
|
+
if (block.length > maxBlock) {
|
|
4351
|
+
const suffix = `
|
|
4352
|
+
…[truncated]`;
|
|
4353
|
+
block = `${block.slice(0, Math.max(0, maxBlock - suffix.length))}${suffix}`;
|
|
4354
|
+
}
|
|
4355
|
+
if (current.length + sep.length + block.length > COMMENT_BODY_BUDGET) {
|
|
4356
|
+
bodies.push(current);
|
|
4357
|
+
current = `${header}${sep}${block}`;
|
|
4358
|
+
} else {
|
|
4359
|
+
current = `${current}${sep}${block}`;
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
if (current.length > header.length)
|
|
4363
|
+
bodies.push(current);
|
|
4364
|
+
return bodies;
|
|
4365
|
+
}
|
|
4366
|
+
function acceptanceSummaryLine(checks) {
|
|
4367
|
+
if (!checks || checks.length === 0)
|
|
4368
|
+
return "";
|
|
4369
|
+
const counts = { pass: 0, partial: 0, fail: 0, unverifiable: 0 };
|
|
4370
|
+
for (const c of checks)
|
|
4371
|
+
counts[c.status]++;
|
|
4372
|
+
const flagged = [];
|
|
4373
|
+
if (counts.fail)
|
|
4374
|
+
flagged.push(`${counts.fail} fail`);
|
|
4375
|
+
if (counts.partial)
|
|
4376
|
+
flagged.push(`${counts.partial} partial`);
|
|
4377
|
+
if (counts.unverifiable)
|
|
4378
|
+
flagged.push(`${counts.unverifiable} unverifiable`);
|
|
4379
|
+
const detail = flagged.length ? ` (${flagged.join(", ")})` : "";
|
|
4380
|
+
return `Acceptance: ${counts.pass}/${checks.length} pass${detail}`;
|
|
4381
|
+
}
|
|
4382
|
+
async function persistReviewedSha(client, card, worktreePath) {
|
|
4383
|
+
const headSha = getHeadSha(worktreePath);
|
|
4384
|
+
if (!headSha)
|
|
4385
|
+
return;
|
|
4386
|
+
const { card: latest } = await client.getCard(card.id);
|
|
4387
|
+
const desc = latest.description || "";
|
|
4388
|
+
const next = upsertReviewedSha(desc, headSha);
|
|
4389
|
+
if (next !== desc) {
|
|
4390
|
+
await client.updateCard(card.id, { description: next });
|
|
4391
|
+
}
|
|
4392
|
+
}
|
|
4393
|
+
function tailRunLog(path, bytes = RUN_LOG_TAIL_BYTES) {
|
|
4394
|
+
try {
|
|
4395
|
+
const size = statSync(path).size;
|
|
4396
|
+
if (size === 0)
|
|
4397
|
+
return null;
|
|
4398
|
+
const start = Math.max(0, size - bytes);
|
|
4399
|
+
const buf = readFileSync2(path);
|
|
3884
4400
|
return buf.subarray(start).toString("utf-8");
|
|
3885
4401
|
} catch {
|
|
3886
4402
|
return null;
|
|
@@ -3932,7 +4448,7 @@ function parseReviewOutput(stdout) {
|
|
|
3932
4448
|
try {
|
|
3933
4449
|
const parsed = JSON.parse(raw);
|
|
3934
4450
|
if (parsed && typeof parsed === "object" && "verdict" in parsed) {
|
|
3935
|
-
|
|
4451
|
+
log12.debug(TAG12, "Parsed review output from fenced JSON block");
|
|
3936
4452
|
return extractResult(parsed);
|
|
3937
4453
|
}
|
|
3938
4454
|
} catch {}
|
|
@@ -3958,21 +4474,21 @@ function parseReviewOutput(stdout) {
|
|
|
3958
4474
|
try {
|
|
3959
4475
|
const parsed = JSON.parse(candidates[i]);
|
|
3960
4476
|
if (parsed && typeof parsed === "object" && "verdict" in parsed) {
|
|
3961
|
-
|
|
4477
|
+
log12.debug(TAG12, "Parsed review output from raw JSON object");
|
|
3962
4478
|
return extractResult(parsed);
|
|
3963
4479
|
}
|
|
3964
4480
|
} catch {}
|
|
3965
4481
|
}
|
|
3966
4482
|
const verdictMatch = stdout.match(/"verdict"\s*:\s*"(approved|rejected)"/i);
|
|
3967
4483
|
if (verdictMatch) {
|
|
3968
|
-
|
|
4484
|
+
log12.warn(TAG12, `Parsed verdict via regex fallback — findings lost (${verdictMatch[1]})`);
|
|
3969
4485
|
return {
|
|
3970
4486
|
verdict: verdictMatch[1].toLowerCase(),
|
|
3971
4487
|
summary: "Parsed via regex fallback — original JSON was malformed. Check run log.",
|
|
3972
4488
|
findings: []
|
|
3973
4489
|
};
|
|
3974
4490
|
}
|
|
3975
|
-
|
|
4491
|
+
log12.warn(TAG12, "Failed to parse review JSON output — returning error verdict (card stays in Review)");
|
|
3976
4492
|
return {
|
|
3977
4493
|
verdict: "error",
|
|
3978
4494
|
summary: stdout.slice(0, 500),
|
|
@@ -4005,7 +4521,7 @@ async function postReviewComment(client, card, commentType, body) {
|
|
|
4005
4521
|
try {
|
|
4006
4522
|
await client.addComment(card.id, body, { commentType });
|
|
4007
4523
|
} catch (err) {
|
|
4008
|
-
|
|
4524
|
+
log12.error(TAG12, `Failed to post review comment to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
4009
4525
|
}
|
|
4010
4526
|
}
|
|
4011
4527
|
async function runReviewCompletion(client, card, result, config, worktreePath, branchName, sessionStats, runLogPath, workspaceId, agentSessionId, stateStore, resolvedFromPrUrl) {
|
|
@@ -4019,11 +4535,11 @@ async function runReviewCompletion(client, card, result, config, worktreePath, b
|
|
|
4019
4535
|
const currentCycle = getReviewCycle(freshDesc) + 1;
|
|
4020
4536
|
const maxCycles = config.review.maxReviewCycles;
|
|
4021
4537
|
if (result.verdict === "error") {
|
|
4022
|
-
|
|
4538
|
+
log12.warn(TAG12, `#${card.short_id} review output unparseable — labelling "${NEED_REVIEW_LABEL}" for manual inspection`);
|
|
4023
4539
|
try {
|
|
4024
4540
|
await addLabelByName(client, card, NEED_REVIEW_LABEL, NEED_REVIEW_LABEL_COLOR);
|
|
4025
4541
|
} catch (err) {
|
|
4026
|
-
|
|
4542
|
+
log12.warn(TAG12, `Failed to add "${NEED_REVIEW_LABEL}" label: ${err instanceof Error ? err.message : err}`);
|
|
4027
4543
|
}
|
|
4028
4544
|
if (config.review.postFindings) {
|
|
4029
4545
|
const rawTail = runLogPath ? tailRunLog(runLogPath) : null;
|
|
@@ -4066,7 +4582,7 @@ ${runLogTail}
|
|
|
4066
4582
|
renameRemoteBranch(branchName, newRef, worktreePath);
|
|
4067
4583
|
approvedBranch = newRef;
|
|
4068
4584
|
} catch (err) {
|
|
4069
|
-
|
|
4585
|
+
log12.warn(TAG12, `Branch rename failed (continuing on ${branchName}): ${err instanceof Error ? err.message : err}`);
|
|
4070
4586
|
}
|
|
4071
4587
|
}
|
|
4072
4588
|
if (config.review.createPR && approvedBranch) {
|
|
@@ -4087,14 +4603,14 @@ ${runLogTail}
|
|
|
4087
4603
|
});
|
|
4088
4604
|
}
|
|
4089
4605
|
} catch (err) {
|
|
4090
|
-
|
|
4606
|
+
log12.warn(TAG12, `Failed to persist PR URL to #${card.short_id} description: ${err instanceof Error ? err.message : err}`);
|
|
4091
4607
|
}
|
|
4092
4608
|
}
|
|
4093
4609
|
if (branchName) {
|
|
4094
4610
|
try {
|
|
4095
4611
|
await persistReviewedSha(client, card, worktreePath);
|
|
4096
4612
|
} catch (err) {
|
|
4097
|
-
|
|
4613
|
+
log12.warn(TAG12, `Failed to persist Reviewed-SHA to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
4098
4614
|
}
|
|
4099
4615
|
}
|
|
4100
4616
|
if (config.review.postFindings) {
|
|
@@ -4116,7 +4632,7 @@ ${runLogTail}
|
|
|
4116
4632
|
progressPercent: 100,
|
|
4117
4633
|
...buildTokenPayload(sessionStats)
|
|
4118
4634
|
});
|
|
4119
|
-
|
|
4635
|
+
log12.info(TAG12, `#${card.short_id} approved${prUrl ? ` — PR: ${prUrl}` : ""} — labeled "${config.review.approvedLabel}"`);
|
|
4120
4636
|
} else {
|
|
4121
4637
|
const reworkFindings = result.findings.filter((f) => f.relatedToDiff !== false);
|
|
4122
4638
|
const criticalFindings = reworkFindings.filter((f) => f.severity === "critical").slice(0, MAX_FINDINGS);
|
|
@@ -4124,7 +4640,7 @@ ${runLogTail}
|
|
|
4124
4640
|
const linkedFindings = [...criticalFindings, ...majorFindings];
|
|
4125
4641
|
const minorFindings = reworkFindings.filter((f) => f.severity === "minor").slice(0, MAX_FINDINGS);
|
|
4126
4642
|
if (currentCycle >= maxCycles) {
|
|
4127
|
-
|
|
4643
|
+
log12.warn(TAG12, `#${card.short_id} reached max review cycles (${maxCycles}), moving to Done with note`);
|
|
4128
4644
|
await moveCardToColumn(client, card, config.review.moveToColumn);
|
|
4129
4645
|
const body = [
|
|
4130
4646
|
"**Review — needs human review.**",
|
|
@@ -4164,7 +4680,7 @@ ${runLogTail}
|
|
|
4164
4680
|
try {
|
|
4165
4681
|
await client.createSubtask(card.id, clampSubtaskTitle(`[${finding.severity}] ${finding.title}`));
|
|
4166
4682
|
} catch (err) {
|
|
4167
|
-
|
|
4683
|
+
log12.error(TAG12, `Failed to create finding subtask: ${err instanceof Error ? err.message : err}`);
|
|
4168
4684
|
}
|
|
4169
4685
|
}));
|
|
4170
4686
|
if (linkedFindings.length > 0) {
|
|
@@ -4176,7 +4692,7 @@ ${runLogTail}
|
|
|
4176
4692
|
try {
|
|
4177
4693
|
await client.createSubtask(card.id, clampSubtaskTitle(finding.title));
|
|
4178
4694
|
} catch (err) {
|
|
4179
|
-
|
|
4695
|
+
log12.error(TAG12, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
|
|
4180
4696
|
}
|
|
4181
4697
|
}));
|
|
4182
4698
|
const baseDesc = stripReviewSummary(freshDesc);
|
|
@@ -4184,7 +4700,7 @@ ${runLogTail}
|
|
|
4184
4700
|
try {
|
|
4185
4701
|
await client.updateCard(card.id, { description: updatedDesc });
|
|
4186
4702
|
} catch (err) {
|
|
4187
|
-
|
|
4703
|
+
log12.error(TAG12, `Failed to update review cycle marker: ${err instanceof Error ? err.message : err}`);
|
|
4188
4704
|
}
|
|
4189
4705
|
const scopeLine = result.scopeCheck ? `Scope: ${result.scopeCheck.status}${result.scopeCheck.notes ? ` — ${result.scopeCheck.notes}` : ""}` : "";
|
|
4190
4706
|
const body = [
|
|
@@ -4201,9 +4717,9 @@ ${runLogTail}
|
|
|
4201
4717
|
if (config.planning.enabled && card.plan_id) {
|
|
4202
4718
|
try {
|
|
4203
4719
|
await client.updateCard(card.id, { needsPlanRefresh: true });
|
|
4204
|
-
|
|
4720
|
+
log12.info(TAG12, `#${card.short_id} flagged needs_plan_refresh after rejected review`);
|
|
4205
4721
|
} catch (err) {
|
|
4206
|
-
|
|
4722
|
+
log12.warn(TAG12, `Failed to flag needs_plan_refresh for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
4207
4723
|
}
|
|
4208
4724
|
}
|
|
4209
4725
|
await moveCardToColumn(client, card, config.review.failColumn);
|
|
@@ -4217,10 +4733,10 @@ ${runLogTail}
|
|
|
4217
4733
|
recoveryBranch
|
|
4218
4734
|
});
|
|
4219
4735
|
} catch (err) {
|
|
4220
|
-
|
|
4736
|
+
log12.debug(TAG12, `recordFailureSummary failed: ${err instanceof Error ? err.message : err}`);
|
|
4221
4737
|
}
|
|
4222
4738
|
if (recoveryBranch) {
|
|
4223
|
-
|
|
4739
|
+
log12.info(TAG12, `#${card.short_id} recovery branch ${recoveryBranch}${recoveryUrl ? ` (${recoveryUrl})` : ""}`);
|
|
4224
4740
|
}
|
|
4225
4741
|
await client.endAgentSession(card.id, {
|
|
4226
4742
|
status: "failed",
|
|
@@ -4229,7 +4745,7 @@ ${runLogTail}
|
|
|
4229
4745
|
recoveryBranch,
|
|
4230
4746
|
...buildTokenPayload(sessionStats)
|
|
4231
4747
|
});
|
|
4232
|
-
|
|
4748
|
+
log12.info(TAG12, `#${card.short_id} rejected (cycle ${currentCycle}/${maxCycles}) — moved to "${config.review.failColumn}"`);
|
|
4233
4749
|
}
|
|
4234
4750
|
if (workspaceId && (result.verdict === "approved" || result.verdict === "rejected")) {
|
|
4235
4751
|
const originalEpisodeId = await findLatestImplementEpisode(client, workspaceId, card.project_id, card.short_id);
|
|
@@ -4251,7 +4767,7 @@ ${runLogTail}
|
|
|
4251
4767
|
cleanupWorktree2(worktreePath, branchName);
|
|
4252
4768
|
}
|
|
4253
4769
|
}
|
|
4254
|
-
var
|
|
4770
|
+
var TAG12 = "review-completion", MAX_FINDINGS = 10, MAX_SUBTASK_TITLE = 120, COMMENT_BODY_BUDGET = 9500, REVIEW_MARKER = `---
|
|
4255
4771
|
**Review:`, RUN_LOG_TAIL_BYTES = 2048;
|
|
4256
4772
|
var init_review_completion = __esm(() => {
|
|
4257
4773
|
init_board_helpers();
|
|
@@ -4398,7 +4914,7 @@ var init_review_prompt = __esm(() => {
|
|
|
4398
4914
|
import { createWriteStream, mkdirSync } from "node:fs";
|
|
4399
4915
|
import { homedir as homedir2 } from "node:os";
|
|
4400
4916
|
import { join as join2 } from "node:path";
|
|
4401
|
-
import { log as
|
|
4917
|
+
import { log as log13 } from "@gethmy/harness";
|
|
4402
4918
|
function openRunLog(tag, runId, shortId) {
|
|
4403
4919
|
if (!runId)
|
|
4404
4920
|
return null;
|
|
@@ -4409,7 +4925,7 @@ function openRunLog(tag, runId, shortId) {
|
|
|
4409
4925
|
const stream = createWriteStream(path, { flags: "a" });
|
|
4410
4926
|
return { path, stream };
|
|
4411
4927
|
} catch (err) {
|
|
4412
|
-
|
|
4928
|
+
log13.warn(tag, `Failed to open run log: ${err instanceof Error ? err.message : err}`);
|
|
4413
4929
|
return null;
|
|
4414
4930
|
}
|
|
4415
4931
|
}
|
|
@@ -4430,6 +4946,8 @@ function isSessionConflict(err) {
|
|
|
4430
4946
|
var exports_state_store = {};
|
|
4431
4947
|
__export(exports_state_store, {
|
|
4432
4948
|
newRunId: () => newRunId,
|
|
4949
|
+
isParkedRun: () => isParkedRun,
|
|
4950
|
+
isBudgetHeldRun: () => isBudgetHeldRun,
|
|
4433
4951
|
defaultStatePath: () => defaultStatePath,
|
|
4434
4952
|
StateStore: () => StateStore
|
|
4435
4953
|
});
|
|
@@ -4442,7 +4960,7 @@ import {
|
|
|
4442
4960
|
} from "node:fs";
|
|
4443
4961
|
import { homedir as homedir3 } from "node:os";
|
|
4444
4962
|
import { dirname, join as join3 } from "node:path";
|
|
4445
|
-
import { log as
|
|
4963
|
+
import { log as log14 } from "@gethmy/harness";
|
|
4446
4964
|
function emptyState() {
|
|
4447
4965
|
return {
|
|
4448
4966
|
version: SCHEMA_VERSION,
|
|
@@ -4457,6 +4975,16 @@ function emptyState() {
|
|
|
4457
4975
|
function todayUtc() {
|
|
4458
4976
|
return new Date().toISOString().slice(0, 10);
|
|
4459
4977
|
}
|
|
4978
|
+
function isParkedRun(run) {
|
|
4979
|
+
return run.status === "parked" && run.endedAt === null;
|
|
4980
|
+
}
|
|
4981
|
+
function isBudgetHeldRun(run) {
|
|
4982
|
+
if (run.endedAt !== null)
|
|
4983
|
+
return false;
|
|
4984
|
+
if (run.status === "parked")
|
|
4985
|
+
return true;
|
|
4986
|
+
return run.grantedTurns != null;
|
|
4987
|
+
}
|
|
4460
4988
|
function newRunId() {
|
|
4461
4989
|
const ts = Date.now().toString(36);
|
|
4462
4990
|
const rand = Math.random().toString(36).slice(2, 10);
|
|
@@ -4488,7 +5016,7 @@ class StateStore {
|
|
|
4488
5016
|
const raw = readFileSync3(this.path, "utf-8");
|
|
4489
5017
|
const parsed = JSON.parse(raw);
|
|
4490
5018
|
if (parsed?.version !== SCHEMA_VERSION) {
|
|
4491
|
-
|
|
5019
|
+
log14.warn(TAG13, `state file has version ${parsed?.version}, expected ${SCHEMA_VERSION} — migrating (preserving card budget/attempts, dropping in-flight runs)`);
|
|
4492
5020
|
return {
|
|
4493
5021
|
version: SCHEMA_VERSION,
|
|
4494
5022
|
daemonId: null,
|
|
@@ -4509,7 +5037,7 @@ class StateStore {
|
|
|
4509
5037
|
daily: parsed.daily ?? []
|
|
4510
5038
|
};
|
|
4511
5039
|
} catch (err) {
|
|
4512
|
-
|
|
5040
|
+
log14.error(TAG13, `failed to read state file: ${err instanceof Error ? err.message : err}`);
|
|
4513
5041
|
return emptyState();
|
|
4514
5042
|
}
|
|
4515
5043
|
}
|
|
@@ -4568,12 +5096,40 @@ class StateStore {
|
|
|
4568
5096
|
}
|
|
4569
5097
|
return this.updateRun(runId, patch);
|
|
4570
5098
|
}
|
|
5099
|
+
parkRun(runId, opts) {
|
|
5100
|
+
const patch = {
|
|
5101
|
+
status: "parked",
|
|
5102
|
+
pauseTrigger: opts.pauseTrigger,
|
|
5103
|
+
blockerCommentId: opts.blockerCommentId,
|
|
5104
|
+
awaitingDecisionUntil: opts.awaitingDecisionUntil,
|
|
5105
|
+
parkedAt: Date.now()
|
|
5106
|
+
};
|
|
5107
|
+
if (opts.costCents !== undefined && opts.costCents > 0) {
|
|
5108
|
+
patch.costCents = opts.costCents;
|
|
5109
|
+
}
|
|
5110
|
+
if (opts.numTurns !== undefined && opts.numTurns > 0) {
|
|
5111
|
+
patch.numTurns = opts.numTurns;
|
|
5112
|
+
}
|
|
5113
|
+
return this.updateRun(runId, patch);
|
|
5114
|
+
}
|
|
4571
5115
|
getRun(runId) {
|
|
4572
5116
|
return this.state.runs.find((r) => r.runId === runId) ?? null;
|
|
4573
5117
|
}
|
|
4574
5118
|
getActiveRuns() {
|
|
4575
5119
|
return this.state.runs.filter((r) => r.endedAt === null);
|
|
4576
5120
|
}
|
|
5121
|
+
getParkedRuns() {
|
|
5122
|
+
return this.state.runs.filter(isParkedRun);
|
|
5123
|
+
}
|
|
5124
|
+
getParkedRunForCard(cardId) {
|
|
5125
|
+
return this.state.runs.find((r) => r.cardId === cardId && isParkedRun(r)) ?? null;
|
|
5126
|
+
}
|
|
5127
|
+
getResumableRunForCard(cardId) {
|
|
5128
|
+
return this.state.runs.find((r) => r.cardId === cardId && r.status === "active" && r.endedAt === null && r.grantedTurns != null) ?? null;
|
|
5129
|
+
}
|
|
5130
|
+
getResumableRuns() {
|
|
5131
|
+
return this.state.runs.filter((r) => r.status === "active" && r.endedAt === null && r.grantedTurns != null);
|
|
5132
|
+
}
|
|
4577
5133
|
getRunsForCard(cardId) {
|
|
4578
5134
|
return this.state.runs.filter((r) => r.cardId === cardId);
|
|
4579
5135
|
}
|
|
@@ -4652,6 +5208,22 @@ class StateStore {
|
|
|
4652
5208
|
rec.loopIterations = 0;
|
|
4653
5209
|
await this.persist();
|
|
4654
5210
|
}
|
|
5211
|
+
async markAwaitingDecision(cardId, opts) {
|
|
5212
|
+
const rec = this.ensureCard(cardId);
|
|
5213
|
+
rec.awaitingDecisionUntil = opts.until;
|
|
5214
|
+
rec.blockerCommentId = opts.blockerCommentId;
|
|
5215
|
+
rec.awaitingDecisionAgentIdentifier = opts.agentIdentifier ?? null;
|
|
5216
|
+
await this.persist();
|
|
5217
|
+
}
|
|
5218
|
+
async clearAwaitingDecision(cardId) {
|
|
5219
|
+
const rec = this.getCard(cardId);
|
|
5220
|
+
if (!rec || rec.awaitingDecisionUntil == null)
|
|
5221
|
+
return;
|
|
5222
|
+
rec.awaitingDecisionUntil = null;
|
|
5223
|
+
rec.blockerCommentId = null;
|
|
5224
|
+
rec.awaitingDecisionAgentIdentifier = null;
|
|
5225
|
+
await this.persist();
|
|
5226
|
+
}
|
|
4655
5227
|
async resetAttempts(cardId) {
|
|
4656
5228
|
const rec = this.getCard(cardId);
|
|
4657
5229
|
if (!rec || rec.attempts === 0)
|
|
@@ -4698,12 +5270,12 @@ class StateStore {
|
|
|
4698
5270
|
return this.state.daily.find((d) => d.date === key)?.costCents ?? 0;
|
|
4699
5271
|
}
|
|
4700
5272
|
}
|
|
4701
|
-
var
|
|
5273
|
+
var TAG13 = "state-store", SCHEMA_VERSION = 1;
|
|
4702
5274
|
var init_state_store = () => {};
|
|
4703
5275
|
|
|
4704
5276
|
// src/stream-parser.ts
|
|
4705
5277
|
import { EventEmitter } from "node:events";
|
|
4706
|
-
import { log as
|
|
5278
|
+
import { log as log15 } from "@gethmy/harness";
|
|
4707
5279
|
function normalizeToolResultContent(raw) {
|
|
4708
5280
|
if (raw == null)
|
|
4709
5281
|
return;
|
|
@@ -4724,7 +5296,7 @@ function normalizeToolResultContent(raw) {
|
|
|
4724
5296
|
return String(raw);
|
|
4725
5297
|
}
|
|
4726
5298
|
}
|
|
4727
|
-
var
|
|
5299
|
+
var TAG14 = "stream-parser", StreamParser;
|
|
4728
5300
|
var init_stream_parser = __esm(() => {
|
|
4729
5301
|
StreamParser = class StreamParser extends EventEmitter {
|
|
4730
5302
|
buffer = "";
|
|
@@ -4771,14 +5343,14 @@ var init_stream_parser = __esm(() => {
|
|
|
4771
5343
|
try {
|
|
4772
5344
|
msg = JSON.parse(line);
|
|
4773
5345
|
} catch {
|
|
4774
|
-
|
|
5346
|
+
log15.debug(TAG14, `Non-JSON line: ${line.slice(0, 100)}`);
|
|
4775
5347
|
return;
|
|
4776
5348
|
}
|
|
4777
5349
|
try {
|
|
4778
5350
|
this.handleMessage(msg);
|
|
4779
5351
|
} catch (err) {
|
|
4780
5352
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4781
|
-
|
|
5353
|
+
log15.warn(TAG14, `Error handling stream event: ${errMsg}`);
|
|
4782
5354
|
this.emit("parse_error", errMsg);
|
|
4783
5355
|
}
|
|
4784
5356
|
}
|
|
@@ -4854,7 +5426,7 @@ var init_stream_parser = __esm(() => {
|
|
|
4854
5426
|
});
|
|
4855
5427
|
|
|
4856
5428
|
// src/transitions.ts
|
|
4857
|
-
import { log as
|
|
5429
|
+
import { log as log16 } from "@gethmy/harness";
|
|
4858
5430
|
async function withRetry(step, cardShortId, op, attempts, backoffMs) {
|
|
4859
5431
|
let lastErr;
|
|
4860
5432
|
for (let i = 0;i < attempts; i++) {
|
|
@@ -4865,7 +5437,7 @@ async function withRetry(step, cardShortId, op, attempts, backoffMs) {
|
|
|
4865
5437
|
const msg2 = err instanceof Error ? err.message : String(err);
|
|
4866
5438
|
if (i < attempts - 1) {
|
|
4867
5439
|
const wait = backoffMs * 2 ** i;
|
|
4868
|
-
|
|
5440
|
+
log16.warn(TAG15, `${step} failed for #${cardShortId} (attempt ${i + 1}/${attempts}): ${msg2} — retrying in ${wait}ms`);
|
|
4869
5441
|
await new Promise((r) => setTimeout(r, wait));
|
|
4870
5442
|
}
|
|
4871
5443
|
}
|
|
@@ -4889,10 +5461,10 @@ async function runTransition(client, card, plan, opts = {}) {
|
|
|
4889
5461
|
if (opts.strictColumn) {
|
|
4890
5462
|
throw new TransitionError("move", 1, msg);
|
|
4891
5463
|
}
|
|
4892
|
-
|
|
5464
|
+
log16.warn(TAG15, `#${shortId}: ${msg} — skipping move`);
|
|
4893
5465
|
} else if (card.column_id !== target.id) {
|
|
4894
5466
|
await withRetry("move", shortId, () => client.moveCard(card.id, target.id), attempts, backoffMs);
|
|
4895
|
-
|
|
5467
|
+
log16.info(TAG15, `#${shortId} → "${target.name}"`);
|
|
4896
5468
|
card.column_id = target.id;
|
|
4897
5469
|
moveLanded = true;
|
|
4898
5470
|
} else {
|
|
@@ -4911,7 +5483,7 @@ async function runTransition(client, card, plan, opts = {}) {
|
|
|
4911
5483
|
continue;
|
|
4912
5484
|
await withRetry("addLabel", shortId, () => client.addLabelToCard(card.id, labelId), attempts, backoffMs);
|
|
4913
5485
|
existing.add(labelId);
|
|
4914
|
-
|
|
5486
|
+
log16.info(TAG15, `#${shortId} +label "${name}"`);
|
|
4915
5487
|
}
|
|
4916
5488
|
card.labelIds = Array.from(existing);
|
|
4917
5489
|
}
|
|
@@ -4923,23 +5495,23 @@ async function runTransition(client, card, plan, opts = {}) {
|
|
|
4923
5495
|
continue;
|
|
4924
5496
|
await withRetry("removeLabel", shortId, () => client.removeLabelFromCard(card.id, match.id), attempts, backoffMs);
|
|
4925
5497
|
existing.delete(match.id);
|
|
4926
|
-
|
|
5498
|
+
log16.info(TAG15, `#${shortId} -label "${name}"`);
|
|
4927
5499
|
}
|
|
4928
5500
|
card.labelIds = Array.from(existing);
|
|
4929
5501
|
}
|
|
4930
5502
|
if (plan.updateCard) {
|
|
4931
5503
|
await withRetry("updateCard", shortId, () => client.updateCard(card.id, plan.updateCard), attempts, backoffMs);
|
|
4932
|
-
|
|
5504
|
+
log16.info(TAG15, `#${shortId} updated`);
|
|
4933
5505
|
}
|
|
4934
5506
|
if (plan.endSession) {
|
|
4935
5507
|
const endResult = await withRetry("endSession", shortId, () => client.endAgentSession(card.id, plan.endSession), attempts, backoffMs);
|
|
4936
5508
|
result.endSession = endResult;
|
|
4937
|
-
|
|
5509
|
+
log16.info(TAG15, `#${shortId} session ended (${plan.endSession.status})`);
|
|
4938
5510
|
}
|
|
4939
5511
|
if (plan.assignAgent !== undefined) {
|
|
4940
5512
|
const assignedAgentId = plan.assignAgent;
|
|
4941
5513
|
await withRetry("assignAgent", shortId, () => client.updateCard(card.id, { assignedAgentId }), attempts, backoffMs);
|
|
4942
|
-
|
|
5514
|
+
log16.info(TAG15, assignedAgentId ? `#${shortId} assigned → agent ${assignedAgentId}` : `#${shortId} unassigned`);
|
|
4943
5515
|
}
|
|
4944
5516
|
if (opts.store && opts.runId) {
|
|
4945
5517
|
try {
|
|
@@ -4953,11 +5525,11 @@ async function ensureLabel(client, projectId, name, color, attempts, backoffMs)
|
|
|
4953
5525
|
const result = await withRetry("addLabel", 0, () => client.createLabel(projectId, { name, color: color ?? "#8b5cf6" }), attempts, backoffMs);
|
|
4954
5526
|
return result?.label?.id ?? null;
|
|
4955
5527
|
} catch (err) {
|
|
4956
|
-
|
|
5528
|
+
log16.warn(TAG15, `ensureLabel "${name}" failed: ${err instanceof Error ? err.message : err}`);
|
|
4957
5529
|
return null;
|
|
4958
5530
|
}
|
|
4959
5531
|
}
|
|
4960
|
-
var
|
|
5532
|
+
var TAG15 = "transition", TransitionError;
|
|
4961
5533
|
var init_transitions = __esm(() => {
|
|
4962
5534
|
TransitionError = class TransitionError extends Error {
|
|
4963
5535
|
step;
|
|
@@ -4974,14 +5546,14 @@ var init_transitions = __esm(() => {
|
|
|
4974
5546
|
});
|
|
4975
5547
|
|
|
4976
5548
|
// src/review-worker.ts
|
|
4977
|
-
import { execFileSync as
|
|
5549
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
4978
5550
|
import {
|
|
4979
5551
|
buildGateCollectorRegistry,
|
|
4980
5552
|
cleanupWorktree as cleanupWorktree3,
|
|
4981
5553
|
collectGateEvidence,
|
|
4982
5554
|
DevServerReadinessError,
|
|
4983
5555
|
formatDiffSummary,
|
|
4984
|
-
log as
|
|
5556
|
+
log as log17,
|
|
4985
5557
|
probeDevServer,
|
|
4986
5558
|
resolveStageGate,
|
|
4987
5559
|
signalGroup,
|
|
@@ -5012,10 +5584,17 @@ class ReviewWorker {
|
|
|
5012
5584
|
lastSessionStats = null;
|
|
5013
5585
|
aborted = false;
|
|
5014
5586
|
timedOut = false;
|
|
5587
|
+
lastStopReason = null;
|
|
5015
5588
|
sessionConflict = false;
|
|
5016
5589
|
runId = null;
|
|
5017
5590
|
lastRunLogPath = null;
|
|
5018
5591
|
sessionId = null;
|
|
5592
|
+
cliSessionId = null;
|
|
5593
|
+
grantedTurns = null;
|
|
5594
|
+
resumeMessage = null;
|
|
5595
|
+
get effectiveMaxTurns() {
|
|
5596
|
+
return this.grantedTurns ?? this.config.claude.reviewMaxTurns;
|
|
5597
|
+
}
|
|
5019
5598
|
constructor(id, config, client, identity, onDone, stateStore, workspaceId, _projectId) {
|
|
5020
5599
|
this.config = config;
|
|
5021
5600
|
this.client = client;
|
|
@@ -5041,6 +5620,14 @@ class ReviewWorker {
|
|
|
5041
5620
|
this.heartbeatTimer = null;
|
|
5042
5621
|
}
|
|
5043
5622
|
}
|
|
5623
|
+
captureCliSessionId(sessionId) {
|
|
5624
|
+
if (!sessionId || sessionId === this.cliSessionId)
|
|
5625
|
+
return;
|
|
5626
|
+
this.cliSessionId = sessionId;
|
|
5627
|
+
if (this.runId) {
|
|
5628
|
+
this.stateStore.updateRun(this.runId, { cliSessionId: sessionId }).catch(() => {});
|
|
5629
|
+
}
|
|
5630
|
+
}
|
|
5044
5631
|
async recordPhase(phase) {
|
|
5045
5632
|
if (!this.runId)
|
|
5046
5633
|
return;
|
|
@@ -5049,14 +5636,16 @@ class ReviewWorker {
|
|
|
5049
5636
|
phase,
|
|
5050
5637
|
lastHeartbeatAt: Date.now(),
|
|
5051
5638
|
worktreePath: this.worktreePath,
|
|
5052
|
-
branchName: this.branchName
|
|
5639
|
+
branchName: this.branchName,
|
|
5640
|
+
sessionId: this.sessionId,
|
|
5641
|
+
cliSessionId: this.cliSessionId
|
|
5053
5642
|
});
|
|
5054
5643
|
} catch (err) {
|
|
5055
|
-
|
|
5644
|
+
log17.warn(this.tag, `state store updateRun failed: ${err instanceof Error ? err.message : err}`);
|
|
5056
5645
|
}
|
|
5057
5646
|
}
|
|
5058
5647
|
get tag() {
|
|
5059
|
-
return `${
|
|
5648
|
+
return `${TAG16}:${this.id}`;
|
|
5060
5649
|
}
|
|
5061
5650
|
get isIdle() {
|
|
5062
5651
|
return this.state === "idle";
|
|
@@ -5083,70 +5672,95 @@ class ReviewWorker {
|
|
|
5083
5672
|
async run(card, column, labels, subtasks) {
|
|
5084
5673
|
this.aborted = false;
|
|
5085
5674
|
this.timedOut = false;
|
|
5675
|
+
this.lastStopReason = null;
|
|
5086
5676
|
this.sessionConflict = false;
|
|
5677
|
+
this.cliSessionId = null;
|
|
5678
|
+
this.grantedTurns = null;
|
|
5679
|
+
this.resumeMessage = null;
|
|
5087
5680
|
this.cardId = card.id;
|
|
5088
5681
|
this.startedAt = Date.now();
|
|
5089
5682
|
this.runId = newRunId();
|
|
5683
|
+
const resuming = this.stateStore.getResumableRunForCard(card.id);
|
|
5684
|
+
if (resuming) {
|
|
5685
|
+
this.runId = resuming.runId;
|
|
5686
|
+
this.worktreePath = resuming.worktreePath;
|
|
5687
|
+
this.branchName = resuming.branchName;
|
|
5688
|
+
this.cliSessionId = resuming.cliSessionId ?? null;
|
|
5689
|
+
this.sessionId = resuming.sessionId;
|
|
5690
|
+
this.grantedTurns = resuming.grantedTurns ?? null;
|
|
5691
|
+
this.resumeMessage = resuming.resumeMessage ?? null;
|
|
5692
|
+
try {
|
|
5693
|
+
await this.stateStore.updateRun(resuming.runId, {
|
|
5694
|
+
grantedTurns: null,
|
|
5695
|
+
resumeMessage: null
|
|
5696
|
+
});
|
|
5697
|
+
} catch (err) {
|
|
5698
|
+
log17.warn(this.tag, `Failed to clear the consumed grant: ${err}`);
|
|
5699
|
+
}
|
|
5700
|
+
}
|
|
5090
5701
|
try {
|
|
5091
5702
|
this.state = "preparing";
|
|
5092
|
-
|
|
5703
|
+
log17.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}"`);
|
|
5093
5704
|
this.startHeartbeat();
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
const repoRoot = execFileSync3("git", ["rev-parse", "--show-toplevel"], {
|
|
5113
|
-
encoding: "utf-8",
|
|
5114
|
-
timeout: 5000
|
|
5115
|
-
}).trim();
|
|
5116
|
-
const resolution = await resolveReviewBranch(card.description, repoRoot);
|
|
5117
|
-
if (resolution.kind !== "branch") {
|
|
5118
|
-
const why = resolution.kind === "skip" ? resolution.reason : "no branch or PR reference";
|
|
5119
|
-
log16.info(this.tag, `#${card.short_id} not auto-reviewable (${why}) — marking for human review (staying in Review)`);
|
|
5120
|
-
await addLabelByName(this.client, card, NEED_REVIEW_LABEL, NEED_REVIEW_LABEL_COLOR);
|
|
5121
|
-
return;
|
|
5122
|
-
}
|
|
5123
|
-
this.branchName = resolution.branch;
|
|
5124
|
-
log16.info(this.tag, `Review branch: ${this.branchName}`);
|
|
5125
|
-
let reviewSession;
|
|
5126
|
-
try {
|
|
5127
|
-
const started = await this.client.startAgentSession(card.id, {
|
|
5128
|
-
agentIdentifier: agentIdentifier(this.id),
|
|
5129
|
-
agentName: `${AGENT_NAME} (Review)`,
|
|
5130
|
-
agentId: this.identity.agentId,
|
|
5131
|
-
status: "working",
|
|
5132
|
-
currentTask: "Setting up review worktree",
|
|
5133
|
-
progressPercent: 5,
|
|
5134
|
-
modelName: this.config.claude.reviewModel,
|
|
5135
|
-
driver: "daemon"
|
|
5705
|
+
if (!resuming) {
|
|
5706
|
+
await this.stateStore.insertRun({
|
|
5707
|
+
runId: this.runId,
|
|
5708
|
+
cardId: card.id,
|
|
5709
|
+
cardShortId: card.short_id,
|
|
5710
|
+
pipeline: "review",
|
|
5711
|
+
workerId: this.id,
|
|
5712
|
+
sessionId: null,
|
|
5713
|
+
worktreePath: null,
|
|
5714
|
+
branchName: null,
|
|
5715
|
+
daemonPid: process.pid,
|
|
5716
|
+
phase: "preparing",
|
|
5717
|
+
startedAt: this.startedAt,
|
|
5718
|
+
lastHeartbeatAt: this.startedAt,
|
|
5719
|
+
endedAt: null,
|
|
5720
|
+
status: "active",
|
|
5721
|
+
costCents: 0,
|
|
5722
|
+
numTurns: 0
|
|
5136
5723
|
});
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5724
|
+
const repoRoot = execFileSync4("git", ["rev-parse", "--show-toplevel"], {
|
|
5725
|
+
encoding: "utf-8",
|
|
5726
|
+
timeout: 5000
|
|
5727
|
+
}).trim();
|
|
5728
|
+
const resolution = await resolveReviewBranch(card.description, repoRoot);
|
|
5729
|
+
if (resolution.kind !== "branch") {
|
|
5730
|
+
const why = resolution.kind === "skip" ? resolution.reason : "no branch or PR reference";
|
|
5731
|
+
log17.info(this.tag, `#${card.short_id} not auto-reviewable (${why}) — marking for human review (staying in Review)`);
|
|
5732
|
+
await addLabelByName(this.client, card, NEED_REVIEW_LABEL, NEED_REVIEW_LABEL_COLOR);
|
|
5142
5733
|
return;
|
|
5143
5734
|
}
|
|
5144
|
-
|
|
5735
|
+
this.branchName = resolution.branch;
|
|
5736
|
+
log17.info(this.tag, `Review branch: ${this.branchName}`);
|
|
5737
|
+
let reviewSession;
|
|
5738
|
+
try {
|
|
5739
|
+
const started = await this.client.startAgentSession(card.id, {
|
|
5740
|
+
agentIdentifier: agentIdentifier(this.id),
|
|
5741
|
+
agentName: `${AGENT_NAME} (Review)`,
|
|
5742
|
+
agentId: this.identity.agentId,
|
|
5743
|
+
status: "working",
|
|
5744
|
+
currentTask: "Setting up review worktree",
|
|
5745
|
+
progressPercent: 5,
|
|
5746
|
+
modelName: this.config.claude.reviewModel,
|
|
5747
|
+
driver: "daemon",
|
|
5748
|
+
awaitingDecisionUntil: null
|
|
5749
|
+
});
|
|
5750
|
+
reviewSession = started.session;
|
|
5751
|
+
} catch (err) {
|
|
5752
|
+
if (isSessionConflict(err)) {
|
|
5753
|
+
this.sessionConflict = true;
|
|
5754
|
+
log17.info(this.tag, `Skipping review of #${card.short_id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
5755
|
+
return;
|
|
5756
|
+
}
|
|
5757
|
+
throw err;
|
|
5758
|
+
}
|
|
5759
|
+
this.sessionId = reviewSession && typeof reviewSession === "object" && "id" in reviewSession ? reviewSession.id ?? null : null;
|
|
5760
|
+
const labelPromise = addLabelByName(this.client, card, "agent", "#8b5cf6");
|
|
5761
|
+
this.worktreePath = checkoutExistingBranch(this.config.worktree.basePath, this.branchName);
|
|
5762
|
+
await labelPromise;
|
|
5145
5763
|
}
|
|
5146
|
-
this.sessionId = reviewSession && typeof reviewSession === "object" && "id" in reviewSession ? reviewSession.id ?? null : null;
|
|
5147
|
-
const labelPromise = addLabelByName(this.client, card, "agent", "#8b5cf6");
|
|
5148
|
-
this.worktreePath = checkoutExistingBranch(this.config.worktree.basePath, this.branchName);
|
|
5149
|
-
await labelPromise;
|
|
5150
5764
|
if (this.aborted)
|
|
5151
5765
|
return;
|
|
5152
5766
|
this.state = "running";
|
|
@@ -5156,7 +5770,7 @@ class ReviewWorker {
|
|
|
5156
5770
|
}
|
|
5157
5771
|
const port = this.reviewPort;
|
|
5158
5772
|
const cwd = this.worktreePath;
|
|
5159
|
-
|
|
5773
|
+
log17.info(this.tag, `Starting dev server on port ${port}...`);
|
|
5160
5774
|
const [devCmd, devArgs] = spawnRunArgs("dev", "--port", String(port));
|
|
5161
5775
|
this.devServerProcess = spawnInGroup(devCmd, devArgs, {
|
|
5162
5776
|
cwd,
|
|
@@ -5178,7 +5792,7 @@ class ReviewWorker {
|
|
|
5178
5792
|
}
|
|
5179
5793
|
await waitForDevServer(this.devServerProcess, 30000);
|
|
5180
5794
|
await probeDevServer(port);
|
|
5181
|
-
|
|
5795
|
+
log17.info(this.tag, `Dev server ready on port ${port}`);
|
|
5182
5796
|
await this.client.updateAgentProgress(card.id, {
|
|
5183
5797
|
agentIdentifier: agentIdentifier(this.id),
|
|
5184
5798
|
agentName: `${AGENT_NAME} (Review)`,
|
|
@@ -5190,7 +5804,7 @@ class ReviewWorker {
|
|
|
5190
5804
|
return;
|
|
5191
5805
|
let diff = "";
|
|
5192
5806
|
try {
|
|
5193
|
-
diff =
|
|
5807
|
+
diff = execFileSync4("git", ["diff", `origin/${this.config.worktree.baseBranch}..HEAD`], { cwd, encoding: "utf-8", timeout: 30000 });
|
|
5194
5808
|
} catch {
|
|
5195
5809
|
diff = "(unable to retrieve diff)";
|
|
5196
5810
|
}
|
|
@@ -5211,14 +5825,19 @@ class ReviewWorker {
|
|
|
5211
5825
|
pinnedContract = extractPinnedContract(comments, this.identity);
|
|
5212
5826
|
}
|
|
5213
5827
|
} catch (err) {
|
|
5214
|
-
|
|
5828
|
+
log17.warn(this.tag, `pinned-contract fetch failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
5215
5829
|
}
|
|
5216
5830
|
if (pinnedContract) {
|
|
5217
|
-
|
|
5831
|
+
log17.info(this.tag, `Grading pinned contract for #${card.short_id} (${pinnedContract.assertions.length} criteria)`);
|
|
5218
5832
|
}
|
|
5219
5833
|
}
|
|
5220
5834
|
const systemPrompt = buildReviewSystemPrompt();
|
|
5221
|
-
|
|
5835
|
+
let userPrompt = buildReviewUserPrompt(enriched, this.branchName, cwd, previewUrl, diffSummary, this.config.worktree.baseBranch, undefined, pinnedContract);
|
|
5836
|
+
if (resuming && this.resumeMessage) {
|
|
5837
|
+
userPrompt = `${buildSteeringPrompt([this.resumeMessage])}
|
|
5838
|
+
|
|
5839
|
+
${userPrompt}`;
|
|
5840
|
+
}
|
|
5222
5841
|
try {
|
|
5223
5842
|
await this.client.recordPromptHistory({
|
|
5224
5843
|
cardId: card.id,
|
|
@@ -5227,7 +5846,7 @@ class ReviewWorker {
|
|
|
5227
5846
|
contextIncluded: { source: "review-knowledge", mode: "review" }
|
|
5228
5847
|
});
|
|
5229
5848
|
} catch (err) {
|
|
5230
|
-
|
|
5849
|
+
log17.warn(this.tag, `prompt_history persistence skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
5231
5850
|
}
|
|
5232
5851
|
await this.client.updateAgentProgress(card.id, {
|
|
5233
5852
|
agentIdentifier: agentIdentifier(this.id),
|
|
@@ -5237,13 +5856,16 @@ class ReviewWorker {
|
|
|
5237
5856
|
progressPercent: 20
|
|
5238
5857
|
});
|
|
5239
5858
|
this.timeoutTimer = setTimeout(() => {
|
|
5240
|
-
|
|
5859
|
+
log17.warn(this.tag, `Review timeout reached (${this.config.review.maxTimeout}ms), cancelling`);
|
|
5241
5860
|
this.timedOut = true;
|
|
5242
5861
|
this.cancel("timeout");
|
|
5243
5862
|
}, this.config.review.maxTimeout);
|
|
5244
5863
|
this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks);
|
|
5245
5864
|
this.progressTracker.setRequestedModel(this.config.claude.reviewModel);
|
|
5246
|
-
const stdout = await this.spawnClaude(userPrompt, systemPrompt, this.progressTracker, card.short_id
|
|
5865
|
+
const stdout = await this.spawnClaude(userPrompt, systemPrompt, this.progressTracker, card.short_id, {
|
|
5866
|
+
maxTurns: this.grantedTurns ?? undefined,
|
|
5867
|
+
resumeSessionId: this.cliSessionId ?? undefined
|
|
5868
|
+
});
|
|
5247
5869
|
this.lastSessionStats = this.progressTracker?.stats ?? null;
|
|
5248
5870
|
this.progressTracker?.stop();
|
|
5249
5871
|
this.progressTracker = null;
|
|
@@ -5256,10 +5878,10 @@ class ReviewWorker {
|
|
|
5256
5878
|
}
|
|
5257
5879
|
this.state = "completing";
|
|
5258
5880
|
await this.recordPhase("completing");
|
|
5259
|
-
|
|
5881
|
+
log17.info(this.tag, `Claude review finished for #${card.short_id}`);
|
|
5260
5882
|
this.killDevServer();
|
|
5261
5883
|
const result = parseReviewOutput(stdout);
|
|
5262
|
-
|
|
5884
|
+
log17.info(this.tag, `Review verdict: ${result.verdict} (${result.findings.length} finding(s))`);
|
|
5263
5885
|
await this.client.updateAgentProgress(card.id, {
|
|
5264
5886
|
agentIdentifier: agentIdentifier(this.id),
|
|
5265
5887
|
agentName: `${AGENT_NAME} (Review)`,
|
|
@@ -5270,9 +5892,17 @@ class ReviewWorker {
|
|
|
5270
5892
|
await runReviewCompletion(this.client, card, result, this.config, cwd, this.branchName, sessionStats, this.lastRunLogPath, this.workspaceId, this.sessionId, this.stateStore, reviewedFromPrUrl(card.description));
|
|
5271
5893
|
await this.collectReviewGate(card, result);
|
|
5272
5894
|
} catch (err) {
|
|
5895
|
+
if (err instanceof BudgetPauseError) {
|
|
5896
|
+
await this.parkForDecision(card, err.trigger);
|
|
5897
|
+
return;
|
|
5898
|
+
}
|
|
5899
|
+
if (resuming && isSessionConflict(err)) {
|
|
5900
|
+
await this.holdParkOnSessionConflict(card, err);
|
|
5901
|
+
return;
|
|
5902
|
+
}
|
|
5273
5903
|
this.state = "error";
|
|
5274
5904
|
const msg = err instanceof Error ? err.message : String(err);
|
|
5275
|
-
|
|
5905
|
+
log17.error(this.tag, `Error reviewing #${card.short_id}: ${msg}`);
|
|
5276
5906
|
try {
|
|
5277
5907
|
const stats = this.lastSessionStats ?? this.progressTracker?.stats;
|
|
5278
5908
|
await runTransition(this.client, card, {
|
|
@@ -5282,21 +5912,21 @@ class ReviewWorker {
|
|
|
5282
5912
|
}
|
|
5283
5913
|
});
|
|
5284
5914
|
} catch (tErr) {
|
|
5285
|
-
|
|
5915
|
+
log17.error(this.tag, `endAgentSession unrecoverable on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
|
|
5286
5916
|
}
|
|
5287
5917
|
if (err instanceof DevServerReadinessError) {
|
|
5288
5918
|
try {
|
|
5289
5919
|
await addLabelByName(this.client, card, NEED_REVIEW_LABEL, NEED_REVIEW_LABEL_COLOR);
|
|
5290
|
-
|
|
5920
|
+
log17.info(this.tag, `#${card.short_id} kept in Review — dev server unavailable, human review needed`);
|
|
5291
5921
|
} catch {
|
|
5292
|
-
|
|
5922
|
+
log17.warn(this.tag, "Failed to add Need Review label after dev-server failure");
|
|
5293
5923
|
}
|
|
5294
5924
|
} else {
|
|
5295
5925
|
try {
|
|
5296
5926
|
await moveCardToColumn(this.client, card, this.config.review.failColumn);
|
|
5297
|
-
|
|
5927
|
+
log17.info(this.tag, `Moved #${card.short_id} to "${this.config.review.failColumn}" after error`);
|
|
5298
5928
|
} catch {
|
|
5299
|
-
|
|
5929
|
+
log17.warn(this.tag, "Failed to move card to fail column after error");
|
|
5300
5930
|
}
|
|
5301
5931
|
}
|
|
5302
5932
|
if (this.runId) {
|
|
@@ -5308,21 +5938,119 @@ class ReviewWorker {
|
|
|
5308
5938
|
if (this.runId) {
|
|
5309
5939
|
try {
|
|
5310
5940
|
const run = this.stateStore.getRun(this.runId);
|
|
5311
|
-
if (run && run.endedAt === null) {
|
|
5941
|
+
if (run && run.endedAt === null && run.status !== "parked") {
|
|
5312
5942
|
const status = this.timedOut ? "failed" : this.state === "error" || this.aborted || this.sessionConflict ? "paused" : "completed";
|
|
5313
5943
|
await this.stateStore.endRun(this.runId, status, this.sessionConflict ? { errorMessage: "session_conflict", ...this.endLedger() } : this.endLedger());
|
|
5314
5944
|
}
|
|
5315
5945
|
} catch {}
|
|
5316
5946
|
}
|
|
5947
|
+
if (this.cardId && this.timedOut && this.config.budget.pause.enabled && this.state !== "parked" && this.state !== "error") {
|
|
5948
|
+
try {
|
|
5949
|
+
await this.client.endAgentSession(this.cardId, {
|
|
5950
|
+
status: "failed",
|
|
5951
|
+
failureReason: "timeout",
|
|
5952
|
+
failureSummary: `Review exceeded the ${Math.round(this.config.review.maxTimeout / 60000)} min timeout`,
|
|
5953
|
+
...buildTokenPayload(this.lastSessionStats)
|
|
5954
|
+
});
|
|
5955
|
+
} catch {}
|
|
5956
|
+
}
|
|
5317
5957
|
this.cleanup();
|
|
5318
5958
|
this.state = "idle";
|
|
5319
5959
|
this.onDone(this);
|
|
5320
5960
|
}
|
|
5321
5961
|
}
|
|
5962
|
+
async holdParkOnSessionConflict(card, err) {
|
|
5963
|
+
this.state = "parked";
|
|
5964
|
+
this.progressTracker?.stop();
|
|
5965
|
+
this.progressTracker = null;
|
|
5966
|
+
const holderMessage = err instanceof Error ? err.message : String(err);
|
|
5967
|
+
const waitHours = this.config.budget.pause.waitHours;
|
|
5968
|
+
const until = computeDecisionDeadline(waitHours);
|
|
5969
|
+
log17.warn(this.tag, `#${card.short_id} stays parked — the resume could not reclaim its session: ${holderMessage}`);
|
|
5970
|
+
try {
|
|
5971
|
+
await this.client.addComment(card.id, formatResumeConflictComment({
|
|
5972
|
+
holderMessage,
|
|
5973
|
+
branchName: this.branchName,
|
|
5974
|
+
cliSessionId: this.cliSessionId,
|
|
5975
|
+
waitHours
|
|
5976
|
+
}), {
|
|
5977
|
+
commentType: "blocker",
|
|
5978
|
+
agentSessionId: this.sessionId ?? undefined
|
|
5979
|
+
});
|
|
5980
|
+
} catch (commentErr) {
|
|
5981
|
+
log17.warn(this.tag, `Failed to post the resume-conflict note for #${card.short_id}: ${commentErr}`);
|
|
5982
|
+
}
|
|
5983
|
+
if (this.runId) {
|
|
5984
|
+
const run = this.stateStore.getRun(this.runId);
|
|
5985
|
+
try {
|
|
5986
|
+
await this.stateStore.parkRun(this.runId, {
|
|
5987
|
+
pauseTrigger: run?.pauseTrigger ?? "timeout",
|
|
5988
|
+
blockerCommentId: run?.blockerCommentId ?? null,
|
|
5989
|
+
awaitingDecisionUntil: until
|
|
5990
|
+
});
|
|
5991
|
+
} catch (storeErr) {
|
|
5992
|
+
log17.error(this.tag, `#${card.short_id} could not be re-parked after a resume conflict — the daemon has no hold on it: ${storeErr}`);
|
|
5993
|
+
}
|
|
5994
|
+
}
|
|
5995
|
+
}
|
|
5996
|
+
async parkForDecision(card, trigger) {
|
|
5997
|
+
this.state = "parked";
|
|
5998
|
+
const stats = this.lastSessionStats ?? this.progressTracker?.stats;
|
|
5999
|
+
const lastAction = this.progressTracker?.lastActionSummary ?? null;
|
|
6000
|
+
this.progressTracker?.stop();
|
|
6001
|
+
this.progressTracker = null;
|
|
6002
|
+
const waitHours = this.config.budget.pause.waitHours;
|
|
6003
|
+
const until = computeDecisionDeadline(waitHours);
|
|
6004
|
+
log17.warn(this.tag, `#${card.short_id} parked (${trigger}) — awaiting a human decision for ${waitHours}h`);
|
|
6005
|
+
const body = formatBudgetComment({
|
|
6006
|
+
trigger,
|
|
6007
|
+
numTurns: stats?.cost?.numTurns ?? 0,
|
|
6008
|
+
maxTurns: this.effectiveMaxTurns,
|
|
6009
|
+
toolCalls: stats?.toolCalls ?? 0,
|
|
6010
|
+
durationMs: stats?.cost?.durationMs ?? 0,
|
|
6011
|
+
costUsd: stats?.cost?.totalCostUsd ?? 0,
|
|
6012
|
+
lastAction,
|
|
6013
|
+
branchName: this.branchName,
|
|
6014
|
+
waitHours
|
|
6015
|
+
});
|
|
6016
|
+
let commentId = null;
|
|
6017
|
+
try {
|
|
6018
|
+
const res = await this.client.addComment(card.id, body, {
|
|
6019
|
+
commentType: "blocker"
|
|
6020
|
+
});
|
|
6021
|
+
commentId = res?.comment?.id ?? null;
|
|
6022
|
+
} catch (err) {
|
|
6023
|
+
log17.warn(this.tag, `Failed to post the budget-pause comment: ${err}`);
|
|
6024
|
+
}
|
|
6025
|
+
try {
|
|
6026
|
+
await this.client.updateAgentProgress(card.id, {
|
|
6027
|
+
agentIdentifier: agentIdentifier(this.id),
|
|
6028
|
+
agentName: `${AGENT_NAME} (Review)`,
|
|
6029
|
+
status: "blocked",
|
|
6030
|
+
currentTask: "Waiting for your decision on the turn budget",
|
|
6031
|
+
awaitingDecisionUntil: new Date(until).toISOString()
|
|
6032
|
+
});
|
|
6033
|
+
} catch (err) {
|
|
6034
|
+
log17.warn(this.tag, `Failed to mark the session blocked: ${err}`);
|
|
6035
|
+
}
|
|
6036
|
+
if (this.runId) {
|
|
6037
|
+
try {
|
|
6038
|
+
await this.stateStore.parkRun(this.runId, {
|
|
6039
|
+
pauseTrigger: trigger,
|
|
6040
|
+
blockerCommentId: commentId,
|
|
6041
|
+
awaitingDecisionUntil: until,
|
|
6042
|
+
costCents: Math.round((stats?.cost?.totalCostUsd ?? 0) * 100),
|
|
6043
|
+
numTurns: stats?.cost?.numTurns ?? 0
|
|
6044
|
+
});
|
|
6045
|
+
} catch (err) {
|
|
6046
|
+
log17.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
|
+
}
|
|
6048
|
+
}
|
|
6049
|
+
}
|
|
5322
6050
|
async pause() {
|
|
5323
6051
|
if (!this.isActive || !this.process || this.process.killed)
|
|
5324
6052
|
return;
|
|
5325
|
-
|
|
6053
|
+
log17.info(this.tag, `Pausing review on ${this.cardId}`);
|
|
5326
6054
|
signalGroup(this.process, "SIGSTOP");
|
|
5327
6055
|
if (this.timeoutTimer) {
|
|
5328
6056
|
clearTimeout(this.timeoutTimer);
|
|
@@ -5336,17 +6064,17 @@ class ReviewWorker {
|
|
|
5336
6064
|
status: "paused"
|
|
5337
6065
|
});
|
|
5338
6066
|
} catch {
|
|
5339
|
-
|
|
6067
|
+
log17.warn(this.tag, "Failed to update agent session to paused");
|
|
5340
6068
|
}
|
|
5341
6069
|
}
|
|
5342
6070
|
}
|
|
5343
6071
|
async resume() {
|
|
5344
6072
|
if (!this.isActive || !this.process || this.process.killed)
|
|
5345
6073
|
return;
|
|
5346
|
-
|
|
6074
|
+
log17.info(this.tag, `Resuming review on ${this.cardId}`);
|
|
5347
6075
|
signalGroup(this.process, "SIGCONT");
|
|
5348
6076
|
this.timeoutTimer = setTimeout(() => {
|
|
5349
|
-
|
|
6077
|
+
log17.warn(this.tag, `Timeout reached (${this.config.review.maxTimeout}ms), cancelling`);
|
|
5350
6078
|
this.timedOut = true;
|
|
5351
6079
|
this.cancel("timeout");
|
|
5352
6080
|
}, this.config.review.maxTimeout);
|
|
@@ -5358,7 +6086,7 @@ class ReviewWorker {
|
|
|
5358
6086
|
status: "working"
|
|
5359
6087
|
});
|
|
5360
6088
|
} catch {
|
|
5361
|
-
|
|
6089
|
+
log17.warn(this.tag, "Failed to update agent session to working");
|
|
5362
6090
|
}
|
|
5363
6091
|
}
|
|
5364
6092
|
}
|
|
@@ -5367,7 +6095,7 @@ class ReviewWorker {
|
|
|
5367
6095
|
return;
|
|
5368
6096
|
this.aborted = true;
|
|
5369
6097
|
this.state = "cancelling";
|
|
5370
|
-
|
|
6098
|
+
log17.info(this.tag, `Cancelling review on ${this.cardId}`);
|
|
5371
6099
|
const snapshotStats = this.lastSessionStats ?? this.progressTracker?.stats;
|
|
5372
6100
|
if (this.progressTracker) {
|
|
5373
6101
|
this.progressTracker?.stop();
|
|
@@ -5380,7 +6108,8 @@ class ReviewWorker {
|
|
|
5380
6108
|
sigtermTimeoutMs: CANCEL_SIGTERM_TIMEOUT
|
|
5381
6109
|
});
|
|
5382
6110
|
}
|
|
5383
|
-
|
|
6111
|
+
const parkingOnTimeout = this.timedOut && this.config.budget.pause.enabled;
|
|
6112
|
+
if (this.cardId && !parkingOnTimeout) {
|
|
5384
6113
|
try {
|
|
5385
6114
|
await this.client.endAgentSession(this.cardId, {
|
|
5386
6115
|
status: this.timedOut ? "failed" : endStatusForCancel(reason),
|
|
@@ -5393,7 +6122,8 @@ class ReviewWorker {
|
|
|
5393
6122
|
} catch {}
|
|
5394
6123
|
}
|
|
5395
6124
|
}
|
|
5396
|
-
spawnClaude(prompt, systemPrompt, tracker, shortId) {
|
|
6125
|
+
spawnClaude(prompt, systemPrompt, tracker, shortId, opts = {}) {
|
|
6126
|
+
const effectiveMaxTurns = opts.maxTurns ?? this.config.claude.reviewMaxTurns;
|
|
5397
6127
|
return new Promise((resolve2, reject) => {
|
|
5398
6128
|
const leanSources = this.config.claude.leanSettingSources;
|
|
5399
6129
|
const reviewDenylist = reviewDisallowedTools();
|
|
@@ -5404,21 +6134,22 @@ class ReviewWorker {
|
|
|
5404
6134
|
"--model",
|
|
5405
6135
|
this.config.claude.reviewModel,
|
|
5406
6136
|
"--max-turns",
|
|
5407
|
-
String(
|
|
6137
|
+
String(effectiveMaxTurns),
|
|
5408
6138
|
"--allowedTools",
|
|
5409
6139
|
"Bash(readonly),Read,Glob,Grep,Agent,mcp__harmony__*",
|
|
5410
6140
|
...reviewDenylist ? ["--disallowedTools", reviewDenylist] : [],
|
|
6141
|
+
...opts.resumeSessionId ? ["--resume", opts.resumeSessionId] : [],
|
|
5411
6142
|
...leanSources ? ["--setting-sources", leanSources] : [],
|
|
5412
6143
|
...systemPrompt ? ["--append-system-prompt", systemPrompt] : [],
|
|
5413
6144
|
...this.config.claude.additionalArgs,
|
|
5414
6145
|
"--",
|
|
5415
6146
|
prompt
|
|
5416
6147
|
];
|
|
5417
|
-
|
|
6148
|
+
log17.info(this.tag, `Spawning review: claude ${args.slice(0, 5).join(" ")} ...`);
|
|
5418
6149
|
const runLog = openRunLog(this.tag, this.runId, shortId);
|
|
5419
6150
|
this.lastRunLogPath = runLog?.path ?? null;
|
|
5420
6151
|
if (runLog) {
|
|
5421
|
-
|
|
6152
|
+
log17.info(this.tag, `Run log: ${runLog.path}`);
|
|
5422
6153
|
runLog.stream.write(`# run=${this.runId} card=#${shortId} pipeline=review started=${new Date().toISOString()}
|
|
5423
6154
|
` + `# args: ${args.slice(0, -2).join(" ")} -- <prompt:${prompt.length} chars>
|
|
5424
6155
|
|
|
@@ -5433,13 +6164,17 @@ class ReviewWorker {
|
|
|
5433
6164
|
const textChunks = [];
|
|
5434
6165
|
parser.on("text", (content) => {
|
|
5435
6166
|
textChunks.push(content);
|
|
6167
|
+
this.captureCliSessionId(parser.sessionId);
|
|
5436
6168
|
});
|
|
5437
6169
|
parser.on("parse_error", (msg) => {
|
|
5438
|
-
|
|
6170
|
+
log17.debug(this.tag, `Stream parse error (non-fatal): ${msg}`);
|
|
5439
6171
|
runLog?.stream.write(`
|
|
5440
6172
|
[parse_error] ${msg}
|
|
5441
6173
|
`);
|
|
5442
6174
|
});
|
|
6175
|
+
parser.on("result", (stop) => {
|
|
6176
|
+
this.lastStopReason = stop;
|
|
6177
|
+
});
|
|
5443
6178
|
if (this.process?.stdout) {
|
|
5444
6179
|
parser.attach(this.process.stdout);
|
|
5445
6180
|
if (runLog) {
|
|
@@ -5458,6 +6193,7 @@ class ReviewWorker {
|
|
|
5458
6193
|
});
|
|
5459
6194
|
this.process?.on("close", (code) => {
|
|
5460
6195
|
this.process = null;
|
|
6196
|
+
this.captureCliSessionId(parser.sessionId);
|
|
5461
6197
|
const stdout = textChunks.join("");
|
|
5462
6198
|
const stats = tracker.stats;
|
|
5463
6199
|
if (runLog) {
|
|
@@ -5466,10 +6202,21 @@ class ReviewWorker {
|
|
|
5466
6202
|
`);
|
|
5467
6203
|
runLog.stream.end();
|
|
5468
6204
|
}
|
|
5469
|
-
|
|
6205
|
+
const trigger = this.config.budget.pause.enabled ? classifyRunExit({
|
|
6206
|
+
exitCode: code ?? 1,
|
|
6207
|
+
stopReason: this.lastStopReason,
|
|
6208
|
+
numTurns: stats?.cost?.numTurns ?? 0,
|
|
6209
|
+
maxTurns: effectiveMaxTurns,
|
|
6210
|
+
timedOut: this.timedOut
|
|
6211
|
+
}) : null;
|
|
6212
|
+
if (this.timedOut && trigger) {
|
|
6213
|
+
reject(new BudgetPauseError(trigger));
|
|
6214
|
+
} else if (this.aborted) {
|
|
5470
6215
|
resolve2(stdout);
|
|
5471
6216
|
} else if (code === 0) {
|
|
5472
6217
|
resolve2(stdout);
|
|
6218
|
+
} else if (trigger) {
|
|
6219
|
+
reject(new BudgetPauseError(trigger));
|
|
5473
6220
|
} else {
|
|
5474
6221
|
reject(new Error(`claude exited with code ${code}${stderr ? `: ${stderr.slice(0, 500)}` : ""}`));
|
|
5475
6222
|
}
|
|
@@ -5499,16 +6246,16 @@ class ReviewWorker {
|
|
|
5499
6246
|
const evidence = await collectGateEvidence(registry, context);
|
|
5500
6247
|
const evaluation = gateEvaluate(resolved.gate, evidence);
|
|
5501
6248
|
await this.client.request("POST", `/cards/${encodeURIComponent(card.id)}/stage-gate-evidence`, toStageGateEvidenceInsert(context, evidence));
|
|
5502
|
-
|
|
6249
|
+
log17.info(this.tag, `Recorded review_passed gate evidence for #${card.short_id} stage "${resolved.stage.name}": result=${evidence.result} passed=${evaluation.passed}`);
|
|
5503
6250
|
} catch (err) {
|
|
5504
|
-
|
|
6251
|
+
log17.warn(this.tag, `review gate evidence collection failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
5505
6252
|
}
|
|
5506
6253
|
}
|
|
5507
6254
|
killDevServer() {
|
|
5508
6255
|
if (this.devServerProcess && !this.devServerProcess.killed) {
|
|
5509
6256
|
signalGroup(this.devServerProcess, "SIGTERM");
|
|
5510
6257
|
this.devServerProcess = null;
|
|
5511
|
-
|
|
6258
|
+
log17.debug(this.tag, "Killed dev server group");
|
|
5512
6259
|
}
|
|
5513
6260
|
}
|
|
5514
6261
|
cleanup() {
|
|
@@ -5516,13 +6263,17 @@ class ReviewWorker {
|
|
|
5516
6263
|
clearTimeout(this.timeoutTimer);
|
|
5517
6264
|
this.timeoutTimer = null;
|
|
5518
6265
|
}
|
|
6266
|
+
if (this.progressTracker) {
|
|
6267
|
+
this.progressTracker.stop();
|
|
6268
|
+
this.progressTracker = null;
|
|
6269
|
+
}
|
|
5519
6270
|
this.stopHeartbeat();
|
|
5520
6271
|
this.killDevServer();
|
|
5521
|
-
if (this.worktreePath && this.state === "error"
|
|
6272
|
+
if (this.worktreePath && this.state === "error") {
|
|
5522
6273
|
try {
|
|
5523
|
-
cleanupWorktree3(this.worktreePath
|
|
6274
|
+
cleanupWorktree3(this.worktreePath);
|
|
5524
6275
|
} catch {
|
|
5525
|
-
|
|
6276
|
+
log17.warn(this.tag, "Failed to cleanup review worktree");
|
|
5526
6277
|
}
|
|
5527
6278
|
}
|
|
5528
6279
|
this.process = null;
|
|
@@ -5534,13 +6285,15 @@ class ReviewWorker {
|
|
|
5534
6285
|
this.lastSessionStats = null;
|
|
5535
6286
|
}
|
|
5536
6287
|
}
|
|
5537
|
-
var
|
|
6288
|
+
var TAG16 = "review-worker", CANCEL_SIGINT_TIMEOUT = 30000, CANCEL_SIGTERM_TIMEOUT = 1e4;
|
|
5538
6289
|
var init_review_worker = __esm(() => {
|
|
5539
6290
|
init_dist();
|
|
5540
6291
|
init_board_helpers();
|
|
6292
|
+
init_budget_pause();
|
|
5541
6293
|
init_completion();
|
|
5542
6294
|
init_contract_phase();
|
|
5543
6295
|
init_progress_tracker();
|
|
6296
|
+
init_prompt();
|
|
5544
6297
|
init_review_completion();
|
|
5545
6298
|
init_review_prompt();
|
|
5546
6299
|
init_review_worktree();
|
|
@@ -5553,7 +6306,7 @@ var init_review_worker = __esm(() => {
|
|
|
5553
6306
|
|
|
5554
6307
|
// src/sleep-guard.ts
|
|
5555
6308
|
import { spawn } from "node:child_process";
|
|
5556
|
-
import { log as
|
|
6309
|
+
import { log as log18 } from "@gethmy/harness";
|
|
5557
6310
|
|
|
5558
6311
|
class SleepGuard {
|
|
5559
6312
|
platform;
|
|
@@ -5581,7 +6334,7 @@ class SleepGuard {
|
|
|
5581
6334
|
if (!this.child.killed)
|
|
5582
6335
|
this.child.kill("SIGTERM");
|
|
5583
6336
|
this.child = null;
|
|
5584
|
-
|
|
6337
|
+
log18.info(TAG17, "sleep assertion released");
|
|
5585
6338
|
}
|
|
5586
6339
|
}
|
|
5587
6340
|
start() {
|
|
@@ -5596,7 +6349,7 @@ class SleepGuard {
|
|
|
5596
6349
|
spawned = true;
|
|
5597
6350
|
});
|
|
5598
6351
|
child.on("error", (err) => {
|
|
5599
|
-
|
|
6352
|
+
log18.warn(TAG17, `caffeinate unavailable: ${err.message}`);
|
|
5600
6353
|
if (this.child === child)
|
|
5601
6354
|
this.child = null;
|
|
5602
6355
|
});
|
|
@@ -5609,23 +6362,23 @@ class SleepGuard {
|
|
|
5609
6362
|
});
|
|
5610
6363
|
child.unref();
|
|
5611
6364
|
this.child = child;
|
|
5612
|
-
|
|
6365
|
+
log18.info(TAG17, "sleep assertion acquired (caffeinate -i)");
|
|
5613
6366
|
} catch (err) {
|
|
5614
|
-
|
|
6367
|
+
log18.warn(TAG17, `failed to spawn caffeinate: ${err instanceof Error ? err.message : err}`);
|
|
5615
6368
|
}
|
|
5616
6369
|
}
|
|
5617
6370
|
}
|
|
5618
|
-
var
|
|
6371
|
+
var TAG17 = "sleep-guard";
|
|
5619
6372
|
var init_sleep_guard = () => {};
|
|
5620
6373
|
|
|
5621
6374
|
// src/unblock.ts
|
|
5622
|
-
import { log as
|
|
6375
|
+
import { log as log19 } from "@gethmy/harness";
|
|
5623
6376
|
async function fetchBlocksLinks(client, cardId) {
|
|
5624
6377
|
try {
|
|
5625
6378
|
const { links } = await client.getCardLinks(cardId);
|
|
5626
6379
|
return links.filter((l) => l.link_type === "blocks");
|
|
5627
6380
|
} catch (err) {
|
|
5628
|
-
|
|
6381
|
+
log19.warn(TAG18, `link fetch failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
5629
6382
|
return null;
|
|
5630
6383
|
}
|
|
5631
6384
|
}
|
|
@@ -5657,31 +6410,31 @@ async function promoteUnblockedSuccessors(completedCard, deps) {
|
|
|
5657
6410
|
const successors = links.filter((l) => l.direction === "outgoing" && !l.target_card.done);
|
|
5658
6411
|
if (successors.length === 0)
|
|
5659
6412
|
return;
|
|
5660
|
-
|
|
6413
|
+
log19.info(TAG18, `#${completedCard.short_id} completed — checking ${successors.length} chained successor(s)`);
|
|
5661
6414
|
for (const link of successors) {
|
|
5662
6415
|
const successorId = link.target_card.id;
|
|
5663
6416
|
try {
|
|
5664
6417
|
const { card } = await deps.client.getCard(successorId);
|
|
5665
6418
|
if (card.assigned_agent_id === deps.agentId) {} else if (card.assigned_agent_id === null && !card.assignee_id) {
|
|
5666
|
-
|
|
6419
|
+
log19.info(TAG18, `successor #${card.short_id} unassigned — auto-assigning to continue chain`);
|
|
5667
6420
|
await deps.client.updateCard(successorId, {
|
|
5668
6421
|
assignedAgentId: deps.agentId
|
|
5669
6422
|
});
|
|
5670
6423
|
} else {
|
|
5671
|
-
|
|
6424
|
+
log19.debug(TAG18, `successor #${card.short_id} assigned to different entity — skipping`);
|
|
5672
6425
|
continue;
|
|
5673
6426
|
}
|
|
5674
6427
|
await deps.enqueue(successorId);
|
|
5675
6428
|
} catch (err) {
|
|
5676
|
-
|
|
6429
|
+
log19.warn(TAG18, `promotion failed for successor ${successorId}: ${err instanceof Error ? err.message : err}`);
|
|
5677
6430
|
}
|
|
5678
6431
|
}
|
|
5679
6432
|
}
|
|
5680
|
-
var
|
|
6433
|
+
var TAG18 = "unblock";
|
|
5681
6434
|
var init_unblock = () => {};
|
|
5682
6435
|
|
|
5683
6436
|
// src/cli-agent-runner.ts
|
|
5684
|
-
import { log as
|
|
6437
|
+
import { log as log20 } from "@gethmy/harness";
|
|
5685
6438
|
function truncateOutput(value) {
|
|
5686
6439
|
return value === undefined ? undefined : value.slice(0, MAX_OUTPUT_LEN);
|
|
5687
6440
|
}
|
|
@@ -5831,7 +6584,7 @@ class CliAgentRunner {
|
|
|
5831
6584
|
events: batch
|
|
5832
6585
|
});
|
|
5833
6586
|
} catch (err) {
|
|
5834
|
-
|
|
6587
|
+
log20.warn(TAG19, `Failed to flush run events: ${err}`);
|
|
5835
6588
|
this.buffer.unshift(...batch);
|
|
5836
6589
|
if (this.buffer.length > MAX_BUFFER) {
|
|
5837
6590
|
this.buffer.length = MAX_BUFFER;
|
|
@@ -5868,15 +6621,19 @@ function mapCost(cost) {
|
|
|
5868
6621
|
durationMs: cost.durationMs
|
|
5869
6622
|
};
|
|
5870
6623
|
}
|
|
5871
|
-
var
|
|
6624
|
+
var TAG19 = "cli-agent-runner", FLUSH_INTERVAL_MS = 2000, MAX_BUFFER = 1000, MAX_TEXT_LEN = 8000, MAX_OUTPUT_LEN = 4000;
|
|
5872
6625
|
var init_cli_agent_runner = () => {};
|
|
5873
6626
|
|
|
5874
6627
|
// src/motor-driver.ts
|
|
5875
|
-
import { spawn as spawn2 } from "node:child_process";
|
|
5876
6628
|
import { mkdtempSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
5877
6629
|
import { createRequire as createRequire2 } from "node:module";
|
|
5878
6630
|
import { tmpdir } from "node:os";
|
|
5879
6631
|
import { dirname as dirname2, join as join4 } from "node:path";
|
|
6632
|
+
import {
|
|
6633
|
+
reapGroup,
|
|
6634
|
+
spawnInGroup as spawnInGroup2,
|
|
6635
|
+
terminateGroup as terminateGroup2
|
|
6636
|
+
} from "@gethmy/harness";
|
|
5880
6637
|
function motorApiBase(apiUrl) {
|
|
5881
6638
|
return apiUrl.replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
5882
6639
|
}
|
|
@@ -5900,7 +6657,7 @@ function evaluationFromVerdict(v, structured) {
|
|
|
5900
6657
|
}
|
|
5901
6658
|
function runMotorStage(args, deps) {
|
|
5902
6659
|
const bin = deps.binPath ?? resolveMotorBin();
|
|
5903
|
-
const spawnFn = deps.spawnFn ??
|
|
6660
|
+
const spawnFn = deps.spawnFn ?? spawnInGroup2;
|
|
5904
6661
|
const argv = [
|
|
5905
6662
|
bin,
|
|
5906
6663
|
"stage",
|
|
@@ -5926,6 +6683,14 @@ function runMotorStage(args, deps) {
|
|
|
5926
6683
|
},
|
|
5927
6684
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5928
6685
|
});
|
|
6686
|
+
const pgid = child.pid;
|
|
6687
|
+
let swept = false;
|
|
6688
|
+
const sweepGroupOnce = () => {
|
|
6689
|
+
if (swept)
|
|
6690
|
+
return;
|
|
6691
|
+
swept = true;
|
|
6692
|
+
reapGroup(pgid);
|
|
6693
|
+
};
|
|
5929
6694
|
const lines = [];
|
|
5930
6695
|
let verdict = null;
|
|
5931
6696
|
let structured = null;
|
|
@@ -5941,15 +6706,15 @@ function runMotorStage(args, deps) {
|
|
|
5941
6706
|
resolve2(result);
|
|
5942
6707
|
};
|
|
5943
6708
|
function onAbort() {
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
}
|
|
5950
|
-
}
|
|
5951
|
-
|
|
5952
|
-
}
|
|
6709
|
+
(async () => {
|
|
6710
|
+
try {
|
|
6711
|
+
await terminateGroup2(child, {
|
|
6712
|
+
sigintTimeoutMs: ABORT_SIGINT_GRACE_MS,
|
|
6713
|
+
sigtermTimeoutMs: ABORT_SIGTERM_GRACE_MS
|
|
6714
|
+
});
|
|
6715
|
+
} catch {}
|
|
6716
|
+
sweepGroupOnce();
|
|
6717
|
+
})();
|
|
5953
6718
|
settleOnce({
|
|
5954
6719
|
exitCode: 1,
|
|
5955
6720
|
lines,
|
|
@@ -5975,7 +6740,8 @@ function runMotorStage(args, deps) {
|
|
|
5975
6740
|
let line = null;
|
|
5976
6741
|
try {
|
|
5977
6742
|
line = JSON.parse(raw);
|
|
5978
|
-
|
|
6743
|
+
if (line.type !== "agent_event")
|
|
6744
|
+
lines.push(line);
|
|
5979
6745
|
if (line.type === "gate_verdict") {
|
|
5980
6746
|
verdict = { passed: line.passed, findings: line.findings };
|
|
5981
6747
|
}
|
|
@@ -6024,6 +6790,7 @@ function runMotorStage(args, deps) {
|
|
|
6024
6790
|
});
|
|
6025
6791
|
});
|
|
6026
6792
|
child.on("close", (code) => {
|
|
6793
|
+
sweepGroupOnce();
|
|
6027
6794
|
settleOnce({
|
|
6028
6795
|
exitCode: code ?? 1,
|
|
6029
6796
|
lines,
|
|
@@ -6034,156 +6801,9 @@ function runMotorStage(args, deps) {
|
|
|
6034
6801
|
});
|
|
6035
6802
|
});
|
|
6036
6803
|
}
|
|
6037
|
-
var
|
|
6804
|
+
var ABORT_SIGINT_GRACE_MS = 12000, ABORT_SIGTERM_GRACE_MS = 6000;
|
|
6038
6805
|
var init_motor_driver = () => {};
|
|
6039
6806
|
|
|
6040
|
-
// src/prompt.ts
|
|
6041
|
-
import { log as log20 } from "@gethmy/harness";
|
|
6042
|
-
function renderPreviousAttemptsSection(failures) {
|
|
6043
|
-
if (failures.length === 0)
|
|
6044
|
-
return "";
|
|
6045
|
-
const lines = failures.map((f) => {
|
|
6046
|
-
const tag = f.reason ? `[${f.reason}] ` : "";
|
|
6047
|
-
return `- ${tag}${f.summary}`;
|
|
6048
|
-
});
|
|
6049
|
-
return [
|
|
6050
|
-
"## Previous attempt feedback",
|
|
6051
|
-
"This is a re-attempt on the branch your last run already pushed — build on that existing work and FIX the issues below. Do NOT reimplement from scratch or revert the prior commits.",
|
|
6052
|
-
...lines
|
|
6053
|
-
].join(`
|
|
6054
|
-
`);
|
|
6055
|
-
}
|
|
6056
|
-
async function buildPrompt(enriched, branchName, worktreePath, client, workspaceId, projectId) {
|
|
6057
|
-
const { card } = enriched;
|
|
6058
|
-
const pastEpisodesSection = await renderPastEpisodesSection(client, card.title, card.description ?? "", workspaceId, projectId);
|
|
6059
|
-
try {
|
|
6060
|
-
const result = await client.generateCardPrompt({
|
|
6061
|
-
cardId: card.id,
|
|
6062
|
-
workspaceId,
|
|
6063
|
-
projectId,
|
|
6064
|
-
variant: "execute",
|
|
6065
|
-
customConstraints: `You are working in a git worktree at \`${worktreePath}\` on branch \`${branchName}\`.
|
|
6066
|
-
Do NOT push to main. All your work stays on \`${branchName}\`.
|
|
6067
|
-
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.`
|
|
6068
|
-
});
|
|
6069
|
-
log20.info(TAG19, `Generated prompt for #${card.short_id} — ${result.contextSummary.memoryCount} memories, ${result.tokenEstimate} tokens`);
|
|
6070
|
-
return result.prompt + pastEpisodesSection;
|
|
6071
|
-
} catch (err) {
|
|
6072
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
6073
|
-
log20.warn(TAG19, `Failed to generate prompt via API, using fallback: ${msg}`);
|
|
6074
|
-
const commentsSection = await renderCommentsSection(client, card.id);
|
|
6075
|
-
return buildFallbackPrompt(enriched, branchName, worktreePath) + commentsSection + pastEpisodesSection;
|
|
6076
|
-
}
|
|
6077
|
-
}
|
|
6078
|
-
async function renderCommentsSection(client, cardId) {
|
|
6079
|
-
try {
|
|
6080
|
-
const { comments } = await client.request("GET", `/cards/${encodeURIComponent(cardId)}/comments?limit=200&order=desc`);
|
|
6081
|
-
if (!Array.isArray(comments) || comments.length === 0)
|
|
6082
|
-
return "";
|
|
6083
|
-
const section = serializeCommentThread(comments, {
|
|
6084
|
-
heading: "Comments",
|
|
6085
|
-
maxComments: 40
|
|
6086
|
-
});
|
|
6087
|
-
return section ? `
|
|
6088
|
-
|
|
6089
|
-
${section}` : "";
|
|
6090
|
-
} catch (err) {
|
|
6091
|
-
log20.warn(TAG19, "comment-thread fetch failed", {
|
|
6092
|
-
event: "comment_fetch_failed",
|
|
6093
|
-
error: err instanceof Error ? err.message : String(err)
|
|
6094
|
-
});
|
|
6095
|
-
return "";
|
|
6096
|
-
}
|
|
6097
|
-
}
|
|
6098
|
-
async function renderPastEpisodesSection(client, title, description, workspaceId, projectId) {
|
|
6099
|
-
if (!projectId)
|
|
6100
|
-
return "";
|
|
6101
|
-
try {
|
|
6102
|
-
const query = `${title}
|
|
6103
|
-
${description}`.trim();
|
|
6104
|
-
const { entities } = await client.harmonyRecall({
|
|
6105
|
-
workspaceId,
|
|
6106
|
-
projectId,
|
|
6107
|
-
query,
|
|
6108
|
-
type: ["solution", "error"],
|
|
6109
|
-
memory_tier: "episode",
|
|
6110
|
-
scope: "project",
|
|
6111
|
-
topK: 3
|
|
6112
|
-
});
|
|
6113
|
-
if (entities.length === 0)
|
|
6114
|
-
return "";
|
|
6115
|
-
const bullets = entities.map((entity) => {
|
|
6116
|
-
const e = entity;
|
|
6117
|
-
const meta = e.metadata ?? {};
|
|
6118
|
-
const outcomeTag = meta.outcome ? `[${meta.outcome}]` : "[?]";
|
|
6119
|
-
const approach = meta.approach_summary ?? "";
|
|
6120
|
-
const lines = [
|
|
6121
|
-
`- ${outcomeTag} ${e.title ?? "(untitled episode)"}`,
|
|
6122
|
-
` Approach: ${approach}`
|
|
6123
|
-
];
|
|
6124
|
-
if (meta.key_insight)
|
|
6125
|
-
lines.push(` Key insight: ${meta.key_insight}`);
|
|
6126
|
-
if (meta.changed_files && meta.changed_files.length > 0) {
|
|
6127
|
-
const shown = meta.changed_files.slice(0, 8);
|
|
6128
|
-
const extra = meta.changed_files.length - shown.length;
|
|
6129
|
-
const suffix = extra > 0 ? ` (+${extra} more)` : "";
|
|
6130
|
-
lines.push(` Changed files: ${shown.join(", ")}${suffix}`);
|
|
6131
|
-
}
|
|
6132
|
-
return lines.join(`
|
|
6133
|
-
`);
|
|
6134
|
-
}).join(`
|
|
6135
|
-
`);
|
|
6136
|
-
return `
|
|
6137
|
-
|
|
6138
|
-
## Similar past tasks
|
|
6139
|
-
${bullets}`;
|
|
6140
|
-
} catch (err) {
|
|
6141
|
-
log20.warn(TAG19, "past-episodes recall failed", {
|
|
6142
|
-
event: "episode_recall_failed",
|
|
6143
|
-
error: err instanceof Error ? err.message : String(err)
|
|
6144
|
-
});
|
|
6145
|
-
return "";
|
|
6146
|
-
}
|
|
6147
|
-
}
|
|
6148
|
-
function buildFallbackPrompt(enriched, branchName, worktreePath) {
|
|
6149
|
-
const { card, column, labels, subtasks } = enriched;
|
|
6150
|
-
const labelStr = labels.length > 0 ? labels.map((l) => l.name).join(", ") : "none";
|
|
6151
|
-
const subtaskStr = subtasks.length > 0 ? subtasks.map((s) => `- [${s.completed ? "x" : " "}] ${s.title}`).join(`
|
|
6152
|
-
`) : "No subtasks defined.";
|
|
6153
|
-
const description = card.description?.trim() || "No description provided.";
|
|
6154
|
-
return `You are an AI agent working on a task from the Harmony project board.
|
|
6155
|
-
|
|
6156
|
-
## Card: #${card.short_id} - ${card.title}
|
|
6157
|
-
**Labels**: ${labelStr}
|
|
6158
|
-
**Column**: ${column.name}
|
|
6159
|
-
**Priority**: ${card.priority}
|
|
6160
|
-
|
|
6161
|
-
## Description
|
|
6162
|
-
${description}
|
|
6163
|
-
|
|
6164
|
-
## Subtasks
|
|
6165
|
-
${subtaskStr}
|
|
6166
|
-
|
|
6167
|
-
## Instructions
|
|
6168
|
-
1. Read the codebase and understand the context needed for this task
|
|
6169
|
-
2. Report progress via harmony_update_agent_progress at key milestones:
|
|
6170
|
-
- After reading codebase and forming a plan (~20%)
|
|
6171
|
-
- After each major implementation step (~30-60%)
|
|
6172
|
-
- After completing each subtask (also toggle via harmony_toggle_subtask)
|
|
6173
|
-
- Before committing (~65%)
|
|
6174
|
-
Include a brief currentTask description.
|
|
6175
|
-
3. Implement the changes on branch \`${branchName}\`
|
|
6176
|
-
4. Commit your work with clear, descriptive commit messages
|
|
6177
|
-
5. When the work is committed, STOP. The daemon owns the run lifecycle: 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 — those tools are disabled for this run.
|
|
6178
|
-
|
|
6179
|
-
You are working in a git worktree at \`${worktreePath}\` on branch \`${branchName}\`.
|
|
6180
|
-
Do NOT push to main. All your work stays on \`${branchName}\`.`;
|
|
6181
|
-
}
|
|
6182
|
-
var TAG19 = "prompt";
|
|
6183
|
-
var init_prompt = __esm(() => {
|
|
6184
|
-
init_dist();
|
|
6185
|
-
});
|
|
6186
|
-
|
|
6187
6807
|
// src/stage-advance.ts
|
|
6188
6808
|
import { gateConfigErrorReason, log as log21 } from "@gethmy/harness";
|
|
6189
6809
|
function handoffText(stage) {
|
|
@@ -6388,10 +7008,7 @@ async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps
|
|
|
6388
7008
|
}
|
|
6389
7009
|
const next = nextStageAfter(def, stageIndex);
|
|
6390
7010
|
if (next.kind === "terminal") {
|
|
6391
|
-
await persistStagePointer(deps.client, card, {
|
|
6392
|
-
currentStage: null,
|
|
6393
|
-
done: true
|
|
6394
|
-
});
|
|
7011
|
+
await persistStagePointer(deps.client, card, { currentStage: null });
|
|
6395
7012
|
deps.sink?.recordPlaybookAdvanced({
|
|
6396
7013
|
fromStageId: stage.id,
|
|
6397
7014
|
fromStageName: stage.name,
|
|
@@ -6401,7 +7018,7 @@ async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps
|
|
|
6401
7018
|
reason: "Playbook complete — final stage gate passed."
|
|
6402
7019
|
});
|
|
6403
7020
|
deps.stateStore.recordOutcome(card.id, "success").catch(() => {});
|
|
6404
|
-
log21.info(TAG20, `#${card.short_id} terminal stage "${stage.name}" passed —
|
|
7021
|
+
log21.info(TAG20, `#${card.short_id} terminal stage "${stage.name}" passed — playbook complete (done left to the column)`);
|
|
6405
7022
|
return { kind: "completed_terminal" };
|
|
6406
7023
|
}
|
|
6407
7024
|
if (next.kind === "out_of_range") {
|
|
@@ -6508,6 +7125,7 @@ var init_stage_advance = __esm(() => {
|
|
|
6508
7125
|
});
|
|
6509
7126
|
|
|
6510
7127
|
// src/worker.ts
|
|
7128
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
6511
7129
|
import { rmSync } from "node:fs";
|
|
6512
7130
|
import { dirname as dirname3 } from "node:path";
|
|
6513
7131
|
import {
|
|
@@ -6521,12 +7139,15 @@ import {
|
|
|
6521
7139
|
makeBranchName,
|
|
6522
7140
|
normalizeGateSpec,
|
|
6523
7141
|
pushBranch as pushBranch3,
|
|
6524
|
-
|
|
7142
|
+
readWorktreeHead,
|
|
7143
|
+
reapGroup as reapGroup2,
|
|
6525
7144
|
SdkAgentRunner,
|
|
6526
7145
|
signalGroup as signalGroup2,
|
|
6527
|
-
|
|
7146
|
+
sizeRun,
|
|
7147
|
+
sizingEventSource,
|
|
7148
|
+
spawnInGroup as spawnInGroup3,
|
|
6528
7149
|
teardownWorktree as teardownWorktree2,
|
|
6529
|
-
terminateGroup as
|
|
7150
|
+
terminateGroup as terminateGroup3,
|
|
6530
7151
|
WorktreeBaseError
|
|
6531
7152
|
} from "@gethmy/harness";
|
|
6532
7153
|
function sdkDraftLogLine(ev) {
|
|
@@ -6574,12 +7195,6 @@ function buildStagePreamble(stage) {
|
|
|
6574
7195
|
return lines.filter(Boolean).join(`
|
|
6575
7196
|
`);
|
|
6576
7197
|
}
|
|
6577
|
-
function buildSteeringPrompt(messages) {
|
|
6578
|
-
if (messages.length === 1)
|
|
6579
|
-
return messages[0];
|
|
6580
|
-
return messages.map((m, i) => `${i + 1}. ${m}`).join(`
|
|
6581
|
-
`);
|
|
6582
|
-
}
|
|
6583
7198
|
function computeRunSpawnGating(stageAllowedTools) {
|
|
6584
7199
|
const denylist = stageDisallowedTools();
|
|
6585
7200
|
return {
|
|
@@ -6587,6 +7202,14 @@ function computeRunSpawnGating(stageAllowedTools) {
|
|
|
6587
7202
|
...denylist ? { disallowedTools: denylist } : {}
|
|
6588
7203
|
};
|
|
6589
7204
|
}
|
|
7205
|
+
function buildRunFailureSummary(apiKind, baseError, msg) {
|
|
7206
|
+
if (apiKind !== null)
|
|
7207
|
+
return describeApiError(apiKind);
|
|
7208
|
+
if (baseError) {
|
|
7209
|
+
return `${msg.slice(0, 300)} — requeued without counting an attempt`;
|
|
7210
|
+
}
|
|
7211
|
+
return `Run failed: ${msg.slice(0, 300)}`;
|
|
7212
|
+
}
|
|
6590
7213
|
|
|
6591
7214
|
class Worker {
|
|
6592
7215
|
config;
|
|
@@ -6603,6 +7226,7 @@ class Worker {
|
|
|
6603
7226
|
cardId = null;
|
|
6604
7227
|
branchName = null;
|
|
6605
7228
|
worktreePath = null;
|
|
7229
|
+
runBaselineSha = null;
|
|
6606
7230
|
startedAt = null;
|
|
6607
7231
|
process = null;
|
|
6608
7232
|
timeoutTimer = null;
|
|
@@ -6615,14 +7239,23 @@ class Worker {
|
|
|
6615
7239
|
aborted = false;
|
|
6616
7240
|
timedOut = false;
|
|
6617
7241
|
verificationFailed = false;
|
|
7242
|
+
lastStopReason = null;
|
|
7243
|
+
lastActionSummary = null;
|
|
6618
7244
|
held = false;
|
|
6619
7245
|
sessionConflict = false;
|
|
6620
7246
|
activeRunSpawnOpts = null;
|
|
6621
7247
|
completionStarted = false;
|
|
6622
7248
|
sessionId = null;
|
|
7249
|
+
sizing = null;
|
|
7250
|
+
modelChoice = null;
|
|
6623
7251
|
runId = null;
|
|
6624
7252
|
cliSessionId = null;
|
|
6625
7253
|
lastDrainedSeq = 0;
|
|
7254
|
+
grantedTurns = null;
|
|
7255
|
+
resumeMessage = null;
|
|
7256
|
+
get effectiveMaxTurns() {
|
|
7257
|
+
return this.grantedTurns ?? this.config.claude.maxTurns;
|
|
7258
|
+
}
|
|
6626
7259
|
runCostCents = 0;
|
|
6627
7260
|
runTurns = 0;
|
|
6628
7261
|
lastRunText = "";
|
|
@@ -6698,6 +7331,8 @@ class Worker {
|
|
|
6698
7331
|
this.aborted = false;
|
|
6699
7332
|
this.timedOut = false;
|
|
6700
7333
|
this.verificationFailed = false;
|
|
7334
|
+
this.lastStopReason = null;
|
|
7335
|
+
this.lastActionSummary = null;
|
|
6701
7336
|
this.held = false;
|
|
6702
7337
|
this.sessionConflict = false;
|
|
6703
7338
|
this.completionStarted = false;
|
|
@@ -6707,92 +7342,138 @@ class Worker {
|
|
|
6707
7342
|
this.cliSessionId = null;
|
|
6708
7343
|
this.lastDrainedSeq = 0;
|
|
6709
7344
|
this.activeRunSpawnOpts = null;
|
|
7345
|
+
this.grantedTurns = null;
|
|
7346
|
+
this.resumeMessage = null;
|
|
7347
|
+
this.sizing = null;
|
|
7348
|
+
this.modelChoice = null;
|
|
6710
7349
|
this.cardId = card.id;
|
|
6711
7350
|
this.startedAt = Date.now();
|
|
6712
7351
|
this.runId = newRunId();
|
|
7352
|
+
const resuming = this.stateStore.getResumableRunForCard(card.id);
|
|
7353
|
+
if (resuming) {
|
|
7354
|
+
this.runId = resuming.runId;
|
|
7355
|
+
this.worktreePath = resuming.worktreePath;
|
|
7356
|
+
this.branchName = resuming.branchName;
|
|
7357
|
+
this.cliSessionId = resuming.cliSessionId ?? null;
|
|
7358
|
+
this.sessionId = resuming.sessionId;
|
|
7359
|
+
this.grantedTurns = resuming.grantedTurns ?? null;
|
|
7360
|
+
this.resumeMessage = resuming.resumeMessage ?? null;
|
|
7361
|
+
try {
|
|
7362
|
+
await this.stateStore.updateRun(resuming.runId, {
|
|
7363
|
+
grantedTurns: null,
|
|
7364
|
+
resumeMessage: null
|
|
7365
|
+
});
|
|
7366
|
+
} catch (err) {
|
|
7367
|
+
log22.warn(this.tag, `Failed to clear the consumed grant: ${err}`);
|
|
7368
|
+
}
|
|
7369
|
+
}
|
|
6713
7370
|
try {
|
|
6714
7371
|
this.state = "preparing";
|
|
6715
|
-
|
|
6716
|
-
|
|
7372
|
+
if (!resuming) {
|
|
7373
|
+
this.branchName = makeBranchName(card.short_id, card.title, this.config.worktree.failedBranchPrefix);
|
|
7374
|
+
}
|
|
7375
|
+
log22.info(this.tag, resuming ? `Resuming #${card.short_id} "${card.title}" with ${this.grantedTurns ?? "the default"} more turns` : `Preparing #${card.short_id} "${card.title}"`);
|
|
6717
7376
|
const attemptCount = await this.stateStore.incrementAttempt(card.id);
|
|
6718
7377
|
const isRework = attemptCount > 1;
|
|
7378
|
+
const recordedBranch = extractBranchRef(card.description);
|
|
7379
|
+
const continuesPushedWork = isRework || recordsPushedWorkOn(card.description, this.branchName);
|
|
7380
|
+
if (continuesPushedWork && !isRework) {
|
|
7381
|
+
log22.info(this.tag, `Card records completed work on ${this.branchName} — continuing that branch instead of rebuilding from ${this.config.worktree.baseBranch}`);
|
|
7382
|
+
} else if (recordedBranch && recordedBranch !== this.branchName) {
|
|
7383
|
+
log22.warn(this.tag, `Card records branch ${recordedBranch} but this run targets ${this.branchName} — starting fresh; the recorded branch is left untouched`);
|
|
7384
|
+
}
|
|
6719
7385
|
this.startHeartbeat();
|
|
6720
|
-
await this.
|
|
6721
|
-
runId: this.runId,
|
|
6722
|
-
cardId: card.id,
|
|
6723
|
-
cardShortId: card.short_id,
|
|
6724
|
-
pipeline: "implement",
|
|
6725
|
-
workerId: this.id,
|
|
6726
|
-
sessionId: null,
|
|
6727
|
-
worktreePath: null,
|
|
6728
|
-
branchName: this.branchName,
|
|
6729
|
-
daemonPid: process.pid,
|
|
6730
|
-
phase: "preparing",
|
|
6731
|
-
startedAt: this.startedAt,
|
|
6732
|
-
lastHeartbeatAt: this.startedAt,
|
|
6733
|
-
endedAt: null,
|
|
6734
|
-
status: "active",
|
|
6735
|
-
costCents: 0,
|
|
6736
|
-
numTurns: 0
|
|
6737
|
-
});
|
|
7386
|
+
this.sizing = await this.sizeThisRun(card);
|
|
6738
7387
|
const implementModel = this.selectImplementModel(card);
|
|
6739
|
-
let
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
7388
|
+
let stageCtx = { kind: "generic" };
|
|
7389
|
+
if (!resuming) {
|
|
7390
|
+
await this.stateStore.insertRun({
|
|
7391
|
+
runId: this.runId,
|
|
7392
|
+
cardId: card.id,
|
|
7393
|
+
cardShortId: card.short_id,
|
|
7394
|
+
pipeline: "implement",
|
|
7395
|
+
workerId: this.id,
|
|
7396
|
+
sessionId: null,
|
|
7397
|
+
worktreePath: null,
|
|
7398
|
+
branchName: this.branchName,
|
|
7399
|
+
daemonPid: process.pid,
|
|
7400
|
+
phase: "preparing",
|
|
7401
|
+
startedAt: this.startedAt,
|
|
7402
|
+
lastHeartbeatAt: this.startedAt,
|
|
7403
|
+
endedAt: null,
|
|
7404
|
+
status: "active",
|
|
7405
|
+
costCents: 0,
|
|
7406
|
+
numTurns: 0
|
|
6750
7407
|
});
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
7408
|
+
let session;
|
|
7409
|
+
try {
|
|
7410
|
+
const started = await this.client.startAgentSession(card.id, {
|
|
7411
|
+
agentIdentifier: agentIdentifier(this.id),
|
|
7412
|
+
agentName: AGENT_NAME,
|
|
7413
|
+
agentId: this.identity.agentId,
|
|
7414
|
+
status: "working",
|
|
7415
|
+
currentTask: "Setting up worktree",
|
|
7416
|
+
progressPercent: 5,
|
|
7417
|
+
modelName: implementModel,
|
|
7418
|
+
driver: "daemon",
|
|
7419
|
+
awaitingDecisionUntil: null
|
|
7420
|
+
});
|
|
7421
|
+
session = started.session;
|
|
7422
|
+
} catch (err) {
|
|
7423
|
+
if (isSessionConflict(err)) {
|
|
7424
|
+
this.sessionConflict = true;
|
|
7425
|
+
log22.info(this.tag, `Skipping #${card.short_id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
7426
|
+
await this.stateStore.decrementAttempt(card.id);
|
|
7427
|
+
return;
|
|
7428
|
+
}
|
|
7429
|
+
throw err;
|
|
6758
7430
|
}
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
|
|
7431
|
+
const sid = session && typeof session === "object" && "id" in session ? session.id : null;
|
|
7432
|
+
if (!sid) {
|
|
7433
|
+
log22.warn(TAG21, "startAgentSession returned no session id");
|
|
7434
|
+
}
|
|
7435
|
+
this.sessionId = sid;
|
|
6764
7436
|
}
|
|
6765
|
-
this.sessionId = sid;
|
|
6766
7437
|
if (this.sessionId) {
|
|
6767
7438
|
this.cliRunner = new CliAgentRunner(this.client, card.id, this.sessionId);
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
7439
|
+
if (!resuming) {
|
|
7440
|
+
this.cliRunner.recordRunStarted({
|
|
7441
|
+
runner: this.config.runner,
|
|
7442
|
+
model: implementModel
|
|
7443
|
+
});
|
|
7444
|
+
this.recordRunSized();
|
|
7445
|
+
}
|
|
6772
7446
|
}
|
|
6773
7447
|
await this.recordPhase("preparing");
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
7448
|
+
if (!resuming) {
|
|
7449
|
+
const moved = await moveCardAndAddLabel(this.client, card, IN_PROGRESS_COLUMN, "agent");
|
|
7450
|
+
if (!moved) {
|
|
7451
|
+
log22.warn(this.tag, `Card #${card.short_id} was NOT moved to "In Progress" — check API logs`);
|
|
7452
|
+
}
|
|
6777
7453
|
}
|
|
6778
7454
|
if (this.aborted)
|
|
6779
7455
|
return;
|
|
6780
|
-
|
|
7456
|
+
if (!resuming) {
|
|
7457
|
+
card = await this.autoBindPlaybookAtPickup(card, labels);
|
|
7458
|
+
}
|
|
6781
7459
|
if (this.aborted)
|
|
6782
7460
|
return;
|
|
6783
|
-
|
|
7461
|
+
stageCtx = await this.resolveStageContext(card);
|
|
6784
7462
|
if (stageCtx.kind === "hold") {
|
|
6785
7463
|
this.held = true;
|
|
6786
7464
|
await this.holdStageCard(card, stageCtx.reason, stageCtx.wait);
|
|
6787
7465
|
return;
|
|
6788
7466
|
}
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
|
|
7467
|
+
if (!resuming) {
|
|
7468
|
+
this.worktreePath = createWorktree(this.config.worktree.basePath, this.config.worktree.baseBranch, this.branchName, {
|
|
7469
|
+
continueExisting: stageCtx.kind === "run" || stageCtx.kind === "motor" || continuesPushedWork
|
|
7470
|
+
});
|
|
7471
|
+
this.runBaselineSha = readWorktreeHead(this.worktreePath);
|
|
7472
|
+
}
|
|
6792
7473
|
if (this.aborted)
|
|
6793
7474
|
return;
|
|
6794
|
-
if (stageCtx.kind === "motor") {
|
|
6795
|
-
await this.runMotorStageCtx(card, stageCtx);
|
|
7475
|
+
if (!resuming && stageCtx.kind === "motor") {
|
|
7476
|
+
await this.runMotorStageCtx(card, stageCtx, subtasks);
|
|
6796
7477
|
return;
|
|
6797
7478
|
}
|
|
6798
7479
|
const enriched = {
|
|
@@ -6802,12 +7483,12 @@ class Worker {
|
|
|
6802
7483
|
subtasks,
|
|
6803
7484
|
mode: "implement"
|
|
6804
7485
|
};
|
|
6805
|
-
if (stageCtx.kind !== "run" && shouldRunContract(this.config.contractFirst)) {
|
|
7486
|
+
if (!resuming && stageCtx.kind !== "run" && shouldRunContract(this.config.contractFirst)) {
|
|
6806
7487
|
await this.runContractPhase(enriched);
|
|
6807
7488
|
if (this.aborted)
|
|
6808
7489
|
return;
|
|
6809
7490
|
}
|
|
6810
|
-
if (shouldPlan(enriched, this.config.planning)) {
|
|
7491
|
+
if (!resuming && shouldPlan(enriched, this.config.planning)) {
|
|
6811
7492
|
this.state = "planning";
|
|
6812
7493
|
await this.recordPhase("planning");
|
|
6813
7494
|
const parked = await this.runPlanningPhase(enriched);
|
|
@@ -6829,33 +7510,40 @@ class Worker {
|
|
|
6829
7510
|
prompt = [buildStagePreamble(stageCtx.stage), inherited, basePrompt].filter(Boolean).join(`
|
|
6830
7511
|
|
|
6831
7512
|
`);
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
stageName: stageCtx.stage.name,
|
|
6835
|
-
owner: stageCtx.stage.owner
|
|
6836
|
-
});
|
|
6837
|
-
if (isLoop && loop) {
|
|
6838
|
-
const priorIterations = this.stateStore.getLoopIterations(card.id, stageCtx.stage.id);
|
|
6839
|
-
this.cliRunner?.recordLoopIterationStarted({
|
|
7513
|
+
if (!resuming) {
|
|
7514
|
+
this.cliRunner?.recordStageEntered({
|
|
6840
7515
|
stageId: stageCtx.stage.id,
|
|
6841
7516
|
stageName: stageCtx.stage.name,
|
|
6842
|
-
|
|
6843
|
-
maxIterations: Math.max(1, Math.floor(loop.max_iterations) || 1),
|
|
6844
|
-
mode: loop.mode
|
|
7517
|
+
owner: stageCtx.stage.owner
|
|
6845
7518
|
});
|
|
7519
|
+
if (isLoop && loop) {
|
|
7520
|
+
const priorIterations = this.stateStore.getLoopIterations(card.id, stageCtx.stage.id);
|
|
7521
|
+
this.cliRunner?.recordLoopIterationStarted({
|
|
7522
|
+
stageId: stageCtx.stage.id,
|
|
7523
|
+
stageName: stageCtx.stage.name,
|
|
7524
|
+
iteration: priorIterations + 1,
|
|
7525
|
+
maxIterations: Math.max(1, Math.floor(loop.max_iterations) || 1),
|
|
7526
|
+
mode: loop.mode
|
|
7527
|
+
});
|
|
7528
|
+
}
|
|
6846
7529
|
}
|
|
6847
|
-
} else if (
|
|
7530
|
+
} else if (continuesPushedWork) {
|
|
6848
7531
|
const digest = renderPreviousAttemptsSection(this.stateStore.getRecentFailures(card.id, 3));
|
|
6849
7532
|
if (digest)
|
|
6850
7533
|
prompt = `${digest}
|
|
6851
7534
|
|
|
6852
7535
|
${basePrompt}`;
|
|
7536
|
+
}
|
|
7537
|
+
if (resuming && this.resumeMessage) {
|
|
7538
|
+
prompt = `${buildSteeringPrompt([this.resumeMessage])}
|
|
7539
|
+
|
|
7540
|
+
${prompt}`;
|
|
6853
7541
|
}
|
|
6854
7542
|
await this.client.updateAgentProgress(card.id, {
|
|
6855
7543
|
agentIdentifier: agentIdentifier(this.id),
|
|
6856
7544
|
agentName: AGENT_NAME,
|
|
6857
7545
|
status: "working",
|
|
6858
|
-
currentTask: stageCtx.kind === "run" ? `Running stage "${stageCtx.stage.name}"` : "Running Claude CLI",
|
|
7546
|
+
currentTask: resuming ? "Resuming Claude CLI" : stageCtx.kind === "run" ? `Running stage "${stageCtx.stage.name}"` : "Running Claude CLI",
|
|
6859
7547
|
progressPercent: 10
|
|
6860
7548
|
});
|
|
6861
7549
|
this.timeoutTimer = setTimeout(() => {
|
|
@@ -6866,6 +7554,8 @@ ${basePrompt}`;
|
|
|
6866
7554
|
this.activeRunSpawnOpts = computeRunSpawnGating(stageCtx.kind === "run" ? stageCtx.allowedTools : null);
|
|
6867
7555
|
await this.spawnClaude(prompt, card, subtasks, {
|
|
6868
7556
|
model: implementModel,
|
|
7557
|
+
maxTurns: this.grantedTurns ?? undefined,
|
|
7558
|
+
resumeSessionId: this.cliSessionId ?? undefined,
|
|
6869
7559
|
...this.activeRunSpawnOpts ?? {}
|
|
6870
7560
|
});
|
|
6871
7561
|
if (this.aborted)
|
|
@@ -6899,7 +7589,11 @@ ${basePrompt}`;
|
|
|
6899
7589
|
stageGateEvaluation = await this.collectStageGateEvidence(card, stageRun.stage, worktreePath, subtasks);
|
|
6900
7590
|
return stageEndDisposition(stageGateEvaluation, stageRun.stage, stageRun.index, stageRun.def);
|
|
6901
7591
|
} : undefined;
|
|
6902
|
-
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);
|
|
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);
|
|
7593
|
+
if (completed === "park") {
|
|
7594
|
+
await this.parkForDecision(card, "max_turns");
|
|
7595
|
+
return;
|
|
7596
|
+
}
|
|
6903
7597
|
this.worktreePath = null;
|
|
6904
7598
|
this.verificationFailed = !completed;
|
|
6905
7599
|
if (completed && stageRun) {
|
|
@@ -6920,6 +7614,14 @@ ${basePrompt}`;
|
|
|
6920
7614
|
}
|
|
6921
7615
|
}
|
|
6922
7616
|
} catch (err) {
|
|
7617
|
+
if (err instanceof BudgetPauseError) {
|
|
7618
|
+
await this.parkForDecision(card, err.trigger);
|
|
7619
|
+
return;
|
|
7620
|
+
}
|
|
7621
|
+
if (resuming && isSessionConflict(err)) {
|
|
7622
|
+
await this.holdParkOnSessionConflict(card, err);
|
|
7623
|
+
return;
|
|
7624
|
+
}
|
|
6923
7625
|
this.state = "error";
|
|
6924
7626
|
const msg = err instanceof Error ? err.message : String(err);
|
|
6925
7627
|
log22.error(this.tag, `Error on #${card.short_id}: ${msg}`);
|
|
@@ -6950,7 +7652,7 @@ ${basePrompt}`;
|
|
|
6950
7652
|
this.worktreePath = null;
|
|
6951
7653
|
}
|
|
6952
7654
|
const failureReason = apiError ? errClass.kind : "other";
|
|
6953
|
-
const failureSummary =
|
|
7655
|
+
const failureSummary = buildRunFailureSummary(errClass.kind, baseError, msg);
|
|
6954
7656
|
try {
|
|
6955
7657
|
await runTransition(this.client, card, {
|
|
6956
7658
|
move: { columnName: this.config.pickupColumns[0] ?? "To Do" },
|
|
@@ -6978,8 +7680,8 @@ ${basePrompt}`;
|
|
|
6978
7680
|
}
|
|
6979
7681
|
}
|
|
6980
7682
|
} finally {
|
|
6981
|
-
const succeeded = this.runId && !this.held && !this.sessionConflict && this.state !== "error" && !this.aborted && !this.verificationFailed;
|
|
6982
|
-
if (this.held) {} else if (this.sessionConflict) {
|
|
7683
|
+
const succeeded = this.runId && !this.held && !this.sessionConflict && this.state !== "error" && this.state !== "parked" && !this.aborted && !this.verificationFailed;
|
|
7684
|
+
if (this.state === "parked") {} else if (this.held) {} else if (this.sessionConflict) {
|
|
6983
7685
|
if (this.runId) {
|
|
6984
7686
|
try {
|
|
6985
7687
|
await this.stateStore.endRun(this.runId, "paused", {
|
|
@@ -7067,6 +7769,11 @@ ${basePrompt}`;
|
|
|
7067
7769
|
status: "stopped",
|
|
7068
7770
|
stopReason: "user_requested"
|
|
7069
7771
|
});
|
|
7772
|
+
} else if (this.state === "parked") {
|
|
7773
|
+
this.cliRunner.recordFinished({
|
|
7774
|
+
status: "stopped",
|
|
7775
|
+
stopReason: "user_requested"
|
|
7776
|
+
});
|
|
7070
7777
|
} else if (succeeded) {
|
|
7071
7778
|
this.cliRunner.recordFinished({ status: "completed" });
|
|
7072
7779
|
} else if (this.timedOut) {
|
|
@@ -7098,8 +7805,6 @@ ${basePrompt}`;
|
|
|
7098
7805
|
const { playbooks } = await this.client.request("GET", `/playbooks?workspaceId=${encodeURIComponent(this.workspaceId)}`);
|
|
7099
7806
|
const subject = {
|
|
7100
7807
|
labels: labels.map((label) => label.name.toLowerCase()),
|
|
7101
|
-
...card.intent != null ? { intent: card.intent } : {},
|
|
7102
|
-
...card.complexity_score != null ? { complexity_score: card.complexity_score } : {},
|
|
7103
7808
|
...card.priority != null ? { priority: card.priority } : {}
|
|
7104
7809
|
};
|
|
7105
7810
|
const { pick, reason } = selectAutoPlaybook(subject, playbooks ?? []);
|
|
@@ -7211,28 +7916,115 @@ ${basePrompt}`;
|
|
|
7211
7916
|
await this.client.addComment(card.id, reason, { commentType: "blocker" });
|
|
7212
7917
|
} catch {}
|
|
7213
7918
|
try {
|
|
7214
|
-
await runTransition(this.client, card, {
|
|
7215
|
-
removeLabels: ["agent"],
|
|
7216
|
-
endSession: {
|
|
7217
|
-
status: wait ? "blocked" : "paused",
|
|
7218
|
-
blockers: wait ? [reason] : undefined,
|
|
7219
|
-
failureReason: "other",
|
|
7220
|
-
failureSummary: reason.slice(0, 300)
|
|
7221
|
-
}
|
|
7919
|
+
await runTransition(this.client, card, {
|
|
7920
|
+
removeLabels: ["agent"],
|
|
7921
|
+
endSession: {
|
|
7922
|
+
status: wait ? "blocked" : "paused",
|
|
7923
|
+
blockers: wait ? [reason] : undefined,
|
|
7924
|
+
failureReason: "other",
|
|
7925
|
+
failureSummary: reason.slice(0, 300)
|
|
7926
|
+
}
|
|
7927
|
+
});
|
|
7928
|
+
} catch (tErr) {
|
|
7929
|
+
log22.warn(this.tag, `hold transition failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
|
|
7930
|
+
}
|
|
7931
|
+
if (this.runId) {
|
|
7932
|
+
try {
|
|
7933
|
+
await this.stateStore.endRun(this.runId, "paused", {
|
|
7934
|
+
errorMessage: "stage_hold",
|
|
7935
|
+
...this.runLedger()
|
|
7936
|
+
});
|
|
7937
|
+
} catch {}
|
|
7938
|
+
}
|
|
7939
|
+
}
|
|
7940
|
+
async holdParkOnSessionConflict(card, err) {
|
|
7941
|
+
this.state = "parked";
|
|
7942
|
+
this.progressTracker?.stop();
|
|
7943
|
+
this.progressTracker = null;
|
|
7944
|
+
const holderMessage = err instanceof Error ? err.message : String(err);
|
|
7945
|
+
const waitHours = this.config.budget.pause.waitHours;
|
|
7946
|
+
const until = computeDecisionDeadline(waitHours);
|
|
7947
|
+
log22.warn(this.tag, `#${card.short_id} stays parked — the resume could not reclaim its session: ${holderMessage}`);
|
|
7948
|
+
try {
|
|
7949
|
+
await this.client.addComment(card.id, formatResumeConflictComment({
|
|
7950
|
+
holderMessage,
|
|
7951
|
+
branchName: this.branchName,
|
|
7952
|
+
cliSessionId: this.cliSessionId,
|
|
7953
|
+
waitHours
|
|
7954
|
+
}), {
|
|
7955
|
+
commentType: "blocker",
|
|
7956
|
+
agentSessionId: this.sessionId ?? undefined
|
|
7957
|
+
});
|
|
7958
|
+
} catch (commentErr) {
|
|
7959
|
+
log22.warn(this.tag, `Failed to post the resume-conflict note for #${card.short_id}: ${commentErr}`);
|
|
7960
|
+
}
|
|
7961
|
+
if (this.runId) {
|
|
7962
|
+
const run = this.stateStore.getRun(this.runId);
|
|
7963
|
+
try {
|
|
7964
|
+
await this.stateStore.parkRun(this.runId, {
|
|
7965
|
+
pauseTrigger: run?.pauseTrigger ?? "timeout",
|
|
7966
|
+
blockerCommentId: run?.blockerCommentId ?? null,
|
|
7967
|
+
awaitingDecisionUntil: until
|
|
7968
|
+
});
|
|
7969
|
+
} catch (storeErr) {
|
|
7970
|
+
log22.error(this.tag, `#${card.short_id} could not be re-parked after a resume conflict — the daemon has no hold on it: ${storeErr}`);
|
|
7971
|
+
}
|
|
7972
|
+
}
|
|
7973
|
+
}
|
|
7974
|
+
async parkForDecision(card, trigger) {
|
|
7975
|
+
this.state = "parked";
|
|
7976
|
+
const stats = this.lastSessionStats;
|
|
7977
|
+
this.progressTracker?.stop();
|
|
7978
|
+
this.progressTracker = null;
|
|
7979
|
+
const waitHours = this.config.budget.pause.waitHours;
|
|
7980
|
+
const until = computeDecisionDeadline(waitHours);
|
|
7981
|
+
log22.warn(this.tag, `#${card.short_id} parked (${trigger}) — awaiting a human decision for ${waitHours}h`);
|
|
7982
|
+
const body = formatBudgetComment({
|
|
7983
|
+
trigger,
|
|
7984
|
+
numTurns: stats?.cost?.numTurns ?? 0,
|
|
7985
|
+
maxTurns: this.effectiveMaxTurns,
|
|
7986
|
+
toolCalls: stats?.toolCalls ?? 0,
|
|
7987
|
+
durationMs: stats?.cost?.durationMs ?? 0,
|
|
7988
|
+
costUsd: stats?.cost?.totalCostUsd ?? 0,
|
|
7989
|
+
lastAction: this.lastActionSummary,
|
|
7990
|
+
branchName: this.branchName,
|
|
7991
|
+
waitHours
|
|
7992
|
+
});
|
|
7993
|
+
let commentId = null;
|
|
7994
|
+
try {
|
|
7995
|
+
const res = await this.client.addComment(card.id, body, {
|
|
7996
|
+
commentType: "blocker"
|
|
7997
|
+
});
|
|
7998
|
+
commentId = res?.comment?.id ?? null;
|
|
7999
|
+
} catch (err) {
|
|
8000
|
+
log22.warn(this.tag, `Failed to post the budget-pause comment: ${err}`);
|
|
8001
|
+
}
|
|
8002
|
+
try {
|
|
8003
|
+
await this.client.updateAgentProgress(card.id, {
|
|
8004
|
+
agentIdentifier: agentIdentifier(this.id),
|
|
8005
|
+
agentName: AGENT_NAME,
|
|
8006
|
+
status: "blocked",
|
|
8007
|
+
currentTask: "Waiting for your decision on the turn budget",
|
|
8008
|
+
awaitingDecisionUntil: new Date(until).toISOString()
|
|
7222
8009
|
});
|
|
7223
|
-
} catch (
|
|
7224
|
-
log22.warn(this.tag, `
|
|
8010
|
+
} catch (err) {
|
|
8011
|
+
log22.warn(this.tag, `Failed to mark the session blocked: ${err}`);
|
|
7225
8012
|
}
|
|
7226
8013
|
if (this.runId) {
|
|
7227
8014
|
try {
|
|
7228
|
-
await this.stateStore.
|
|
7229
|
-
|
|
7230
|
-
|
|
8015
|
+
await this.stateStore.parkRun(this.runId, {
|
|
8016
|
+
pauseTrigger: trigger,
|
|
8017
|
+
blockerCommentId: commentId,
|
|
8018
|
+
awaitingDecisionUntil: until,
|
|
8019
|
+
costCents: Math.round((stats?.cost?.totalCostUsd ?? 0) * 100),
|
|
8020
|
+
numTurns: stats?.cost?.numTurns ?? 0
|
|
7231
8021
|
});
|
|
7232
|
-
} catch {
|
|
8022
|
+
} catch (err) {
|
|
8023
|
+
log22.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
|
+
}
|
|
7233
8025
|
}
|
|
7234
8026
|
}
|
|
7235
|
-
async runMotorStageCtx(card, ctx) {
|
|
8027
|
+
async runMotorStageCtx(card, ctx, subtasks = []) {
|
|
7236
8028
|
const stageId = ctx.stage.id;
|
|
7237
8029
|
const worktreePath = this.worktreePath;
|
|
7238
8030
|
const sessionId = this.sessionId;
|
|
@@ -7267,7 +8059,31 @@ ${basePrompt}`;
|
|
|
7267
8059
|
this.timedOut = true;
|
|
7268
8060
|
this.cancel("timeout");
|
|
7269
8061
|
}, this.config.maxTimeout);
|
|
8062
|
+
const motorTracker = new ProgressTracker(this.client, card.id, this.id, subtasks, "exploring");
|
|
8063
|
+
if (this.cliRunner)
|
|
8064
|
+
motorTracker.setRunEventSink(this.cliRunner);
|
|
8065
|
+
let motorTrackerLive = false;
|
|
8066
|
+
let motorRunSettled = false;
|
|
8067
|
+
const onMotorLine = (line) => {
|
|
8068
|
+
if (line.type !== "agent_event") {
|
|
8069
|
+
log22.info(this.tag, `motor: ${line.type}`);
|
|
8070
|
+
return;
|
|
8071
|
+
}
|
|
8072
|
+
if (motorRunSettled)
|
|
8073
|
+
return;
|
|
8074
|
+
log22.debug(this.tag, `motor: agent_event ${line.event.kind}`);
|
|
8075
|
+
if (line.event.kind === "tool_started" && STAGE_DAEMON_OWNED_TOOLS.includes(line.event.payload.toolName)) {
|
|
8076
|
+
return;
|
|
8077
|
+
}
|
|
8078
|
+
if (!motorTrackerLive) {
|
|
8079
|
+
motorTrackerLive = true;
|
|
8080
|
+
this.progressTracker = motorTracker;
|
|
8081
|
+
}
|
|
8082
|
+
motorTracker.ingest(line.event);
|
|
8083
|
+
};
|
|
7270
8084
|
const heartbeat = setInterval(() => {
|
|
8085
|
+
if (motorTrackerLive && !motorTracker.isStopped)
|
|
8086
|
+
return;
|
|
7271
8087
|
this.client.updateAgentProgress(card.id, {
|
|
7272
8088
|
agentIdentifier: agentIdentifier(this.id),
|
|
7273
8089
|
agentName: AGENT_NAME,
|
|
@@ -7293,7 +8109,7 @@ ${basePrompt}`;
|
|
|
7293
8109
|
apiUrl: this.client.getApiUrl(),
|
|
7294
8110
|
apiKey: this.client.getApiKey()
|
|
7295
8111
|
},
|
|
7296
|
-
onLine:
|
|
8112
|
+
onLine: onMotorLine,
|
|
7297
8113
|
signal: motorAbort.signal
|
|
7298
8114
|
});
|
|
7299
8115
|
} finally {
|
|
@@ -7302,6 +8118,17 @@ ${basePrompt}`;
|
|
|
7302
8118
|
this.timeoutTimer = null;
|
|
7303
8119
|
}
|
|
7304
8120
|
clearInterval(heartbeat);
|
|
8121
|
+
motorRunSettled = true;
|
|
8122
|
+
if (motorTrackerLive) {
|
|
8123
|
+
this.lastSessionStats = motorTracker.stats;
|
|
8124
|
+
const motorCost = this.lastSessionStats.cost;
|
|
8125
|
+
if (motorCost) {
|
|
8126
|
+
this.runCostCents += Math.round(motorCost.totalCostUsd * 100);
|
|
8127
|
+
this.runTurns += motorCost.numTurns;
|
|
8128
|
+
}
|
|
8129
|
+
}
|
|
8130
|
+
motorTracker.stop();
|
|
8131
|
+
this.progressTracker = null;
|
|
7305
8132
|
this.motorAbort = null;
|
|
7306
8133
|
if (metricsPath) {
|
|
7307
8134
|
try {
|
|
@@ -7494,12 +8321,59 @@ ${basePrompt}`;
|
|
|
7494
8321
|
}
|
|
7495
8322
|
selectImplementModel(card) {
|
|
7496
8323
|
const attempts = this.stateStore.getCard(card.id)?.attempts ?? 1;
|
|
7497
|
-
const
|
|
8324
|
+
const choice = chooseImplementModel(this.config.claude, card, attempts, this.sizing ?? undefined);
|
|
8325
|
+
this.modelChoice = choice;
|
|
8326
|
+
const { model, escalated, source } = choice;
|
|
7498
8327
|
if (source !== "policy" || escalated) {
|
|
7499
|
-
log22.info(this.tag, `Implement model "${model}" (source=${source}, escalated=${escalated}, attempts=${attempts}, priority=${card.priority ?? "none"}, tier=${
|
|
8328
|
+
log22.info(this.tag, `Implement model "${model}" (source=${source}, escalated=${escalated}, attempts=${attempts}, priority=${card.priority ?? "none"}, tier=${this.sizing?.tier ?? "none"})`);
|
|
7500
8329
|
}
|
|
7501
8330
|
return model;
|
|
7502
8331
|
}
|
|
8332
|
+
async sizeThisRun(card) {
|
|
8333
|
+
const model = this.config.claude.sizingModel;
|
|
8334
|
+
if (!model)
|
|
8335
|
+
return null;
|
|
8336
|
+
let repoRoot;
|
|
8337
|
+
try {
|
|
8338
|
+
repoRoot = execFileSync5("git", ["rev-parse", "--show-toplevel"], {
|
|
8339
|
+
encoding: "utf-8"
|
|
8340
|
+
}).trim();
|
|
8341
|
+
} catch (err) {
|
|
8342
|
+
log22.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
|
+
return null;
|
|
8344
|
+
}
|
|
8345
|
+
const sized = await sizeRun({
|
|
8346
|
+
cwd: repoRoot,
|
|
8347
|
+
cardId: card.id,
|
|
8348
|
+
workspaceId: this.workspaceId,
|
|
8349
|
+
runId: this.runId ?? card.id,
|
|
8350
|
+
title: card.title,
|
|
8351
|
+
description: card.description,
|
|
8352
|
+
model
|
|
8353
|
+
});
|
|
8354
|
+
log22.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
|
+
return sized;
|
|
8356
|
+
}
|
|
8357
|
+
recordRunSized() {
|
|
8358
|
+
if (!this.modelChoice)
|
|
8359
|
+
return;
|
|
8360
|
+
const { model, escalated, source } = this.modelChoice;
|
|
8361
|
+
this.cliRunner?.record({
|
|
8362
|
+
kind: "run_sized",
|
|
8363
|
+
source: "system",
|
|
8364
|
+
payload: {
|
|
8365
|
+
source: sizingEventSource(source),
|
|
8366
|
+
model,
|
|
8367
|
+
escalated,
|
|
8368
|
+
...this.sizing ? {
|
|
8369
|
+
tier: this.sizing.tier,
|
|
8370
|
+
complexity: this.sizing.complexity,
|
|
8371
|
+
...this.sizing.reasoning ? { reasoning: this.sizing.reasoning } : {},
|
|
8372
|
+
...this.sizing.filesInspected ? { filesInspected: this.sizing.filesInspected } : {}
|
|
8373
|
+
} : {}
|
|
8374
|
+
}
|
|
8375
|
+
});
|
|
8376
|
+
}
|
|
7503
8377
|
async recordOutcome(cardId, outcome) {
|
|
7504
8378
|
try {
|
|
7505
8379
|
const cost = this.lastSessionStats?.cost;
|
|
@@ -7512,15 +8386,41 @@ ${basePrompt}`;
|
|
|
7512
8386
|
const max = this.config.budget.maxAttemptsPerCard;
|
|
7513
8387
|
const attempts = this.stateStore.getCard(cardId)?.attempts ?? 0;
|
|
7514
8388
|
if (attempts >= max) {
|
|
8389
|
+
let giveUpCommentId = null;
|
|
7515
8390
|
try {
|
|
7516
|
-
const body = buildGaveUpComment(max, this.stateStore.getRecentFailures(cardId, 3));
|
|
7517
|
-
await this.client.addComment(cardId, body, {
|
|
8391
|
+
const body = buildGaveUpComment(max, this.stateStore.getRecentFailures(cardId, 3), this.config.budget.pause.enabled);
|
|
8392
|
+
const res = await this.client.addComment(cardId, body, {
|
|
7518
8393
|
commentType: "blocker"
|
|
7519
8394
|
});
|
|
8395
|
+
giveUpCommentId = res?.comment?.id ?? null;
|
|
7520
8396
|
log22.warn(this.tag, `gave up on ${cardId} after ${attempts} attempts`);
|
|
7521
8397
|
} catch (err) {
|
|
7522
8398
|
log22.warn(this.tag, `failed to post give-up comment for ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
7523
8399
|
}
|
|
8400
|
+
if (this.config.budget.pause.enabled) {
|
|
8401
|
+
const waitHours = this.config.budget.pause.waitHours;
|
|
8402
|
+
const until = computeDecisionDeadline(waitHours);
|
|
8403
|
+
try {
|
|
8404
|
+
await this.client.updateAgentProgress(cardId, {
|
|
8405
|
+
agentIdentifier: agentIdentifier(this.id),
|
|
8406
|
+
agentName: AGENT_NAME,
|
|
8407
|
+
status: "blocked",
|
|
8408
|
+
currentTask: "Waiting for your decision on the attempt budget",
|
|
8409
|
+
awaitingDecisionUntil: new Date(until).toISOString()
|
|
8410
|
+
});
|
|
8411
|
+
} catch (err) {
|
|
8412
|
+
log22.warn(this.tag, `Failed to mark the attempt cap pending for ${cardId}: ${err}`);
|
|
8413
|
+
}
|
|
8414
|
+
try {
|
|
8415
|
+
await this.stateStore.markAwaitingDecision(cardId, {
|
|
8416
|
+
until,
|
|
8417
|
+
blockerCommentId: giveUpCommentId,
|
|
8418
|
+
agentIdentifier: agentIdentifier(this.id)
|
|
8419
|
+
});
|
|
8420
|
+
} catch (err) {
|
|
8421
|
+
log22.warn(this.tag, `Failed to record the attempt-cap decision locally for ${cardId}: ${err}`);
|
|
8422
|
+
}
|
|
8423
|
+
}
|
|
7524
8424
|
}
|
|
7525
8425
|
}
|
|
7526
8426
|
} catch (err) {
|
|
@@ -7580,7 +8480,7 @@ ${basePrompt}`;
|
|
|
7580
8480
|
if (this.sdkRunner) {
|
|
7581
8481
|
await this.sdkRunner.stop(this.timedOut ? "timeout" : "user_requested");
|
|
7582
8482
|
} else if (this.process && !this.process.killed) {
|
|
7583
|
-
await
|
|
8483
|
+
await terminateGroup3(this.process, {
|
|
7584
8484
|
sigintTimeoutMs: CANCEL_SIGINT_TIMEOUT2,
|
|
7585
8485
|
sigtermTimeoutMs: CANCEL_SIGTERM_TIMEOUT2
|
|
7586
8486
|
});
|
|
@@ -7617,7 +8517,7 @@ ${basePrompt}`;
|
|
|
7617
8517
|
if (this.sdkRunner) {
|
|
7618
8518
|
this.sdkRunner.stop("timeout").catch(() => {});
|
|
7619
8519
|
} else if (this.process && !this.process.killed) {
|
|
7620
|
-
|
|
8520
|
+
terminateGroup3(this.process, {
|
|
7621
8521
|
sigintTimeoutMs: 1e4,
|
|
7622
8522
|
sigtermTimeoutMs: 5000
|
|
7623
8523
|
}).catch(() => {});
|
|
@@ -7735,7 +8635,7 @@ ${basePrompt}`;
|
|
|
7735
8635
|
if (this.sdkRunner) {
|
|
7736
8636
|
this.sdkRunner.stop("timeout").catch(() => {});
|
|
7737
8637
|
} else if (this.process && !this.process.killed) {
|
|
7738
|
-
|
|
8638
|
+
terminateGroup3(this.process, {
|
|
7739
8639
|
sigintTimeoutMs: 1e4,
|
|
7740
8640
|
sigtermTimeoutMs: 5000
|
|
7741
8641
|
}).catch(() => {});
|
|
@@ -7862,7 +8762,7 @@ ${basePrompt}`;
|
|
|
7862
8762
|
|
|
7863
8763
|
`);
|
|
7864
8764
|
}
|
|
7865
|
-
this.process =
|
|
8765
|
+
this.process = spawnInGroup3("claude", args, {
|
|
7866
8766
|
cwd: this.worktreePath,
|
|
7867
8767
|
stdio: ["ignore", "pipe", "pipe"]
|
|
7868
8768
|
});
|
|
@@ -7892,6 +8792,9 @@ ${basePrompt}`;
|
|
|
7892
8792
|
[parse_error] ${msg}
|
|
7893
8793
|
`);
|
|
7894
8794
|
});
|
|
8795
|
+
parser.on("result", (stop) => {
|
|
8796
|
+
this.lastStopReason = stop;
|
|
8797
|
+
});
|
|
7895
8798
|
let stderr = "";
|
|
7896
8799
|
this.process.stderr?.on("data", (data) => {
|
|
7897
8800
|
stderr += data.toString();
|
|
@@ -7905,6 +8808,7 @@ ${basePrompt}`;
|
|
|
7905
8808
|
this.process = null;
|
|
7906
8809
|
this.captureCliSessionId(parser.sessionId);
|
|
7907
8810
|
this.lastSessionStats = this.progressTracker?.stats;
|
|
8811
|
+
this.lastActionSummary = this.progressTracker?.lastActionSummary ?? null;
|
|
7908
8812
|
const spawnCost = this.lastSessionStats?.cost;
|
|
7909
8813
|
if (spawnCost) {
|
|
7910
8814
|
this.runCostCents += Math.round(spawnCost.totalCostUsd * 100);
|
|
@@ -7919,11 +8823,22 @@ ${basePrompt}`;
|
|
|
7919
8823
|
`);
|
|
7920
8824
|
runLog.stream.end();
|
|
7921
8825
|
}
|
|
7922
|
-
|
|
7923
|
-
|
|
8826
|
+
reapGroup2(leaderPid);
|
|
8827
|
+
const trigger = this.config.budget.pause.enabled ? classifyRunExit({
|
|
8828
|
+
exitCode: code ?? 1,
|
|
8829
|
+
stopReason: this.lastStopReason,
|
|
8830
|
+
numTurns: this.lastSessionStats?.cost?.numTurns ?? 0,
|
|
8831
|
+
maxTurns,
|
|
8832
|
+
timedOut: this.timedOut
|
|
8833
|
+
}) : null;
|
|
8834
|
+
if (this.timedOut && trigger) {
|
|
8835
|
+
reject(new BudgetPauseError(trigger));
|
|
8836
|
+
} else if (this.aborted) {
|
|
7924
8837
|
resolve2();
|
|
7925
8838
|
} else if (code === 0) {
|
|
7926
8839
|
resolve2();
|
|
8840
|
+
} else if (trigger) {
|
|
8841
|
+
reject(new BudgetPauseError(trigger));
|
|
7927
8842
|
} else {
|
|
7928
8843
|
const err = new Error(`claude exited with code ${code}${stderr ? `: ${stderr.slice(0, 500)}` : ""}`);
|
|
7929
8844
|
err.stderr = stderr;
|
|
@@ -7998,6 +8913,7 @@ ${basePrompt}`;
|
|
|
7998
8913
|
} finally {
|
|
7999
8914
|
this.captureCliSessionId(runner.sessionId);
|
|
8000
8915
|
this.lastSessionStats = this.progressTracker?.stats;
|
|
8916
|
+
this.lastActionSummary = this.progressTracker?.lastActionSummary ?? null;
|
|
8001
8917
|
const spawnCost = this.lastSessionStats?.cost;
|
|
8002
8918
|
if (spawnCost) {
|
|
8003
8919
|
this.runCostCents += Math.round(spawnCost.totalCostUsd * 100);
|
|
@@ -8015,9 +8931,30 @@ ${basePrompt}`;
|
|
|
8015
8931
|
this.process = null;
|
|
8016
8932
|
this.sdkRunner = null;
|
|
8017
8933
|
}
|
|
8934
|
+
if (this.timedOut && failure && this.config.budget.pause.enabled) {
|
|
8935
|
+
const timeoutTrigger = classifyRunExit({
|
|
8936
|
+
exitCode: 1,
|
|
8937
|
+
stopReason: null,
|
|
8938
|
+
numTurns: this.lastSessionStats?.cost?.numTurns ?? 0,
|
|
8939
|
+
maxTurns,
|
|
8940
|
+
timedOut: true
|
|
8941
|
+
});
|
|
8942
|
+
if (timeoutTrigger)
|
|
8943
|
+
throw new BudgetPauseError(timeoutTrigger);
|
|
8944
|
+
}
|
|
8018
8945
|
if (this.aborted)
|
|
8019
8946
|
return;
|
|
8020
8947
|
if (failure) {
|
|
8948
|
+
const trigger = classifyRunExit({
|
|
8949
|
+
exitCode: 1,
|
|
8950
|
+
stopReason: null,
|
|
8951
|
+
numTurns: this.lastSessionStats?.cost?.numTurns ?? 0,
|
|
8952
|
+
maxTurns,
|
|
8953
|
+
timedOut: this.timedOut
|
|
8954
|
+
});
|
|
8955
|
+
if (trigger && this.config.budget.pause.enabled) {
|
|
8956
|
+
throw new BudgetPauseError(trigger);
|
|
8957
|
+
}
|
|
8021
8958
|
const err = new Error(failure);
|
|
8022
8959
|
err.stderr = runner.capturedStderrText;
|
|
8023
8960
|
err.errorKind = failureKind;
|
|
@@ -8040,7 +8977,7 @@ ${basePrompt}`;
|
|
|
8040
8977
|
clearTimeout(this.timeoutTimer);
|
|
8041
8978
|
this.timeoutTimer = null;
|
|
8042
8979
|
}
|
|
8043
|
-
if (this.worktreePath && (this.state === "error" || this.timedOut || this.aborted)) {
|
|
8980
|
+
if (this.worktreePath && this.state !== "parked" && (this.state === "error" || this.timedOut || this.aborted)) {
|
|
8044
8981
|
try {
|
|
8045
8982
|
await teardownWorktree2(this.client, this.cardId, this.worktreePath, this.branchName ?? undefined);
|
|
8046
8983
|
} catch {
|
|
@@ -8050,6 +8987,7 @@ ${basePrompt}`;
|
|
|
8050
8987
|
this.process = null;
|
|
8051
8988
|
this.cardId = null;
|
|
8052
8989
|
this.branchName = null;
|
|
8990
|
+
this.runBaselineSha = null;
|
|
8053
8991
|
this.worktreePath = null;
|
|
8054
8992
|
this.startedAt = null;
|
|
8055
8993
|
this.runId = null;
|
|
@@ -8062,6 +9000,7 @@ var TAG21 = "worker", CANCEL_SIGINT_TIMEOUT2 = 30000, CANCEL_SIGTERM_TIMEOUT2 =
|
|
|
8062
9000
|
var init_worker = __esm(() => {
|
|
8063
9001
|
init_dist();
|
|
8064
9002
|
init_board_helpers();
|
|
9003
|
+
init_budget_pause();
|
|
8065
9004
|
init_cli_agent_runner();
|
|
8066
9005
|
init_completion();
|
|
8067
9006
|
init_contract_phase();
|
|
@@ -8086,8 +9025,31 @@ import {
|
|
|
8086
9025
|
describeApiError as describeApiError2,
|
|
8087
9026
|
log as log23
|
|
8088
9027
|
} from "@gethmy/harness";
|
|
9028
|
+
async function routeBudgetDecision(d, run, actions, cardId) {
|
|
9029
|
+
if (!run) {
|
|
9030
|
+
if (!cardId)
|
|
9031
|
+
return;
|
|
9032
|
+
if (d.decision === "continue")
|
|
9033
|
+
await actions.grantAttempt(cardId);
|
|
9034
|
+
else
|
|
9035
|
+
await actions.stopAttemptCap(cardId);
|
|
9036
|
+
return;
|
|
9037
|
+
}
|
|
9038
|
+
if (d.decision === "stop") {
|
|
9039
|
+
await actions.stopRun(run);
|
|
9040
|
+
return;
|
|
9041
|
+
}
|
|
9042
|
+
await actions.continueRun(run, {
|
|
9043
|
+
extraTurns: d.extraTurns,
|
|
9044
|
+
message: d.message
|
|
9045
|
+
});
|
|
9046
|
+
}
|
|
9047
|
+
function hasParkedRun(store, cardId) {
|
|
9048
|
+
return store.getParkedRunForCard(cardId) !== null;
|
|
9049
|
+
}
|
|
8089
9050
|
|
|
8090
9051
|
class Pool {
|
|
9052
|
+
config;
|
|
8091
9053
|
client;
|
|
8092
9054
|
identity;
|
|
8093
9055
|
projectId;
|
|
@@ -8103,6 +9065,7 @@ class Pool {
|
|
|
8103
9065
|
authPaused = false;
|
|
8104
9066
|
onCardCompleted = null;
|
|
8105
9067
|
constructor(config, client, identity, workspaceId, projectId, stateStore) {
|
|
9068
|
+
this.config = config;
|
|
8106
9069
|
this.client = client;
|
|
8107
9070
|
this.identity = identity;
|
|
8108
9071
|
this.projectId = projectId;
|
|
@@ -8249,7 +9212,7 @@ class Pool {
|
|
|
8249
9212
|
await this.stateStore.resetAttempts(cardId);
|
|
8250
9213
|
}
|
|
8251
9214
|
isCardActive(cardId) {
|
|
8252
|
-
return this.implWorkers.some((w) => w.cardId === cardId && w.isActive) || this.reviewWorkers.some((w) => w.cardId === cardId && w.isActive);
|
|
9215
|
+
return hasParkedRun(this.stateStore, cardId) || this.implWorkers.some((w) => w.cardId === cardId && w.isActive) || this.reviewWorkers.some((w) => w.cardId === cardId && w.isActive);
|
|
8253
9216
|
}
|
|
8254
9217
|
isCardKnown(cardId) {
|
|
8255
9218
|
return this.implQueue.has(cardId) || this.reviewQueue.has(cardId) || this.isCardActive(cardId);
|
|
@@ -8270,6 +9233,10 @@ class Pool {
|
|
|
8270
9233
|
return ids;
|
|
8271
9234
|
}
|
|
8272
9235
|
async handleAgentCommand(cardId, command) {
|
|
9236
|
+
if (command === "continue") {
|
|
9237
|
+
await this.drainBudgetDecisions(cardId);
|
|
9238
|
+
return;
|
|
9239
|
+
}
|
|
8273
9240
|
const worker = this.implWorkers.find((w) => w.cardId === cardId && w.isActive) ?? this.reviewWorkers.find((w) => w.cardId === cardId && w.isActive);
|
|
8274
9241
|
if (!worker) {
|
|
8275
9242
|
log23.debug(TAG22, `No active worker for card ${cardId}, ignoring ${command}`);
|
|
@@ -8333,6 +9300,268 @@ class Pool {
|
|
|
8333
9300
|
this.sleepGuard.stop();
|
|
8334
9301
|
log23.info(TAG22, "Pool shutdown complete");
|
|
8335
9302
|
}
|
|
9303
|
+
async drainBudgetDecisions(cardId) {
|
|
9304
|
+
const targets = cardId ? [cardId] : [
|
|
9305
|
+
...new Set([
|
|
9306
|
+
...this.stateStore.getParkedRuns().map((r) => r.cardId),
|
|
9307
|
+
...this.stateStore.getResumableRuns().map((r) => r.cardId),
|
|
9308
|
+
...this.stateStore.listCards().filter((c) => c.attempts >= this.config.budget.maxAttemptsPerCard).map((c) => c.cardId)
|
|
9309
|
+
])
|
|
9310
|
+
];
|
|
9311
|
+
for (const id of targets) {
|
|
9312
|
+
await this.drainBudgetDecisionsForCard(id);
|
|
9313
|
+
}
|
|
9314
|
+
}
|
|
9315
|
+
draining = new Set;
|
|
9316
|
+
async drainBudgetDecisionsForCard(cardId) {
|
|
9317
|
+
if (this.draining.has(cardId))
|
|
9318
|
+
return;
|
|
9319
|
+
this.draining.add(cardId);
|
|
9320
|
+
try {
|
|
9321
|
+
const granted = this.stateStore.getResumableRunForCard(cardId);
|
|
9322
|
+
if (granted) {
|
|
9323
|
+
await this.adoptGrantedRun(granted);
|
|
9324
|
+
return;
|
|
9325
|
+
}
|
|
9326
|
+
const run = this.stateStore.getParkedRunForCard(cardId);
|
|
9327
|
+
const sinceMs = run ? run.parkedAt ?? run.startedAt : this.stateStore.getCard(cardId)?.lastAttemptAt ?? 0;
|
|
9328
|
+
let decisions;
|
|
9329
|
+
try {
|
|
9330
|
+
({ decisions } = await this.client.getBudgetDecisions(cardId, new Date(sinceMs).toISOString()));
|
|
9331
|
+
} catch (err) {
|
|
9332
|
+
log23.warn(TAG22, `getBudgetDecisions failed for ${cardId}: ${err}`);
|
|
9333
|
+
return;
|
|
9334
|
+
}
|
|
9335
|
+
if (decisions.length > 0) {
|
|
9336
|
+
await routeBudgetDecision(decisions[0], run, {
|
|
9337
|
+
continueRun: (r, opts) => this.continueRun(r, opts),
|
|
9338
|
+
stopRun: (r) => this.stopRun(r),
|
|
9339
|
+
grantAttempt: (id) => this.grantAttempt(id),
|
|
9340
|
+
stopAttemptCap: (id) => this.stopAttemptCap(id)
|
|
9341
|
+
}, cardId);
|
|
9342
|
+
return;
|
|
9343
|
+
}
|
|
9344
|
+
if (run && run.awaitingDecisionUntil != null && run.awaitingDecisionUntil < Date.now()) {
|
|
9345
|
+
await this.releaseExpiredPark(run);
|
|
9346
|
+
return;
|
|
9347
|
+
}
|
|
9348
|
+
const card = this.stateStore.getCard(cardId);
|
|
9349
|
+
if (!run && card?.awaitingDecisionUntil != null && card.awaitingDecisionUntil < Date.now()) {
|
|
9350
|
+
await this.releaseExpiredAttemptCap(cardId, card.blockerCommentId);
|
|
9351
|
+
}
|
|
9352
|
+
} finally {
|
|
9353
|
+
this.draining.delete(cardId);
|
|
9354
|
+
}
|
|
9355
|
+
}
|
|
9356
|
+
async releaseExpiredPark(run) {
|
|
9357
|
+
try {
|
|
9358
|
+
await this.client.addComment(run.cardId, formatExpiredParkComment({
|
|
9359
|
+
branchName: run.branchName,
|
|
9360
|
+
cliSessionId: run.cliSessionId ?? null
|
|
9361
|
+
}), {
|
|
9362
|
+
commentType: "summary",
|
|
9363
|
+
agentSessionId: run.sessionId ?? undefined,
|
|
9364
|
+
...run.blockerCommentId ? { replyToId: run.blockerCommentId } : {}
|
|
9365
|
+
});
|
|
9366
|
+
} catch (err) {
|
|
9367
|
+
log23.warn(TAG22, `Failed to post the expired-park closing comment for ${run.cardId}: ${err}`);
|
|
9368
|
+
}
|
|
9369
|
+
try {
|
|
9370
|
+
const { card } = await this.client.getCard(run.cardId);
|
|
9371
|
+
const failColumn = this.failColumnFor(run.pipeline);
|
|
9372
|
+
if (failColumn) {
|
|
9373
|
+
await runTransition(this.client, card, {
|
|
9374
|
+
move: { columnName: failColumn }
|
|
9375
|
+
});
|
|
9376
|
+
}
|
|
9377
|
+
} catch (err) {
|
|
9378
|
+
log23.error(TAG22, `Failed to move #${run.cardShortId} after an expired park: ${err}`);
|
|
9379
|
+
}
|
|
9380
|
+
try {
|
|
9381
|
+
await this.stateStore.endRun(run.runId, "failed", {
|
|
9382
|
+
errorMessage: "budget decision expired"
|
|
9383
|
+
});
|
|
9384
|
+
} catch (err) {
|
|
9385
|
+
log23.warn(TAG22, `Failed to release the expired park for ${run.cardId}: ${err}`);
|
|
9386
|
+
}
|
|
9387
|
+
}
|
|
9388
|
+
async releaseExpiredAttemptCap(cardId, blockerCommentId) {
|
|
9389
|
+
try {
|
|
9390
|
+
await this.client.addComment(cardId, formatExpiredAttemptCapComment(), {
|
|
9391
|
+
commentType: "summary",
|
|
9392
|
+
...blockerCommentId ? { replyToId: blockerCommentId } : {}
|
|
9393
|
+
});
|
|
9394
|
+
} catch (err) {
|
|
9395
|
+
log23.warn(TAG22, `Failed to post the expired attempt-cap note for ${cardId}: ${err}`);
|
|
9396
|
+
}
|
|
9397
|
+
try {
|
|
9398
|
+
await this.client.endAgentSession(cardId, {
|
|
9399
|
+
status: "failed",
|
|
9400
|
+
failureReason: "budget",
|
|
9401
|
+
failureSummary: "The attempt-budget decision expired with no answer. Reassign the card to grant a fresh attempt."
|
|
9402
|
+
});
|
|
9403
|
+
} catch (err) {
|
|
9404
|
+
log23.warn(TAG22, `Failed to end the expired attempt-cap session for ${cardId}: ${err}`);
|
|
9405
|
+
}
|
|
9406
|
+
await this.stateStore.clearAwaitingDecision(cardId);
|
|
9407
|
+
}
|
|
9408
|
+
async adoptGrantedRun(run) {
|
|
9409
|
+
if (this.isCardKnown(run.cardId))
|
|
9410
|
+
return;
|
|
9411
|
+
log23.warn(TAG22, `#${run.cardShortId}: granted continue never reached a worker — re-enqueueing (${run.pipeline})`);
|
|
9412
|
+
await this.enqueueCard(run.cardId, run.pipeline);
|
|
9413
|
+
}
|
|
9414
|
+
sessionIdentityFor(run) {
|
|
9415
|
+
return {
|
|
9416
|
+
agentIdentifier: agentIdentifier(run.workerId),
|
|
9417
|
+
agentName: run.pipeline === "review" ? `${AGENT_NAME} (Review)` : AGENT_NAME
|
|
9418
|
+
};
|
|
9419
|
+
}
|
|
9420
|
+
attemptCapIdentityFor(cardId) {
|
|
9421
|
+
return {
|
|
9422
|
+
agentIdentifier: this.stateStore.getCard(cardId)?.awaitingDecisionAgentIdentifier ?? agentIdentifier(0),
|
|
9423
|
+
agentName: AGENT_NAME
|
|
9424
|
+
};
|
|
9425
|
+
}
|
|
9426
|
+
async continueRun(run, opts) {
|
|
9427
|
+
await this.stateStore.decrementAttempt(run.cardId);
|
|
9428
|
+
const grantedTurns = opts.extraTurns > 0 ? Math.min(opts.extraTurns, MAX_GRANTED_TURNS) : this.config.budget.pause.extraTurns ?? this.defaultTurnsFor(run.pipeline);
|
|
9429
|
+
await this.stateStore.updateRun(run.runId, {
|
|
9430
|
+
status: "active",
|
|
9431
|
+
awaitingDecisionUntil: null,
|
|
9432
|
+
grantedTurns,
|
|
9433
|
+
resumeMessage: opts.message ?? null,
|
|
9434
|
+
daemonPid: process.pid,
|
|
9435
|
+
lastHeartbeatAt: Date.now()
|
|
9436
|
+
});
|
|
9437
|
+
try {
|
|
9438
|
+
await this.client.updateAgentProgress(run.cardId, {
|
|
9439
|
+
...this.sessionIdentityFor(run),
|
|
9440
|
+
awaitingDecisionUntil: null
|
|
9441
|
+
});
|
|
9442
|
+
} catch (err) {
|
|
9443
|
+
log23.warn(TAG22, `Failed to clear the decision deadline for ${run.cardId}: ${err}`);
|
|
9444
|
+
}
|
|
9445
|
+
if (run.blockerCommentId) {
|
|
9446
|
+
try {
|
|
9447
|
+
await this.client.updateComment(run.blockerCommentId, {
|
|
9448
|
+
resolve: true
|
|
9449
|
+
});
|
|
9450
|
+
} catch (err) {
|
|
9451
|
+
log23.warn(TAG22, `Failed to resolve the blocker comment: ${err}`);
|
|
9452
|
+
}
|
|
9453
|
+
}
|
|
9454
|
+
await this.enqueueCard(run.cardId, run.pipeline);
|
|
9455
|
+
}
|
|
9456
|
+
async stopRun(run) {
|
|
9457
|
+
if (run.blockerCommentId) {
|
|
9458
|
+
try {
|
|
9459
|
+
await this.client.updateComment(run.blockerCommentId, {
|
|
9460
|
+
resolve: true
|
|
9461
|
+
});
|
|
9462
|
+
} catch (err) {
|
|
9463
|
+
log23.warn(TAG22, `Failed to resolve the blocker comment: ${err}`);
|
|
9464
|
+
}
|
|
9465
|
+
}
|
|
9466
|
+
try {
|
|
9467
|
+
await this.client.updateAgentProgress(run.cardId, {
|
|
9468
|
+
...this.sessionIdentityFor(run),
|
|
9469
|
+
awaitingDecisionUntil: null
|
|
9470
|
+
});
|
|
9471
|
+
} catch (err) {
|
|
9472
|
+
log23.warn(TAG22, `Failed to clear the decision deadline for ${run.cardId}: ${err}`);
|
|
9473
|
+
}
|
|
9474
|
+
try {
|
|
9475
|
+
const { card } = await this.client.getCard(run.cardId);
|
|
9476
|
+
const failColumn = this.failColumnFor(run.pipeline);
|
|
9477
|
+
await runTransition(this.client, card, {
|
|
9478
|
+
...failColumn ? { move: { columnName: failColumn } } : {},
|
|
9479
|
+
endSession: {
|
|
9480
|
+
status: "failed",
|
|
9481
|
+
failureReason: "budget",
|
|
9482
|
+
failureSummary: "Stopped by a human decision on the turn budget."
|
|
9483
|
+
}
|
|
9484
|
+
});
|
|
9485
|
+
} catch (err) {
|
|
9486
|
+
log23.error(TAG22, `Failed to hand #${run.cardShortId} back after a stop: ${err}`);
|
|
9487
|
+
}
|
|
9488
|
+
try {
|
|
9489
|
+
await this.stateStore.endRun(run.runId, "failed", {
|
|
9490
|
+
errorMessage: "budget_decision_stop"
|
|
9491
|
+
});
|
|
9492
|
+
} catch (err) {
|
|
9493
|
+
log23.warn(TAG22, `Failed to end the local run record for ${run.cardId}: ${err}`);
|
|
9494
|
+
}
|
|
9495
|
+
}
|
|
9496
|
+
async grantAttempt(cardId) {
|
|
9497
|
+
const identity = this.attemptCapIdentityFor(cardId);
|
|
9498
|
+
await this.stateStore.resetAttempts(cardId);
|
|
9499
|
+
await this.stateStore.clearAwaitingDecision(cardId);
|
|
9500
|
+
try {
|
|
9501
|
+
await this.client.updateAgentProgress(cardId, {
|
|
9502
|
+
...identity,
|
|
9503
|
+
awaitingDecisionUntil: null
|
|
9504
|
+
});
|
|
9505
|
+
} catch (err) {
|
|
9506
|
+
log23.warn(TAG22, `Failed to clear the attempt-cap decision deadline for ${cardId}: ${err}`);
|
|
9507
|
+
}
|
|
9508
|
+
await this.enqueueCard(cardId, "implement");
|
|
9509
|
+
}
|
|
9510
|
+
async stopAttemptCap(cardId) {
|
|
9511
|
+
const blockerCommentId = this.stateStore.getCard(cardId)?.blockerCommentId ?? null;
|
|
9512
|
+
const identity = this.attemptCapIdentityFor(cardId);
|
|
9513
|
+
if (blockerCommentId) {
|
|
9514
|
+
try {
|
|
9515
|
+
await this.client.updateComment(blockerCommentId, { resolve: true });
|
|
9516
|
+
} catch (err) {
|
|
9517
|
+
log23.warn(TAG22, `Failed to resolve the blocker comment: ${err}`);
|
|
9518
|
+
}
|
|
9519
|
+
}
|
|
9520
|
+
try {
|
|
9521
|
+
await this.client.updateAgentProgress(cardId, {
|
|
9522
|
+
...identity,
|
|
9523
|
+
awaitingDecisionUntil: null
|
|
9524
|
+
});
|
|
9525
|
+
} catch (err) {
|
|
9526
|
+
log23.warn(TAG22, `Failed to clear the decision deadline for ${cardId}: ${err}`);
|
|
9527
|
+
}
|
|
9528
|
+
try {
|
|
9529
|
+
await this.client.endAgentSession(cardId, {
|
|
9530
|
+
status: "failed",
|
|
9531
|
+
failureReason: "budget",
|
|
9532
|
+
failureSummary: "Stopped by a human decision on the attempt budget."
|
|
9533
|
+
});
|
|
9534
|
+
} catch (err) {
|
|
9535
|
+
log23.warn(TAG22, `Failed to end the attempt-cap session for ${cardId}: ${err}`);
|
|
9536
|
+
}
|
|
9537
|
+
await this.stateStore.clearAwaitingDecision(cardId);
|
|
9538
|
+
}
|
|
9539
|
+
failColumnFor(pipeline) {
|
|
9540
|
+
return pipeline === "review" ? this.config.review.failColumn : this.config.pickupColumns[0];
|
|
9541
|
+
}
|
|
9542
|
+
defaultTurnsFor(pipeline) {
|
|
9543
|
+
return pipeline === "review" ? this.config.claude.reviewMaxTurns : this.config.claude.maxTurns;
|
|
9544
|
+
}
|
|
9545
|
+
async enqueueCard(cardId, mode) {
|
|
9546
|
+
try {
|
|
9547
|
+
const { card } = await this.client.getCard(cardId);
|
|
9548
|
+
const board = await this.client.getBoard(this.projectId, {
|
|
9549
|
+
summary: true
|
|
9550
|
+
});
|
|
9551
|
+
const columns = board.columns ?? [];
|
|
9552
|
+
const column = columns.find((c) => c.id === card.column_id);
|
|
9553
|
+
if (!column) {
|
|
9554
|
+
log23.warn(TAG22, `#${card.short_id}: column not found — cannot re-enqueue`);
|
|
9555
|
+
return;
|
|
9556
|
+
}
|
|
9557
|
+
const labelMap = buildLabelMap(board.labels ?? []);
|
|
9558
|
+
const cardLabels = resolveCardLabels(card, labelMap);
|
|
9559
|
+
const subtasks = card.subtasks ?? [];
|
|
9560
|
+
await this.enqueue(card, column, cardLabels, subtasks, mode);
|
|
9561
|
+
} catch (err) {
|
|
9562
|
+
log23.error(TAG22, `Failed to re-enqueue ${cardId} after a budget decision: ${err}`);
|
|
9563
|
+
}
|
|
9564
|
+
}
|
|
8336
9565
|
reservations = new Set;
|
|
8337
9566
|
cardDataCache = new Map;
|
|
8338
9567
|
tryDispatchFor(workers, queue, label) {
|
|
@@ -8361,9 +9590,12 @@ class Pool {
|
|
|
8361
9590
|
}
|
|
8362
9591
|
var TAG22 = "pool";
|
|
8363
9592
|
var init_pool = __esm(() => {
|
|
9593
|
+
init_board_helpers();
|
|
9594
|
+
init_budget_pause();
|
|
8364
9595
|
init_queue();
|
|
8365
9596
|
init_review_worker();
|
|
8366
9597
|
init_sleep_guard();
|
|
9598
|
+
init_transitions();
|
|
8367
9599
|
init_types2();
|
|
8368
9600
|
init_unblock();
|
|
8369
9601
|
init_worker();
|
|
@@ -8478,6 +9710,11 @@ async function recoverOrphans(store, client, config) {
|
|
|
8478
9710
|
errors: []
|
|
8479
9711
|
};
|
|
8480
9712
|
outcomes.push(outcome);
|
|
9713
|
+
if (isBudgetHeldRun(run)) {
|
|
9714
|
+
log25.info(TAG24, `run ${run.runId} (#${run.cardShortId}) is held for a human budget decision — leaving it`);
|
|
9715
|
+
outcome.actions.push("skipped: held for a human budget decision");
|
|
9716
|
+
continue;
|
|
9717
|
+
}
|
|
8481
9718
|
if (isProcessAlive(run.daemonPid, process.pid)) {
|
|
8482
9719
|
log25.warn(TAG24, `run ${run.runId} claims live daemon pid ${run.daemonPid} — skipping`);
|
|
8483
9720
|
outcome.actions.push("skipped: daemon pid still alive");
|
|
@@ -8560,6 +9797,7 @@ async function recoverRun(run, store, client, config, outcome, opts = {}) {
|
|
|
8560
9797
|
var TAG24 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
|
|
8561
9798
|
var init_recovery = __esm(() => {
|
|
8562
9799
|
init_board_helpers();
|
|
9800
|
+
init_state_store();
|
|
8563
9801
|
});
|
|
8564
9802
|
|
|
8565
9803
|
// src/claim.ts
|
|
@@ -8703,6 +9941,10 @@ class Reconciler {
|
|
|
8703
9941
|
const active = this.stateStore.getActiveRuns();
|
|
8704
9942
|
const pool = this.pool;
|
|
8705
9943
|
for (const run of active) {
|
|
9944
|
+
if (isBudgetHeldRun(run)) {
|
|
9945
|
+
log28.info(TAG27, `run ${run.runId} (#${run.cardShortId}) is held for a human budget decision — leaving it`);
|
|
9946
|
+
continue;
|
|
9947
|
+
}
|
|
8706
9948
|
const foreignDaemon = run.daemonPid !== process.pid;
|
|
8707
9949
|
const daemonDead = foreignDaemon && !isProcessAlive(run.daemonPid, process.pid);
|
|
8708
9950
|
const heartbeatStale = now - run.lastHeartbeatAt > stale;
|
|
@@ -8866,6 +10108,11 @@ class Reconciler {
|
|
|
8866
10108
|
if (this.stateStore && this.agentConfig) {
|
|
8867
10109
|
await this.recoverStaleRuns();
|
|
8868
10110
|
}
|
|
10111
|
+
try {
|
|
10112
|
+
await this.pool.drainBudgetDecisions();
|
|
10113
|
+
} catch (err) {
|
|
10114
|
+
log28.error(TAG27, `budget decisions were not drained this tick: ${err instanceof Error ? err.message : err}`);
|
|
10115
|
+
}
|
|
8869
10116
|
await this.recoverStrandedInProgress(cards, columns, knownCardIds);
|
|
8870
10117
|
await this.recoverStrandedReview(cards, columns, labelMap, knownCardIds);
|
|
8871
10118
|
for (const knownId of knownCardIds) {
|
|
@@ -8886,6 +10133,7 @@ var init_reconcile = __esm(() => {
|
|
|
8886
10133
|
init_board_helpers();
|
|
8887
10134
|
init_recovery();
|
|
8888
10135
|
init_review_worktree();
|
|
10136
|
+
init_state_store();
|
|
8889
10137
|
init_strand_recovery();
|
|
8890
10138
|
init_types2();
|
|
8891
10139
|
});
|
|
@@ -9179,6 +10427,9 @@ class Watcher {
|
|
|
9179
10427
|
reconnectTimer = null;
|
|
9180
10428
|
reconnectAttempts = 0;
|
|
9181
10429
|
broadcastGen = 0;
|
|
10430
|
+
presenceReconnectTimer = null;
|
|
10431
|
+
presenceReconnectAttempts = 0;
|
|
10432
|
+
presenceGen = 0;
|
|
9182
10433
|
get isConnected() {
|
|
9183
10434
|
return this.connected;
|
|
9184
10435
|
}
|
|
@@ -9206,29 +10457,85 @@ class Watcher {
|
|
|
9206
10457
|
log30.info(TAG29, "Connecting to Supabase realtime (broadcast)...");
|
|
9207
10458
|
}
|
|
9208
10459
|
this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
|
|
9209
|
-
const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
|
|
9210
10460
|
this.subscribeBroadcast();
|
|
10461
|
+
this.subscribePresence();
|
|
10462
|
+
}
|
|
10463
|
+
subscribePresence() {
|
|
10464
|
+
if (!this.supabase)
|
|
10465
|
+
return;
|
|
10466
|
+
const gen = ++this.presenceGen;
|
|
10467
|
+
const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
|
|
10468
|
+
this.presenceChannel = presenceChannel;
|
|
9211
10469
|
presenceChannel.on("presence", { event: "sync" }, () => {
|
|
9212
10470
|
log30.debug(TAG29, "Presence sync");
|
|
9213
10471
|
}).subscribe(async (status) => {
|
|
10472
|
+
if (gen !== this.presenceGen)
|
|
10473
|
+
return;
|
|
9214
10474
|
if (status === "SUBSCRIBED") {
|
|
9215
|
-
|
|
9216
|
-
|
|
9217
|
-
|
|
9218
|
-
|
|
9219
|
-
|
|
9220
|
-
|
|
9221
|
-
|
|
9222
|
-
|
|
9223
|
-
|
|
10475
|
+
let trackStatus;
|
|
10476
|
+
try {
|
|
10477
|
+
trackStatus = await presenceChannel.track({
|
|
10478
|
+
daemonId: this.daemonId,
|
|
10479
|
+
startedAt: new Date().toISOString(),
|
|
10480
|
+
userId: this.identity.userId,
|
|
10481
|
+
agentId: this.identity.agentId,
|
|
10482
|
+
userEmail: this.identity.userEmail,
|
|
10483
|
+
agentIdentifier: this.identity.agentIdentifier,
|
|
10484
|
+
agentName: this.identity.agentName
|
|
10485
|
+
});
|
|
10486
|
+
} catch (err) {
|
|
10487
|
+
trackStatus = `error (${String(err)})`;
|
|
10488
|
+
}
|
|
10489
|
+
if (this.stopping || gen !== this.presenceGen)
|
|
10490
|
+
return;
|
|
10491
|
+
if (trackStatus !== "ok") {
|
|
10492
|
+
this.presenceTracked = false;
|
|
10493
|
+
if (!this.stopping) {
|
|
10494
|
+
log30.warn(TAG29, `Presence track returned "${trackStatus}" — scheduling reconnect`);
|
|
10495
|
+
this.schedulePresenceReconnect();
|
|
10496
|
+
}
|
|
10497
|
+
return;
|
|
10498
|
+
}
|
|
9224
10499
|
if (!isPretty2() || !this.suppressStartupLogs) {
|
|
9225
10500
|
log30.info(TAG29, "Presence tracked on board-presence channel");
|
|
9226
10501
|
}
|
|
9227
10502
|
this.presenceTracked = true;
|
|
10503
|
+
this.presenceReconnectAttempts = 0;
|
|
9228
10504
|
this.maybeResolveReady();
|
|
10505
|
+
} else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
|
|
10506
|
+
this.presenceTracked = false;
|
|
10507
|
+
if (!this.stopping) {
|
|
10508
|
+
log30.warn(TAG29, `Presence subscription ${status} — scheduling reconnect`);
|
|
10509
|
+
this.schedulePresenceReconnect();
|
|
10510
|
+
}
|
|
9229
10511
|
}
|
|
9230
10512
|
});
|
|
9231
|
-
|
|
10513
|
+
}
|
|
10514
|
+
schedulePresenceReconnect() {
|
|
10515
|
+
if (this.stopping || this.presenceReconnectTimer)
|
|
10516
|
+
return;
|
|
10517
|
+
const delay = Math.min(30000, 1000 * 2 ** this.presenceReconnectAttempts);
|
|
10518
|
+
this.presenceReconnectAttempts++;
|
|
10519
|
+
this.presenceReconnectTimer = setTimeout(() => {
|
|
10520
|
+
this.presenceReconnectTimer = null;
|
|
10521
|
+
this.reconnectPresence();
|
|
10522
|
+
}, delay);
|
|
10523
|
+
}
|
|
10524
|
+
async reconnectPresence() {
|
|
10525
|
+
if (this.stopping || !this.supabase)
|
|
10526
|
+
return;
|
|
10527
|
+
log30.warn(TAG29, `Reconnecting presence subscription (attempt ${this.presenceReconnectAttempts})`);
|
|
10528
|
+
if (this.presenceChannel) {
|
|
10529
|
+
const old = this.presenceChannel;
|
|
10530
|
+
this.presenceChannel = null;
|
|
10531
|
+
this.presenceGen++;
|
|
10532
|
+
try {
|
|
10533
|
+
await this.supabase.removeChannel(old);
|
|
10534
|
+
} catch {}
|
|
10535
|
+
}
|
|
10536
|
+
if (this.stopping || !this.supabase)
|
|
10537
|
+
return;
|
|
10538
|
+
this.subscribePresence();
|
|
9232
10539
|
}
|
|
9233
10540
|
subscribeBroadcast() {
|
|
9234
10541
|
if (!this.supabase)
|
|
@@ -9295,6 +10602,8 @@ class Watcher {
|
|
|
9295
10602
|
await this.supabase.removeChannel(old);
|
|
9296
10603
|
} catch {}
|
|
9297
10604
|
}
|
|
10605
|
+
if (this.stopping || !this.supabase)
|
|
10606
|
+
return;
|
|
9298
10607
|
this.subscribeBroadcast();
|
|
9299
10608
|
}
|
|
9300
10609
|
async stop() {
|
|
@@ -9303,6 +10612,10 @@ class Watcher {
|
|
|
9303
10612
|
clearTimeout(this.reconnectTimer);
|
|
9304
10613
|
this.reconnectTimer = null;
|
|
9305
10614
|
}
|
|
10615
|
+
if (this.presenceReconnectTimer) {
|
|
10616
|
+
clearTimeout(this.presenceReconnectTimer);
|
|
10617
|
+
this.presenceReconnectTimer = null;
|
|
10618
|
+
}
|
|
9306
10619
|
if (this.presenceChannel) {
|
|
9307
10620
|
await this.supabase?.removeChannel(this.presenceChannel);
|
|
9308
10621
|
this.presenceChannel = null;
|
|
@@ -9316,6 +10629,7 @@ class Watcher {
|
|
|
9316
10629
|
this.supabase = null;
|
|
9317
10630
|
}
|
|
9318
10631
|
this.connected = false;
|
|
10632
|
+
this.presenceTracked = false;
|
|
9319
10633
|
log30.info(TAG29, "Broadcast subscription stopped");
|
|
9320
10634
|
}
|
|
9321
10635
|
}
|
|
@@ -9330,7 +10644,7 @@ __export(exports_worktree_gc, {
|
|
|
9330
10644
|
isTransientGitNetworkError: () => isTransientGitNetworkError,
|
|
9331
10645
|
WorktreeGc: () => WorktreeGc
|
|
9332
10646
|
});
|
|
9333
|
-
import { execFileSync as
|
|
10647
|
+
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
9334
10648
|
import { readdirSync, statSync as statSync2 } from "node:fs";
|
|
9335
10649
|
import { resolve as resolve2 } from "node:path";
|
|
9336
10650
|
import { cleanupWorktree as cleanupWorktree4, log as log31 } from "@gethmy/harness";
|
|
@@ -9399,7 +10713,7 @@ function runWorktreeGc(basePath, store, opts = {}) {
|
|
|
9399
10713
|
}
|
|
9400
10714
|
}
|
|
9401
10715
|
try {
|
|
9402
|
-
|
|
10716
|
+
execFileSync6("git", ["worktree", "prune", "--expire=now"], {
|
|
9403
10717
|
cwd: repoRoot,
|
|
9404
10718
|
stdio: "pipe"
|
|
9405
10719
|
});
|
|
@@ -9430,7 +10744,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
9430
10744
|
return result;
|
|
9431
10745
|
}
|
|
9432
10746
|
try {
|
|
9433
|
-
|
|
10747
|
+
execFileSync6("git", ["fetch", "--prune", "origin"], {
|
|
9434
10748
|
cwd: repoRoot,
|
|
9435
10749
|
stdio: "pipe",
|
|
9436
10750
|
...GIT_NETWORK_EXEC
|
|
@@ -9446,7 +10760,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
9446
10760
|
const refPattern = `refs/remotes/origin/${opts.prefix}*`;
|
|
9447
10761
|
let listing = "";
|
|
9448
10762
|
try {
|
|
9449
|
-
listing =
|
|
10763
|
+
listing = execFileSync6("git", [
|
|
9450
10764
|
"for-each-ref",
|
|
9451
10765
|
"--format=%(refname:strip=3) %(committerdate:unix)",
|
|
9452
10766
|
refPattern
|
|
@@ -9481,7 +10795,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
9481
10795
|
break;
|
|
9482
10796
|
}
|
|
9483
10797
|
try {
|
|
9484
|
-
|
|
10798
|
+
execFileSync6("git", ["push", "origin", `:refs/heads/${ref}`], {
|
|
9485
10799
|
cwd: repoRoot,
|
|
9486
10800
|
stdio: "pipe",
|
|
9487
10801
|
...GIT_NETWORK_EXEC
|
|
@@ -9544,7 +10858,7 @@ class WorktreeGc {
|
|
|
9544
10858
|
}
|
|
9545
10859
|
function getRepoRoot2() {
|
|
9546
10860
|
try {
|
|
9547
|
-
return
|
|
10861
|
+
return execFileSync6("git", ["rev-parse", "--show-toplevel"], {
|
|
9548
10862
|
encoding: "utf-8"
|
|
9549
10863
|
}).trim();
|
|
9550
10864
|
} catch {
|
|
@@ -9583,7 +10897,7 @@ __export(exports_src, {
|
|
|
9583
10897
|
validatePrerequisites: () => validatePrerequisites,
|
|
9584
10898
|
main: () => main
|
|
9585
10899
|
});
|
|
9586
|
-
import { execFileSync as
|
|
10900
|
+
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
9587
10901
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
9588
10902
|
import { createRequire as createRequire3 } from "node:module";
|
|
9589
10903
|
import {
|
|
@@ -9593,7 +10907,7 @@ import {
|
|
|
9593
10907
|
} from "@gethmy/harness";
|
|
9594
10908
|
async function validatePrerequisites(config, banner) {
|
|
9595
10909
|
try {
|
|
9596
|
-
const ver =
|
|
10910
|
+
const ver = execFileSync7("claude", ["--version"], {
|
|
9597
10911
|
encoding: "utf-8"
|
|
9598
10912
|
}).trim();
|
|
9599
10913
|
banner.check(`Claude CLI ${ver}`);
|
|
@@ -9608,20 +10922,23 @@ async function validatePrerequisites(config, banner) {
|
|
|
9608
10922
|
validateGitProviderCli(provider);
|
|
9609
10923
|
}
|
|
9610
10924
|
try {
|
|
9611
|
-
const status =
|
|
9612
|
-
encoding: "utf-8"
|
|
10925
|
+
const status = execFileSync7("git", ["status", "--porcelain"], {
|
|
10926
|
+
encoding: "utf-8",
|
|
10927
|
+
stdio: "pipe"
|
|
9613
10928
|
}).trim();
|
|
9614
10929
|
if (status) {
|
|
9615
10930
|
banner.warn(`Working directory has uncommitted changes:
|
|
9616
10931
|
${status}`);
|
|
9617
10932
|
}
|
|
9618
|
-
execFileSync5("git", ["rev-parse", "--verify", `origin/${config.agent.worktree.baseBranch}`], {
|
|
9619
|
-
encoding: "utf-8",
|
|
9620
|
-
stdio: "pipe"
|
|
9621
|
-
});
|
|
9622
10933
|
} catch {
|
|
9623
|
-
throw new Error(`
|
|
10934
|
+
throw new Error(`Not a git repository: ${process.cwd()}. Start the daemon from the checkout its worktrees should branch from.`);
|
|
9624
10935
|
}
|
|
10936
|
+
const base = config.agent.worktree.baseBranch;
|
|
10937
|
+
const resolution = resolveBaseBranch(base, BASE_REMOTE, createGitProbe(process.cwd()));
|
|
10938
|
+
if (!resolution.ok) {
|
|
10939
|
+
throw new Error(`Git base branch "${BASE_REMOTE}/${base}" is unusable. ${resolution.message}`);
|
|
10940
|
+
}
|
|
10941
|
+
banner.check(resolution.fetched ? `Base branch ${BASE_REMOTE}/${base} (fetched)` : `Base branch ${BASE_REMOTE}/${base}`);
|
|
9625
10942
|
const client = createApiClient(config);
|
|
9626
10943
|
try {
|
|
9627
10944
|
await client.listWorkspaces();
|
|
@@ -9687,10 +11004,17 @@ async function main() {
|
|
|
9687
11004
|
const { agent: registeredAgent } = await client.registerWorkspaceAgent(config.workspaceId, {
|
|
9688
11005
|
identifier: config.agentIdentifier,
|
|
9689
11006
|
name: config.agentName,
|
|
9690
|
-
color: config.agentColor
|
|
11007
|
+
color: config.agentColor,
|
|
11008
|
+
declaredGateMetrics: declaredMetricNames(config.agent.playbooks.metrics)
|
|
9691
11009
|
});
|
|
9692
11010
|
const agentId = registeredAgent.id;
|
|
9693
11011
|
banner.check(`Agent registered (${config.agentName})`);
|
|
11012
|
+
try {
|
|
11013
|
+
const undeclared = await findUndeclaredGateMetrics(client, config.projectId, config.agent);
|
|
11014
|
+
for (const finding of undeclared) {
|
|
11015
|
+
banner.warn(formatUndeclaredMetricWarning(finding));
|
|
11016
|
+
}
|
|
11017
|
+
} catch {}
|
|
9694
11018
|
const identity = { userId: agentUserId, agentId };
|
|
9695
11019
|
const realtimeCreds = await fetchRealtimeCredentials(client);
|
|
9696
11020
|
banner.check("Realtime credentials");
|
|
@@ -9920,14 +11244,16 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
|
9920
11244
|
}
|
|
9921
11245
|
await pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
9922
11246
|
}
|
|
9923
|
-
var TAG31 = "daemon", PKG_VERSION;
|
|
11247
|
+
var TAG31 = "daemon", BASE_REMOTE = "origin", PKG_VERSION;
|
|
9924
11248
|
var init_src = __esm(() => {
|
|
11249
|
+
init_base_branch();
|
|
9925
11250
|
init_board_helpers();
|
|
9926
11251
|
init_board_reviewer();
|
|
9927
11252
|
init_config();
|
|
9928
11253
|
init_config_validation();
|
|
9929
11254
|
init_http_server();
|
|
9930
11255
|
init_merge_monitor();
|
|
11256
|
+
init_metric_validation();
|
|
9931
11257
|
init_pool();
|
|
9932
11258
|
init_port_registry();
|
|
9933
11259
|
init_reconcile();
|
|
@@ -10148,7 +11474,7 @@ function computeRunStats(runs, cards, logs = []) {
|
|
|
10148
11474
|
const verifyFailCount = failureReasons.verification ?? 0;
|
|
10149
11475
|
return {
|
|
10150
11476
|
totalRuns: runs.length,
|
|
10151
|
-
activeRuns: runs.length - ended.length,
|
|
11477
|
+
activeRuns: runs.length - ended.length - byStatus.parked,
|
|
10152
11478
|
endedRuns: ended.length,
|
|
10153
11479
|
byPipeline,
|
|
10154
11480
|
byStatus,
|
|
@@ -10195,6 +11521,7 @@ function buildRunListRows(runs, logs, opts = {}) {
|
|
|
10195
11521
|
}
|
|
10196
11522
|
var EMPTY_STATUS_HISTOGRAM = () => ({
|
|
10197
11523
|
active: 0,
|
|
11524
|
+
parked: 0,
|
|
10198
11525
|
completed: 0,
|
|
10199
11526
|
paused: 0,
|
|
10200
11527
|
failed: 0,
|
|
@@ -10370,10 +11697,12 @@ async function recoverCommand() {
|
|
|
10370
11697
|
const { buildLabelMap: buildLabelMap2 } = await Promise.resolve().then(() => (init_board_helpers(), exports_board_helpers));
|
|
10371
11698
|
const config = loadDaemonConfig2();
|
|
10372
11699
|
const client = createApiClient2(config);
|
|
11700
|
+
const { declaredMetricNames: declaredMetricNames2 } = await Promise.resolve().then(() => exports_declared_metrics);
|
|
10373
11701
|
const { agent: registeredAgent } = await client.registerWorkspaceAgent(config.workspaceId, {
|
|
10374
11702
|
identifier: config.agentIdentifier,
|
|
10375
11703
|
name: config.agentName,
|
|
10376
|
-
color: config.agentColor
|
|
11704
|
+
color: config.agentColor,
|
|
11705
|
+
declaredGateMetrics: declaredMetricNames2(config.agent.playbooks.metrics)
|
|
10377
11706
|
});
|
|
10378
11707
|
const agentId = registeredAgent.id;
|
|
10379
11708
|
const monitor = new MergeMonitor2(client, config.projectId, config.agent);
|
|
@@ -10633,12 +11962,12 @@ async function statsCommand() {
|
|
|
10633
11962
|
return 0;
|
|
10634
11963
|
}
|
|
10635
11964
|
const pct = (x) => `${(x * 100).toFixed(0)}%`;
|
|
10636
|
-
out.write(`runs total=${stats.totalRuns} active=${stats.activeRuns} ended=${stats.endedRuns} (failure rate ${pct(stats.runFailureRate)})
|
|
11965
|
+
out.write(`runs total=${stats.totalRuns} active=${stats.activeRuns} parked=${stats.byStatus.parked} ended=${stats.endedRuns} (failure rate ${pct(stats.runFailureRate)})
|
|
10637
11966
|
`);
|
|
10638
11967
|
out.write(`pipelines implement=${stats.byPipeline.implement} review=${stats.byPipeline.review}
|
|
10639
11968
|
`);
|
|
10640
11969
|
const s = stats.byStatus;
|
|
10641
|
-
out.write(`status completed=${s.completed} failed=${s.failed} orphaned=${s.orphaned} paused=${s.paused} active=${s.active}
|
|
11970
|
+
out.write(`status completed=${s.completed} failed=${s.failed} orphaned=${s.orphaned} paused=${s.paused} parked=${s.parked} active=${s.active}
|
|
10642
11971
|
`);
|
|
10643
11972
|
out.write(`cards distinct=${stats.distinctCards} avg runs/card=${stats.avgRunsPerCard.toFixed(1)}
|
|
10644
11973
|
`);
|