@gethmy/agent 1.14.2 → 1.14.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.
package/dist/cli.js CHANGED
@@ -5851,6 +5851,11 @@ async function runTransition(client, card, plan, opts = {}) {
5851
5851
  await withRetry("endSession", shortId, () => client.endAgentSession(card.id, plan.endSession), attempts, backoffMs);
5852
5852
  log.info(TAG22, `#${shortId} session ended (${plan.endSession.status})`);
5853
5853
  }
5854
+ if (plan.assignAgent !== undefined) {
5855
+ const assignedAgentId = plan.assignAgent;
5856
+ await withRetry("assignAgent", shortId, () => client.updateCard(card.id, { assignedAgentId }), attempts, backoffMs);
5857
+ log.info(TAG22, assignedAgentId ? `#${shortId} assigned → agent ${assignedAgentId}` : `#${shortId} unassigned`);
5858
+ }
5854
5859
  if (opts.store && opts.runId) {
5855
5860
  try {
5856
5861
  await opts.store.heartbeat(opts.runId);
@@ -6986,7 +6991,8 @@ async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps
6986
6991
  });
6987
6992
  await runTransition(deps.client, card, {
6988
6993
  move: { columnName: toColumn },
6989
- addLabels: [{ name: AGENT_LABEL }]
6994
+ addLabels: [{ name: AGENT_LABEL }],
6995
+ ...isAgentRunnableOwner(next.stage.owner) ? { assignAgent: deps.agentId } : {}
6990
6996
  }, { store: deps.stateStore, runId: deps.runId });
6991
6997
  deps.stateStore.recordOutcome(card.id, "success").catch(() => {});
6992
6998
  log.info(TAG28, `#${card.short_id} advanced "${stage.name}" → "${next.stage.name}" (column "${toColumn}")`);
@@ -7015,7 +7021,11 @@ async function handleGateUnmet(card, stage, summary, deps) {
7015
7021
  try {
7016
7022
  await deps.client.addComment(card.id, `Stage gate unmet — re-running "${stage.name}". ${summary}.`, { commentType: "progress" });
7017
7023
  } catch {}
7018
- await runTransition(deps.client, card, { move: { columnName: toColumn }, addLabels: [{ name: AGENT_LABEL }] }, { store: deps.stateStore, runId: deps.runId });
7024
+ await runTransition(deps.client, card, {
7025
+ move: { columnName: toColumn },
7026
+ addLabels: [{ name: AGENT_LABEL }],
7027
+ ...isAgentRunnableOwner(stage.owner) ? { assignAgent: deps.agentId } : {}
7028
+ }, { store: deps.stateStore, runId: deps.runId });
7019
7029
  log.info(TAG28, `#${card.short_id} gate unmet for "${stage.name}" — requeued to "${toColumn}" for re-run (attempt ${attempts}/${deps.maxAttempts})`);
7020
7030
  return { kind: "requeued_gate_unmet", toColumn };
7021
7031
  }
@@ -7078,6 +7088,9 @@ function buildStagePreamble(stage) {
7078
7088
  }
7079
7089
  }
7080
7090
  lines.push("Do only this stage's work. When the stage's handoff is met, end your session — advancement to the next stage is handled by the board.");
7091
+ if (normalizeGateSpec(stage.gate)?.kind === "review_passed") {
7092
+ lines.push("", "**This stage's gate is `review_passed` — your deliverable is a review verdict, not a prose handoff.** Do NOT end the session until you have actually reviewed the change and emitted EXACTLY one JSON block as the LAST thing you output (nothing after it):", "```json", REVIEW_VERDICT_SCHEMA, "```", "Decision rules:", REVIEW_DECISION_RULES, "The board reads this verdict to decide advancement: `approved` advances the card, `rejected` sends it back. A missing or unparseable verdict blocks the card. Do NOT modify any code — this is a read-only review.");
7093
+ }
7081
7094
  return lines.filter(Boolean).join(`
7082
7095
  `);
7083
7096
  }
