@gethmy/agent 1.20.0 → 1.21.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 +138 -62
  2. package/dist/index.js +138 -62
  3. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -2459,17 +2459,26 @@ function cleanupWorktree(worktreePath, branchName) {
2459
2459
  const repoRoot = execFileSync3("git", ["rev-parse", "--show-toplevel"], {
2460
2460
  encoding: "utf-8"
2461
2461
  }).trim();
2462
- try {
2463
- execFileSync3("git", ["worktree", "remove", worktreePath, "--force"], {
2464
- cwd: repoRoot,
2465
- stdio: "pipe"
2466
- });
2467
- log.info(TAG6, `Removed worktree: ${worktreePath}`);
2468
- } catch (err) {
2469
- log.warn(TAG6, `Failed to remove worktree cleanly: ${err instanceof Error ? err.message : err}`);
2470
- if (existsSync2(worktreePath)) {
2471
- rmSync(worktreePath, { recursive: true, force: true });
2462
+ if (existsSync2(worktreePath)) {
2463
+ try {
2464
+ execFileSync3("git", ["worktree", "remove", worktreePath, "--force"], {
2465
+ cwd: repoRoot,
2466
+ stdio: "pipe"
2467
+ });
2468
+ log.info(TAG6, `Removed worktree: ${worktreePath}`);
2469
+ } catch (err) {
2470
+ log.warn(TAG6, `Failed to remove worktree cleanly: ${err instanceof Error ? err.message : err}`);
2471
+ if (existsSync2(worktreePath)) {
2472
+ rmSync(worktreePath, { recursive: true, force: true });
2473
+ }
2474
+ try {
2475
+ execFileSync3("git", ["worktree", "prune", "--expire=now"], {
2476
+ cwd: repoRoot,
2477
+ stdio: "pipe"
2478
+ });
2479
+ } catch {}
2472
2480
  }
2481
+ } else {
2473
2482
  try {
2474
2483
  execFileSync3("git", ["worktree", "prune", "--expire=now"], {
2475
2484
  cwd: repoRoot,
@@ -2491,9 +2500,19 @@ function resolveRepoRoot() {
2491
2500
  encoding: "utf-8"
2492
2501
  }).trim();
2493
2502
  }
2503
+ function localBranchExists(branchName, repoRoot) {
2504
+ try {
2505
+ execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { cwd: repoRoot, stdio: "ignore" });
2506
+ return true;
2507
+ } catch {
2508
+ return false;
2509
+ }
2510
+ }
2494
2511
  function branchAheadOfItsRemote(branchName, repoRoot = resolveRepoRoot()) {
2512
+ if (!localBranchExists(branchName, repoRoot))
2513
+ return false;
2495
2514
  try {
2496
- const out = execFileSync3("git", ["rev-list", branchName, "--not", "--remotes=origin"], { cwd: repoRoot, encoding: "utf-8" }).trim();
2515
+ const out = execFileSync3("git", ["rev-list", branchName, "--not", "--remotes=origin"], { cwd: repoRoot, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
2497
2516
  return out.length > 0;
2498
2517
  } catch {
2499
2518
  return false;
@@ -3221,12 +3240,48 @@ ${summary}`,
3221
3240
  agent_identifier: "harmony-agent"
3222
3241
  };
3223
3242
  }
3224
- async function writeEpisode(client, input) {
3243
+ async function writeEpisode(client, input, options) {
3225
3244
  const payload = buildEpisodePayload(input, input.card.project_id);
3245
+ let content = payload.content;
3246
+ if (options?.distiller) {
3247
+ try {
3248
+ const distilled = await options.distiller(input, payload.content);
3249
+ if (distilled && distilled.trim().length > 0) {
3250
+ content = distilled.trim();
3251
+ }
3252
+ } catch (err) {
3253
+ log.warn(TAG10, `episode distillation failed for #${input.card.short_id}`, {
3254
+ cardId: input.card.id,
3255
+ event: "episode_distill_failed",
3256
+ kind: input.kind,
3257
+ error: err instanceof Error ? err.message : String(err)
3258
+ });
3259
+ }
3260
+ }
3261
+ const metadata = payload.metadata;
3226
3262
  try {
3263
+ const existingId = await findRollingEpisode(client, input.workspaceId, input.card.project_id, input.card.short_id, input.kind);
3264
+ if (existingId) {
3265
+ await client.updateMemoryEntity(existingId, {
3266
+ title: payload.title,
3267
+ content,
3268
+ metadata,
3269
+ confidence: payload.confidence,
3270
+ tags: payload.tags,
3271
+ type: payload.type
3272
+ });
3273
+ log.info(TAG10, `episode rolled for #${input.card.short_id}`, {
3274
+ cardId: input.card.id,
3275
+ event: "episode_rolled",
3276
+ kind: input.kind,
3277
+ entityId: existingId
3278
+ });
3279
+ return existingId;
3280
+ }
3227
3281
  const { entity } = await client.createMemoryEntity({
3228
3282
  ...payload,
3229
- metadata: payload.metadata
3283
+ content,
3284
+ metadata
3230
3285
  });
3231
3286
  const id = entity && typeof entity === "object" && "id" in entity ? entity.id ?? null : null;
3232
3287
  log.info(TAG10, `episode written for #${input.card.short_id}`, {
@@ -3245,31 +3300,44 @@ async function writeEpisode(client, input) {
3245
3300
  return null;
3246
3301
  }
3247
3302
  }
3248
- async function findLatestImplementEpisode(client, workspaceId, projectId, cardShortId) {
3303
+ async function findRollingEpisode(client, workspaceId, projectId, cardShortId, kind) {
3304
+ const type = kind === "implement" ? ["solution", "error"] : ["decision"];
3249
3305
  try {
3250
3306
  const { entities } = await client.harmonyRecall({
3251
3307
  workspaceId,
3252
3308
  projectId,
3253
- type: ["solution", "error"],
3309
+ type,
3254
3310
  memory_tier: "episode",
3255
3311
  scope: "project",
3256
3312
  tags: [`card:${cardShortId}`],
3257
- topK: 1
3313
+ topK: 10,
3314
+ includeEpisodes: true
3258
3315
  });
3259
- const first = entities[0];
3260
- if (first && typeof first === "object" && "id" in first && typeof first.id === "string") {
3261
- return first.id;
3316
+ for (const entity of entities) {
3317
+ if (!entity || typeof entity !== "object")
3318
+ continue;
3319
+ const meta = entity.metadata;
3320
+ if (meta?.episode_kind !== kind || meta?.origin?.source !== "agent-run") {
3321
+ continue;
3322
+ }
3323
+ const id = entity.id;
3324
+ if (typeof id === "string")
3325
+ return id;
3262
3326
  }
3263
3327
  return null;
3264
3328
  } catch (err) {
3265
- log.warn(TAG10, "implement-episode lookup failed", {
3329
+ log.warn(TAG10, "rolling-episode lookup failed", {
3266
3330
  event: "episode_lookup_failed",
3267
3331
  cardShortId,
3332
+ kind,
3268
3333
  error: err instanceof Error ? err.message : String(err)
3269
3334
  });
3270
3335
  return null;
3271
3336
  }
3272
3337
  }
3338
+ async function findLatestImplementEpisode(client, workspaceId, projectId, cardShortId) {
3339
+ return findRollingEpisode(client, workspaceId, projectId, cardShortId, "implement");
3340
+ }
3273
3341
  async function backfillReviewVerdict(client, originalEpisodeId, verdict, reviewEpisodeId) {
3274
3342
  try {
3275
3343
  if (verdict === "approved") {
@@ -8003,6 +8071,7 @@ class Worker {
8003
8071
  stageGateEvaluation = await this.collectStageGateEvidence(card, stageRun.stage, worktreePath, subtasks);
8004
8072
  } : undefined;
8005
8073
  const completed = await runCompletion(this.client, card, this.branchName, this.worktreePath, this.config, this.id, this.lastSessionStats, this.workspaceId, this.sessionId, this.stateStore, this.onCardCompleted, onBeforeWorktreeCleanup);
8074
+ this.worktreePath = null;
8006
8075
  this.verificationFailed = !completed;
8007
8076
  if (completed && stageRun) {
8008
8077
  const outcome = await this.advanceFromGateEvaluation(card, stageRun.stage, stageRun.index, stageRun.def, stageGateEvaluation);
@@ -8917,53 +8986,58 @@ class Pool {
8917
8986
  }
8918
8987
  }
8919
8988
  async enqueue(card, column, labels, subtasks, mode = "implement") {
8920
- if (this.implQueue.has(card.id) || this.reviewQueue.has(card.id) || this.isCardActive(card.id)) {
8921
- log.debug(TAG31, `Card ${card.id} already queued or active, skipping`);
8989
+ if (this.isCardKnown(card.id) || this.reservations.has(card.id)) {
8990
+ log.debug(TAG31, `Card ${card.id} already queued, active, or reserved, skipping`);
8922
8991
  return;
8923
8992
  }
8924
- if (mode === "implement") {
8925
- if (this.authPaused) {
8926
- log.debug(TAG31, `#${card.short_id} held — agent paused (auth error)`);
8927
- await this.emitWaiting(card.id, "Agent paused — Anthropic auth error, check API credentials");
8928
- return;
8929
- }
8930
- const cooldownMs = this.apiCooldownRemainingMs();
8931
- if (cooldownMs > 0) {
8932
- log.debug(TAG31, `#${card.short_id} held — API cooldown ${Math.round(cooldownMs / 1000)}s remaining`);
8933
- await this.emitWaiting(card.id, `Paused Anthropic API limit, retrying in ~${Math.round(cooldownMs / 1000)}s`);
8934
- return;
8935
- }
8936
- const decision = this.budget.check(card.id);
8937
- if (!decision.allow) {
8938
- if (decision.reason === "daily_budget") {
8939
- log.warn(TAG31, `#${card.short_id} skipped (daily_budget): ${decision.detail}`);
8940
- await this.emitWaiting(card.id, `Daily budget reached — waiting for reset (${decision.detail})`);
8941
- } else {
8942
- log.debug(TAG31, `#${card.short_id} gave up: ${decision.detail}`);
8993
+ this.reservations.add(card.id);
8994
+ try {
8995
+ if (mode === "implement") {
8996
+ if (this.authPaused) {
8997
+ log.debug(TAG31, `#${card.short_id} held — agent paused (auth error)`);
8998
+ await this.emitWaiting(card.id, "Agent paused — Anthropic auth error, check API credentials");
8999
+ return;
9000
+ }
9001
+ const cooldownMs = this.apiCooldownRemainingMs();
9002
+ if (cooldownMs > 0) {
9003
+ log.debug(TAG31, `#${card.short_id} held — API cooldown ${Math.round(cooldownMs / 1000)}s remaining`);
9004
+ await this.emitWaiting(card.id, `Paused — Anthropic API limit, retrying in ~${Math.round(cooldownMs / 1000)}s`);
9005
+ return;
9006
+ }
9007
+ const decision = this.budget.check(card.id);
9008
+ if (!decision.allow) {
9009
+ if (decision.reason === "daily_budget") {
9010
+ log.warn(TAG31, `#${card.short_id} skipped (daily_budget): ${decision.detail}`);
9011
+ await this.emitWaiting(card.id, `Daily budget reached — waiting for reset (${decision.detail})`);
9012
+ } else {
9013
+ log.debug(TAG31, `#${card.short_id} gave up: ${decision.detail}`);
9014
+ }
9015
+ return;
9016
+ }
9017
+ const blockers = await getUnresolvedBlockers(this.client, card, this.projectId);
9018
+ if (blockers === null) {
9019
+ log.warn(TAG31, `#${card.short_id} blocker check failed — deferring to next tick`);
9020
+ return;
9021
+ }
9022
+ if (blockers.length > 0) {
9023
+ const list = blockers.map((b) => `#${b.shortId}`).join(", ");
9024
+ log.info(TAG31, `#${card.short_id} blocked by ${list} — waiting`);
9025
+ await this.emitWaiting(card.id, `Blocked by ${list} — waiting for chain`);
9026
+ return;
8943
9027
  }
8944
- return;
8945
- }
8946
- const blockers = await getUnresolvedBlockers(this.client, card, this.projectId);
8947
- if (blockers === null) {
8948
- log.warn(TAG31, `#${card.short_id} blocker check failed — deferring to next tick`);
8949
- return;
8950
9028
  }
8951
- if (blockers.length > 0) {
8952
- const list = blockers.map((b) => `#${b.shortId}`).join(", ");
8953
- log.info(TAG31, `#${card.short_id} blocked by ${list} waiting`);
8954
- await this.emitWaiting(card.id, `Blocked by ${list} waiting for chain`);
8955
- return;
9029
+ const queue = mode === "review" ? this.reviewQueue : this.implQueue;
9030
+ queue.enqueue(card, column, labels, mode);
9031
+ this.cardDataCache.set(card.id, { card, column, labels, subtasks, mode });
9032
+ const workers = mode === "review" ? this.reviewWorkers : this.implWorkers;
9033
+ const dispatched = this.tryDispatchFor(workers, queue, mode);
9034
+ if (!dispatched) {
9035
+ const position = queue.cardIds().indexOf(card.id) + 1;
9036
+ const total = queue.length;
9037
+ await this.emitWaiting(card.id, position > 0 ? `Queued (${position}/${total}) — waiting for ${mode} worker` : `Queued — waiting for ${mode} worker`);
8956
9038
  }
8957
- }
8958
- const queue = mode === "review" ? this.reviewQueue : this.implQueue;
8959
- queue.enqueue(card, column, labels, mode);
8960
- this.cardDataCache.set(card.id, { card, column, labels, subtasks, mode });
8961
- const workers = mode === "review" ? this.reviewWorkers : this.implWorkers;
8962
- const dispatched = this.tryDispatchFor(workers, queue, mode);
8963
- if (!dispatched) {
8964
- const position = queue.cardIds().indexOf(card.id) + 1;
8965
- const total = queue.length;
8966
- await this.emitWaiting(card.id, position > 0 ? `Queued (${position}/${total}) — waiting for ${mode} worker` : `Queued — waiting for ${mode} worker`);
9039
+ } finally {
9040
+ this.reservations.delete(card.id);
8967
9041
  }
8968
9042
  }
8969
9043
  lastWaitingEmit = new Map;
@@ -9005,6 +9079,7 @@ class Pool {
9005
9079
  async removeCard(cardId) {
9006
9080
  await this.stateStore.resetAttempts(cardId);
9007
9081
  this.lastWaitingEmit.delete(cardId);
9082
+ this.reservations.delete(cardId);
9008
9083
  for (const queue of [this.implQueue, this.reviewQueue]) {
9009
9084
  const removed = queue.remove(cardId);
9010
9085
  if (removed) {
@@ -9109,6 +9184,7 @@ class Pool {
9109
9184
  this.sleepGuard.stop();
9110
9185
  log.info(TAG31, "Pool shutdown complete");
9111
9186
  }
9187
+ reservations = new Set;
9112
9188
  cardDataCache = new Map;
9113
9189
  tryDispatchFor(workers, queue, label) {
9114
9190
  if (this.shuttingDown)
package/dist/index.js CHANGED
@@ -2458,17 +2458,26 @@ function cleanupWorktree(worktreePath, branchName) {
2458
2458
  const repoRoot = execFileSync3("git", ["rev-parse", "--show-toplevel"], {
2459
2459
  encoding: "utf-8"
2460
2460
  }).trim();
2461
- try {
2462
- execFileSync3("git", ["worktree", "remove", worktreePath, "--force"], {
2463
- cwd: repoRoot,
2464
- stdio: "pipe"
2465
- });
2466
- log.info(TAG6, `Removed worktree: ${worktreePath}`);
2467
- } catch (err) {
2468
- log.warn(TAG6, `Failed to remove worktree cleanly: ${err instanceof Error ? err.message : err}`);
2469
- if (existsSync2(worktreePath)) {
2470
- rmSync(worktreePath, { recursive: true, force: true });
2461
+ if (existsSync2(worktreePath)) {
2462
+ try {
2463
+ execFileSync3("git", ["worktree", "remove", worktreePath, "--force"], {
2464
+ cwd: repoRoot,
2465
+ stdio: "pipe"
2466
+ });
2467
+ log.info(TAG6, `Removed worktree: ${worktreePath}`);
2468
+ } catch (err) {
2469
+ log.warn(TAG6, `Failed to remove worktree cleanly: ${err instanceof Error ? err.message : err}`);
2470
+ if (existsSync2(worktreePath)) {
2471
+ rmSync(worktreePath, { recursive: true, force: true });
2472
+ }
2473
+ try {
2474
+ execFileSync3("git", ["worktree", "prune", "--expire=now"], {
2475
+ cwd: repoRoot,
2476
+ stdio: "pipe"
2477
+ });
2478
+ } catch {}
2471
2479
  }
2480
+ } else {
2472
2481
  try {
2473
2482
  execFileSync3("git", ["worktree", "prune", "--expire=now"], {
2474
2483
  cwd: repoRoot,
@@ -2490,9 +2499,19 @@ function resolveRepoRoot() {
2490
2499
  encoding: "utf-8"
2491
2500
  }).trim();
2492
2501
  }
2502
+ function localBranchExists(branchName, repoRoot) {
2503
+ try {
2504
+ execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { cwd: repoRoot, stdio: "ignore" });
2505
+ return true;
2506
+ } catch {
2507
+ return false;
2508
+ }
2509
+ }
2493
2510
  function branchAheadOfItsRemote(branchName, repoRoot = resolveRepoRoot()) {
2511
+ if (!localBranchExists(branchName, repoRoot))
2512
+ return false;
2494
2513
  try {
2495
- const out = execFileSync3("git", ["rev-list", branchName, "--not", "--remotes=origin"], { cwd: repoRoot, encoding: "utf-8" }).trim();
2514
+ const out = execFileSync3("git", ["rev-list", branchName, "--not", "--remotes=origin"], { cwd: repoRoot, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
2496
2515
  return out.length > 0;
2497
2516
  } catch {
2498
2517
  return false;
@@ -3220,12 +3239,48 @@ ${summary}`,
3220
3239
  agent_identifier: "harmony-agent"
3221
3240
  };
3222
3241
  }
3223
- async function writeEpisode(client, input) {
3242
+ async function writeEpisode(client, input, options) {
3224
3243
  const payload = buildEpisodePayload(input, input.card.project_id);
3244
+ let content = payload.content;
3245
+ if (options?.distiller) {
3246
+ try {
3247
+ const distilled = await options.distiller(input, payload.content);
3248
+ if (distilled && distilled.trim().length > 0) {
3249
+ content = distilled.trim();
3250
+ }
3251
+ } catch (err) {
3252
+ log.warn(TAG10, `episode distillation failed for #${input.card.short_id}`, {
3253
+ cardId: input.card.id,
3254
+ event: "episode_distill_failed",
3255
+ kind: input.kind,
3256
+ error: err instanceof Error ? err.message : String(err)
3257
+ });
3258
+ }
3259
+ }
3260
+ const metadata = payload.metadata;
3225
3261
  try {
3262
+ const existingId = await findRollingEpisode(client, input.workspaceId, input.card.project_id, input.card.short_id, input.kind);
3263
+ if (existingId) {
3264
+ await client.updateMemoryEntity(existingId, {
3265
+ title: payload.title,
3266
+ content,
3267
+ metadata,
3268
+ confidence: payload.confidence,
3269
+ tags: payload.tags,
3270
+ type: payload.type
3271
+ });
3272
+ log.info(TAG10, `episode rolled for #${input.card.short_id}`, {
3273
+ cardId: input.card.id,
3274
+ event: "episode_rolled",
3275
+ kind: input.kind,
3276
+ entityId: existingId
3277
+ });
3278
+ return existingId;
3279
+ }
3226
3280
  const { entity } = await client.createMemoryEntity({
3227
3281
  ...payload,
3228
- metadata: payload.metadata
3282
+ content,
3283
+ metadata
3229
3284
  });
3230
3285
  const id = entity && typeof entity === "object" && "id" in entity ? entity.id ?? null : null;
3231
3286
  log.info(TAG10, `episode written for #${input.card.short_id}`, {
@@ -3244,31 +3299,44 @@ async function writeEpisode(client, input) {
3244
3299
  return null;
3245
3300
  }
3246
3301
  }
3247
- async function findLatestImplementEpisode(client, workspaceId, projectId, cardShortId) {
3302
+ async function findRollingEpisode(client, workspaceId, projectId, cardShortId, kind) {
3303
+ const type = kind === "implement" ? ["solution", "error"] : ["decision"];
3248
3304
  try {
3249
3305
  const { entities } = await client.harmonyRecall({
3250
3306
  workspaceId,
3251
3307
  projectId,
3252
- type: ["solution", "error"],
3308
+ type,
3253
3309
  memory_tier: "episode",
3254
3310
  scope: "project",
3255
3311
  tags: [`card:${cardShortId}`],
3256
- topK: 1
3312
+ topK: 10,
3313
+ includeEpisodes: true
3257
3314
  });
3258
- const first = entities[0];
3259
- if (first && typeof first === "object" && "id" in first && typeof first.id === "string") {
3260
- return first.id;
3315
+ for (const entity of entities) {
3316
+ if (!entity || typeof entity !== "object")
3317
+ continue;
3318
+ const meta = entity.metadata;
3319
+ if (meta?.episode_kind !== kind || meta?.origin?.source !== "agent-run") {
3320
+ continue;
3321
+ }
3322
+ const id = entity.id;
3323
+ if (typeof id === "string")
3324
+ return id;
3261
3325
  }
3262
3326
  return null;
3263
3327
  } catch (err) {
3264
- log.warn(TAG10, "implement-episode lookup failed", {
3328
+ log.warn(TAG10, "rolling-episode lookup failed", {
3265
3329
  event: "episode_lookup_failed",
3266
3330
  cardShortId,
3331
+ kind,
3267
3332
  error: err instanceof Error ? err.message : String(err)
3268
3333
  });
3269
3334
  return null;
3270
3335
  }
3271
3336
  }
3337
+ async function findLatestImplementEpisode(client, workspaceId, projectId, cardShortId) {
3338
+ return findRollingEpisode(client, workspaceId, projectId, cardShortId, "implement");
3339
+ }
3272
3340
  async function backfillReviewVerdict(client, originalEpisodeId, verdict, reviewEpisodeId) {
3273
3341
  try {
3274
3342
  if (verdict === "approved") {
@@ -8002,6 +8070,7 @@ class Worker {
8002
8070
  stageGateEvaluation = await this.collectStageGateEvidence(card, stageRun.stage, worktreePath, subtasks);
8003
8071
  } : undefined;
8004
8072
  const completed = await runCompletion(this.client, card, this.branchName, this.worktreePath, this.config, this.id, this.lastSessionStats, this.workspaceId, this.sessionId, this.stateStore, this.onCardCompleted, onBeforeWorktreeCleanup);
8073
+ this.worktreePath = null;
8005
8074
  this.verificationFailed = !completed;
8006
8075
  if (completed && stageRun) {
8007
8076
  const outcome = await this.advanceFromGateEvaluation(card, stageRun.stage, stageRun.index, stageRun.def, stageGateEvaluation);
@@ -8916,53 +8985,58 @@ class Pool {
8916
8985
  }
8917
8986
  }
8918
8987
  async enqueue(card, column, labels, subtasks, mode = "implement") {
8919
- if (this.implQueue.has(card.id) || this.reviewQueue.has(card.id) || this.isCardActive(card.id)) {
8920
- log.debug(TAG31, `Card ${card.id} already queued or active, skipping`);
8988
+ if (this.isCardKnown(card.id) || this.reservations.has(card.id)) {
8989
+ log.debug(TAG31, `Card ${card.id} already queued, active, or reserved, skipping`);
8921
8990
  return;
8922
8991
  }
8923
- if (mode === "implement") {
8924
- if (this.authPaused) {
8925
- log.debug(TAG31, `#${card.short_id} held — agent paused (auth error)`);
8926
- await this.emitWaiting(card.id, "Agent paused — Anthropic auth error, check API credentials");
8927
- return;
8928
- }
8929
- const cooldownMs = this.apiCooldownRemainingMs();
8930
- if (cooldownMs > 0) {
8931
- log.debug(TAG31, `#${card.short_id} held — API cooldown ${Math.round(cooldownMs / 1000)}s remaining`);
8932
- await this.emitWaiting(card.id, `Paused Anthropic API limit, retrying in ~${Math.round(cooldownMs / 1000)}s`);
8933
- return;
8934
- }
8935
- const decision = this.budget.check(card.id);
8936
- if (!decision.allow) {
8937
- if (decision.reason === "daily_budget") {
8938
- log.warn(TAG31, `#${card.short_id} skipped (daily_budget): ${decision.detail}`);
8939
- await this.emitWaiting(card.id, `Daily budget reached — waiting for reset (${decision.detail})`);
8940
- } else {
8941
- log.debug(TAG31, `#${card.short_id} gave up: ${decision.detail}`);
8992
+ this.reservations.add(card.id);
8993
+ try {
8994
+ if (mode === "implement") {
8995
+ if (this.authPaused) {
8996
+ log.debug(TAG31, `#${card.short_id} held — agent paused (auth error)`);
8997
+ await this.emitWaiting(card.id, "Agent paused — Anthropic auth error, check API credentials");
8998
+ return;
8999
+ }
9000
+ const cooldownMs = this.apiCooldownRemainingMs();
9001
+ if (cooldownMs > 0) {
9002
+ log.debug(TAG31, `#${card.short_id} held — API cooldown ${Math.round(cooldownMs / 1000)}s remaining`);
9003
+ await this.emitWaiting(card.id, `Paused — Anthropic API limit, retrying in ~${Math.round(cooldownMs / 1000)}s`);
9004
+ return;
9005
+ }
9006
+ const decision = this.budget.check(card.id);
9007
+ if (!decision.allow) {
9008
+ if (decision.reason === "daily_budget") {
9009
+ log.warn(TAG31, `#${card.short_id} skipped (daily_budget): ${decision.detail}`);
9010
+ await this.emitWaiting(card.id, `Daily budget reached — waiting for reset (${decision.detail})`);
9011
+ } else {
9012
+ log.debug(TAG31, `#${card.short_id} gave up: ${decision.detail}`);
9013
+ }
9014
+ return;
9015
+ }
9016
+ const blockers = await getUnresolvedBlockers(this.client, card, this.projectId);
9017
+ if (blockers === null) {
9018
+ log.warn(TAG31, `#${card.short_id} blocker check failed — deferring to next tick`);
9019
+ return;
9020
+ }
9021
+ if (blockers.length > 0) {
9022
+ const list = blockers.map((b) => `#${b.shortId}`).join(", ");
9023
+ log.info(TAG31, `#${card.short_id} blocked by ${list} — waiting`);
9024
+ await this.emitWaiting(card.id, `Blocked by ${list} — waiting for chain`);
9025
+ return;
8942
9026
  }
8943
- return;
8944
- }
8945
- const blockers = await getUnresolvedBlockers(this.client, card, this.projectId);
8946
- if (blockers === null) {
8947
- log.warn(TAG31, `#${card.short_id} blocker check failed — deferring to next tick`);
8948
- return;
8949
9027
  }
8950
- if (blockers.length > 0) {
8951
- const list = blockers.map((b) => `#${b.shortId}`).join(", ");
8952
- log.info(TAG31, `#${card.short_id} blocked by ${list} waiting`);
8953
- await this.emitWaiting(card.id, `Blocked by ${list} waiting for chain`);
8954
- return;
9028
+ const queue = mode === "review" ? this.reviewQueue : this.implQueue;
9029
+ queue.enqueue(card, column, labels, mode);
9030
+ this.cardDataCache.set(card.id, { card, column, labels, subtasks, mode });
9031
+ const workers = mode === "review" ? this.reviewWorkers : this.implWorkers;
9032
+ const dispatched = this.tryDispatchFor(workers, queue, mode);
9033
+ if (!dispatched) {
9034
+ const position = queue.cardIds().indexOf(card.id) + 1;
9035
+ const total = queue.length;
9036
+ await this.emitWaiting(card.id, position > 0 ? `Queued (${position}/${total}) — waiting for ${mode} worker` : `Queued — waiting for ${mode} worker`);
8955
9037
  }
8956
- }
8957
- const queue = mode === "review" ? this.reviewQueue : this.implQueue;
8958
- queue.enqueue(card, column, labels, mode);
8959
- this.cardDataCache.set(card.id, { card, column, labels, subtasks, mode });
8960
- const workers = mode === "review" ? this.reviewWorkers : this.implWorkers;
8961
- const dispatched = this.tryDispatchFor(workers, queue, mode);
8962
- if (!dispatched) {
8963
- const position = queue.cardIds().indexOf(card.id) + 1;
8964
- const total = queue.length;
8965
- await this.emitWaiting(card.id, position > 0 ? `Queued (${position}/${total}) — waiting for ${mode} worker` : `Queued — waiting for ${mode} worker`);
9038
+ } finally {
9039
+ this.reservations.delete(card.id);
8966
9040
  }
8967
9041
  }
8968
9042
  lastWaitingEmit = new Map;
@@ -9004,6 +9078,7 @@ class Pool {
9004
9078
  async removeCard(cardId) {
9005
9079
  await this.stateStore.resetAttempts(cardId);
9006
9080
  this.lastWaitingEmit.delete(cardId);
9081
+ this.reservations.delete(cardId);
9007
9082
  for (const queue of [this.implQueue, this.reviewQueue]) {
9008
9083
  const removed = queue.remove(cardId);
9009
9084
  if (removed) {
@@ -9108,6 +9183,7 @@ class Pool {
9108
9183
  this.sleepGuard.stop();
9109
9184
  log.info(TAG31, "Pool shutdown complete");
9110
9185
  }
9186
+ reservations = new Set;
9111
9187
  cardDataCache = new Map;
9112
9188
  tryDispatchFor(workers, queue, label) {
9113
9189
  if (this.shuttingDown)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/agent",
3
- "version": "1.20.0",
3
+ "version": "1.21.0",
4
4
  "description": "Push-based agent daemon for Harmony — watches board assignments and spawns Claude CLI workers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -46,7 +46,7 @@
46
46
  "dependencies": {
47
47
  "@anthropic-ai/claude-agent-sdk": "^0.3.178",
48
48
  "@supabase/supabase-js": "2.95.3",
49
- "@gethmy/mcp": "^2.16.0"
49
+ "@gethmy/mcp": "^2.17.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@harmony/shared": "workspace:*",