@gethmy/agent 1.30.0 → 1.31.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 +283 -130
  2. package/dist/index.js +283 -130
  3. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -833,7 +833,7 @@ function isDaemonAuthoredComment(comment, identity) {
833
833
  return true;
834
834
  }
835
835
  // ../harmony-shared/dist/agentStaleness.js
836
- var AGENT_HEARTBEAT_LIVENESS_MS, AGENT_MILESTONE_LIVENESS_MS, AGENT_SWEEP_DAEMON_MS, AGENT_SWEEP_INTERACTIVE_MS, AGENT_SWEEP_PAUSED_MS, SWEPT_SESSION_WRITE_GRACE_MS, ACTIVE_STATUSES;
836
+ var AGENT_HEARTBEAT_LIVENESS_MS, AGENT_MILESTONE_LIVENESS_MS, AGENT_SWEEP_DAEMON_MS, AGENT_SWEEP_INTERACTIVE_MS, AGENT_SWEEP_PAUSED_MS, SWEPT_SESSION_WRITE_GRACE_MS, ACTIVE_STATUSES, NOTICE_DRIVER = "notice";
837
837
  var init_agentStaleness = __esm(() => {
838
838
  AGENT_HEARTBEAT_LIVENESS_MS = 5 * 60 * 1000;
839
839
  AGENT_MILESTONE_LIVENESS_MS = 30 * 60 * 1000;
@@ -892,6 +892,37 @@ function recordsPushedWorkOn(description, branchName) {
892
892
  }
893
893
  return false;
894
894
  }
895
+ function recordedBranchForCard(description, shortId, branchPrefixes) {
896
+ if (!description || !Number.isFinite(shortId))
897
+ return null;
898
+ const anchors = [
899
+ ...new Set(branchPrefixes.filter((p) => p.length > 0)),
900
+ ""
901
+ ];
902
+ let recorded = null;
903
+ for (const match of description.matchAll(DAEMON_BRANCH_LINE_PATTERN)) {
904
+ const ref = match[1];
905
+ if (!ref || !SAFE_GIT_REF_PATTERN.test(ref))
906
+ continue;
907
+ if (anchors.some((p) => ref.startsWith(`${p}${shortId}-`)))
908
+ recorded = ref;
909
+ }
910
+ return recorded;
911
+ }
912
+ function rewriteDaemonBranchLines(description, fromRef, toRef2) {
913
+ if (!description || !fromRef || !toRef2)
914
+ return null;
915
+ if (!SAFE_GIT_REF_PATTERN.test(toRef2))
916
+ return null;
917
+ let matched = false;
918
+ const rewritten = description.replace(DAEMON_BRANCH_LINE_PATTERN, (line, ref) => {
919
+ if (ref !== fromRef)
920
+ return line;
921
+ matched = true;
922
+ return line.replace(`\`${ref}\``, `\`${toRef2}\``);
923
+ });
924
+ return matched ? rewritten : null;
925
+ }
895
926
  var BRANCH_REF_PATTERN, DAEMON_BRANCH_LINE_PATTERN, SAFE_GIT_REF_PATTERN, PR_LINK_PATTERN;
896
927
  var init_branchRef = __esm(() => {
897
928
  BRANCH_REF_PATTERN = /Branch:\s*`([^`]+)`/g;
@@ -1640,6 +1671,18 @@ function entryActionAllowlist(entryAction) {
1640
1671
  function stageDisallowedTools() {
1641
1672
  return STAGE_DAEMON_OWNED_TOOLS.length > 0 ? STAGE_DAEMON_OWNED_TOOLS.join(",") : null;
1642
1673
  }
1674
+ function toolsCanCommit(allowedTools) {
1675
+ return allowedTools.split(",").map((t) => t.trim().replace(/\(.*$/, "")).some((t) => COMMIT_CAPABLE_TOOLS.includes(t));
1676
+ }
1677
+ function stageRunExpectsCommit(stage, allowedTools) {
1678
+ if (!toolsCanCommit(allowedTools))
1679
+ return false;
1680
+ const artifact = stage.artifact_type;
1681
+ if (artifact !== null && artifact !== undefined && NON_DIFF_ARTIFACTS.includes(artifact)) {
1682
+ return false;
1683
+ }
1684
+ return true;
1685
+ }
1643
1686
  function customGateMetric(gate) {
1644
1687
  if (gate === null || typeof gate !== "object" || Array.isArray(gate)) {
1645
1688
  return null;
@@ -1676,7 +1719,7 @@ function referencedGateMetrics(def) {
1676
1719
  }
1677
1720
  return out;
1678
1721
  }
1679
- var DEFAULT_LOOP_MAX_ITERATIONS = 5, DEFAULT_LOOP_CONCURRENCY = 1, DEFAULT_ON_ITEM_FAIL = "continue", PLAYBOOK_STAGE_ROLES, SKILL_TOOL_ALLOWLIST, HARMONY_TOOL_RE, STAGE_DAEMON_OWNED_TOOLS;
1722
+ var DEFAULT_LOOP_MAX_ITERATIONS = 5, DEFAULT_LOOP_CONCURRENCY = 1, DEFAULT_ON_ITEM_FAIL = "continue", PLAYBOOK_STAGE_ROLES, SKILL_TOOL_ALLOWLIST, HARMONY_TOOL_RE, STAGE_DAEMON_OWNED_TOOLS, COMMIT_CAPABLE_TOOLS, NON_DIFF_ARTIFACTS;
1680
1723
  var init_playbookStage = __esm(() => {
1681
1724
  PLAYBOOK_STAGE_ROLES = [
1682
1725
  "author",
@@ -1697,11 +1740,20 @@ var init_playbookStage = __esm(() => {
1697
1740
  "mcp__harmony__harmony_start_agent_session",
1698
1741
  "mcp__harmony__harmony_move_card"
1699
1742
  ];
1743
+ COMMIT_CAPABLE_TOOLS = ["Bash", "Write", "Edit"];
1744
+ NON_DIFF_ARTIFACTS = ["plan", "review", "document", "decision"];
1700
1745
  });
1701
1746
 
1702
1747
  // ../harmony-shared/dist/projectTemplates.js
1703
1748
  var init_projectTemplates = () => {};
1704
1749
 
1750
+ // ../harmony-shared/dist/realtimeChannel.js
1751
+ var inFlightDetach;
1752
+ var init_realtimeChannel = __esm(() => {
1753
+ init_logger();
1754
+ inFlightDetach = new WeakMap;
1755
+ });
1756
+
1705
1757
  // ../harmony-shared/dist/reviewMethodology.js
1706
1758
  var REVIEW_SYSTEM_PROMPT = `You are a senior code reviewer. Follow this two-pass methodology strictly.
1707
1759
  Report findings; do NOT fix them. This is a read-only review.
@@ -2022,6 +2074,7 @@ var init_dist = __esm(() => {
2022
2074
  init_playbookCatalog();
2023
2075
  init_playbookStage();
2024
2076
  init_projectTemplates();
2077
+ init_realtimeChannel();
2025
2078
  init_reviewTools();
2026
2079
  init_stageHandoff();
2027
2080
  init_types();
@@ -2330,12 +2383,12 @@ var init_plan_phase = __esm(() => {
2330
2383
 
2331
2384
  // src/types.ts
2332
2385
  function agentIdentifier(workerId) {
2333
- return `harmony-daemon-${workerId}`;
2386
+ return `${DAEMON_IDENTIFIER_BASE}-${workerId}`;
2334
2387
  }
2335
2388
  function endStatusForCancel(reason) {
2336
2389
  return reason === "human_stop" ? "cancelled" : "paused";
2337
2390
  }
2338
- var DEFAULT_AGENT_CONFIG, IN_PROGRESS_COLUMN = "In Progress", NEED_REVIEW_LABEL = "Need Review", NEED_REVIEW_LABEL_COLOR = "#f59e0b", AGENT_NAME = "Harmony Agent";
2391
+ var DEFAULT_AGENT_CONFIG, IN_PROGRESS_COLUMN = "In Progress", NEED_REVIEW_LABEL = "Need Review", NEED_REVIEW_LABEL_COLOR = "#f59e0b", AGENT_NAME = "Harmony Agent", DAEMON_IDENTIFIER_BASE = "harmony-daemon", NOTICE_IDENTIFIER;
2339
2392
  var init_types2 = __esm(() => {
2340
2393
  init_board_review();
2341
2394
  init_contract_phase();
@@ -2467,6 +2520,7 @@ var init_types2 = __esm(() => {
2467
2520
  maxCardsPerSweep: 10
2468
2521
  }
2469
2522
  };
2523
+ NOTICE_IDENTIFIER = `${DAEMON_IDENTIFIER_BASE}-notice`;
2470
2524
  });
2471
2525
 
2472
2526
  // src/config.ts
@@ -5102,6 +5156,7 @@ import {
5102
5156
  captureDiffStat,
5103
5157
  createPullRequest,
5104
5158
  detectGitProvider as detectGitProvider3,
5159
+ extractPrUrl as extractPrUrl2,
5105
5160
  getBranchWebUrl,
5106
5161
  log as log16,
5107
5162
  pushBranch,
@@ -5140,7 +5195,7 @@ function buildTokenPayload(stats) {
5140
5195
  numTurns: stats.cost.numTurns
5141
5196
  };
5142
5197
  }
5143
- async function runCompletion(client, card, branchName, worktreePath, config, workerId, sessionIdentifier, agentId, sessionStats, workspaceId, agentSessionId, stateStore, onMovedToCompletion, onBeforeWorktreeCleanup, runBaselineSha, effectiveMaxTurns) {
5198
+ async function runCompletion(client, card, branchName, worktreePath, config, workerId, sessionIdentifier, agentId, sessionStats, workspaceId, agentSessionId, stateStore, onMovedToCompletion, onBeforeWorktreeCleanup, runBaselineSha, effectiveMaxTurns, expectsCommits) {
5144
5199
  let verificationResult = {
5145
5200
  passed: true,
5146
5201
  buildErrors: [],
@@ -5160,6 +5215,20 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
5160
5215
  log16.warn(TAG15, `No commits on branch ${branchName} — ${failureSummary}; parking for a decision`);
5161
5216
  return "park";
5162
5217
  }
5218
+ if (expectsCommits === false) {
5219
+ log16.info(TAG15, `No commits on branch ${branchName} — this stage's deliverable is not a diff; its gate decides the run`);
5220
+ if (onBeforeWorktreeCleanup) {
5221
+ try {
5222
+ await onBeforeWorktreeCleanup(worktreePath);
5223
+ } catch (err) {
5224
+ log16.warn(TAG15, `onBeforeWorktreeCleanup hook failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
5225
+ }
5226
+ } else {
5227
+ await endRunSession({ client, tag: TAG15 }, card, { status: "completed" }, buildTokenPayload(sessionStats), "throw");
5228
+ }
5229
+ await teardownWorktree(client, card.id, worktreePath, branchName);
5230
+ return true;
5231
+ }
5163
5232
  log16.warn(TAG15, `No commits on branch ${branchName} — ${failureSummary}; counting as a failed attempt`);
5164
5233
  const noCommitHandback = await guardedHandback(client, card.id, {
5165
5234
  agentId,
@@ -5186,6 +5255,9 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
5186
5255
  log16.error(TAG15, `pre-verify push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
5187
5256
  }
5188
5257
  const recoveryUrl = lastPushedSha ? getBranchWebUrl(branchName, worktreePath) : null;
5258
+ if (lastPushedSha) {
5259
+ await recordBranchProvenance(client, card, branchName);
5260
+ }
5189
5261
  if (config.verification.enabled) {
5190
5262
  await client.updateAgentProgress(card.id, {
5191
5263
  agentIdentifier: sessionIdentifier,
@@ -5282,17 +5354,16 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
5282
5354
  if (config.completion.postSummary) {
5283
5355
  await postSummary(client, card, branchName, worktreePath, prUrl, config.worktree.baseBranch, sessionStats);
5284
5356
  }
5285
- let endDisposition = { status: "completed" };
5286
5357
  if (onBeforeWorktreeCleanup) {
5287
5358
  try {
5288
- const disposition = await onBeforeWorktreeCleanup(worktreePath);
5289
- if (disposition)
5290
- endDisposition = disposition;
5359
+ await onBeforeWorktreeCleanup(worktreePath);
5291
5360
  } catch (err) {
5292
5361
  log16.warn(TAG15, `onBeforeWorktreeCleanup hook failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
5293
5362
  }
5294
5363
  }
5295
- await endRunSession({ client, tag: TAG15 }, card, endDisposition, buildTokenPayload(sessionStats), "throw");
5364
+ if (!onBeforeWorktreeCleanup) {
5365
+ await endRunSession({ client, tag: TAG15 }, card, { status: "completed" }, buildTokenPayload(sessionStats), "throw");
5366
+ }
5296
5367
  if (workspaceId) {
5297
5368
  const diffStat = captureDiffStat(worktreePath, config.worktree.baseBranch);
5298
5369
  const changedFiles = diffStat && diffStat.files.length > 0 ? diffStat.files : sessionStats?.filesEditedPaths ?? [];
@@ -5396,18 +5467,47 @@ function checkHasCommits(worktreePath, baseBranch, baselineSha, gitImpl = (args,
5396
5467
  return false;
5397
5468
  }
5398
5469
  }
5470
+ function stripDaemonBlocks(description) {
5471
+ const indices = [SUMMARY_MARKER, BRANCH_PROVENANCE_MARKER].map((marker) => description.indexOf(marker)).filter((index) => index >= 0);
5472
+ if (indices.length === 0)
5473
+ return description;
5474
+ return description.slice(0, Math.min(...indices)).trimEnd();
5475
+ }
5476
+ async function recordBranchProvenance(client, card, branchName) {
5477
+ try {
5478
+ let description = card.description || "";
5479
+ try {
5480
+ const { card: latest } = await client.getCard(card.id);
5481
+ description = latest.description ?? description;
5482
+ } catch {}
5483
+ if (recordsPushedWorkOn(description, branchName))
5484
+ return;
5485
+ const block = `
5486
+
5487
+ ${BRANCH_PROVENANCE_MARKER} (pushed — a later run continues it)
5488
+ Branch: \`${branchName}\``;
5489
+ await client.updateCard(card.id, { description: description + block });
5490
+ log16.info(TAG15, `Recorded branch provenance on #${card.short_id}`);
5491
+ } catch (err) {
5492
+ log16.warn(TAG15, `Failed to record branch provenance on #${card.short_id}: ${err instanceof Error ? err.message : err}`);
5493
+ }
5494
+ }
5399
5495
  async function postSummary(client, card, branchName, worktreePath, prUrl, baseBranch, sessionStats) {
5400
5496
  let commitLog = "";
5401
5497
  try {
5402
5498
  commitLog = execFileSync5("git", ["log", "--oneline", `origin/${baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
5403
5499
  } catch {}
5404
- const SUMMARY_MARKER = `---
5405
- **Agent completed**`;
5500
+ let existingDesc = card.description || "";
5501
+ try {
5502
+ const { card: latest } = await client.getCard(card.id);
5503
+ existingDesc = latest.description ?? existingDesc;
5504
+ } catch {}
5505
+ const carriedPrUrl = prUrl ?? extractPrUrl2(existingDesc);
5406
5506
  const parts = [`
5407
5507
 
5408
5508
  ${SUMMARY_MARKER}`];
5409
- if (prUrl) {
5410
- parts.push(`PR: ${prUrl}`);
5509
+ if (carriedPrUrl) {
5510
+ parts.push(`PR: ${carriedPrUrl}`);
5411
5511
  }
5412
5512
  parts.push(`Branch: \`${branchName}\``);
5413
5513
  if (sessionStats) {
@@ -5438,8 +5538,7 @@ ${commitLog}
5438
5538
  \`\`\``);
5439
5539
  }
5440
5540
  try {
5441
- const existingDesc = card.description || "";
5442
- const baseDesc = existingDesc.includes(SUMMARY_MARKER) ? existingDesc.slice(0, existingDesc.indexOf(SUMMARY_MARKER)).trimEnd() : existingDesc;
5541
+ const baseDesc = stripDaemonBlocks(existingDesc);
5443
5542
  await client.updateCard(card.id, {
5444
5543
  description: baseDesc + parts.join(`
5445
5544
  `)
@@ -5449,8 +5548,11 @@ ${commitLog}
5449
5548
  log16.error(TAG15, `Failed to post summary: ${err instanceof Error ? err.message : err}`);
5450
5549
  }
5451
5550
  }
5452
- var TAG15 = "completion";
5551
+ var TAG15 = "completion", SUMMARY_MARKER = `---
5552
+ **Agent completed**`, BRANCH_PROVENANCE_MARKER = `---
5553
+ **Agent branch**`;
5453
5554
  var init_completion = __esm(() => {
5555
+ init_dist();
5454
5556
  init_board_helpers();
5455
5557
  init_episode_writer();
5456
5558
  init_handback();
@@ -5837,6 +5939,9 @@ var init_progress_tracker = __esm(() => {
5837
5939
 
5838
5940
  // src/prompt.ts
5839
5941
  import { log as log18 } from "@gethmy/harness";
5942
+ import {
5943
+ buildMemoryQuery
5944
+ } from "@gethmy/mcp/src/api-client.js";
5840
5945
  function buildSteeringPrompt(messages) {
5841
5946
  if (messages.length === 1)
5842
5947
  return messages[0];
@@ -5869,11 +5974,11 @@ function renderPreviousAttemptsSection(failures) {
5869
5974
  ].join(`
5870
5975
  `);
5871
5976
  }
5872
- async function buildPrompt(enriched, branchName, worktreePath, client, workspaceId, projectId) {
5977
+ async function buildPrompt(enriched, branchName, worktreePath, client, workspaceId, projectId, onRecallOutcome) {
5873
5978
  const { card } = enriched;
5874
5979
  const [pastEpisodesSection, referenceSection] = await Promise.all([
5875
- renderPastEpisodesSection(client, card.title, card.description ?? "", workspaceId, projectId),
5876
- renderReferenceSection(client, card.title, card.description ?? "", workspaceId, projectId)
5980
+ renderPastEpisodesSection(client, card.title, card.description ?? "", workspaceId, projectId, onRecallOutcome),
5981
+ renderReferenceSection(client, card.title, card.description ?? "", workspaceId, projectId, onRecallOutcome)
5877
5982
  ]);
5878
5983
  try {
5879
5984
  const result = await client.generateCardPrompt({
@@ -5914,12 +6019,11 @@ ${section}` : "";
5914
6019
  return "";
5915
6020
  }
5916
6021
  }
5917
- async function renderPastEpisodesSection(client, title, description, workspaceId, projectId) {
6022
+ async function renderPastEpisodesSection(client, title, description, workspaceId, projectId, onOutcome) {
5918
6023
  if (!projectId)
5919
6024
  return "";
5920
6025
  try {
5921
- const query = `${title}
5922
- ${description}`.trim();
6026
+ const query = buildMemoryQuery(title, description);
5923
6027
  const { entities } = await client.harmonyRecall({
5924
6028
  workspaceId,
5925
6029
  projectId,
@@ -5931,6 +6035,7 @@ ${description}`.trim();
5931
6035
  includeEpisodes: true,
5932
6036
  consumer: "agent-prompt"
5933
6037
  });
6038
+ onOutcome?.({ flavour: "past_episodes", count: entities.length });
5934
6039
  if (entities.length === 0)
5935
6040
  return "";
5936
6041
  const bullets = entities.map((entity) => {
@@ -5959,17 +6064,18 @@ ${description}`.trim();
5959
6064
  ## Similar past tasks
5960
6065
  ${bullets}`;
5961
6066
  } catch (err) {
6067
+ const error = err instanceof Error ? err.message : String(err);
5962
6068
  log18.warn(TAG17, "past-episodes recall failed", {
5963
6069
  event: "episode_recall_failed",
5964
- error: err instanceof Error ? err.message : String(err)
6070
+ error
5965
6071
  });
6072
+ onOutcome?.({ flavour: "past_episodes", count: 0, failure: error });
5966
6073
  return "";
5967
6074
  }
5968
6075
  }
5969
- async function renderReferenceSection(client, title, description, workspaceId, projectId) {
6076
+ async function renderReferenceSection(client, title, description, workspaceId, projectId, onOutcome) {
5970
6077
  try {
5971
- const query = `${title}
5972
- ${description}`.trim();
6078
+ const query = buildMemoryQuery(title, description);
5973
6079
  const { entities } = await client.harmonyRecall({
5974
6080
  workspaceId,
5975
6081
  projectId,
@@ -5978,6 +6084,7 @@ ${description}`.trim();
5978
6084
  topK: 5,
5979
6085
  consumer: "agent-prompt"
5980
6086
  });
6087
+ onOutcome?.({ flavour: "reference", count: entities.length });
5981
6088
  if (entities.length === 0)
5982
6089
  return "";
5983
6090
  const bullets = entities.map((entity) => {
@@ -5992,10 +6099,12 @@ ${description}`.trim();
5992
6099
  ## How we work here
5993
6100
  ${bullets}`;
5994
6101
  } catch (err) {
6102
+ const error = err instanceof Error ? err.message : String(err);
5995
6103
  log18.warn(TAG17, "reference recall failed", {
5996
6104
  event: "reference_recall_failed",
5997
- error: err instanceof Error ? err.message : String(err)
6105
+ error
5998
6106
  });
6107
+ onOutcome?.({ flavour: "reference", count: 0, failure: error });
5999
6108
  return "";
6000
6109
  }
6001
6110
  }
@@ -6055,7 +6164,7 @@ import {
6055
6164
  cleanupWorktree as cleanupWorktree3,
6056
6165
  createPullRequest as createPullRequest2,
6057
6166
  detectGitProvider as detectGitProvider4,
6058
- extractPrUrl as extractPrUrl2,
6167
+ extractPrUrl as extractPrUrl3,
6059
6168
  getBranchWebUrl as getBranchWebUrl2,
6060
6169
  getHeadSha,
6061
6170
  log as log19,
@@ -6353,19 +6462,31 @@ ${runLogTail}
6353
6462
  }
6354
6463
  }
6355
6464
  await addLabelByName(client, card, config.review.approvedLabel, config.review.approvedLabelColor);
6356
- if (prUrl) {
6465
+ const renamedFrom = branchName && approvedBranch && approvedBranch !== branchName ? branchName : null;
6466
+ const renamedTo = renamedFrom ? approvedBranch : null;
6467
+ if (prUrl || renamedFrom) {
6357
6468
  try {
6358
6469
  const { card: latest } = await client.getCard(card.id);
6359
- const desc = latest.description || "";
6360
- if (!extractPrUrl2(desc)) {
6470
+ let desc = latest.description || "";
6471
+ let changed = false;
6472
+ if (renamedFrom && renamedTo) {
6473
+ const rewritten = rewriteDaemonBranchLines(desc, renamedFrom, renamedTo);
6474
+ if (rewritten !== null) {
6475
+ desc = rewritten;
6476
+ changed = true;
6477
+ }
6478
+ }
6479
+ if (prUrl && !extractPrUrl3(desc)) {
6361
6480
  const separator = desc ? `
6362
6481
  ` : "";
6363
- await client.updateCard(card.id, {
6364
- description: `${desc}${separator}PR: ${prUrl}`
6365
- });
6482
+ desc = `${desc}${separator}PR: ${prUrl}`;
6483
+ changed = true;
6484
+ }
6485
+ if (changed) {
6486
+ await client.updateCard(card.id, { description: desc });
6366
6487
  }
6367
6488
  } catch (err) {
6368
- log19.warn(TAG18, `Failed to persist PR URL to #${card.short_id} description: ${err instanceof Error ? err.message : err}`);
6489
+ log19.warn(TAG18, `Failed to persist PR URL / branch rename to #${card.short_id} description: ${err instanceof Error ? err.message : err}`);
6369
6490
  }
6370
6491
  }
6371
6492
  if (branchName) {
@@ -6542,6 +6663,7 @@ ${runLogTail}
6542
6663
  var TAG18 = "review-completion", MAX_FINDINGS = 10, MAX_SUBTASK_TITLE = 120, COMMENT_BODY_BUDGET = 9500, REVIEW_MARKER = `---
6543
6664
  **Review:`, RUN_LOG_TAIL_BYTES = 2048;
6544
6665
  var init_review_completion = __esm(() => {
6666
+ init_dist();
6545
6667
  init_board_helpers();
6546
6668
  init_completion();
6547
6669
  init_episode_writer();
@@ -9085,6 +9207,9 @@ var init_motor_driver = () => {};
9085
9207
 
9086
9208
  // src/stage-advance.ts
9087
9209
  import { gateConfigErrorReason, log as log29 } from "@gethmy/harness";
9210
+ function endDispositionFor(outcome) {
9211
+ return "endDisposition" in outcome && outcome.endDisposition ? outcome.endDisposition : { status: "completed" };
9212
+ }
9088
9213
  function handoffText(stage) {
9089
9214
  if (stage.handoff && typeof stage.handoff === "object") {
9090
9215
  const summary = stage.handoff.summary ?? stage.handoff.description;
@@ -9093,16 +9218,6 @@ function handoffText(stage) {
9093
9218
  }
9094
9219
  return stage.name;
9095
9220
  }
9096
- function stageEndDisposition(evaluation, _stage, stageIndex, def) {
9097
- if (!evaluation?.passed)
9098
- return { status: "completed" };
9099
- const next = nextStageAfter(def, stageIndex);
9100
- if (next.kind === "next" && next.stage.owner === "human") {
9101
- const reason = `Stage "${next.stage.name}" is yours: ${handoffText(next.stage)}`;
9102
- return { status: "blocked", blockers: [reason] };
9103
- }
9104
- return { status: "completed" };
9105
- }
9106
9221
  function gateKindOf(stage) {
9107
9222
  return stage.gate && typeof stage.gate === "object" ? String(stage.gate.kind ?? "gate") : "gate";
9108
9223
  }
@@ -9160,9 +9275,9 @@ async function holdGateMisconfigured(card, stage, detail, deps) {
9160
9275
  currentTask: reason
9161
9276
  });
9162
9277
  } catch {}
9163
- await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore);
9278
+ const endDisposition = await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore);
9164
9279
  log29.info(TAG27, `#${card.short_id} GateMisconfigured: ${reason}`);
9165
- return { kind: "held_misconfigured", reason };
9280
+ return { kind: "held_misconfigured", reason, endDisposition };
9166
9281
  }
9167
9282
  function firstErrorMessage(evaluation) {
9168
9283
  const e = evaluation?.findings.find((f) => f.level === "error");
@@ -9231,13 +9346,9 @@ async function advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loo
9231
9346
  currentTask: reason
9232
9347
  });
9233
9348
  } catch {}
9234
- await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore, {
9235
- keepAttempts: true,
9236
- endStatus: "blocked",
9237
- blockers: [reason]
9238
- });
9349
+ const endDisposition = await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore, { keepAttempts: true, endStatus: "blocked", blockers: [reason] });
9239
9350
  log29.info(TAG27, `#${card.short_id} LoopExhausted: ${reason}`);
9240
- return { kind: "held_gate_unmet", reason };
9351
+ return { kind: "held_gate_unmet", reason, endDisposition };
9241
9352
  }
9242
9353
  const guard = await guardStageReclaim(card, `converge-loop iteration of "${stage.name}"`, deps);
9243
9354
  if (!guard.proceed) {
@@ -9310,14 +9421,14 @@ async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps
9310
9421
  }
9311
9422
  if (next.kind === "out_of_range") {
9312
9423
  const reason = `Stage advancement aborted: stage index ${stageIndex} is out of range for the pinned playbook version — holding for a human.`;
9313
- await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore);
9314
- return { kind: "held_misconfigured", reason };
9424
+ const endDisposition = await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore);
9425
+ return { kind: "held_misconfigured", reason, endDisposition };
9315
9426
  }
9316
9427
  const toColumn = await resolveStageColumnName(deps.client, card, next.stage);
9317
9428
  if (!toColumn) {
9318
9429
  const reason = `Stage "${stage.name}" passed but the next stage "${next.stage.name}" has no resolvable board column — holding for a human (never moving to an undefined column).`;
9319
- await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore);
9320
- return { kind: "held_misconfigured", reason };
9430
+ const endDisposition = await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore);
9431
+ return { kind: "held_misconfigured", reason, endDisposition };
9321
9432
  }
9322
9433
  await persistStagePointer(deps.client, card, {
9323
9434
  currentStage: next.stage.id,
@@ -9340,11 +9451,13 @@ async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps
9340
9451
  log29.info(TAG27, `#${card.short_id} advanced "${stage.name}" → "${next.stage.name}" (column "${toColumn}")`);
9341
9452
  if (next.stage.owner === "human") {
9342
9453
  const reason = `Stage "${next.stage.name}" is yours: ${handoffText(next.stage)}`;
9343
- await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore, {
9344
- keepAttempts: true,
9345
- endStatus: "blocked",
9346
- blockers: [reason]
9347
- });
9454
+ const endDisposition = await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore, { keepAttempts: true, endStatus: "blocked", blockers: [reason] });
9455
+ return {
9456
+ kind: "advanced",
9457
+ toStageId: next.stage.id,
9458
+ toColumn,
9459
+ endDisposition
9460
+ };
9348
9461
  }
9349
9462
  return { kind: "advanced", toStageId: next.stage.id, toColumn };
9350
9463
  }
@@ -9361,13 +9474,9 @@ async function handleGateUnmet(card, stage, summary, deps) {
9361
9474
  currentTask: reason
9362
9475
  });
9363
9476
  } catch {}
9364
- await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore, {
9365
- keepAttempts: true,
9366
- endStatus: "blocked",
9367
- blockers: [reason]
9368
- });
9477
+ const endDisposition = await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore, { keepAttempts: true, endStatus: "blocked", blockers: [reason] });
9369
9478
  log29.info(TAG27, `#${card.short_id} GateUnmetExhausted: ${reason}`);
9370
- return { kind: "held_gate_unmet", reason };
9479
+ return { kind: "held_gate_unmet", reason, endDisposition };
9371
9480
  }
9372
9481
  const guard = await guardStageReclaim(card, `gate-unmet re-run of "${stage.name}"`, deps);
9373
9482
  if (!guard.proceed) {
@@ -9429,21 +9538,16 @@ async function holdForHuman(client, card, reason, runId, stateStore, opts = {})
9429
9538
  await client.addComment(card.id, reason, { commentType: "blocker" });
9430
9539
  } catch {}
9431
9540
  try {
9432
- const result = await runTransition(client, card, {
9433
- removeLabels: [AGENT_LABEL],
9434
- endSession: {
9435
- status: opts.endStatus ?? "paused",
9436
- blockers: opts.blockers,
9437
- failureReason: "other",
9438
- failureSummary: reason.slice(0, 300)
9439
- }
9440
- }, { store: stateStore, runId });
9441
- if (opts.endStatus === "blocked" && result.endSession?.ended === false) {
9442
- log29.warn(TAG27, `#${card.short_id} hold intended to end the session BLOCKED, but it was already ended (${result.endSession.reason ?? "unknown reason"}) — no agent_blocked push fired from this write.`);
9443
- }
9541
+ await runTransition(client, card, { removeLabels: [AGENT_LABEL] }, { store: stateStore, runId });
9444
9542
  } catch (err) {
9445
9543
  log29.warn(TAG27, `hold transition failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
9446
9544
  }
9545
+ return {
9546
+ status: opts.endStatus ?? "paused",
9547
+ blockers: opts.blockers,
9548
+ failureReason: "other",
9549
+ failureSummary: reason.slice(0, 300)
9550
+ };
9447
9551
  }
9448
9552
  var TAG27 = "stage-advance", AGENT_LABEL = "agent";
9449
9553
  var init_stage_advance = __esm(() => {
@@ -9463,12 +9567,14 @@ import {
9463
9567
  collectGateEvidence as collectGateEvidence2,
9464
9568
  createWorktree,
9465
9569
  describeApiError as describeApiError2,
9570
+ fetchExistingBranch,
9466
9571
  log as log30,
9467
9572
  makeBranchName,
9468
9573
  normalizeGateSpec,
9469
9574
  pushBranch as pushBranch3,
9470
9575
  readWorktreeHead,
9471
9576
  reapGroup as reapGroup2,
9577
+ resolveContinuationTarget,
9472
9578
  SdkAgentRunner as SdkAgentRunner2,
9473
9579
  signalGroup as signalGroup2,
9474
9580
  sizeRun,
@@ -9754,12 +9860,18 @@ class Worker {
9754
9860
  log30.info(this.tag, resuming ? `Resuming #${card.short_id} "${card.title}" with ${this.grantedTurns ?? "the default"} more turns` : `Preparing #${card.short_id} "${card.title}"`);
9755
9861
  const attemptCount = await this.stateStore.incrementAttempt(card.id);
9756
9862
  const isRework = attemptCount > 1;
9757
- const recordedBranch = extractBranchRef(card.description);
9758
- const continuesPushedWork = isRework || recordsPushedWorkOn(card.description, this.branchName);
9759
- if (continuesPushedWork && !isRework) {
9760
- log30.info(this.tag, `Card records completed work on ${this.branchName} — continuing that branch instead of rebuilding from ${this.config.worktree.baseBranch}`);
9761
- } else if (recordedBranch && recordedBranch !== this.branchName) {
9762
- log30.warn(this.tag, `Card records branch ${recordedBranch} but this run targets ${this.branchName} — starting fresh; the recorded branch is left untouched`);
9863
+ const recordedBranch = recordedBranchForCard(card.description, card.short_id, [
9864
+ this.config.worktree.failedBranchPrefix,
9865
+ this.config.worktree.approvedBranchPrefix
9866
+ ]);
9867
+ const continuesPushedWork = isRework || recordedBranch !== null;
9868
+ if (!resuming && recordedBranch) {
9869
+ if (recordedBranch !== this.branchName) {
9870
+ log30.info(this.tag, `Card records pushed work on ${recordedBranch} — continuing that branch instead of ${this.branchName}`);
9871
+ this.branchName = recordedBranch;
9872
+ } else if (!isRework) {
9873
+ log30.info(this.tag, `Card records pushed work on ${this.branchName} — continuing that branch instead of rebuilding from ${this.config.worktree.baseBranch}`);
9874
+ }
9763
9875
  }
9764
9876
  this.startHeartbeat();
9765
9877
  this.sizingOutcome = await this.sizeThisRun(card);
@@ -9849,8 +9961,20 @@ class Worker {
9849
9961
  return;
9850
9962
  }
9851
9963
  if (!resuming) {
9964
+ const continueRequested = stageCtx.kind === "run" || stageCtx.kind === "motor" || continuesPushedWork;
9965
+ const repoRoot = execFileSync7("git", ["rev-parse", "--show-toplevel"], {
9966
+ encoding: "utf-8"
9967
+ }).trim();
9968
+ const target = resolveContinuationTarget(this.branchName, continueRequested, this.config.worktree.failedBranchPrefix, this.config.worktree.approvedBranchPrefix, (ref) => fetchExistingBranch(repoRoot, ref));
9969
+ if (target.branchName !== this.branchName) {
9970
+ log30.info(this.tag, `Branch ${this.branchName} is gone from origin but its approved rename ${target.branchName} exists — continuing that branch`);
9971
+ this.branchName = target.branchName;
9972
+ } else if (target.reason === "exists_on_origin") {
9973
+ log30.info(this.tag, `Branch ${this.branchName} exists on origin with no card record — continuing it rather than resetting it`);
9974
+ }
9852
9975
  this.worktreePath = createWorktree(this.config.worktree.basePath, this.config.worktree.baseBranch, this.branchName, {
9853
- continueExisting: stageCtx.kind === "run" || stageCtx.kind === "motor" || continuesPushedWork
9976
+ continueExisting: target.continueExisting,
9977
+ branchExistsOnOrigin: target.existsOnOrigin
9854
9978
  });
9855
9979
  this.runBaselineSha = readWorktreeHead(this.worktreePath);
9856
9980
  }
@@ -9940,9 +10064,8 @@ class Worker {
9940
10064
  const stageRun = stageCtx.kind === "run" ? stageCtx : null;
9941
10065
  const onBeforeWorktreeCleanup = stageRun ? async (worktreePath) => {
9942
10066
  stageGateEvaluation = await this.collectStageGateEvidence(card, stageRun.stage, worktreePath, subtasks);
9943
- return stageEndDisposition(stageGateEvaluation, stageRun.stage, stageRun.index, stageRun.def);
9944
10067
  } : undefined;
9945
- const completed = await runCompletion(this.client, card, this.branchName, this.worktreePath, this.config, this.id, this.sessionIdentifier, this.identity.agentId, this.lastSessionStats, this.workspaceId, this.sessionId, this.stateStore, this.onCardCompleted, onBeforeWorktreeCleanup, this.runBaselineSha, this.effectiveMaxTurns);
10068
+ const completed = await runCompletion(this.client, card, this.branchName, this.worktreePath, this.config, this.id, this.sessionIdentifier, this.identity.agentId, this.lastSessionStats, this.workspaceId, this.sessionId, this.stateStore, this.onCardCompleted, onBeforeWorktreeCleanup, this.runBaselineSha, this.effectiveMaxTurns, stageRun ? stageRunExpectsCommit(stageRun.stage, stageRun.allowedTools) : true);
9946
10069
  if (completed === "park") {
9947
10070
  await this.parkForDecision(card, "max_turns");
9948
10071
  return;
@@ -9950,24 +10073,29 @@ class Worker {
9950
10073
  this.worktreePath = null;
9951
10074
  this.verificationFailed = !completed;
9952
10075
  if (completed && stageRun) {
9953
- const outcome = await this.advanceFromGateEvaluation(card, stageRun.stage, stageRun.index, stageRun.def, stageGateEvaluation);
9954
- switch (outcome.kind) {
9955
- case "requeued_gate_unmet":
9956
- case "held_gate_unmet":
9957
- case "held_misconfigured":
9958
- this.held = true;
9959
- break;
9960
- case "reclaim_refused":
9961
- this.held = true;
9962
- log30.info(this.tag, `#${card.short_id} stage reclaim refused (${outcome.reason})${outcome.released ? " — released the daemon's claim" : ""}`);
9963
- break;
9964
- case "advanced":
9965
- case "completed_terminal":
9966
- case "no_advance":
9967
- break;
9968
- default: {
9969
- const _exhaustive = outcome;
10076
+ let outcome = { kind: "no_advance" };
10077
+ try {
10078
+ outcome = await this.advanceFromGateEvaluation(card, stageRun.stage, stageRun.index, stageRun.def, stageGateEvaluation);
10079
+ switch (outcome.kind) {
10080
+ case "requeued_gate_unmet":
10081
+ case "held_gate_unmet":
10082
+ case "held_misconfigured":
10083
+ this.held = true;
10084
+ break;
10085
+ case "reclaim_refused":
10086
+ this.held = true;
10087
+ log30.info(this.tag, `#${card.short_id} stage reclaim refused (${outcome.reason})${outcome.released ? " — released the daemon's claim" : ""}`);
10088
+ break;
10089
+ case "advanced":
10090
+ case "completed_terminal":
10091
+ case "no_advance":
10092
+ break;
10093
+ default: {
10094
+ const _exhaustive = outcome;
10095
+ }
9970
10096
  }
10097
+ } finally {
10098
+ await this.endStageRunSession(card, outcome);
9971
10099
  }
9972
10100
  }
9973
10101
  } catch (err) {
@@ -10626,28 +10754,33 @@ class Worker {
10626
10754
  this.completionStarted = true;
10627
10755
  await this.recordPhase("completing");
10628
10756
  const evaluation = verdict ? evaluationFromVerdict(verdict, result.structured) : null;
10629
- await this.finishMotorStageRun(card, ctx, stageEndDisposition(evaluation, ctx.stage, ctx.index, ctx.def));
10630
- const outcome = await this.advanceFromGateEvaluation(card, ctx.stage, ctx.index, ctx.def, evaluation);
10631
- switch (outcome.kind) {
10632
- case "requeued_gate_unmet":
10633
- case "held_gate_unmet":
10634
- case "held_misconfigured":
10635
- this.held = true;
10636
- break;
10637
- case "reclaim_refused":
10638
- this.held = true;
10639
- log30.info(this.tag, `#${card.short_id} stage reclaim refused (${outcome.reason})${outcome.released ? " — released the daemon's claim" : ""}`);
10640
- break;
10641
- case "advanced":
10642
- case "completed_terminal":
10643
- case "no_advance":
10644
- break;
10645
- default: {
10646
- const _exhaustive = outcome;
10757
+ await this.finishMotorStageRun(card, ctx);
10758
+ let outcome = { kind: "no_advance" };
10759
+ try {
10760
+ outcome = await this.advanceFromGateEvaluation(card, ctx.stage, ctx.index, ctx.def, evaluation);
10761
+ switch (outcome.kind) {
10762
+ case "requeued_gate_unmet":
10763
+ case "held_gate_unmet":
10764
+ case "held_misconfigured":
10765
+ this.held = true;
10766
+ break;
10767
+ case "reclaim_refused":
10768
+ this.held = true;
10769
+ log30.info(this.tag, `#${card.short_id} stage reclaim refused (${outcome.reason})${outcome.released ? " — released the daemon's claim" : ""}`);
10770
+ break;
10771
+ case "advanced":
10772
+ case "completed_terminal":
10773
+ case "no_advance":
10774
+ break;
10775
+ default: {
10776
+ const _exhaustive = outcome;
10777
+ }
10647
10778
  }
10779
+ } finally {
10780
+ await this.endStageRunSession(card, outcome);
10648
10781
  }
10649
10782
  }
10650
- async finishMotorStageRun(card, ctx, disposition = { status: "completed" }) {
10783
+ async finishMotorStageRun(card, ctx) {
10651
10784
  const worktreePath = this.worktreePath;
10652
10785
  if (worktreePath) {
10653
10786
  commitUncommittedChanges(worktreePath, card);
@@ -10665,7 +10798,6 @@ class Worker {
10665
10798
  } else {
10666
10799
  log30.warn(this.tag, `completion.moveToColumn is empty — #${card.short_id} stays in its current column after the motor stage run`);
10667
10800
  }
10668
- await endRunSession({ client: this.client, tag: this.tag }, card, disposition, {}, "log");
10669
10801
  await this.closeoutMotorWorktree(card);
10670
10802
  }
10671
10803
  async closeoutMotorWorktree(card) {
@@ -10680,7 +10812,9 @@ class Worker {
10680
10812
  this.worktreePath = null;
10681
10813
  }
10682
10814
  async buildFreshRunPrompt(enriched, card, stageCtx, continuesPushedWork, resuming) {
10683
- const basePrompt = await buildPrompt(enriched, this.branchName, this.worktreePath, this.client, this.workspaceId, this.projectId);
10815
+ const recallOutcomes = [];
10816
+ const basePrompt = await buildPrompt(enriched, this.branchName, this.worktreePath, this.client, this.workspaceId, this.projectId, (outcome) => recallOutcomes.push(outcome));
10817
+ this.recordPromptAssembled(recallOutcomes);
10684
10818
  let prompt = basePrompt;
10685
10819
  if (stageCtx.kind === "run") {
10686
10820
  const loop = getStageLoop(stageCtx.stage);
@@ -10819,6 +10953,9 @@ ${prompt}`;
10819
10953
  return { kind: "no_advance" };
10820
10954
  }
10821
10955
  }
10956
+ async endStageRunSession(card, outcome) {
10957
+ await endRunSession({ client: this.client, tag: this.tag }, card, endDispositionFor(outcome), buildTokenPayload(this.lastSessionStats), "log");
10958
+ }
10822
10959
  selectImplementModel(card) {
10823
10960
  const attempts = this.stateStore.getCard(card.id)?.attempts ?? 1;
10824
10961
  const choice = chooseImplementModel(this.config.claude, card, attempts, this.sizing ?? undefined);
@@ -10858,6 +10995,19 @@ ${prompt}`;
10858
10995
  }
10859
10996
  return outcome;
10860
10997
  }
10998
+ recordPromptAssembled(outcomes) {
10999
+ const countOf = (flavour) => outcomes.find((o) => o.flavour === flavour)?.count ?? 0;
11000
+ const failures = outcomes.flatMap((o) => o.failure ? [{ flavour: o.flavour, message: o.failure.slice(0, 300) }] : []);
11001
+ this.cliRunner?.record({
11002
+ kind: "prompt_assembled",
11003
+ source: "system",
11004
+ payload: {
11005
+ episodeCount: countOf("past_episodes"),
11006
+ referenceCount: countOf("reference"),
11007
+ ...failures.length > 0 ? { recallFailures: failures } : {}
11008
+ }
11009
+ });
11010
+ }
10861
11011
  recordRunSized() {
10862
11012
  if (!this.modelChoice)
10863
11013
  return;
@@ -11679,10 +11829,11 @@ class Pool {
11679
11829
  return;
11680
11830
  try {
11681
11831
  await this.client.updateAgentProgress(cardId, {
11682
- agentIdentifier: agentIdentifier(0),
11832
+ agentIdentifier: NOTICE_IDENTIFIER,
11683
11833
  agentName: AGENT_NAME,
11684
11834
  status: "waiting",
11685
- currentTask
11835
+ currentTask,
11836
+ driver: NOTICE_DRIVER
11686
11837
  });
11687
11838
  this.lastWaitingEmit.set(cardId, currentTask);
11688
11839
  } catch (err) {
@@ -12218,6 +12369,7 @@ Reassign the card — that clears the turn count and starts it fresh — or rais
12218
12369
  }
12219
12370
  var TAG29 = "pool";
12220
12371
  var init_pool = __esm(() => {
12372
+ init_dist();
12221
12373
  init_board_helpers();
12222
12374
  init_budget_pause();
12223
12375
  init_handback();
@@ -14519,6 +14671,7 @@ async function main() {
14519
14671
  };
14520
14672
  process.on("SIGINT", () => shutdown("SIGINT"));
14521
14673
  process.on("SIGTERM", () => shutdown("SIGTERM"));
14674
+ process.on("SIGHUP", () => shutdown("SIGHUP"));
14522
14675
  process.on("uncaughtException", (err) => {
14523
14676
  log42.error(TAG40, `Uncaught exception: ${err.message}`);
14524
14677
  exitCode = 1;