@@ -7115,12 +7128,14 @@ class Worker {
7115
7128
  timedOut = false;
7116
7129
  verificationFailed = false;
7117
7130
  held = false;
7131
+ completionStarted = false;
7118
7132
  sessionId = null;
7119
7133
  runId = null;
7120
7134
  cliSessionId = null;
7121
7135
  lastDrainedSeq = 0;
7122
7136
  runCostCents = 0;
7123
7137
  runTurns = 0;
7138
+ lastRunText = "";
7124
7139
  constructor(id, config, client, agentId, onDone, workspaceId, projectId, stateStore, onCardCompleted, onApiError) {
7125
7140
  this.config = config;
7126
7141
  this.client = client;
@@ -7186,8 +7201,10 @@ class Worker {
7186
7201
  this.timedOut = false;
7187
7202
  this.verificationFailed = false;
7188
7203
  this.held = false;
7204
+ this.completionStarted = false;
7189
7205
  this.runCostCents = 0;
7190
7206
  this.runTurns = 0;
7207
+ this.lastRunText = "";
7191
7208
  this.cliSessionId = null;
7192
7209
  this.lastDrainedSeq = 0;
7193
7210
  this.cardId = card.id;
@@ -7325,6 +7342,7 @@ class Worker {
7325
7342
  progressPercent: 75
7326
7343
  });
7327
7344
  this.state = "completing";
7345
+ this.completionStarted = true;
7328
7346
  await this.recordPhase("completing");
7329
7347
  let stageGateEvaluation = null;
7330
7348
  const stageRun = stageCtx.kind === "run" ? stageCtx : null;
@@ -7639,6 +7657,10 @@ class Worker {
7639
7657
  log.info(this.tag, `Stage "${stage.name}" gate "${gate.kind}" is advisory — skipping enforcement`);
7640
7658
  return null;
7641
7659
  }
7660
+ const review = gate.kind === "review_passed" ? parseReviewOutput(this.lastRunText) : undefined;
7661
+ if (review) {
7662
+ log.info(this.tag, `Review-gated stage "${stage.name}" verdict: ${review.verdict} (${review.findings.length} finding(s))`);
7663
+ }
7642
7664
  const registry = buildGateCollectorRegistry({
7643
7665
  build: {
7644
7666
  worktreePath,
@@ -7649,7 +7671,8 @@ class Worker {
7649
7671
  worktreePath,
7650
7672
  artifactType: stage.artifact_type
7651
7673
  },
7652
- checklist: { subtasks, cardDone: card.done ?? false }
7674
+ checklist: { subtasks, cardDone: card.done ?? false },
7675
+ ...review ? { review } : {}
7653
7676
  });
7654
7677
  const context = {
7655
7678
  cardId: card.id,
@@ -7676,6 +7699,7 @@ class Worker {
7676
7699
  return await advanceStageOnGate(card, stage, stageIndex, def, evaluation, {
7677
7700
  client: this.client,
7678
7701
  stateStore: this.stateStore,
7702
+ agentId: this.agentId,
7679
7703
  maxAttempts: this.config.budget.maxAttemptsPerCard,
7680
7704
  fallbackColumn: this.config.pickupColumns[0] ?? "To Do",
7681
7705
  sink: this.cliRunner,
@@ -7778,7 +7802,7 @@ class Worker {
7778
7802
  sigtermTimeoutMs: CANCEL_SIGTERM_TIMEOUT2
7779
7803
  });
7780
7804
  }
7781
- if (this.cardId && !this.timedOut) {
7805
+ if (this.cardId && !this.timedOut && !this.completionStarted) {
7782
7806
  try {
7783
7807
  const stats = this.lastSessionStats ?? this.progressTracker?.stats;
7784
7808
  await this.client.endAgentSession(this.cardId, {
@@ -7986,6 +8010,9 @@ class Worker {
7986
8010
  });
7987
8011
  }
7988
8012
  }
8013
+ parser.on("text", (content) => {
8014
+ this.lastRunText += content;
8015
+ });
7989
8016
  parser.on("parse_error", (msg) => {
7990
8017
  log.debug(this.tag, `Stream parse error (non-fatal): ${msg}`);
7991
8018
  runLog?.stream.write(`
@@ -8079,6 +8106,10 @@ class Worker {
8079
8106
  try {
8080
8107
  for await (const ev of stream) {
8081
8108
  this.progressTracker?.ingest(ev);
8109
+ if (ev.kind === "assistant_text") {
8110
+ this.lastRunText += `${ev.payload.text}
8111
+ `;
8112
+ }
8082
8113
  if (ev.source === "agent")
8083
8114
  this.cliRunner?.record(ev);
8084
8115
  if (ev.kind === "error") {
@@ -8166,6 +8197,8 @@ var init_worker = __esm(() => {
8166
8197
  init_process_group();
8167
8198
  init_progress_tracker();
8168
8199
  init_prompt();
8200
+ init_review_completion();
8201
+ init_review_knowledge();
8169
8202
  init_run_log();
8170
8203
  init_sdk_agent_runner();
8171
8204
  init_stage_advance();
package/dist/index.js CHANGED
@@ -5850,6 +5850,11 @@ async function runTransition(client, card, plan, opts = {}) {
5850
5850
  await withRetry("endSession", shortId, () => client.endAgentSession(card.id, plan.endSession), attempts, backoffMs);
5851
5851
  log.info(TAG22, `#${shortId} session ended (${plan.endSession.status})`);
5852
5852
  }
5853
+ if (plan.assignAgent !== undefined) {
5854
+ const assignedAgentId = plan.assignAgent;
5855
+ await withRetry("assignAgent", shortId, () => client.updateCard(card.id, { assignedAgentId }), attempts, backoffMs);
5856
+ log.info(TAG22, assignedAgentId ? `#${shortId} assigned → agent ${assignedAgentId}` : `#${shortId} unassigned`);
5857
+ }
5853
5858
  if (opts.store && opts.runId) {
5854
5859
  try {
5855
5860
  await opts.store.heartbeat(opts.runId);
@@ -6985,7 +6990,8 @@ async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps
6985
6990
  });
6986
6991
  await runTransition(deps.client, card, {
6987
6992
  move: { columnName: toColumn },
6988
- addLabels: [{ name: AGENT_LABEL }]
6993
+ addLabels: [{ name: AGENT_LABEL }],
6994
+ ...isAgentRunnableOwner(next.stage.owner) ? { assignAgent: deps.agentId } : {}
6989
6995
  }, { store: deps.stateStore, runId: deps.runId });
6990
6996
  deps.stateStore.recordOutcome(card.id, "success").catch(() => {});
6991
6997
  log.info(TAG28, `#${card.short_id} advanced "${stage.name}" → "${next.stage.name}" (column "${toColumn}")`);
@@ -7014,7 +7020,11 @@ async function handleGateUnmet(card, stage, summary, deps) {
7014
7020
  try {
7015
7021
  await deps.client.addComment(card.id, `Stage gate unmet — re-running "${stage.name}". ${summary}.`, { commentType: "progress" });
7016
7022
  } catch {}
7017
- await runTransition(deps.client, card, { move: { columnName: toColumn }, addLabels: [{ name: AGENT_LABEL }] }, { store: deps.stateStore, runId: deps.runId });
7023
+ await runTransition(deps.client, card, {
7024
+ move: { columnName: toColumn },
7025
+ addLabels: [{ name: AGENT_LABEL }],
7026
+ ...isAgentRunnableOwner(stage.owner) ? { assignAgent: deps.agentId } : {}
7027
+ }, { store: deps.stateStore, runId: deps.runId });
7018
7028
  log.info(TAG28, `#${card.short_id} gate unmet for "${stage.name}" — requeued to "${toColumn}" for re-run (attempt ${attempts}/${deps.maxAttempts})`);
7019
7029
  return { kind: "requeued_gate_unmet", toColumn };
7020
7030
  }
@@ -7077,6 +7087,9 @@ function buildStagePreamble(stage) {
7077
7087
  }
7078
7088
  }
7079
7089
  lines.push("Do only this stage's work. When the stage's handoff is met, end your session — advancement to the next stage is handled by the board.");
7090
+ if (normalizeGateSpec(stage.gate)?.kind === "review_passed") {
7091
+ lines.push("", "**This stage's gate is `review_passed` — your deliverable is a review verdict, not a prose handoff.** Do NOT end the session until you have actually reviewed the change and emitted EXACTLY one JSON block as the LAST thing you output (nothing after it):", "```json", REVIEW_VERDICT_SCHEMA, "```", "Decision rules:", REVIEW_DECISION_RULES, "The board reads this verdict to decide advancement: `approved` advances the card, `rejected` sends it back. A missing or unparseable verdict blocks the card. Do NOT modify any code — this is a read-only review.");
7092
+ }
7080
7093
  return lines.filter(Boolean).join(`
7081
7094
  `);
7082
7095
  }
@@ -7114,12 +7127,14 @@ class Worker {
7114
7127
  timedOut = false;
7115
7128
  verificationFailed = false;
7116
7129
  held = false;
7130
+ completionStarted = false;
7117
7131
  sessionId = null;
7118
7132
  runId = null;
7119
7133
  cliSessionId = null;
7120
7134
  lastDrainedSeq = 0;
7121
7135
  runCostCents = 0;
7122
7136
  runTurns = 0;
7137
+ lastRunText = "";
7123
7138
  constructor(id, config, client, agentId, onDone, workspaceId, projectId, stateStore, onCardCompleted, onApiError) {
7124
7139
  this.config = config;
7125
7140
  this.client = client;
@@ -7185,8 +7200,10 @@ class Worker {
7185
7200
  this.timedOut = false;
7186
7201
  this.verificationFailed = false;
7187
7202
  this.held = false;
7203
+ this.completionStarted = false;
7188
7204
  this.runCostCents = 0;
7189
7205
  this.runTurns = 0;
7206
+ this.lastRunText = "";
7190
7207
  this.cliSessionId = null;
7191
7208
  this.lastDrainedSeq = 0;
7192
7209
  this.cardId = card.id;
@@ -7324,6 +7341,7 @@ class Worker {
7324
7341
  progressPercent: 75
7325
7342
  });
7326
7343
  this.state = "completing";
7344
+ this.completionStarted = true;
7327
7345
  await this.recordPhase("completing");
7328
7346
  let stageGateEvaluation = null;
7329
7347
  const stageRun = stageCtx.kind === "run" ? stageCtx : null;
@@ -7638,6 +7656,10 @@ class Worker {
7638
7656
  log.info(this.tag, `Stage "${stage.name}" gate "${gate.kind}" is advisory — skipping enforcement`);
7639
7657
  return null;
7640
7658
  }
7659
+ const review = gate.kind === "review_passed" ? parseReviewOutput(this.lastRunText) : undefined;
7660
+ if (review) {
7661
+ log.info(this.tag, `Review-gated stage "${stage.name}" verdict: ${review.verdict} (${review.findings.length} finding(s))`);
7662
+ }
7641
7663
  const registry = buildGateCollectorRegistry({
7642
7664
  build: {
7643
7665
  worktreePath,
@@ -7648,7 +7670,8 @@ class Worker {
7648
7670
  worktreePath,
7649
7671
  artifactType: stage.artifact_type
7650
7672
  },
7651
- checklist: { subtasks, cardDone: card.done ?? false }
7673
+ checklist: { subtasks, cardDone: card.done ?? false },
7674
+ ...review ? { review } : {}
7652
7675
  });
7653
7676
  const context = {
7654
7677
  cardId: card.id,
@@ -7675,6 +7698,7 @@ class Worker {
7675
7698
  return await advanceStageOnGate(card, stage, stageIndex, def, evaluation, {
7676
7699
  client: this.client,
7677
7700
  stateStore: this.stateStore,
7701
+ agentId: this.agentId,
7678
7702
  maxAttempts: this.config.budget.maxAttemptsPerCard,
7679
7703
  fallbackColumn: this.config.pickupColumns[0] ?? "To Do",
7680
7704
  sink: this.cliRunner,
@@ -7777,7 +7801,7 @@ class Worker {
7777
7801
  sigtermTimeoutMs: CANCEL_SIGTERM_TIMEOUT2
7778
7802
  });
7779
7803
  }
7780
- if (this.cardId && !this.timedOut) {
7804
+ if (this.cardId && !this.timedOut && !this.completionStarted) {
7781
7805
  try {
7782
7806
  const stats = this.lastSessionStats ?? this.progressTracker?.stats;
7783
7807
  await this.client.endAgentSession(this.cardId, {
@@ -7985,6 +8009,9 @@ class Worker {
7985
8009
  });
7986
8010
  }
7987
8011
  }
8012
+ parser.on("text", (content) => {
8013
+ this.lastRunText += content;
8014
+ });
7988
8015
  parser.on("parse_error", (msg) => {
7989
8016
  log.debug(this.tag, `Stream parse error (non-fatal): ${msg}`);
7990
8017
  runLog?.stream.write(`
@@ -8078,6 +8105,10 @@ class Worker {
8078
8105
  try {
8079
8106
  for await (const ev of stream) {
8080
8107
  this.progressTracker?.ingest(ev);
8108
+ if (ev.kind === "assistant_text") {
8109
+ this.lastRunText += `${ev.payload.text}
8110
+ `;
8111
+ }
8081
8112
  if (ev.source === "agent")
8082
8113
  this.cliRunner?.record(ev);
8083
8114
  if (ev.kind === "error") {
@@ -8165,6 +8196,8 @@ var init_worker = __esm(() => {
8165
8196
  init_process_group();
8166
8197
  init_progress_tracker();
8167
8198
  init_prompt();
8199
+ init_review_completion();
8200
+ init_review_knowledge();
8168
8201
  init_run_log();
8169
8202
  init_sdk_agent_runner();
8170
8203
  init_stage_advance();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/agent",
3
- "version": "1.14.2",
3
+ "version": "1.14.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",