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