@gethmy/agent 1.22.2 → 1.22.4

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 +131 -13
  2. package/dist/index.js +131 -13
  3. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -258,6 +258,25 @@ function extractBranchRef(description) {
258
258
  }
259
259
  return null;
260
260
  }
261
+ function hasReviewableBranch(description) {
262
+ return extractBranchRef(description) !== null;
263
+ }
264
+ function hasReviewablePrLink(description) {
265
+ if (!description)
266
+ return false;
267
+ const m = description.match(PR_LINK_PATTERN);
268
+ if (!m)
269
+ return false;
270
+ try {
271
+ new URL(m[1]);
272
+ return true;
273
+ } catch {
274
+ return false;
275
+ }
276
+ }
277
+ function qualifiesForReview(description) {
278
+ return hasReviewableBranch(description) || hasReviewablePrLink(description);
279
+ }
261
280
  function hasUnsafeDaemonBranchLine(description) {
262
281
  if (!description)
263
282
  return false;
@@ -267,11 +286,12 @@ function hasUnsafeDaemonBranchLine(description) {
267
286
  }
268
287
  return false;
269
288
  }
270
- var BRANCH_REF_PATTERN, DAEMON_BRANCH_LINE_PATTERN, SAFE_GIT_REF_PATTERN;
289
+ var BRANCH_REF_PATTERN, DAEMON_BRANCH_LINE_PATTERN, SAFE_GIT_REF_PATTERN, PR_LINK_PATTERN;
271
290
  var init_branchRef = __esm(() => {
272
291
  BRANCH_REF_PATTERN = /Branch:\s*`([^`]+)`/g;
273
292
  DAEMON_BRANCH_LINE_PATTERN = /^[ \t]*Branch:\s*`([^`]+)`/gm;
274
293
  SAFE_GIT_REF_PATTERN = /^[a-zA-Z0-9/_.+-]+$/;
294
+ PR_LINK_PATTERN = /PR:\s*(https?:\/\/[^\s)]+)/;
275
295
  });
276
296
 
277
297
  // ../harmony-shared/dist/cardLinks.js
