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