@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.
- package/dist/cli.js +138 -62
- package/dist/index.js +138 -62
- 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
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
3309
|
+
type,
|
|
3254
3310
|
memory_tier: "episode",
|
|
3255
3311
|
scope: "project",
|
|
3256
3312
|
tags: [`card:${cardShortId}`],
|
|
3257
|
-
topK:
|
|
3313
|
+
topK: 10,
|
|
3314
|
+
includeEpisodes: true
|
|
3258
3315
|
});
|
|
3259
|
-
const
|
|
3260
|
-
|
|
3261
|
-
|
|
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, "
|
|
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.
|
|
8921
|
-
log.debug(TAG31, `Card ${card.id} already queued or
|
|
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
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
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
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
3308
|
+
type,
|
|
3253
3309
|
memory_tier: "episode",
|
|
3254
3310
|
scope: "project",
|
|
3255
3311
|
tags: [`card:${cardShortId}`],
|
|
3256
|
-
topK:
|
|
3312
|
+
topK: 10,
|
|
3313
|
+
includeEpisodes: true
|
|
3257
3314
|
});
|
|
3258
|
-
const
|
|
3259
|
-
|
|
3260
|
-
|
|
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, "
|
|
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.
|
|
8920
|
-
log.debug(TAG31, `Card ${card.id} already queued or
|
|
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
|
-
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
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
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
49
|
+
"@gethmy/mcp": "^2.17.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@harmony/shared": "workspace:*",
|