@@ -1444,7 +1464,7 @@ var init_types2 = __esm(() => {
1444
1464
  moveToColumn: "Done",
1445
1465
  failColumn: "To Do",
1446
1466
  devServerPort: 4300,
1447
- maxTimeout: 600000,
1467
+ maxTimeout: 1800000,
1448
1468
  postFindings: true,
1449
1469
  maxReviewCycles: 3,
1450
1470
  createPR: true,
@@ -2627,6 +2647,7 @@ function createWorktree(basePath, baseBranch, branchName, opts = {}) {
2627
2647
  } catch (err) {
2628
2648
  const msg = err instanceof Error ? err.message : String(err);
2629
2649
  log.warn(TAG6, `worktree add failed, attempting forced recovery: ${msg}`);
2650
+ removeWorktreeHoldingBranch(repoRoot, branchName, worktreeDir);
2630
2651
  try {
2631
2652
  execFileSync3("git", ["worktree", "remove", worktreeDir, "--force"], {
2632
2653
  cwd: repoRoot,
@@ -2699,6 +2720,54 @@ function cleanupWorktree(worktreePath, branchName) {
2699
2720
  } catch {}
2700
2721
  }
2701
2722
  }
2723
+ function removeWorktreeHoldingBranch(repoRoot, branchName, exceptDir) {
2724
+ let listing;
2725
+ try {
2726
+ listing = execFileSync3("git", ["worktree", "list", "--porcelain"], {
2727
+ cwd: repoRoot,
2728
+ encoding: "utf-8",
2729
+ stdio: ["ignore", "pipe", "pipe"]
2730
+ });
2731
+ } catch {
2732
+ return null;
2733
+ }
2734
+ const target = `refs/heads/${branchName}`;
2735
+ let currentPath = null;
2736
+ let holderPath = null;
2737
+ for (const line of listing.split(`
2738
+ `)) {
2739
+ if (line.startsWith("worktree ")) {
2740
+ currentPath = line.slice("worktree ".length).trim();
2741
+ } else if (line.startsWith("branch ")) {
2742
+ const ref = line.slice("branch ".length).trim();
2743
+ if (ref === target && currentPath) {
2744
+ holderPath = currentPath;
2745
+ break;
2746
+ }
2747
+ }
2748
+ }
2749
+ if (!holderPath)
2750
+ return null;
2751
+ if (exceptDir && resolve(holderPath) === resolve(exceptDir))
2752
+ return null;
2753
+ try {
2754
+ execFileSync3("git", ["worktree", "remove", holderPath, "--force"], {
2755
+ cwd: repoRoot,
2756
+ stdio: "pipe"
2757
+ });
2758
+ log.warn(TAG6, `Evicted worktree ${holderPath} holding branch ${branchName} so it can be reused (#732)`);
2759
+ } catch (err) {
2760
+ log.warn(TAG6, `Failed to evict worktree ${holderPath} holding ${branchName}: ${err instanceof Error ? err.message : err}`);
2761
+ return null;
2762
+ }
2763
+ try {
2764
+ execFileSync3("git", ["worktree", "prune", "--expire=now"], {
2765
+ cwd: repoRoot,
2766
+ stdio: "pipe"
2767
+ });
2768
+ } catch {}
2769
+ return holderPath;
2770
+ }
2702
2771
  function resolveRepoRoot() {
2703
2772
  return execFileSync3("git", ["rev-parse", "--show-toplevel"], {
2704
2773
  encoding: "utf-8"
@@ -2814,6 +2883,7 @@ function checkoutExistingBranch(basePath, branchName) {
2814
2883
  } catch (err) {
2815
2884
  throw new Error(`Failed to fetch remote branch ${branchName}: ${gitErrorDetail(err)}`);
2816
2885
  }
2886
+ removeWorktreeHoldingBranch(repoRoot, branchName, worktreeDir);
2817
2887
  try {
2818
2888
  execFileSync4("git", ["branch", "-D", branchName], {
2819
2889
  cwd: repoRoot,
@@ -2854,7 +2924,7 @@ function extractBranchFromDescription(description) {
2854
2924
  return branch;
2855
2925
  }
2856
2926
  function qualifiesForAutoReview(description) {
2857
- return Boolean(extractBranchFromDescription(description) || extractPrUrl(description ?? null));
2927
+ return qualifiesForReview(description);
2858
2928
  }
2859
2929
  async function resolveReviewBranch(description, cwd) {
2860
2930
  const fromLine = extractBranchFromDescription(description);
@@ -6404,8 +6474,16 @@ class StateStore {
6404
6474
  const raw = readFileSync4(this.path, "utf-8");
6405
6475
  const parsed = JSON.parse(raw);
6406
6476
  if (parsed?.version !== SCHEMA_VERSION) {
6407
- log.warn(TAG21, `state file has version ${parsed?.version}, expected ${SCHEMA_VERSION} — starting fresh`);
6408
- return emptyState();
6477
+ log.warn(TAG21, `state file has version ${parsed?.version}, expected ${SCHEMA_VERSION} — migrating (preserving card budget/attempts, dropping in-flight runs)`);
6478
+ return {
6479
+ version: SCHEMA_VERSION,
6480
+ daemonId: null,
6481
+ daemonPid: null,
6482
+ daemonStartedAt: null,
6483
+ runs: [],
6484
+ cards: parsed.cards ?? [],
6485
+ daily: parsed.daily ?? []
6486
+ };
6409
6487
  }
6410
6488
  return {
6411
6489
  version: SCHEMA_VERSION,
@@ -7749,6 +7827,20 @@ var init_cli_agent_runner = __esm(() => {
7749
7827
  });
7750
7828
 
7751
7829
  // src/prompt.ts
7830
+ function renderPreviousAttemptsSection(failures) {
7831
+ if (failures.length === 0)
7832
+ return "";
7833
+ const lines = failures.map((f) => {
7834
+ const tag = f.reason ? `[${f.reason}] ` : "";
7835
+ return `- ${tag}${f.summary}`;
7836
+ });
7837
+ return [
7838
+ "## Previous attempt feedback",
7839
+ "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.",
7840
+ ...lines
7841
+ ].join(`
7842
+ `);
7843
+ }
7752
7844
  async function buildPrompt(enriched, branchName, worktreePath, client, workspaceId, projectId) {
7753
7845
  const { card } = enriched;
7754
7846
  const pastEpisodesSection = await renderPastEpisodesSection(client, card.title, card.description ?? "", workspaceId, projectId);
@@ -8257,6 +8349,14 @@ class Worker {
8257
8349
  this.heartbeatTimer = null;
8258
8350
  }
8259
8351
  }
8352
+ captureCliSessionId(sessionId) {
8353
+ if (!sessionId || sessionId === this.cliSessionId)
8354
+ return;
8355
+ this.cliSessionId = sessionId;
8356
+ if (this.runId) {
8357
+ this.stateStore.updateRun(this.runId, { cliSessionId: sessionId }).catch(() => {});
8358
+ }
8359
+ }
8260
8360
  async recordPhase(phase) {
8261
8361
  if (!this.runId)
8262
8362
  return;
@@ -8308,7 +8408,8 @@ class Worker {
8308
8408
  this.state = "preparing";
8309
8409
  this.branchName = makeBranchName(card.short_id, card.title, this.config.worktree.failedBranchPrefix);
8310
8410
  log.info(this.tag, `Preparing #${card.short_id} "${card.title}"`);
8311
- await this.stateStore.incrementAttempt(card.id);
8411
+ const attemptCount = await this.stateStore.incrementAttempt(card.id);
8412
+ const isRework = attemptCount > 1;
8312
8413
  this.startHeartbeat();
8313
8414
  await this.stateStore.insertRun({
8314
8415
  runId: this.runId,
@@ -8361,7 +8462,7 @@ class Worker {
8361
8462
  await this.holdStageCard(card, stageCtx.reason);
8362
8463
  return;
8363
8464
  }
8364
- this.worktreePath = createWorktree(this.config.worktree.basePath, this.config.worktree.baseBranch, this.branchName, { continueExisting: stageCtx.kind === "run" });
8465
+ this.worktreePath = createWorktree(this.config.worktree.basePath, this.config.worktree.baseBranch, this.branchName, { continueExisting: stageCtx.kind === "run" || isRework });
8365
8466
  if (this.aborted)
8366
8467
  return;
8367
8468
  const enriched = {
@@ -8413,6 +8514,12 @@ class Worker {
8413
8514
  mode: loop.mode
8414
8515
  });
8415
8516
  }
8517
+ } else if (isRework) {
8518
+ const digest = renderPreviousAttemptsSection(this.stateStore.getRecentFailures(card.id, 3));
8519
+ if (digest)
8520
+ prompt = `${digest}
8521
+
8522
+ ${basePrompt}`;
8416
8523
  }
8417
8524
  await this.client.updateAgentProgress(card.id, {
8418
8525
  agentIdentifier: agentIdentifier(this.id),
@@ -9214,6 +9321,7 @@ class Worker {
9214
9321
  }
9215
9322
  parser.on("text", (content) => {
9216
9323
  this.lastRunText += content;
9324
+ this.captureCliSessionId(parser.sessionId);
9217
9325
  });
9218
9326
  parser.on("parse_error", (msg) => {
9219
9327
  log.debug(this.tag, `Stream parse error (non-fatal): ${msg}`);
@@ -9232,8 +9340,7 @@ class Worker {
9232
9340
  this.process.on("close", (code) => {
9233
9341
  const leaderPid = this.process?.pid;
9234
9342
  this.process = null;
9235
- if (parser.sessionId)
9236
- this.cliSessionId = parser.sessionId;
9343
+ this.captureCliSessionId(parser.sessionId);
9237
9344
  this.lastSessionStats = this.progressTracker?.stats;
9238
9345
  const spawnCost = this.lastSessionStats?.cost;
9239
9346
  if (spawnCost) {
@@ -9325,7 +9432,7 @@ class Worker {
9325
9432
  `);
9326
9433
  }
9327
9434
  } finally {
9328
- this.cliSessionId = runner.sessionId ?? this.cliSessionId;
9435
+ this.captureCliSessionId(runner.sessionId);
9329
9436
  this.lastSessionStats = this.progressTracker?.stats;
9330
9437
  const spawnCost = this.lastSessionStats?.cost;
9331
9438
  if (spawnCost) {
@@ -9815,11 +9922,13 @@ async function recoverOrphans(store, client, config) {
9815
9922
  continue;
9816
9923
  }
9817
9924
  log.info(TAG33, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
9818
- await recoverRun(run, store, client, config, outcome);
9925
+ await recoverRun(run, store, client, config, outcome, {
9926
+ rollbackAttempt: true
9927
+ });
9819
9928
  }
9820
9929
  return outcomes;
9821
9930
  }
9822
- async function recoverRun(run, store, client, config, outcome) {
9931
+ async function recoverRun(run, store, client, config, outcome, opts = {}) {
9823
9932
  try {
9824
9933
  await client.endAgentSession(run.cardId, {
9825
9934
  status: "failed",
@@ -9875,6 +9984,15 @@ async function recoverRun(run, store, client, config, outcome) {
9875
9984
  const msg = err instanceof Error ? err.message : String(err);
9876
9985
  outcome.errors.push(`endRun: ${msg}`);
9877
9986
  }
9987
+ if (opts.rollbackAttempt && run.pipeline === "implement") {
9988
+ try {
9989
+ await store.decrementAttempt(run.cardId);
9990
+ outcome.actions.push("rolled back give-up attempt (daemon restart)");
9991
+ } catch (err) {
9992
+ const msg = err instanceof Error ? err.message : String(err);
9993
+ outcome.errors.push(`decrementAttempt: ${msg}`);
9994
+ }
9995
+ }
9878
9996
  log.info(TAG33, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
9879
9997
  }
9880
9998
  var TAG33 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
@@ -10040,7 +10158,7 @@ class Reconciler {
10040
10158
  pipeline: run.pipeline,
10041
10159
  actions: [],
10042
10160
  errors: []
10043
- });
10161
+ }, { rollbackAttempt: daemonDead });
10044
10162
  }
10045
10163
  }
10046
10164
  async recoverStrandedInProgress(cards, columns, knownCardIds) {
package/dist/index.js CHANGED
@@ -257,6 +257,25 @@ function extractBranchRef(description) {
257
257
  }
258
258
  return null;
259
259
  }
260
+ function hasReviewableBranch(description) {
261
+ return extractBranchRef(description) !== null;
262
+ }
263
+ function hasReviewablePrLink(description) {
264
+ if (!description)
265
+ return false;
266
+ const m = description.match(PR_LINK_PATTERN);
267
+ if (!m)
268
+ return false;
269
+ try {
270
+ new URL(m[1]);
271
+ return true;
272
+ } catch {
273
+ return false;
274
+ }
275
+ }
276
+ function qualifiesForReview(description) {
277
+ return hasReviewableBranch(description) || hasReviewablePrLink(description);
278
+ }
260
279
  function hasUnsafeDaemonBranchLine(description) {
261
280
  if (!description)
262
281
  return false;
@@ -266,11 +285,12 @@ function hasUnsafeDaemonBranchLine(description) {
266
285
  }
267
286
  return false;
268
287
  }
269
- var BRANCH_REF_PATTERN, DAEMON_BRANCH_LINE_PATTERN, SAFE_GIT_REF_PATTERN;
288
+ var BRANCH_REF_PATTERN, DAEMON_BRANCH_LINE_PATTERN, SAFE_GIT_REF_PATTERN, PR_LINK_PATTERN;
270
289
  var init_branchRef = __esm(() => {
271
290
  BRANCH_REF_PATTERN = /Branch:\s*`([^`]+)`/g;
272
291
  DAEMON_BRANCH_LINE_PATTERN = /^[ \t]*Branch:\s*`([^`]+)`/gm;
273
292
  SAFE_GIT_REF_PATTERN = /^[a-zA-Z0-9/_.+-]+$/;
293
+ PR_LINK_PATTERN = /PR:\s*(https?:\/\/[^\s)]+)/;
274
294
  });
275
295
 
276
296
  // ../harmony-shared/dist/cardLinks.js
@@ -1443,7 +1463,7 @@ var init_types2 = __esm(() => {
1443
1463
  moveToColumn: "Done",
1444
1464
  failColumn: "To Do",
1445
1465
  devServerPort: 4300,
1446
- maxTimeout: 600000,
1466
+ maxTimeout: 1800000,
1447
1467
  postFindings: true,
1448
1468
  maxReviewCycles: 3,
1449
1469
  createPR: true,
@@ -2626,6 +2646,7 @@ function createWorktree(basePath, baseBranch, branchName, opts = {}) {
2626
2646
  } catch (err) {
2627
2647
  const msg = err instanceof Error ? err.message : String(err);
2628
2648
  log.warn(TAG6, `worktree add failed, attempting forced recovery: ${msg}`);
2649
+ removeWorktreeHoldingBranch(repoRoot, branchName, worktreeDir);
2629
2650
  try {
2630
2651
  execFileSync3("git", ["worktree", "remove", worktreeDir, "--force"], {
2631
2652
  cwd: repoRoot,
@@ -2698,6 +2719,54 @@ function cleanupWorktree(worktreePath, branchName) {
2698
2719
  } catch {}
2699
2720
  }
2700
2721
  }
2722
+ function removeWorktreeHoldingBranch(repoRoot, branchName, exceptDir) {
2723
+ let listing;
2724
+ try {
2725
+ listing = execFileSync3("git", ["worktree", "list", "--porcelain"], {
2726
+ cwd: repoRoot,
2727
+ encoding: "utf-8",
2728
+ stdio: ["ignore", "pipe", "pipe"]
2729
+ });
2730
+ } catch {
2731
+ return null;
2732
+ }
2733
+ const target = `refs/heads/${branchName}`;
2734
+ let currentPath = null;
2735
+ let holderPath = null;
2736
+ for (const line of listing.split(`
2737
+ `)) {
2738
+ if (line.startsWith("worktree ")) {
2739
+ currentPath = line.slice("worktree ".length).trim();
2740
+ } else if (line.startsWith("branch ")) {
2741
+ const ref = line.slice("branch ".length).trim();
2742
+ if (ref === target && currentPath) {
2743
+ holderPath = currentPath;
2744
+ break;
2745
+ }
2746
+ }
2747
+ }
2748
+ if (!holderPath)
2749
+ return null;
2750
+ if (exceptDir && resolve(holderPath) === resolve(exceptDir))
2751
+ return null;
2752
+ try {
2753
+ execFileSync3("git", ["worktree", "remove", holderPath, "--force"], {
2754
+ cwd: repoRoot,
2755
+ stdio: "pipe"
2756
+ });
2757
+ log.warn(TAG6, `Evicted worktree ${holderPath} holding branch ${branchName} so it can be reused (#732)`);
2758
+ } catch (err) {
2759
+ log.warn(TAG6, `Failed to evict worktree ${holderPath} holding ${branchName}: ${err instanceof Error ? err.message : err}`);
2760
+ return null;
2761
+ }
2762
+ try {
2763
+ execFileSync3("git", ["worktree", "prune", "--expire=now"], {
2764
+ cwd: repoRoot,
2765
+ stdio: "pipe"
2766
+ });
2767
+ } catch {}
2768
+ return holderPath;
2769
+ }
2701
2770
  function resolveRepoRoot() {
2702
2771
  return execFileSync3("git", ["rev-parse", "--show-toplevel"], {
2703
2772
  encoding: "utf-8"
@@ -2813,6 +2882,7 @@ function checkoutExistingBranch(basePath, branchName) {
2813
2882
  } catch (err) {
2814
2883
  throw new Error(`Failed to fetch remote branch ${branchName}: ${gitErrorDetail(err)}`);
2815
2884
  }
2885
+ removeWorktreeHoldingBranch(repoRoot, branchName, worktreeDir);
2816
2886
  try {
2817
2887
  execFileSync4("git", ["branch", "-D", branchName], {
2818
2888
  cwd: repoRoot,
@@ -2853,7 +2923,7 @@ function extractBranchFromDescription(description) {
2853
2923
  return branch;
2854
2924
  }
2855
2925
  function qualifiesForAutoReview(description) {
2856
- return Boolean(extractBranchFromDescription(description) || extractPrUrl(description ?? null));
2926
+ return qualifiesForReview(description);
2857
2927
  }
2858
2928
  async function resolveReviewBranch(description, cwd) {
2859
2929
  const fromLine = extractBranchFromDescription(description);
@@ -6403,8 +6473,16 @@ class StateStore {
6403
6473
  const raw = readFileSync4(this.path, "utf-8");
6404
6474
  const parsed = JSON.parse(raw);
6405
6475
  if (parsed?.version !== SCHEMA_VERSION) {
6406
- log.warn(TAG21, `state file has version ${parsed?.version}, expected ${SCHEMA_VERSION} — starting fresh`);
6407
- return emptyState();
6476
+ log.warn(TAG21, `state file has version ${parsed?.version}, expected ${SCHEMA_VERSION} — migrating (preserving card budget/attempts, dropping in-flight runs)`);
6477
+ return {
6478
+ version: SCHEMA_VERSION,
6479
+ daemonId: null,
6480
+ daemonPid: null,
6481
+ daemonStartedAt: null,
6482
+ runs: [],
6483
+ cards: parsed.cards ?? [],
6484
+ daily: parsed.daily ?? []
6485
+ };
6408
6486
  }
6409
6487
  return {
6410
6488
  version: SCHEMA_VERSION,
@@ -7748,6 +7826,20 @@ var init_cli_agent_runner = __esm(() => {
7748
7826
  });
7749
7827
 
7750
7828
  // src/prompt.ts
7829
+ function renderPreviousAttemptsSection(failures) {
7830
+ if (failures.length === 0)
7831
+ return "";
7832
+ const lines = failures.map((f) => {
7833
+ const tag = f.reason ? `[${f.reason}] ` : "";
7834
+ return `- ${tag}${f.summary}`;
7835
+ });
7836
+ return [
7837
+ "## Previous attempt feedback",
7838
+ "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.",
7839
+ ...lines
7840
+ ].join(`
7841
+ `);
7842
+ }
7751
7843
  async function buildPrompt(enriched, branchName, worktreePath, client, workspaceId, projectId) {
7752
7844
  const { card } = enriched;
7753
7845
  const pastEpisodesSection = await renderPastEpisodesSection(client, card.title, card.description ?? "", workspaceId, projectId);
@@ -8256,6 +8348,14 @@ class Worker {
8256
8348
  this.heartbeatTimer = null;
8257
8349
  }
8258
8350
  }
8351
+ captureCliSessionId(sessionId) {
8352
+ if (!sessionId || sessionId === this.cliSessionId)
8353
+ return;
8354
+ this.cliSessionId = sessionId;
8355
+ if (this.runId) {
8356
+ this.stateStore.updateRun(this.runId, { cliSessionId: sessionId }).catch(() => {});
8357
+ }
8358
+ }
8259
8359
  async recordPhase(phase) {
8260
8360
  if (!this.runId)
8261
8361
  return;
@@ -8307,7 +8407,8 @@ class Worker {
8307
8407
  this.state = "preparing";
8308
8408
  this.branchName = makeBranchName(card.short_id, card.title, this.config.worktree.failedBranchPrefix);
8309
8409
  log.info(this.tag, `Preparing #${card.short_id} "${card.title}"`);
8310
- await this.stateStore.incrementAttempt(card.id);
8410
+ const attemptCount = await this.stateStore.incrementAttempt(card.id);
8411
+ const isRework = attemptCount > 1;
8311
8412
  this.startHeartbeat();
8312
8413
  await this.stateStore.insertRun({
8313
8414
  runId: this.runId,
@@ -8360,7 +8461,7 @@ class Worker {
8360
8461
  await this.holdStageCard(card, stageCtx.reason);
8361
8462
  return;
8362
8463
  }
8363
- this.worktreePath = createWorktree(this.config.worktree.basePath, this.config.worktree.baseBranch, this.branchName, { continueExisting: stageCtx.kind === "run" });
8464
+ this.worktreePath = createWorktree(this.config.worktree.basePath, this.config.worktree.baseBranch, this.branchName, { continueExisting: stageCtx.kind === "run" || isRework });
8364
8465
  if (this.aborted)
8365
8466
  return;
8366
8467
  const enriched = {
@@ -8412,6 +8513,12 @@ class Worker {
8412
8513
  mode: loop.mode
8413
8514
  });
8414
8515
  }
8516
+ } else if (isRework) {
8517
+ const digest = renderPreviousAttemptsSection(this.stateStore.getRecentFailures(card.id, 3));
8518
+ if (digest)
8519
+ prompt = `${digest}
8520
+
8521
+ ${basePrompt}`;
8415
8522
  }
8416
8523
  await this.client.updateAgentProgress(card.id, {
8417
8524
  agentIdentifier: agentIdentifier(this.id),
@@ -9213,6 +9320,7 @@ class Worker {
9213
9320
  }
9214
9321
  parser.on("text", (content) => {
9215
9322
  this.lastRunText += content;
9323
+ this.captureCliSessionId(parser.sessionId);
9216
9324
  });
9217
9325
  parser.on("parse_error", (msg) => {
9218
9326
  log.debug(this.tag, `Stream parse error (non-fatal): ${msg}`);
@@ -9231,8 +9339,7 @@ class Worker {
9231
9339
  this.process.on("close", (code) => {
9232
9340
  const leaderPid = this.process?.pid;
9233
9341
  this.process = null;
9234
- if (parser.sessionId)
9235
- this.cliSessionId = parser.sessionId;
9342
+ this.captureCliSessionId(parser.sessionId);
9236
9343
  this.lastSessionStats = this.progressTracker?.stats;
9237
9344
  const spawnCost = this.lastSessionStats?.cost;
9238
9345
  if (spawnCost) {
@@ -9324,7 +9431,7 @@ class Worker {
9324
9431
  `);
9325
9432
  }
9326
9433
  } finally {
9327
- this.cliSessionId = runner.sessionId ?? this.cliSessionId;
9434
+ this.captureCliSessionId(runner.sessionId);
9328
9435
  this.lastSessionStats = this.progressTracker?.stats;
9329
9436
  const spawnCost = this.lastSessionStats?.cost;
9330
9437
  if (spawnCost) {
@@ -9814,11 +9921,13 @@ async function recoverOrphans(store, client, config) {
9814
9921
  continue;
9815
9922
  }
9816
9923
  log.info(TAG33, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
9817
- await recoverRun(run, store, client, config, outcome);
9924
+ await recoverRun(run, store, client, config, outcome, {
9925
+ rollbackAttempt: true
9926
+ });
9818
9927
  }
9819
9928
  return outcomes;
9820
9929
  }
9821
- async function recoverRun(run, store, client, config, outcome) {
9930
+ async function recoverRun(run, store, client, config, outcome, opts = {}) {
9822
9931
  try {
9823
9932
  await client.endAgentSession(run.cardId, {
9824
9933
  status: "failed",
@@ -9874,6 +9983,15 @@ async function recoverRun(run, store, client, config, outcome) {
9874
9983
  const msg = err instanceof Error ? err.message : String(err);
9875
9984
  outcome.errors.push(`endRun: ${msg}`);
9876
9985
  }
9986
+ if (opts.rollbackAttempt && run.pipeline === "implement") {
9987
+ try {
9988
+ await store.decrementAttempt(run.cardId);
9989
+ outcome.actions.push("rolled back give-up attempt (daemon restart)");
9990
+ } catch (err) {
9991
+ const msg = err instanceof Error ? err.message : String(err);
9992
+ outcome.errors.push(`decrementAttempt: ${msg}`);
9993
+ }
9994
+ }
9877
9995
  log.info(TAG33, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
9878
9996
  }
9879
9997
  var TAG33 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
@@ -10039,7 +10157,7 @@ class Reconciler {
10039
10157
  pipeline: run.pipeline,
10040
10158
  actions: [],
10041
10159
  errors: []
10042
- });
10160
+ }, { rollbackAttempt: daemonDead });
10043
10161
  }
10044
10162
  }
10045
10163
  async recoverStrandedInProgress(cards, columns, knownCardIds) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/agent",
3
- "version": "1.22.2",
3
+ "version": "1.22.4",
4
4
  "description": "Push-based agent daemon for Harmony — watches board assignments and spawns Claude CLI workers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -46,7 +46,7 @@
46
46
  "dependencies": {
47
47
  "@anthropic-ai/claude-agent-sdk": "^0.3.178",
48
48
  "@supabase/supabase-js": "2.95.3",
49
- "@gethmy/mcp": "2.19.0"
49
+ "@gethmy/mcp": "2.20.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@harmony/shared": "workspace:*",