@gethmy/agent 1.6.1 → 1.7.1

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.
@@ -32,9 +32,4 @@ export interface ReviewResult {
32
32
  * bouncing it to To Do for a parse failure that isn't a code quality signal.
33
33
  */
34
34
  export declare function parseReviewOutput(stdout: string): ReviewResult;
35
- /**
36
- * Post-review completion pipeline.
37
- * Handles approved/rejected verdicts, creates subtasks for findings,
38
- * and moves the card to the appropriate column.
39
- */
40
35
  export declare function runReviewCompletion(client: HarmonyApiClient, card: Card, result: ReviewResult, config: AgentConfig, worktreePath: string, branchName: string | null, sessionStats: SessionStats | null | undefined, runLogPath: string | null | undefined, workspaceId: string | undefined, agentSessionId: string | null | undefined, stateStore: StateStore): Promise<void>;
@@ -183,6 +183,18 @@ function stripReviewSummary(description) {
183
183
  * Handles approved/rejected verdicts, creates subtasks for findings,
184
184
  * and moves the card to the appropriate column.
185
185
  */
186
+ /**
187
+ * Post a review verdict as a typed comment instead of mutating the card
188
+ * description (card-comments plan, Phase 3). Best-effort — never throws.
189
+ */
190
+ async function postReviewComment(client, card, commentType, body) {
191
+ try {
192
+ await client.addComment(card.id, body, { commentType });
193
+ }
194
+ catch (err) {
195
+ log.error(TAG, `Failed to post review comment to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
196
+ }
197
+ }
186
198
  export async function runReviewCompletion(client, card, result, config, worktreePath, branchName, sessionStats, runLogPath, workspaceId, agentSessionId, stateStore) {
187
199
  // Re-fetch card for fresh description (avoids stale data from enqueue time)
188
200
  let freshDesc;
@@ -208,32 +220,28 @@ export async function runReviewCompletion(client, card, result, config, worktree
208
220
  log.warn(TAG, `Failed to add "${NEED_REVIEW_LABEL}" label: ${err instanceof Error ? err.message : err}`);
209
221
  }
210
222
  if (config.review.postFindings) {
211
- const baseDesc = stripReviewSummary(freshDesc);
212
223
  const rawTail = runLogPath ? tailRunLog(runLogPath) : null;
213
224
  // Log content routinely contains ```json fences from Claude's own
214
- // output; embedding it inside a 3-backtick fence would break the card's
215
- // markdown. Use a 4-backtick fence and downgrade any 4+-backtick runs.
225
+ // output; embedding it inside a 3-backtick fence would break markdown.
226
+ // Use a 4-backtick fence and downgrade any 4+-backtick runs.
216
227
  const runLogTail = rawTail
217
- ? rawTail.replace(/`{4,}/g, (_m) => "`".repeat(3))
228
+ ? rawTail.replace(/`{4,}/g, () => "`".repeat(3))
218
229
  : null;
219
230
  const runLogHint = runLogPath
220
- ? `\nRun log: \`${runLogPath}\``
221
- : "\nRun log: (not captured)";
222
- const summary = [
223
- `\n\n${REVIEW_MARKER} Parse error**`,
224
- '\nThe review agent\'s output could not be parsed. Card stays in Review with the "Need Review" label — inspect the run log below to diagnose.',
231
+ ? `Run log: \`${runLogPath}\``
232
+ : "Run log: (not captured)";
233
+ const body = [
234
+ "**Review parse error.**",
235
+ 'The review agent\'s output could not be parsed. Card stays in Review with the "Need Review" label — inspect the run log below to diagnose.',
225
236
  runLogHint,
226
- result.summary ? `\n\nRaw output (truncated):\n${result.summary}` : "",
237
+ result.summary ? `Raw output (truncated):\n${result.summary}` : "",
227
238
  runLogTail
228
- ? `\n\nRun log tail (last ${RUN_LOG_TAIL_BYTES}B):\n\`\`\`\`\n${runLogTail}\n\`\`\`\``
239
+ ? `Run log tail (last ${RUN_LOG_TAIL_BYTES}B):\n\`\`\`\`\n${runLogTail}\n\`\`\`\``
229
240
  : "",
230
- ].join("");
231
- try {
232
- await client.updateCard(card.id, { description: baseDesc + summary });
233
- }
234
- catch (err) {
235
- log.error(TAG, `Failed to update description: ${err instanceof Error ? err.message : err}`);
236
- }
241
+ ]
242
+ .filter(Boolean)
243
+ .join("\n\n");
244
+ await postReviewComment(client, card, "blocker", body);
237
245
  }
238
246
  await client.endAgentSession(card.id, {
239
247
  status: "paused",
@@ -276,30 +284,23 @@ export async function runReviewCompletion(client, card, result, config, worktree
276
284
  }
277
285
  // Add "Ready to Merge" label
278
286
  await addLabelByName(client, card, config.review.approvedLabel, config.review.approvedLabelColor);
279
- // Post approval summary (card stays in Review, strip stale cycle marker)
287
+ // Post the approval verdict as a decision comment (card stays in Review).
280
288
  if (config.review.postFindings) {
281
- const baseDesc = stripReviewSummary(freshDesc).replace(/\n\nReview cycle:\s*\d+\/\d+/, "");
282
289
  const scopeLine = result.scopeCheck
283
- ? `\nScope: ${result.scopeCheck.status}${result.scopeCheck.notes ? ` — ${result.scopeCheck.notes}` : ""}`
290
+ ? `Scope: ${result.scopeCheck.status}${result.scopeCheck.notes ? ` — ${result.scopeCheck.notes}` : ""}`
284
291
  : "";
285
- const summaryParts = [
286
- `\n\n${REVIEW_MARKER} Approved**`,
287
- result.summary ? `\n${result.summary}` : "",
292
+ const body = [
293
+ "**Review — approved.**",
294
+ result.summary || "",
288
295
  scopeLine,
289
296
  result.findings.length > 0
290
- ? `\n${result.findings.length} minor finding(s) noted.`
297
+ ? `${result.findings.length} minor finding(s) noted.`
291
298
  : "",
292
- ];
293
- if (prUrl) {
294
- summaryParts.push(`\nPR: ${prUrl}`);
295
- }
296
- const summary = summaryParts.join("");
297
- try {
298
- await client.updateCard(card.id, { description: baseDesc + summary });
299
- }
300
- catch (err) {
301
- log.error(TAG, `Failed to update description: ${err instanceof Error ? err.message : err}`);
302
- }
299
+ prUrl ? `PR: ${prUrl}` : "",
300
+ ]
301
+ .filter(Boolean)
302
+ .join("\n\n");
303
+ await postReviewComment(client, card, "decision", body);
303
304
  }
304
305
  await client.endAgentSession(card.id, {
305
306
  status: "completed",
@@ -324,18 +325,14 @@ export async function runReviewCompletion(client, card, result, config, worktree
324
325
  if (currentCycle >= maxCycles) {
325
326
  log.warn(TAG, `#${card.short_id} reached max review cycles (${maxCycles}), moving to Done with note`);
326
327
  await moveCardToColumn(client, card, config.review.moveToColumn);
327
- const baseDesc = stripReviewSummary(freshDesc);
328
- const summary = [
329
- `\n\n${REVIEW_MARKER} Needs human review**`,
330
- `\nReached max review cycles (${maxCycles}). Please review manually.`,
331
- result.summary ? `\n${result.summary}` : "",
332
- ].join("");
333
- try {
334
- await client.updateCard(card.id, { description: baseDesc + summary });
335
- }
336
- catch (err) {
337
- log.error(TAG, `Failed to update description: ${err instanceof Error ? err.message : err}`);
338
- }
328
+ const body = [
329
+ "**Review needs human review.**",
330
+ `Reached max review cycles (${maxCycles}). Please review manually.`,
331
+ result.summary || "",
332
+ ]
333
+ .filter(Boolean)
334
+ .join("\n\n");
335
+ await postReviewComment(client, card, "blocker", body);
339
336
  await client.endAgentSession(card.id, {
340
337
  status: "completed",
341
338
  ...buildTokenPayload(sessionStats),
@@ -396,26 +393,30 @@ export async function runReviewCompletion(client, card, result, config, worktree
396
393
  log.error(TAG, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
397
394
  }
398
395
  }));
399
- // Update description with review summary and cycle counter
396
+ // The review cycle counter stays in the description — it is functional
397
+ // state used to enforce maxReviewCycles, not narrative. (This also strips
398
+ // any legacy summary block from the description.) The review narrative
399
+ // goes to a summary comment instead.
400
400
  const baseDesc = stripReviewSummary(freshDesc);
401
401
  const updatedDesc = updateReviewCycleMarker(baseDesc, currentCycle, maxCycles);
402
- const scopeLine = result.scopeCheck
403
- ? `\nScope: ${result.scopeCheck.status}${result.scopeCheck.notes ? ` — ${result.scopeCheck.notes}` : ""}`
404
- : "";
405
- const summary = [
406
- `\n\n${REVIEW_MARKER} Rejected**`,
407
- result.summary ? `\n${result.summary}` : "",
408
- scopeLine,
409
- `\n${criticalFindings.length} critical, ${majorFindings.length} major, ${minorFindings.length} minor finding(s).`,
410
- ].join("");
411
402
  try {
412
- await client.updateCard(card.id, {
413
- description: updatedDesc + summary,
414
- });
403
+ await client.updateCard(card.id, { description: updatedDesc });
415
404
  }
416
405
  catch (err) {
417
- log.error(TAG, `Failed to update description: ${err instanceof Error ? err.message : err}`);
406
+ log.error(TAG, `Failed to update review cycle marker: ${err instanceof Error ? err.message : err}`);
418
407
  }
408
+ const scopeLine = result.scopeCheck
409
+ ? `Scope: ${result.scopeCheck.status}${result.scopeCheck.notes ? ` — ${result.scopeCheck.notes}` : ""}`
410
+ : "";
411
+ const body = [
412
+ "**Review — rejected.**",
413
+ result.summary || "",
414
+ scopeLine,
415
+ `${criticalFindings.length} critical, ${majorFindings.length} major, ${minorFindings.length} minor finding(s).`,
416
+ ]
417
+ .filter(Boolean)
418
+ .join("\n\n");
419
+ await postReviewComment(client, card, "summary", body);
419
420
  }
420
421
  // Move back to failColumn (To Do) for re-implementation
421
422
  await moveCardToColumn(client, card, config.review.failColumn);
@@ -104,7 +104,7 @@ function configRows(config, projectName, gitProvider) {
104
104
  rows.push({ label: "User", value: config.userEmail });
105
105
  const reviewEnabled = config.agent.review.enabled;
106
106
  const poolDesc = reviewEnabled
107
- ? `Pool ${config.agent.poolSize} impl + 1 review`
107
+ ? `Pool ${config.agent.poolSize} impl + ${config.agent.review.poolSize} review`
108
108
  : `Pool ${config.agent.poolSize} impl`;
109
109
  const flowDesc = reviewEnabled
110
110
  ? `Pickup ${config.agent.pickupColumns[0]} → ${config.agent.completion.moveToColumn} → ${config.agent.review.moveToColumn}`
@@ -28,8 +28,6 @@ export interface CardRecord {
28
28
  cardId: string;
29
29
  attempts: number;
30
30
  totalCostCents: number;
31
- dlq: boolean;
32
- dlqReason?: string;
33
31
  lastAttemptAt: number | null;
34
32
  lastOutcome: "success" | "failure" | null;
35
33
  failureHistory?: FailureSummaryRecord[];
@@ -70,9 +68,16 @@ export declare class StateStore {
70
68
  getCard(cardId: string): CardRecord | null;
71
69
  incrementAttempt(cardId: string): Promise<number>;
72
70
  recordOutcome(cardId: string, outcome: "success" | "failure"): Promise<void>;
71
+ /**
72
+ * Reset a card's attempt counter so a card that previously gave up
73
+ * (attempts >= maxAttemptsPerCard) becomes eligible again. Called when a
74
+ * card is unassigned/reassigned — reassignment is the human's "try again".
75
+ * No-op for an unknown card.
76
+ */
77
+ resetAttempts(cardId: string): Promise<void>;
73
78
  /**
74
79
  * Push a failure summary onto the card's bounded history (most-recent first,
75
- * capped at 5). Read back by DLQ comment formatting and the Agent History
80
+ * capped at 5). Read back by the give-up comment and the Agent History
76
81
  * UI section to give users a post-mortem trail across attempts.
77
82
  */
78
83
  recordFailureSummary(cardId: string, entry: Omit<FailureSummaryRecord, "ts"> & {
@@ -81,8 +86,4 @@ export declare class StateStore {
81
86
  getRecentFailures(cardId: string, limit?: number): FailureSummaryRecord[];
82
87
  addCost(cardId: string, cents: number): Promise<void>;
83
88
  getDailyCostCents(date?: string): number;
84
- markDlq(cardId: string, reason: string): Promise<void>;
85
- clearDlq(cardId: string): Promise<void>;
86
- isDlq(cardId: string): boolean;
87
- listDlq(): CardRecord[];
88
89
  }
@@ -145,7 +145,6 @@ export class StateStore {
145
145
  cardId,
146
146
  attempts: 0,
147
147
  totalCostCents: 0,
148
- dlq: false,
149
148
  lastAttemptAt: null,
150
149
  lastOutcome: null,
151
150
  };
@@ -170,9 +169,22 @@ export class StateStore {
170
169
  rec.attempts = 0;
171
170
  await this.persist();
172
171
  }
172
+ /**
173
+ * Reset a card's attempt counter so a card that previously gave up
174
+ * (attempts >= maxAttemptsPerCard) becomes eligible again. Called when a
175
+ * card is unassigned/reassigned — reassignment is the human's "try again".
176
+ * No-op for an unknown card.
177
+ */
178
+ async resetAttempts(cardId) {
179
+ const rec = this.getCard(cardId);
180
+ if (!rec || rec.attempts === 0)
181
+ return;
182
+ rec.attempts = 0;
183
+ await this.persist();
184
+ }
173
185
  /**
174
186
  * Push a failure summary onto the card's bounded history (most-recent first,
175
- * capped at 5). Read back by DLQ comment formatting and the Agent History
187
+ * capped at 5). Read back by the give-up comment and the Agent History
176
188
  * UI section to give users a post-mortem trail across attempts.
177
189
  */
178
190
  async recordFailureSummary(cardId, entry) {
@@ -215,25 +227,4 @@ export class StateStore {
215
227
  const key = date ?? todayUtc();
216
228
  return this.state.daily.find((d) => d.date === key)?.costCents ?? 0;
217
229
  }
218
- // ---------- DLQ ----------
219
- async markDlq(cardId, reason) {
220
- const rec = this.ensureCard(cardId);
221
- rec.dlq = true;
222
- rec.dlqReason = reason;
223
- await this.persist();
224
- }
225
- async clearDlq(cardId) {
226
- const rec = this.getCard(cardId);
227
- if (!rec)
228
- return;
229
- rec.dlq = false;
230
- delete rec.dlqReason;
231
- await this.persist();
232
- }
233
- isDlq(cardId) {
234
- return this.getCard(cardId)?.dlq === true;
235
- }
236
- listDlq() {
237
- return this.state.cards.filter((c) => c.dlq);
238
- }
239
230
  }
package/dist/types.d.ts CHANGED
@@ -40,6 +40,8 @@ export interface AgentConfig {
40
40
  };
41
41
  review: {
42
42
  enabled: boolean;
43
+ /** Concurrent review workers. Each gets its own dev-server port slot. */
44
+ poolSize: number;
43
45
  pickupColumns: string[];
44
46
  moveToColumn: string;
45
47
  failColumn: string;
@@ -55,15 +57,13 @@ export interface AgentConfig {
55
57
  mergedLabelColor: string;
56
58
  };
57
59
  budget: {
58
- /** Max implement attempts per card before DLQ (reset on success). */
60
+ /**
61
+ * Max implement attempts per card before the daemon gives up (resets on
62
+ * success and on reassign). Repeated verification failures count.
63
+ */
59
64
  maxAttemptsPerCard: number;
60
- /** Max cumulative spend per card, in cents, before DLQ. */
61
- maxCentsPerCard: number;
62
65
  /** Daily spend cap, in cents (UTC day). Exceeded → pause pickups. */
63
66
  dailyBudgetCents: number;
64
- /** Label applied to DLQ'd cards. */
65
- dlqLabel: string;
66
- dlqLabelColor: string;
67
67
  };
68
68
  http: {
69
69
  /** Local HTTP status/control server. Bound to 127.0.0.1 by default. */
@@ -129,6 +129,35 @@ export interface EpisodeMeta {
129
129
  files_touched: number;
130
130
  num_turns: number;
131
131
  error?: string;
132
+ /**
133
+ * Changed file paths for the run (#272). Capped (default first 30). Prefer
134
+ * the diff's authoritative list; falls back to ProgressTracker-tracked edit
135
+ * paths when a diff is unavailable.
136
+ */
137
+ changed_files?: string[];
138
+ /** Line churn for the run (#272), best-effort from `git diff --numstat`. */
139
+ churn?: {
140
+ insertions: number;
141
+ deletions: number;
142
+ };
143
+ /**
144
+ * Cheap deterministic root-cause / gotcha line extracted from the run's
145
+ * assistant text (#272). No LLM call — left unset when nothing matches.
146
+ */
147
+ key_insight?: string;
148
+ /**
149
+ * Provenance record (#273). Agent episodes are always `source: 'agent-run'`.
150
+ * Mirrors the `MemoryOrigin` shape stamped by `harmony_remember`; lives in
151
+ * metadata jsonb (no dedicated column). Input for provenance badges and
152
+ * auto-vs-curated hygiene filtering.
153
+ */
154
+ origin?: {
155
+ source: "manual" | "assistant" | "agent-run" | "import";
156
+ source_card_id?: string;
157
+ source_session_id?: string;
158
+ author?: string;
159
+ source_trust?: string;
160
+ };
132
161
  /** Provenance only — never used as memory scope. */
133
162
  agent_session_id?: string;
134
163
  /** Set on back-fill from review pipeline. */
package/dist/types.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const DEFAULT_AGENT_CONFIG = {
2
- poolSize: 1,
2
+ poolSize: 3,
3
3
  maxTimeout: 1_800_000, // 30 minutes
4
4
  pickupColumns: ["To Do"],
5
5
  priorityLabels: { urgent: 100, critical: 90, bug: 50 },
@@ -35,6 +35,7 @@ export const DEFAULT_AGENT_CONFIG = {
35
35
  },
36
36
  review: {
37
37
  enabled: true,
38
+ poolSize: 2,
38
39
  pickupColumns: ["Review"],
39
40
  moveToColumn: "Done",
40
41
  failColumn: "To Do",
@@ -51,10 +52,7 @@ export const DEFAULT_AGENT_CONFIG = {
51
52
  },
52
53
  budget: {
53
54
  maxAttemptsPerCard: 3,
54
- maxCentsPerCard: 500, // $5.00
55
55
  dailyBudgetCents: 5000, // $50.00
56
- dlqLabel: "dlq",
57
- dlqLabelColor: "#dc2626",
58
56
  },
59
57
  http: {
60
58
  enabled: true,
package/dist/worker.d.ts CHANGED
@@ -21,6 +21,7 @@ export declare class Worker {
21
21
  private progressTracker;
22
22
  private lastSessionStats;
23
23
  private aborted;
24
+ private verificationFailed;
24
25
  private sessionId;
25
26
  private runId;
26
27
  constructor(id: number, config: AgentConfig, client: HarmonyApiClient, _userEmail: string, onDone: (worker: Worker) => void, workspaceId: string, projectId: string, stateStore: StateStore);
package/dist/worker.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { moveCardAndAddLabel } from "./board-helpers.js";
2
+ import { buildGaveUpComment } from "./budget.js";
2
3
  import { buildTokenPayload, runCompletion, } from "./completion.js";
3
4
  import { log } from "./log.js";
4
5
  import { signalGroup, spawnInGroup, terminateGroup } from "./process-group.js";
@@ -32,6 +33,7 @@ export class Worker {
32
33
  progressTracker = null;
33
34
  lastSessionStats;
34
35
  aborted = false;
36
+ verificationFailed = false;
35
37
  sessionId = null;
36
38
  runId = null;
37
39
  constructor(id, config, client, _userEmail, onDone, workspaceId, projectId, stateStore) {
@@ -96,6 +98,7 @@ export class Worker {
96
98
  */
97
99
  async run(card, column, labels, subtasks) {
98
100
  this.aborted = false;
101
+ this.verificationFailed = false;
99
102
  this.cardId = card.id;
100
103
  this.startedAt = Date.now();
101
104
  this.runId = newRunId();
@@ -104,7 +107,7 @@ export class Worker {
104
107
  this.state = "preparing";
105
108
  this.branchName = makeBranchName(card.short_id, card.title, this.config.worktree.failedBranchPrefix);
106
109
  log.info(this.tag, `Preparing #${card.short_id} "${card.title}"`);
107
- // Per-card attempt counter resets on success; DLQ triggers off it.
110
+ // Per-card attempt counter resets on success; give-up triggers off it.
108
111
  await this.stateStore.incrementAttempt(card.id);
109
112
  // Start the heartbeat loop so the reconciler knows this run is
110
113
  // still alive even if no phase transitions happen for a while
@@ -195,7 +198,12 @@ export class Worker {
195
198
  });
196
199
  this.state = "completing";
197
200
  await this.recordPhase("completing");
198
- await runCompletion(this.client, card, this.branchName, this.worktreePath, this.config, this.id, this.lastSessionStats, this.workspaceId, this.sessionId, this.stateStore);
201
+ const completed = await runCompletion(this.client, card, this.branchName, this.worktreePath, this.config, this.id, this.lastSessionStats, this.workspaceId, this.sessionId, this.stateStore);
202
+ // A verification failure is a failed attempt, not a success — even
203
+ // though runCompletion returns normally (it has already moved the card
204
+ // to the fail column and ended the session). Flag it so the finally
205
+ // block records a failure and counts it toward the give-up budget.
206
+ this.verificationFailed = !completed;
199
207
  }
200
208
  catch (err) {
201
209
  this.state = "error";
@@ -228,8 +236,11 @@ export class Worker {
228
236
  // Only bookkeep success when we actually succeeded. "cancelling"
229
237
  // and aborted runs are failures/user-initiated stops, not wins —
230
238
  // counting them as success would reset attempts and mask real
231
- // failure loops.
232
- const succeeded = this.runId && this.state !== "error" && !this.aborted;
239
+ // failure loops. A verification failure is likewise a failed attempt.
240
+ const succeeded = this.runId &&
241
+ this.state !== "error" &&
242
+ !this.aborted &&
243
+ !this.verificationFailed;
233
244
  if (succeeded) {
234
245
  try {
235
246
  await this.stateStore.endRun(this.runId, "completed");
@@ -249,6 +260,18 @@ export class Worker {
249
260
  // best-effort
250
261
  }
251
262
  }
263
+ else if (this.runId && this.verificationFailed) {
264
+ // Verification failed (no exception was thrown). Count it as a
265
+ // failed attempt so repeated failures eventually trip the give-up
266
+ // budget — runCompletion already moved the card + ended the session.
267
+ try {
268
+ await this.stateStore.endRun(this.runId, "paused", "verification");
269
+ }
270
+ catch {
271
+ // best-effort
272
+ }
273
+ await this.recordOutcome(card.id, "failure");
274
+ }
252
275
  this.cleanup();
253
276
  this.state = "idle";
254
277
  this.onDone(this);
@@ -262,6 +285,26 @@ export class Worker {
262
285
  await this.stateStore.addCost(cardId, cents);
263
286
  }
264
287
  await this.stateStore.recordOutcome(cardId, outcome);
288
+ // Give-up: if this failure exhausted the attempt budget, post a single
289
+ // human-facing comment here at the crossing. The pool guard then skips
290
+ // the card quietly until it is reassigned (which resets attempts), so
291
+ // this fires exactly once — no permanent DLQ, no label, no manual clear.
292
+ if (outcome === "failure") {
293
+ const max = this.config.budget.maxAttemptsPerCard;
294
+ const attempts = this.stateStore.getCard(cardId)?.attempts ?? 0;
295
+ if (attempts >= max) {
296
+ try {
297
+ const body = buildGaveUpComment(max, this.stateStore.getRecentFailures(cardId, 3));
298
+ await this.client.addComment(cardId, body, {
299
+ commentType: "blocker",
300
+ });
301
+ log.warn(this.tag, `gave up on ${cardId} after ${attempts} attempts`);
302
+ }
303
+ catch (err) {
304
+ log.warn(this.tag, `failed to post give-up comment for ${cardId}: ${err instanceof Error ? err.message : err}`);
305
+ }
306
+ }
307
+ }
265
308
  }
266
309
  catch (err) {
267
310
  log.warn(this.tag, `recordOutcome(${outcome}) failed: ${err instanceof Error ? err.message : err}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/agent",
3
- "version": "1.6.1",
3
+ "version": "1.7.1",
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",