@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.
Files changed (3) hide show
  1. package/dist/cli.js +1896 -567
  2. package/dist/index.js +1889 -563
  3. 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 && reviewedSha && headSha && reviewedSha !== headSha) {
2276
- return "rereview";
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
- log4.debug(TAG4, `#${card.short_id} waiting (ci=${ciStatus})`);
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 = execFileSync("git", ["rev-parse", "--show-toplevel"], {
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
- execFileSync("git", ["worktree", "prune", "--expire=now"], {
2533
+ execFileSync2("git", ["worktree", "prune", "--expire=now"], {
2365
2534
  cwd: repoRoot,
2366
2535
  stdio: "pipe"
2367
2536
  });
2368
2537
  } catch {}
2369
2538
  try {
2370
- execFileSync("git", ["fetch", "origin", branchName], {
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
- execFileSync("git", ["branch", "-D", branchName], {
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
- execFileSync("git", [
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
- "**Agent gave upneeds a human.**",
2673
- `Stopped after ${maxAttempts} failed attempt${maxAttempts === 1 ? "" : "s"}. Reassign the card to try again.`
2893
+ "**Out of attemptsover 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 execFileSync2 } from "node:child_process";
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 execFileSync2("git", ["rev-parse", "HEAD"], {
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 = execFileSync2("git", ["status", "--porcelain"], {
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
- execFileSync2("git", ["add", "-A"], {
3657
+ execFileSync3("git", ["add", "-A"], {
3347
3658
  cwd: worktreePath,
3348
3659
  encoding: "utf-8"
3349
3660
  });
3350
- execFileSync2("git", ["commit", "-m", message], {
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 = execFileSync2("git", ["rev-list", "--count", `origin/${baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
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 = execFileSync2("git", ["log", "--oneline", `origin/${baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
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/review-completion.ts
3801
- import { readFileSync as readFileSync2, statSync } from "node:fs";
3802
- import {
3803
- cleanupWorktree as cleanupWorktree2,
3804
- createPullRequest as createPullRequest2,
3805
- detectGitProvider as detectGitProvider4,
3806
- extractPrUrl as extractPrUrl2,
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 renderFindingBlock(f) {
3818
- const locationLine = f.location ? `
3819
- Location: ${f.location}` : "";
3820
- return `**[${f.severity}] ${f.title}**
3821
- ${f.description}${locationLine}`;
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 buildFindingComments(findings) {
3824
- const header = `**Review findings — ${findings.length} blocking issue(s) to resolve.**`;
3825
- const sep = `
3826
-
3827
- `;
3828
- const bodies = [];
3829
- let current = header;
3830
- for (const f of findings) {
3831
- let block = renderFindingBlock(f);
3832
- const maxBlock = COMMENT_BODY_BUDGET - header.length - sep.length;
3833
- if (block.length > maxBlock) {
3834
- const suffix = `
3835
- …[truncated]`;
3836
- block = `${block.slice(0, Math.max(0, maxBlock - suffix.length))}${suffix}`;
3837
- }
3838
- if (current.length + sep.length + block.length > COMMENT_BODY_BUDGET) {
3839
- bodies.push(current);
3840
- current = `${header}${sep}${block}`;
3841
- } else {
3842
- current = `${current}${sep}${block}`;
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 acceptanceSummaryLine(checks) {
3850
- if (!checks || checks.length === 0)
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 tailRunLog(path, bytes = RUN_LOG_TAIL_BYTES) {
4192
+ async function renderPastEpisodesSection(client, title, description, workspaceId, projectId) {
4193
+ if (!projectId)
4194
+ return "";
3877
4195
  try {
3878
- const size = statSync(path).size;
3879
- if (size === 0)
3880
- return null;
3881
- const start = Math.max(0, size - bytes);
3882
- const buf = readFileSync2(path);
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
- log11.debug(TAG11, "Parsed review output from fenced JSON block");
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
- log11.debug(TAG11, "Parsed review output from raw JSON object");
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
- log11.warn(TAG11, `Parsed verdict via regex fallback — findings lost (${verdictMatch[1]})`);
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
- log11.warn(TAG11, "Failed to parse review JSON output — returning error verdict (card stays in Review)");
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
- log11.error(TAG11, `Failed to post review comment to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
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
- log11.warn(TAG11, `#${card.short_id} review output unparseable — labelling "${NEED_REVIEW_LABEL}" for manual inspection`);
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
- log11.warn(TAG11, `Failed to add "${NEED_REVIEW_LABEL}" label: ${err instanceof Error ? err.message : err}`);
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
- log11.warn(TAG11, `Branch rename failed (continuing on ${branchName}): ${err instanceof Error ? err.message : err}`);
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
- log11.warn(TAG11, `Failed to persist PR URL to #${card.short_id} description: ${err instanceof Error ? err.message : err}`);
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
- log11.warn(TAG11, `Failed to persist Reviewed-SHA to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
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
- log11.info(TAG11, `#${card.short_id} approved${prUrl ? ` — PR: ${prUrl}` : ""} — labeled "${config.review.approvedLabel}"`);
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
- log11.warn(TAG11, `#${card.short_id} reached max review cycles (${maxCycles}), moving to Done with note`);
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
- log11.error(TAG11, `Failed to create finding subtask: ${err instanceof Error ? err.message : err}`);
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
- log11.error(TAG11, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
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
- log11.error(TAG11, `Failed to update review cycle marker: ${err instanceof Error ? err.message : err}`);
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
- log11.info(TAG11, `#${card.short_id} flagged needs_plan_refresh after rejected review`);
4719
+ log12.info(TAG12, `#${card.short_id} flagged needs_plan_refresh after rejected review`);
4204
4720
  } catch (err) {
4205
- log11.warn(TAG11, `Failed to flag needs_plan_refresh for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
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
- log11.debug(TAG11, `recordFailureSummary failed: ${err instanceof Error ? err.message : err}`);
4735
+ log12.debug(TAG12, `recordFailureSummary failed: ${err instanceof Error ? err.message : err}`);
4220
4736
  }
4221
4737
  if (recoveryBranch) {
4222
- log11.info(TAG11, `#${card.short_id} recovery branch ${recoveryBranch}${recoveryUrl ? ` (${recoveryUrl})` : ""}`);
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
- log11.info(TAG11, `#${card.short_id} rejected (cycle ${currentCycle}/${maxCycles}) — moved to "${config.review.failColumn}"`);
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 TAG11 = "review-completion", MAX_FINDINGS = 10, MAX_SUBTASK_TITLE = 120, COMMENT_BODY_BUDGET = 9500, REVIEW_MARKER = `---
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 log12 } from "@gethmy/harness";
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
- log12.warn(tag, `Failed to open run log: ${err instanceof Error ? err.message : err}`);
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 log13 } from "@gethmy/harness";
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
- log13.warn(TAG12, `state file has version ${parsed?.version}, expected ${SCHEMA_VERSION} — migrating (preserving card budget/attempts, dropping in-flight runs)`);
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
- log13.error(TAG12, `failed to read state file: ${err instanceof Error ? err.message : err}`);
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 TAG12 = "state-store", SCHEMA_VERSION = 1;
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 log14 } from "@gethmy/harness";
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 TAG13 = "stream-parser", StreamParser;
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
- log14.debug(TAG13, `Non-JSON line: ${line.slice(0, 100)}`);
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
- log14.warn(TAG13, `Error handling stream event: ${errMsg}`);
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 log15 } from "@gethmy/harness";
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
- log15.warn(TAG14, `${step} failed for #${cardShortId} (attempt ${i + 1}/${attempts}): ${msg2} — retrying in ${wait}ms`);
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
- log15.warn(TAG14, `#${shortId}: ${msg} — skipping move`);
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
- log15.info(TAG14, `#${shortId} → "${target.name}"`);
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
- log15.info(TAG14, `#${shortId} +label "${name}"`);
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
- log15.info(TAG14, `#${shortId} -label "${name}"`);
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
- log15.info(TAG14, `#${shortId} updated`);
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
- log15.info(TAG14, `#${shortId} session ended (${plan.endSession.status})`);
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
- log15.info(TAG14, assignedAgentId ? `#${shortId} assigned → agent ${assignedAgentId}` : `#${shortId} unassigned`);
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
- log15.warn(TAG14, `ensureLabel "${name}" failed: ${err instanceof Error ? err.message : err}`);
5527
+ log16.warn(TAG15, `ensureLabel "${name}" failed: ${err instanceof Error ? err.message : err}`);
4956
5528
  return null;
4957
5529
  }
4958
5530
  }
4959
- var TAG14 = "transition", TransitionError;
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 execFileSync3 } from "node:child_process";
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 log16,
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
- log16.warn(this.tag, `state store updateRun failed: ${err instanceof Error ? err.message : err}`);
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 `${TAG15}:${this.id}`;
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
- log16.info(this.tag, `Preparing review for #${card.short_id} "${card.title}"`);
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
- await this.stateStore.insertRun({
5094
- runId: this.runId,
5095
- cardId: card.id,
5096
- cardShortId: card.short_id,
5097
- pipeline: "review",
5098
- workerId: this.id,
5099
- sessionId: null,
5100
- worktreePath: null,
5101
- branchName: null,
5102
- daemonPid: process.pid,
5103
- phase: "preparing",
5104
- startedAt: this.startedAt,
5105
- lastHeartbeatAt: this.startedAt,
5106
- endedAt: null,
5107
- status: "active",
5108
- costCents: 0,
5109
- numTurns: 0
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
- reviewSession = started.session;
5137
- } catch (err) {
5138
- if (isSessionConflict(err)) {
5139
- this.sessionConflict = true;
5140
- log16.info(this.tag, `Skipping review of #${card.short_id}: ${err instanceof Error ? err.message : String(err)}`);
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
- throw err;
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
- log16.info(this.tag, `Starting dev server on port ${port}...`);
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
- log16.info(this.tag, `Dev server ready on port ${port}`);
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 = execFileSync3("git", ["diff", `origin/${this.config.worktree.baseBranch}..HEAD`], { cwd, encoding: "utf-8", timeout: 30000 });
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
- log16.warn(this.tag, `pinned-contract fetch failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
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
- log16.info(this.tag, `Grading pinned contract for #${card.short_id} (${pinnedContract.assertions.length} criteria)`);
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
- const userPrompt = buildReviewUserPrompt(enriched, this.branchName, cwd, previewUrl, diffSummary, this.config.worktree.baseBranch, undefined, pinnedContract);
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
- log16.warn(this.tag, `prompt_history persistence skipped: ${err instanceof Error ? err.message : String(err)}`);
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
- log16.warn(this.tag, `Review timeout reached (${this.config.review.maxTimeout}ms), cancelling`);
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
- log16.info(this.tag, `Claude review finished for #${card.short_id}`);
5880
+ log17.info(this.tag, `Claude review finished for #${card.short_id}`);
5259
5881
  this.killDevServer();
5260
5882
  const result = parseReviewOutput(stdout);
5261
- log16.info(this.tag, `Review verdict: ${result.verdict} (${result.findings.length} finding(s))`);
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
- log16.error(this.tag, `Error reviewing #${card.short_id}: ${msg}`);
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
- log16.error(this.tag, `endAgentSession unrecoverable on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
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
- log16.info(this.tag, `#${card.short_id} kept in Review — dev server unavailable, human review needed`);
5919
+ log17.info(this.tag, `#${card.short_id} kept in Review — dev server unavailable, human review needed`);
5290
5920
  } catch {
5291
- log16.warn(this.tag, "Failed to add Need Review label after dev-server failure");
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
- log16.info(this.tag, `Moved #${card.short_id} to "${this.config.review.failColumn}" after error`);
5926
+ log17.info(this.tag, `Moved #${card.short_id} to "${this.config.review.failColumn}" after error`);
5297
5927
  } catch {
5298
- log16.warn(this.tag, "Failed to move card to fail column after error");
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
- log16.info(this.tag, `Pausing review on ${this.cardId}`);
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
- log16.warn(this.tag, "Failed to update agent session to paused");
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
- log16.info(this.tag, `Resuming review on ${this.cardId}`);
6073
+ log17.info(this.tag, `Resuming review on ${this.cardId}`);
5346
6074
  signalGroup(this.process, "SIGCONT");
5347
6075
  this.timeoutTimer = setTimeout(() => {
5348
- log16.warn(this.tag, `Timeout reached (${this.config.review.maxTimeout}ms), cancelling`);
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
- log16.warn(this.tag, "Failed to update agent session to working");
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
- log16.info(this.tag, `Cancelling review on ${this.cardId}`);
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
- if (this.cardId) {
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(this.config.claude.reviewMaxTurns),
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
- log16.info(this.tag, `Spawning review: claude ${args.slice(0, 5).join(" ")} ...`);
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
- log16.info(this.tag, `Run log: ${runLog.path}`);
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
- log16.debug(this.tag, `Stream parse error (non-fatal): ${msg}`);
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
- if (this.aborted) {
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
- log16.info(this.tag, `Recorded review_passed gate evidence for #${card.short_id} stage "${resolved.stage.name}": result=${evidence.result} passed=${evaluation.passed}`);
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
- log16.warn(this.tag, `review gate evidence collection failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
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
- log16.debug(this.tag, "Killed dev server group");
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" && this.branchName) {
6271
+ if (this.worktreePath && this.state === "error") {
5521
6272
  try {
5522
- cleanupWorktree3(this.worktreePath, this.branchName);
6273
+ cleanupWorktree3(this.worktreePath);
5523
6274
  } catch {
5524
- log16.warn(this.tag, "Failed to cleanup review worktree");
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 TAG15 = "review-worker", CANCEL_SIGINT_TIMEOUT = 30000, CANCEL_SIGTERM_TIMEOUT = 1e4;
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 log17 } from "@gethmy/harness";
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
- log17.info(TAG16, "sleep assertion released");
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
- log17.warn(TAG16, `caffeinate unavailable: ${err.message}`);
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
- log17.info(TAG16, "sleep assertion acquired (caffeinate -i)");
6364
+ log18.info(TAG17, "sleep assertion acquired (caffeinate -i)");
5612
6365
  } catch (err) {
5613
- log17.warn(TAG16, `failed to spawn caffeinate: ${err instanceof Error ? err.message : err}`);
6366
+ log18.warn(TAG17, `failed to spawn caffeinate: ${err instanceof Error ? err.message : err}`);
5614
6367
  }
5615
6368
  }
5616
6369
  }
5617
- var TAG16 = "sleep-guard";
6370
+ var TAG17 = "sleep-guard";
5618
6371
  var init_sleep_guard = () => {};
5619
6372
 
5620
6373
  // src/unblock.ts
5621
- import { log as log18 } from "@gethmy/harness";
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
- log18.warn(TAG17, `link fetch failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
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
- log18.info(TAG17, `#${completedCard.short_id} completed — checking ${successors.length} chained successor(s)`);
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
- log18.info(TAG17, `successor #${card.short_id} unassigned — auto-assigning to continue chain`);
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
- log18.debug(TAG17, `successor #${card.short_id} assigned to different entity — skipping`);
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
- log18.warn(TAG17, `promotion failed for successor ${successorId}: ${err instanceof Error ? err.message : err}`);
6428
+ log19.warn(TAG18, `promotion failed for successor ${successorId}: ${err instanceof Error ? err.message : err}`);
5676
6429
  }
5677
6430
  }
5678
6431
  }
5679
- var TAG17 = "unblock";
6432
+ var TAG18 = "unblock";
5680
6433
  var init_unblock = () => {};
5681
6434
 
5682
6435
  // src/cli-agent-runner.ts
5683
- import { log as log19 } from "@gethmy/harness";
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
- log19.warn(TAG18, `Failed to flush run events: ${err}`);
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 TAG18 = "cli-agent-runner", FLUSH_INTERVAL_MS = 2000, MAX_BUFFER = 1000, MAX_TEXT_LEN = 8000, MAX_OUTPUT_LEN = 4000;
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 ?? spawn2;
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
- try {
5944
- child.kill("SIGTERM");
5945
- const escalate = setTimeout(() => {
5946
- try {
5947
- child.kill("SIGKILL");
5948
- } catch {}
5949
- }, ABORT_SIGKILL_GRACE_MS);
5950
- escalate.unref?.();
5951
- } catch {}
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
- lines.push(line);
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 ABORT_SIGKILL_GRACE_MS = 1e4;
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 — marked done`);
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
- reapGroup,
7141
+ readWorktreeHead,
7142
+ reapGroup as reapGroup2,
6524
7143
  SdkAgentRunner,
6525
7144
  signalGroup as signalGroup2,
6526
- spawnInGroup as spawnInGroup2,
7145
+ sizeRun,
7146
+ sizingEventSource,
7147
+ spawnInGroup as spawnInGroup3,
6527
7148
  teardownWorktree as teardownWorktree2,
6528
- terminateGroup as terminateGroup2,
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
- this.branchName = makeBranchName(card.short_id, card.title, this.config.worktree.failedBranchPrefix);
6715
- log22.info(this.tag, `Preparing #${card.short_id} "${card.title}"`);
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.stateStore.insertRun({
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 session;
6739
- try {
6740
- const started = await this.client.startAgentSession(card.id, {
6741
- agentIdentifier: agentIdentifier(this.id),
6742
- agentName: AGENT_NAME,
6743
- agentId: this.identity.agentId,
6744
- status: "working",
6745
- currentTask: "Setting up worktree",
6746
- progressPercent: 5,
6747
- modelName: implementModel,
6748
- driver: "daemon"
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
- session = started.session;
6751
- } catch (err) {
6752
- if (isSessionConflict(err)) {
6753
- this.sessionConflict = true;
6754
- log22.info(this.tag, `Skipping #${card.short_id}: ${err instanceof Error ? err.message : String(err)}`);
6755
- await this.stateStore.decrementAttempt(card.id);
6756
- return;
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
- throw err;
6759
- }
6760
- const sid = session && typeof session === "object" && "id" in session ? session.id : null;
6761
- if (!sid) {
6762
- log22.warn(TAG21, "startAgentSession returned no session id");
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
- this.cliRunner.recordRunStarted({
6768
- runner: this.config.runner,
6769
- model: implementModel
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
- const moved = await moveCardAndAddLabel(this.client, card, IN_PROGRESS_COLUMN, "agent");
6774
- if (!moved) {
6775
- log22.warn(this.tag, `Card #${card.short_id} was NOT moved to "In Progress" — check API logs`);
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
- card = await this.autoBindPlaybookAtPickup(card, labels);
7455
+ if (!resuming) {
7456
+ card = await this.autoBindPlaybookAtPickup(card, labels);
7457
+ }
6780
7458
  if (this.aborted)
6781
7459
  return;
6782
- const stageCtx = await this.resolveStageContext(card);
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
- this.worktreePath = createWorktree(this.config.worktree.basePath, this.config.worktree.baseBranch, this.branchName, {
6789
- continueExisting: stageCtx.kind === "run" || stageCtx.kind === "motor" || isRework
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
- this.cliRunner?.recordStageEntered({
6832
- stageId: stageCtx.stage.id,
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
- iteration: priorIterations + 1,
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 (isRework) {
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 = apiError ? describeApiError(errClass.kind) : baseError ? "Could not fetch current base branch — requeued without counting an attempt" : `Run failed: ${msg.slice(0, 300)}`;
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 (tErr) {
7223
- log22.warn(this.tag, `hold transition failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
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.endRun(this.runId, "paused", {
7228
- errorMessage: "stage_hold",
7229
- ...this.runLedger()
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: (line) => log22.info(this.tag, `motor: ${line.type}`),
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 { model, escalated, source } = chooseImplementModel(this.config.claude, card, attempts);
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=${card.model_tier ?? "none"})`);
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 terminateGroup2(this.process, {
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
- terminateGroup2(this.process, {
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
- terminateGroup2(this.process, {
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 = spawnInGroup2("claude", args, {
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
- reapGroup(leaderPid);
7922
- if (this.aborted) {
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
- await presenceChannel.track({
9215
- daemonId: this.daemonId,
9216
- startedAt: new Date().toISOString(),
9217
- userId: this.identity.userId,
9218
- agentId: this.identity.agentId,
9219
- userEmail: this.identity.userEmail,
9220
- agentIdentifier: this.identity.agentIdentifier,
9221
- agentName: this.identity.agentName
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
- this.presenceChannel = presenceChannel;
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 execFileSync4 } from "node:child_process";
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
- execFileSync4("git", ["worktree", "prune", "--expire=now"], {
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
- execFileSync4("git", ["fetch", "--prune", "origin"], {
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 = execFileSync4("git", [
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
- execFileSync4("git", ["push", "origin", `:refs/heads/${ref}`], {
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 execFileSync4("git", ["rev-parse", "--show-toplevel"], {
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 execFileSync5 } from "node:child_process";
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 = execFileSync5("claude", ["--version"], {
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 = execFileSync5("git", ["status", "--porcelain"], {
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(`Git base branch "origin/${config.agent.worktree.baseBranch}" not found. Fetch or configure a different base branch.`);
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();