@gethmy/agent 1.7.3 → 1.8.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.
- package/dist/cli.js +292 -88
- package/dist/index.js +292 -88
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1162,6 +1162,7 @@ class MergeMonitor {
|
|
|
1162
1162
|
running = false;
|
|
1163
1163
|
provider;
|
|
1164
1164
|
cwd;
|
|
1165
|
+
onCardCompleted = null;
|
|
1165
1166
|
constructor(client, projectId, config, intervalMs = 60000) {
|
|
1166
1167
|
this.client = client;
|
|
1167
1168
|
this.projectId = projectId;
|
|
@@ -1282,6 +1283,13 @@ class MergeMonitor {
|
|
|
1282
1283
|
log.info(TAG7, `Deleted local branch ${branchName}`);
|
|
1283
1284
|
} catch {}
|
|
1284
1285
|
}
|
|
1286
|
+
if (this.onCardCompleted) {
|
|
1287
|
+
try {
|
|
1288
|
+
await this.onCardCompleted(card);
|
|
1289
|
+
} catch (err) {
|
|
1290
|
+
log.warn(TAG7, `successor promotion failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1285
1293
|
log.info(TAG7, `#${card.short_id} completed (merged)`);
|
|
1286
1294
|
}
|
|
1287
1295
|
}
|
|
@@ -2028,7 +2036,7 @@ function buildTokenPayload(stats) {
|
|
|
2028
2036
|
modelName: stats.cost.modelName
|
|
2029
2037
|
};
|
|
2030
2038
|
}
|
|
2031
|
-
async function runCompletion(client, card, branchName, worktreePath, config, workerId, sessionStats, workspaceId, agentSessionId, stateStore) {
|
|
2039
|
+
async function runCompletion(client, card, branchName, worktreePath, config, workerId, sessionStats, workspaceId, agentSessionId, stateStore, onMovedToCompletion) {
|
|
2032
2040
|
let verificationResult = {
|
|
2033
2041
|
passed: true,
|
|
2034
2042
|
buildErrors: [],
|
|
@@ -2137,6 +2145,13 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
2137
2145
|
}
|
|
2138
2146
|
if (config.completion.moveToColumn) {
|
|
2139
2147
|
await moveCardToColumn(client, card, config.completion.moveToColumn);
|
|
2148
|
+
if (onMovedToCompletion) {
|
|
2149
|
+
try {
|
|
2150
|
+
await onMovedToCompletion(card);
|
|
2151
|
+
} catch (err) {
|
|
2152
|
+
log.warn(TAG12, `successor promotion failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2140
2155
|
}
|
|
2141
2156
|
if (config.completion.postSummary) {
|
|
2142
2157
|
await postSummary(client, card, branchName, worktreePath, prUrl, config.worktree.baseBranch, sessionStats);
|
|
@@ -3739,6 +3754,7 @@ class ReviewWorker {
|
|
|
3739
3754
|
progressTracker = null;
|
|
3740
3755
|
lastSessionStats = null;
|
|
3741
3756
|
aborted = false;
|
|
3757
|
+
timedOut = false;
|
|
3742
3758
|
runId = null;
|
|
3743
3759
|
lastRunLogPath = null;
|
|
3744
3760
|
sessionId = null;
|
|
@@ -3794,6 +3810,7 @@ class ReviewWorker {
|
|
|
3794
3810
|
}
|
|
3795
3811
|
async run(card, column, labels, subtasks) {
|
|
3796
3812
|
this.aborted = false;
|
|
3813
|
+
this.timedOut = false;
|
|
3797
3814
|
this.cardId = card.id;
|
|
3798
3815
|
this.startedAt = Date.now();
|
|
3799
3816
|
this.runId = newRunId();
|
|
@@ -3931,6 +3948,7 @@ class ReviewWorker {
|
|
|
3931
3948
|
});
|
|
3932
3949
|
this.timeoutTimer = setTimeout(() => {
|
|
3933
3950
|
log.warn(this.tag, `Review timeout reached (${this.config.review.maxTimeout}ms), cancelling`);
|
|
3951
|
+
this.timedOut = true;
|
|
3934
3952
|
this.cancel();
|
|
3935
3953
|
}, this.config.review.maxTimeout);
|
|
3936
3954
|
this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks);
|
|
@@ -3997,7 +4015,7 @@ class ReviewWorker {
|
|
|
3997
4015
|
try {
|
|
3998
4016
|
const run = this.stateStore.getRun(this.runId);
|
|
3999
4017
|
if (run && run.endedAt === null) {
|
|
4000
|
-
const status = this.state === "error" || this.aborted ? "paused" : "completed";
|
|
4018
|
+
const status = this.timedOut ? "failed" : this.state === "error" || this.aborted ? "paused" : "completed";
|
|
4001
4019
|
await this.stateStore.endRun(this.runId, status);
|
|
4002
4020
|
}
|
|
4003
4021
|
} catch {}
|
|
@@ -4035,6 +4053,7 @@ class ReviewWorker {
|
|
|
4035
4053
|
signalGroup(this.process, "SIGCONT");
|
|
4036
4054
|
this.timeoutTimer = setTimeout(() => {
|
|
4037
4055
|
log.warn(this.tag, `Timeout reached (${this.config.review.maxTimeout}ms), cancelling`);
|
|
4056
|
+
this.timedOut = true;
|
|
4038
4057
|
this.cancel();
|
|
4039
4058
|
}, this.config.review.maxTimeout);
|
|
4040
4059
|
if (this.cardId) {
|
|
@@ -4070,7 +4089,11 @@ class ReviewWorker {
|
|
|
4070
4089
|
if (this.cardId) {
|
|
4071
4090
|
try {
|
|
4072
4091
|
await this.client.endAgentSession(this.cardId, {
|
|
4073
|
-
status: "paused",
|
|
4092
|
+
status: this.timedOut ? "failed" : "paused",
|
|
4093
|
+
...this.timedOut ? {
|
|
4094
|
+
failureReason: "timeout",
|
|
4095
|
+
failureSummary: `Review exceeded the ${Math.round(this.config.review.maxTimeout / 60000)} min timeout`
|
|
4096
|
+
} : {},
|
|
4074
4097
|
...buildTokenPayload(snapshotStats)
|
|
4075
4098
|
});
|
|
4076
4099
|
} catch {}
|
|
@@ -4248,9 +4271,127 @@ var init_review_worker = __esm(() => {
|
|
|
4248
4271
|
init_worktree();
|
|
4249
4272
|
});
|
|
4250
4273
|
|
|
4274
|
+
// src/sleep-guard.ts
|
|
4275
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
4276
|
+
|
|
4277
|
+
class SleepGuard {
|
|
4278
|
+
platform;
|
|
4279
|
+
child = null;
|
|
4280
|
+
holds = 0;
|
|
4281
|
+
constructor(platform = process.platform) {
|
|
4282
|
+
this.platform = platform;
|
|
4283
|
+
}
|
|
4284
|
+
get active() {
|
|
4285
|
+
return this.child !== null;
|
|
4286
|
+
}
|
|
4287
|
+
acquire() {
|
|
4288
|
+
this.holds++;
|
|
4289
|
+
if (this.holds === 1)
|
|
4290
|
+
this.start();
|
|
4291
|
+
}
|
|
4292
|
+
release() {
|
|
4293
|
+
this.holds = Math.max(0, this.holds - 1);
|
|
4294
|
+
if (this.holds === 0)
|
|
4295
|
+
this.stop();
|
|
4296
|
+
}
|
|
4297
|
+
stop() {
|
|
4298
|
+
this.holds = 0;
|
|
4299
|
+
if (this.child) {
|
|
4300
|
+
if (!this.child.killed)
|
|
4301
|
+
this.child.kill("SIGTERM");
|
|
4302
|
+
this.child = null;
|
|
4303
|
+
log.info(TAG20, "sleep assertion released");
|
|
4304
|
+
}
|
|
4305
|
+
}
|
|
4306
|
+
start() {
|
|
4307
|
+
if (this.platform !== "darwin" || this.child)
|
|
4308
|
+
return;
|
|
4309
|
+
try {
|
|
4310
|
+
const child = spawn3("caffeinate", ["-i", "-w", String(process.pid)], {
|
|
4311
|
+
stdio: "ignore"
|
|
4312
|
+
});
|
|
4313
|
+
child.on("error", (err) => {
|
|
4314
|
+
log.warn(TAG20, `caffeinate unavailable: ${err.message}`);
|
|
4315
|
+
if (this.child === child)
|
|
4316
|
+
this.child = null;
|
|
4317
|
+
});
|
|
4318
|
+
child.on("exit", () => {
|
|
4319
|
+
if (this.child === child)
|
|
4320
|
+
this.child = null;
|
|
4321
|
+
});
|
|
4322
|
+
child.unref();
|
|
4323
|
+
this.child = child;
|
|
4324
|
+
log.info(TAG20, "sleep assertion acquired (caffeinate -i)");
|
|
4325
|
+
} catch (err) {
|
|
4326
|
+
log.warn(TAG20, `failed to spawn caffeinate: ${err instanceof Error ? err.message : err}`);
|
|
4327
|
+
}
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
var TAG20 = "sleep-guard";
|
|
4331
|
+
var init_sleep_guard = __esm(() => {
|
|
4332
|
+
init_log();
|
|
4333
|
+
});
|
|
4334
|
+
|
|
4335
|
+
// src/unblock.ts
|
|
4336
|
+
async function fetchBlocksLinks(client, cardId) {
|
|
4337
|
+
try {
|
|
4338
|
+
const { links } = await client.getCardLinks(cardId);
|
|
4339
|
+
return links.filter((l) => l.link_type === "blocks");
|
|
4340
|
+
} catch (err) {
|
|
4341
|
+
log.warn(TAG21, `link fetch failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
4342
|
+
return null;
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4345
|
+
function isBlockerResolved(blocker, columns) {
|
|
4346
|
+
if (blocker.done)
|
|
4347
|
+
return true;
|
|
4348
|
+
const blockerColumn = columns.find((c) => c.id === blocker.column_id);
|
|
4349
|
+
return blockerColumn?.mark_cards_done === true;
|
|
4350
|
+
}
|
|
4351
|
+
async function getUnresolvedBlockers(client, card, projectId) {
|
|
4352
|
+
const links = await fetchBlocksLinks(client, card.id);
|
|
4353
|
+
if (!links)
|
|
4354
|
+
return null;
|
|
4355
|
+
const incoming = links.filter((l) => l.direction === "incoming");
|
|
4356
|
+
if (incoming.length === 0)
|
|
4357
|
+
return [];
|
|
4358
|
+
const board = await client.getBoard(projectId, { summary: true });
|
|
4359
|
+
const columns = board.columns ?? [];
|
|
4360
|
+
return incoming.filter((l) => !isBlockerResolved(l.target_card, columns)).map((l) => ({
|
|
4361
|
+
cardId: l.target_card.id,
|
|
4362
|
+
shortId: l.target_card.short_id,
|
|
4363
|
+
title: l.target_card.title
|
|
4364
|
+
}));
|
|
4365
|
+
}
|
|
4366
|
+
async function promoteUnblockedSuccessors(completedCard, deps) {
|
|
4367
|
+
const links = await fetchBlocksLinks(deps.client, completedCard.id);
|
|
4368
|
+
if (!links)
|
|
4369
|
+
return;
|
|
4370
|
+
const successors = links.filter((l) => l.direction === "outgoing" && !l.target_card.done);
|
|
4371
|
+
if (successors.length === 0)
|
|
4372
|
+
return;
|
|
4373
|
+
log.info(TAG21, `#${completedCard.short_id} completed — checking ${successors.length} chained successor(s)`);
|
|
4374
|
+
for (const link of successors) {
|
|
4375
|
+
const successorId = link.target_card.id;
|
|
4376
|
+
try {
|
|
4377
|
+
const { card } = await deps.client.getCard(successorId);
|
|
4378
|
+
if (card.assignee_id !== deps.agentUserId) {
|
|
4379
|
+
log.debug(TAG21, `successor #${card.short_id} not assigned to agent — skipping promotion`);
|
|
4380
|
+
continue;
|
|
4381
|
+
}
|
|
4382
|
+
await deps.enqueue(successorId);
|
|
4383
|
+
} catch (err) {
|
|
4384
|
+
log.warn(TAG21, `promotion failed for successor ${successorId}: ${err instanceof Error ? err.message : err}`);
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
var TAG21 = "unblock";
|
|
4389
|
+
var init_unblock = __esm(() => {
|
|
4390
|
+
init_log();
|
|
4391
|
+
});
|
|
4392
|
+
|
|
4251
4393
|
// ../harmony-shared/dist/cardLinks.js
|
|
4252
4394
|
var init_cardLinks = () => {};
|
|
4253
|
-
|
|
4254
4395
|
// ../harmony-shared/dist/commentSerializer.js
|
|
4255
4396
|
function sanitizeHeaderField(value) {
|
|
4256
4397
|
return value.replace(/[\]\r\n|<>]/g, " ").trim() || "—";
|
|
@@ -4388,11 +4529,11 @@ async function buildPrompt(enriched, branchName, worktreePath, client, workspace
|
|
|
4388
4529
|
Do NOT push to main. All your work stays on \`${branchName}\`.
|
|
4389
4530
|
When finished, call harmony_end_agent_session with status="completed".`
|
|
4390
4531
|
});
|
|
4391
|
-
log.info(
|
|
4532
|
+
log.info(TAG22, `Generated prompt for #${card.short_id} — ${result.contextSummary.memoryCount} memories, ${result.tokenEstimate} tokens`);
|
|
4392
4533
|
return result.prompt + pastEpisodesSection;
|
|
4393
4534
|
} catch (err) {
|
|
4394
4535
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4395
|
-
log.warn(
|
|
4536
|
+
log.warn(TAG22, `Failed to generate prompt via API, using fallback: ${msg}`);
|
|
4396
4537
|
const commentsSection = await renderCommentsSection(client, card.id);
|
|
4397
4538
|
return buildFallbackPrompt(enriched, branchName, worktreePath) + commentsSection + pastEpisodesSection;
|
|
4398
4539
|
}
|
|
@@ -4410,7 +4551,7 @@ async function renderCommentsSection(client, cardId) {
|
|
|
4410
4551
|
|
|
4411
4552
|
${section}` : "";
|
|
4412
4553
|
} catch (err) {
|
|
4413
|
-
log.warn(
|
|
4554
|
+
log.warn(TAG22, "comment-thread fetch failed", {
|
|
4414
4555
|
event: "comment_fetch_failed",
|
|
4415
4556
|
error: err instanceof Error ? err.message : String(err)
|
|
4416
4557
|
});
|
|
@@ -4460,7 +4601,7 @@ ${description}`.trim();
|
|
|
4460
4601
|
## Similar past tasks
|
|
4461
4602
|
${bullets}`;
|
|
4462
4603
|
} catch (err) {
|
|
4463
|
-
log.warn(
|
|
4604
|
+
log.warn(TAG22, "past-episodes recall failed", {
|
|
4464
4605
|
event: "episode_recall_failed",
|
|
4465
4606
|
error: err instanceof Error ? err.message : String(err)
|
|
4466
4607
|
});
|
|
@@ -4501,7 +4642,7 @@ ${subtaskStr}
|
|
|
4501
4642
|
You are working in a git worktree at \`${worktreePath}\` on branch \`${branchName}\`.
|
|
4502
4643
|
Do NOT push to main. All your work stays on \`${branchName}\`.`;
|
|
4503
4644
|
}
|
|
4504
|
-
var
|
|
4645
|
+
var TAG22 = "prompt";
|
|
4505
4646
|
var init_prompt = __esm(() => {
|
|
4506
4647
|
init_dist();
|
|
4507
4648
|
init_log();
|
|
@@ -4515,6 +4656,7 @@ class Worker {
|
|
|
4515
4656
|
workspaceId;
|
|
4516
4657
|
projectId;
|
|
4517
4658
|
stateStore;
|
|
4659
|
+
onCardCompleted;
|
|
4518
4660
|
id;
|
|
4519
4661
|
state = "idle";
|
|
4520
4662
|
cardId = null;
|
|
@@ -4527,16 +4669,18 @@ class Worker {
|
|
|
4527
4669
|
progressTracker = null;
|
|
4528
4670
|
lastSessionStats;
|
|
4529
4671
|
aborted = false;
|
|
4672
|
+
timedOut = false;
|
|
4530
4673
|
verificationFailed = false;
|
|
4531
4674
|
sessionId = null;
|
|
4532
4675
|
runId = null;
|
|
4533
|
-
constructor(id, config, client, _userEmail, onDone, workspaceId, projectId, stateStore) {
|
|
4676
|
+
constructor(id, config, client, _userEmail, onDone, workspaceId, projectId, stateStore, onCardCompleted) {
|
|
4534
4677
|
this.config = config;
|
|
4535
4678
|
this.client = client;
|
|
4536
4679
|
this.onDone = onDone;
|
|
4537
4680
|
this.workspaceId = workspaceId;
|
|
4538
4681
|
this.projectId = projectId;
|
|
4539
4682
|
this.stateStore = stateStore;
|
|
4683
|
+
this.onCardCompleted = onCardCompleted;
|
|
4540
4684
|
this.id = id;
|
|
4541
4685
|
}
|
|
4542
4686
|
startHeartbeat() {
|
|
@@ -4571,7 +4715,7 @@ class Worker {
|
|
|
4571
4715
|
}
|
|
4572
4716
|
}
|
|
4573
4717
|
get tag() {
|
|
4574
|
-
return `${
|
|
4718
|
+
return `${TAG23}:${this.id}`;
|
|
4575
4719
|
}
|
|
4576
4720
|
get isIdle() {
|
|
4577
4721
|
return this.state === "idle";
|
|
@@ -4581,6 +4725,7 @@ class Worker {
|
|
|
4581
4725
|
}
|
|
4582
4726
|
async run(card, column, labels, subtasks) {
|
|
4583
4727
|
this.aborted = false;
|
|
4728
|
+
this.timedOut = false;
|
|
4584
4729
|
this.verificationFailed = false;
|
|
4585
4730
|
this.cardId = card.id;
|
|
4586
4731
|
this.startedAt = Date.now();
|
|
@@ -4617,7 +4762,7 @@ class Worker {
|
|
|
4617
4762
|
});
|
|
4618
4763
|
const sid = session && typeof session === "object" && "id" in session ? session.id : null;
|
|
4619
4764
|
if (!sid) {
|
|
4620
|
-
log.warn(
|
|
4765
|
+
log.warn(TAG23, "startAgentSession returned no session id");
|
|
4621
4766
|
}
|
|
4622
4767
|
this.sessionId = sid;
|
|
4623
4768
|
await this.recordPhase("preparing");
|
|
@@ -4649,6 +4794,7 @@ class Worker {
|
|
|
4649
4794
|
});
|
|
4650
4795
|
this.timeoutTimer = setTimeout(() => {
|
|
4651
4796
|
log.warn(this.tag, `Timeout reached (${this.config.maxTimeout}ms), cancelling`);
|
|
4797
|
+
this.timedOut = true;
|
|
4652
4798
|
this.cancel();
|
|
4653
4799
|
}, this.config.maxTimeout);
|
|
4654
4800
|
await this.spawnClaude(prompt, card, subtasks);
|
|
@@ -4666,7 +4812,7 @@ class Worker {
|
|
|
4666
4812
|
});
|
|
4667
4813
|
this.state = "completing";
|
|
4668
4814
|
await this.recordPhase("completing");
|
|
4669
|
-
const completed = await runCompletion(this.client, card, this.branchName, this.worktreePath, this.config, this.id, this.lastSessionStats, this.workspaceId, this.sessionId, this.stateStore);
|
|
4815
|
+
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);
|
|
4670
4816
|
this.verificationFailed = !completed;
|
|
4671
4817
|
} catch (err) {
|
|
4672
4818
|
this.state = "error";
|
|
@@ -4695,6 +4841,24 @@ class Worker {
|
|
|
4695
4841
|
await this.stateStore.endRun(this.runId, "completed");
|
|
4696
4842
|
} catch {}
|
|
4697
4843
|
await this.recordOutcome(card.id, "success");
|
|
4844
|
+
} else if (this.runId && this.timedOut) {
|
|
4845
|
+
try {
|
|
4846
|
+
await runTransition(this.client, card, {
|
|
4847
|
+
move: { columnName: this.config.pickupColumns[0] ?? "To Do" },
|
|
4848
|
+
endSession: {
|
|
4849
|
+
status: "failed",
|
|
4850
|
+
failureReason: "timeout",
|
|
4851
|
+
failureSummary: `Run exceeded the ${Math.round(this.config.maxTimeout / 60000)} min timeout`,
|
|
4852
|
+
...buildTokenPayload(this.lastSessionStats)
|
|
4853
|
+
}
|
|
4854
|
+
});
|
|
4855
|
+
} catch (tErr) {
|
|
4856
|
+
log.error(this.tag, `timeout transition failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
|
|
4857
|
+
}
|
|
4858
|
+
try {
|
|
4859
|
+
await this.stateStore.endRun(this.runId, "failed", "timeout");
|
|
4860
|
+
} catch {}
|
|
4861
|
+
await this.recordOutcome(card.id, "failure");
|
|
4698
4862
|
} else if (this.runId && this.aborted) {
|
|
4699
4863
|
try {
|
|
4700
4864
|
await this.stateStore.endRun(this.runId, "paused", "cancelled");
|
|
@@ -4765,6 +4929,7 @@ class Worker {
|
|
|
4765
4929
|
signalGroup(this.process, "SIGCONT");
|
|
4766
4930
|
this.timeoutTimer = setTimeout(() => {
|
|
4767
4931
|
log.warn(this.tag, `Timeout reached (${this.config.maxTimeout}ms), cancelling`);
|
|
4932
|
+
this.timedOut = true;
|
|
4768
4933
|
this.cancel();
|
|
4769
4934
|
}, this.config.maxTimeout);
|
|
4770
4935
|
if (this.cardId) {
|
|
@@ -4791,7 +4956,7 @@ class Worker {
|
|
|
4791
4956
|
sigtermTimeoutMs: CANCEL_SIGTERM_TIMEOUT2
|
|
4792
4957
|
});
|
|
4793
4958
|
}
|
|
4794
|
-
if (this.cardId) {
|
|
4959
|
+
if (this.cardId && !this.timedOut) {
|
|
4795
4960
|
try {
|
|
4796
4961
|
const stats = this.lastSessionStats ?? this.progressTracker?.stats;
|
|
4797
4962
|
await this.client.endAgentSession(this.cardId, {
|
|
@@ -4895,7 +5060,7 @@ class Worker {
|
|
|
4895
5060
|
clearTimeout(this.timeoutTimer);
|
|
4896
5061
|
this.timeoutTimer = null;
|
|
4897
5062
|
}
|
|
4898
|
-
if (this.worktreePath && this.state === "error") {
|
|
5063
|
+
if (this.worktreePath && (this.state === "error" || this.timedOut)) {
|
|
4899
5064
|
try {
|
|
4900
5065
|
cleanupWorktree(this.worktreePath, this.branchName ?? undefined);
|
|
4901
5066
|
} catch {
|
|
@@ -4911,7 +5076,7 @@ class Worker {
|
|
|
4911
5076
|
this.sessionId = null;
|
|
4912
5077
|
}
|
|
4913
5078
|
}
|
|
4914
|
-
var
|
|
5079
|
+
var TAG23 = "worker", CANCEL_SIGINT_TIMEOUT2 = 30000, CANCEL_SIGTERM_TIMEOUT2 = 1e4;
|
|
4915
5080
|
var init_worker = __esm(() => {
|
|
4916
5081
|
init_board_helpers();
|
|
4917
5082
|
init_completion();
|
|
@@ -4930,14 +5095,18 @@ var init_worker = __esm(() => {
|
|
|
4930
5095
|
// src/pool.ts
|
|
4931
5096
|
class Pool {
|
|
4932
5097
|
client;
|
|
5098
|
+
projectId;
|
|
4933
5099
|
stateStore;
|
|
4934
5100
|
implWorkers = [];
|
|
4935
5101
|
reviewWorkers = [];
|
|
4936
5102
|
implQueue;
|
|
4937
5103
|
reviewQueue;
|
|
4938
5104
|
budget;
|
|
5105
|
+
sleepGuard = new SleepGuard;
|
|
5106
|
+
onCardCompleted = null;
|
|
4939
5107
|
constructor(config, client, userEmail, workspaceId, projectId, stateStore) {
|
|
4940
5108
|
this.client = client;
|
|
5109
|
+
this.projectId = projectId;
|
|
4941
5110
|
this.stateStore = stateStore;
|
|
4942
5111
|
this.implQueue = new PriorityQueue(config);
|
|
4943
5112
|
this.reviewQueue = new PriorityQueue(config);
|
|
@@ -4945,33 +5114,48 @@ class Pool {
|
|
|
4945
5114
|
for (let i = 0;i < config.poolSize; i++) {
|
|
4946
5115
|
this.implWorkers.push(new Worker(i, config, client, userEmail, () => {
|
|
4947
5116
|
this.tryDispatchFor(this.implWorkers, this.implQueue, "impl");
|
|
4948
|
-
|
|
5117
|
+
this.sleepGuard.release();
|
|
5118
|
+
}, workspaceId, projectId, stateStore, async (completedCard) => {
|
|
5119
|
+
await this.onCardCompleted?.(completedCard);
|
|
5120
|
+
}));
|
|
4949
5121
|
}
|
|
4950
5122
|
if (config.review.enabled) {
|
|
4951
5123
|
for (let i = 0;i < config.review.poolSize; i++) {
|
|
4952
5124
|
const reviewWorkerId = config.poolSize + i;
|
|
4953
5125
|
this.reviewWorkers.push(new ReviewWorker(reviewWorkerId, config, client, userEmail, () => {
|
|
4954
5126
|
this.tryDispatchFor(this.reviewWorkers, this.reviewQueue, "review");
|
|
5127
|
+
this.sleepGuard.release();
|
|
4955
5128
|
}, stateStore, workspaceId, projectId));
|
|
4956
5129
|
}
|
|
4957
5130
|
}
|
|
4958
5131
|
}
|
|
4959
5132
|
async enqueue(card, column, labels, subtasks, mode = "implement") {
|
|
4960
5133
|
if (this.implQueue.has(card.id) || this.reviewQueue.has(card.id) || this.isCardActive(card.id)) {
|
|
4961
|
-
log.debug(
|
|
5134
|
+
log.debug(TAG24, `Card ${card.id} already queued or active, skipping`);
|
|
4962
5135
|
return;
|
|
4963
5136
|
}
|
|
4964
5137
|
if (mode === "implement") {
|
|
4965
5138
|
const decision = this.budget.check(card.id);
|
|
4966
5139
|
if (!decision.allow) {
|
|
4967
5140
|
if (decision.reason === "daily_budget") {
|
|
4968
|
-
log.warn(
|
|
5141
|
+
log.warn(TAG24, `#${card.short_id} skipped (daily_budget): ${decision.detail}`);
|
|
4969
5142
|
await this.emitWaiting(card.id, `Daily budget reached — waiting for reset (${decision.detail})`);
|
|
4970
5143
|
} else {
|
|
4971
|
-
log.debug(
|
|
5144
|
+
log.debug(TAG24, `#${card.short_id} gave up: ${decision.detail}`);
|
|
4972
5145
|
}
|
|
4973
5146
|
return;
|
|
4974
5147
|
}
|
|
5148
|
+
const blockers = await getUnresolvedBlockers(this.client, card, this.projectId);
|
|
5149
|
+
if (blockers === null) {
|
|
5150
|
+
log.warn(TAG24, `#${card.short_id} blocker check failed — deferring to next tick`);
|
|
5151
|
+
return;
|
|
5152
|
+
}
|
|
5153
|
+
if (blockers.length > 0) {
|
|
5154
|
+
const list = blockers.map((b) => `#${b.shortId}`).join(", ");
|
|
5155
|
+
log.info(TAG24, `#${card.short_id} blocked by ${list} — waiting`);
|
|
5156
|
+
await this.emitWaiting(card.id, `Blocked by ${list} — waiting for chain`);
|
|
5157
|
+
return;
|
|
5158
|
+
}
|
|
4975
5159
|
}
|
|
4976
5160
|
const queue = mode === "review" ? this.reviewQueue : this.implQueue;
|
|
4977
5161
|
queue.enqueue(card, column, labels, mode);
|
|
@@ -4984,7 +5168,10 @@ class Pool {
|
|
|
4984
5168
|
await this.emitWaiting(card.id, position > 0 ? `Queued (${position}/${total}) — waiting for ${mode} worker` : `Queued — waiting for ${mode} worker`);
|
|
4985
5169
|
}
|
|
4986
5170
|
}
|
|
5171
|
+
lastWaitingEmit = new Map;
|
|
4987
5172
|
async emitWaiting(cardId, currentTask) {
|
|
5173
|
+
if (this.lastWaitingEmit.get(cardId) === currentTask)
|
|
5174
|
+
return;
|
|
4988
5175
|
try {
|
|
4989
5176
|
await this.client.updateAgentProgress(cardId, {
|
|
4990
5177
|
agentIdentifier: agentIdentifier(0),
|
|
@@ -4992,23 +5179,25 @@ class Pool {
|
|
|
4992
5179
|
status: "waiting",
|
|
4993
5180
|
currentTask
|
|
4994
5181
|
});
|
|
5182
|
+
this.lastWaitingEmit.set(cardId, currentTask);
|
|
4995
5183
|
} catch (err) {
|
|
4996
|
-
log.debug(
|
|
5184
|
+
log.debug(TAG24, `waiting emit failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
4997
5185
|
}
|
|
4998
5186
|
}
|
|
4999
5187
|
async removeCard(cardId) {
|
|
5000
5188
|
await this.stateStore.resetAttempts(cardId);
|
|
5189
|
+
this.lastWaitingEmit.delete(cardId);
|
|
5001
5190
|
for (const queue of [this.implQueue, this.reviewQueue]) {
|
|
5002
5191
|
const removed = queue.remove(cardId);
|
|
5003
5192
|
if (removed) {
|
|
5004
5193
|
this.cardDataCache.delete(cardId);
|
|
5005
|
-
log.info(
|
|
5194
|
+
log.info(TAG24, `Removed #${removed.shortId} from ${removed.mode} queue`);
|
|
5006
5195
|
return;
|
|
5007
5196
|
}
|
|
5008
5197
|
}
|
|
5009
5198
|
const worker = this.implWorkers.find((w) => w.cardId === cardId) ?? this.reviewWorkers.find((w) => w.cardId === cardId);
|
|
5010
5199
|
if (worker) {
|
|
5011
|
-
log.info(
|
|
5200
|
+
log.info(TAG24, `Cancelling worker ${worker.id} for card ${cardId}`);
|
|
5012
5201
|
await worker.cancel();
|
|
5013
5202
|
}
|
|
5014
5203
|
}
|
|
@@ -5036,10 +5225,10 @@ class Pool {
|
|
|
5036
5225
|
async handleAgentCommand(cardId, command) {
|
|
5037
5226
|
const worker = this.implWorkers.find((w) => w.cardId === cardId && w.isActive) ?? this.reviewWorkers.find((w) => w.cardId === cardId && w.isActive);
|
|
5038
5227
|
if (!worker) {
|
|
5039
|
-
log.debug(
|
|
5228
|
+
log.debug(TAG24, `No active worker for card ${cardId}, ignoring ${command}`);
|
|
5040
5229
|
return;
|
|
5041
5230
|
}
|
|
5042
|
-
log.info(
|
|
5231
|
+
log.info(TAG24, `Agent command: ${command} → worker ${worker.id} (card ${cardId})`);
|
|
5043
5232
|
switch (command) {
|
|
5044
5233
|
case "pause":
|
|
5045
5234
|
await worker.pause();
|
|
@@ -5085,19 +5274,20 @@ class Pool {
|
|
|
5085
5274
|
};
|
|
5086
5275
|
}
|
|
5087
5276
|
async shutdown() {
|
|
5088
|
-
log.info(
|
|
5277
|
+
log.info(TAG24, "Shutting down pool...");
|
|
5089
5278
|
const active = [
|
|
5090
5279
|
...this.implWorkers.filter((w) => w.isActive),
|
|
5091
5280
|
...this.reviewWorkers.filter((w) => w.isActive)
|
|
5092
5281
|
];
|
|
5093
5282
|
await Promise.all(active.map((w) => w.cancel()));
|
|
5094
|
-
|
|
5283
|
+
this.sleepGuard.stop();
|
|
5284
|
+
log.info(TAG24, "Pool shutdown complete");
|
|
5095
5285
|
}
|
|
5096
5286
|
cardDataCache = new Map;
|
|
5097
5287
|
tryDispatchFor(workers, queue, label) {
|
|
5098
5288
|
const idle = workers.find((w) => w.isIdle);
|
|
5099
5289
|
if (!idle) {
|
|
5100
|
-
log.debug(
|
|
5290
|
+
log.debug(TAG24, `No idle ${label} workers (queue: ${queue.length})`);
|
|
5101
5291
|
return false;
|
|
5102
5292
|
}
|
|
5103
5293
|
const next = queue.dequeue();
|
|
@@ -5105,21 +5295,25 @@ class Pool {
|
|
|
5105
5295
|
return false;
|
|
5106
5296
|
const data = this.cardDataCache.get(next.cardId);
|
|
5107
5297
|
if (!data) {
|
|
5108
|
-
log.warn(
|
|
5298
|
+
log.warn(TAG24, `No cached data for card ${next.cardId}, skipping`);
|
|
5109
5299
|
return false;
|
|
5110
5300
|
}
|
|
5111
5301
|
this.cardDataCache.delete(next.cardId);
|
|
5112
|
-
|
|
5302
|
+
this.lastWaitingEmit.delete(next.cardId);
|
|
5303
|
+
log.info(TAG24, `Dispatching #${next.shortId} to ${label} worker ${idle.id}`);
|
|
5304
|
+
this.sleepGuard.acquire();
|
|
5113
5305
|
idle.run(data.card, data.column, data.labels, data.subtasks);
|
|
5114
5306
|
return true;
|
|
5115
5307
|
}
|
|
5116
5308
|
}
|
|
5117
|
-
var
|
|
5309
|
+
var TAG24 = "pool";
|
|
5118
5310
|
var init_pool = __esm(() => {
|
|
5119
5311
|
init_log();
|
|
5120
5312
|
init_queue();
|
|
5121
5313
|
init_review_worker();
|
|
5314
|
+
init_sleep_guard();
|
|
5122
5315
|
init_types();
|
|
5316
|
+
init_unblock();
|
|
5123
5317
|
init_worker();
|
|
5124
5318
|
});
|
|
5125
5319
|
|
|
@@ -5139,7 +5333,7 @@ async function fetchCardSafely(client, cardId) {
|
|
|
5139
5333
|
const { card } = await client.getCard(cardId);
|
|
5140
5334
|
return card;
|
|
5141
5335
|
} catch (err) {
|
|
5142
|
-
log.warn(
|
|
5336
|
+
log.warn(TAG25, `cannot fetch card ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
5143
5337
|
return null;
|
|
5144
5338
|
}
|
|
5145
5339
|
}
|
|
@@ -5149,7 +5343,7 @@ async function recoverOrphans(store, client, config) {
|
|
|
5149
5343
|
return [];
|
|
5150
5344
|
}
|
|
5151
5345
|
const outcomes = [];
|
|
5152
|
-
log.info(
|
|
5346
|
+
log.info(TAG25, `recovering ${active.length} orphan run(s) from prior daemon`);
|
|
5153
5347
|
for (const run of active) {
|
|
5154
5348
|
const outcome = {
|
|
5155
5349
|
runId: run.runId,
|
|
@@ -5161,11 +5355,11 @@ async function recoverOrphans(store, client, config) {
|
|
|
5161
5355
|
};
|
|
5162
5356
|
outcomes.push(outcome);
|
|
5163
5357
|
if (isProcessAlive(run.daemonPid, process.pid)) {
|
|
5164
|
-
log.warn(
|
|
5358
|
+
log.warn(TAG25, `run ${run.runId} claims live daemon pid ${run.daemonPid} — skipping`);
|
|
5165
5359
|
outcome.actions.push("skipped: daemon pid still alive");
|
|
5166
5360
|
continue;
|
|
5167
5361
|
}
|
|
5168
|
-
log.info(
|
|
5362
|
+
log.info(TAG25, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
|
|
5169
5363
|
await recoverRun(run, store, client, config, outcome);
|
|
5170
5364
|
}
|
|
5171
5365
|
return outcomes;
|
|
@@ -5183,7 +5377,7 @@ async function recoverRun(run, store, client, config, outcome) {
|
|
|
5183
5377
|
} catch (err) {
|
|
5184
5378
|
const msg = err instanceof Error ? err.message : String(err);
|
|
5185
5379
|
outcome.errors.push(`endAgentSession: ${msg}`);
|
|
5186
|
-
log.warn(
|
|
5380
|
+
log.warn(TAG25, `endAgentSession failed for ${run.cardId}: ${msg}`);
|
|
5187
5381
|
}
|
|
5188
5382
|
const card = await fetchCardSafely(client, run.cardId);
|
|
5189
5383
|
if (card) {
|
|
@@ -5224,9 +5418,9 @@ async function recoverRun(run, store, client, config, outcome) {
|
|
|
5224
5418
|
const msg = err instanceof Error ? err.message : String(err);
|
|
5225
5419
|
outcome.errors.push(`endRun: ${msg}`);
|
|
5226
5420
|
}
|
|
5227
|
-
log.info(
|
|
5421
|
+
log.info(TAG25, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
|
|
5228
5422
|
}
|
|
5229
|
-
var
|
|
5423
|
+
var TAG25 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
|
|
5230
5424
|
var init_recovery = __esm(() => {
|
|
5231
5425
|
init_board_helpers();
|
|
5232
5426
|
init_log();
|
|
@@ -5274,7 +5468,7 @@ class Reconciler {
|
|
|
5274
5468
|
clearInterval(this.timer);
|
|
5275
5469
|
this.timer = null;
|
|
5276
5470
|
}
|
|
5277
|
-
log.info(
|
|
5471
|
+
log.info(TAG26, "Heartbeat stopped");
|
|
5278
5472
|
}
|
|
5279
5473
|
async recoverStaleRuns() {
|
|
5280
5474
|
if (!this.stateStore || !this.agentConfig)
|
|
@@ -5291,7 +5485,7 @@ class Reconciler {
|
|
|
5291
5485
|
if (!daemonDead && !(heartbeatStale && ourZombie))
|
|
5292
5486
|
continue;
|
|
5293
5487
|
const reason = daemonDead ? `foreign daemon ${run.daemonPid} is dead` : `our worker lost card ${run.cardId} with ${Math.round((now - run.lastHeartbeatAt) / 1000)}s stale heartbeat`;
|
|
5294
|
-
log.warn(
|
|
5488
|
+
log.warn(TAG26, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
|
|
5295
5489
|
await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
|
|
5296
5490
|
runId: run.runId,
|
|
5297
5491
|
cardId: run.cardId,
|
|
@@ -5327,18 +5521,18 @@ class Reconciler {
|
|
|
5327
5521
|
const subtasks = card.subtasks ?? [];
|
|
5328
5522
|
const mode = reviewColumnIds.has(card.column_id) ? "review" : "implement";
|
|
5329
5523
|
if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
|
|
5330
|
-
log.debug(
|
|
5524
|
+
log.debug(TAG26, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
|
|
5331
5525
|
continue;
|
|
5332
5526
|
}
|
|
5333
5527
|
if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
|
|
5334
|
-
log.debug(
|
|
5528
|
+
log.debug(TAG26, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
|
|
5335
5529
|
continue;
|
|
5336
5530
|
}
|
|
5337
5531
|
if (mode === "review" && !extractBranchFromDescription(card.description)) {
|
|
5338
|
-
log.debug(
|
|
5532
|
+
log.debug(TAG26, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
|
|
5339
5533
|
continue;
|
|
5340
5534
|
}
|
|
5341
|
-
log.info(
|
|
5535
|
+
log.info(TAG26, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
|
|
5342
5536
|
await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
5343
5537
|
}
|
|
5344
5538
|
}
|
|
@@ -5347,17 +5541,17 @@ class Reconciler {
|
|
|
5347
5541
|
}
|
|
5348
5542
|
for (const knownId of knownCardIds) {
|
|
5349
5543
|
if (!allAgentCardIds.has(knownId)) {
|
|
5350
|
-
log.info(
|
|
5544
|
+
log.info(TAG26, `Missed unassign: ${knownId} — removing`);
|
|
5351
5545
|
await this.pool.removeCard(knownId);
|
|
5352
5546
|
}
|
|
5353
5547
|
}
|
|
5354
|
-
log.debug(
|
|
5548
|
+
log.debug(TAG26, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
|
|
5355
5549
|
} catch (err) {
|
|
5356
|
-
log.error(
|
|
5550
|
+
log.error(TAG26, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
|
|
5357
5551
|
}
|
|
5358
5552
|
}
|
|
5359
5553
|
}
|
|
5360
|
-
var
|
|
5554
|
+
var TAG26 = "reconcile";
|
|
5361
5555
|
var init_reconcile = __esm(() => {
|
|
5362
5556
|
init_board_helpers();
|
|
5363
5557
|
init_log();
|
|
@@ -5391,7 +5585,7 @@ function prettyBanner(config, version) {
|
|
|
5391
5585
|
checks.push({ kind: "ok", message });
|
|
5392
5586
|
},
|
|
5393
5587
|
warn(message) {
|
|
5394
|
-
log.warn(
|
|
5588
|
+
log.warn(TAG27, message);
|
|
5395
5589
|
checks.push({ kind: "warn", message: message.split(`
|
|
5396
5590
|
`, 1)[0] });
|
|
5397
5591
|
},
|
|
@@ -5415,22 +5609,22 @@ function prettyBanner(config, version) {
|
|
|
5415
5609
|
};
|
|
5416
5610
|
}
|
|
5417
5611
|
function jsonBanner(config, version) {
|
|
5418
|
-
log.info(
|
|
5419
|
-
log.info(
|
|
5612
|
+
log.info(TAG27, `Harmony Agent Daemon v${version} starting...`);
|
|
5613
|
+
log.info(TAG27, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
|
|
5420
5614
|
if (config.agent.review.enabled) {
|
|
5421
|
-
log.info(
|
|
5615
|
+
log.info(TAG27, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
|
|
5422
5616
|
}
|
|
5423
5617
|
let failed = false;
|
|
5424
5618
|
return {
|
|
5425
5619
|
setProjectName(_name) {},
|
|
5426
5620
|
setGitProvider(provider) {
|
|
5427
|
-
log.info(
|
|
5621
|
+
log.info(TAG27, `Git provider: ${provider}`);
|
|
5428
5622
|
},
|
|
5429
5623
|
check(message) {
|
|
5430
|
-
log.info(
|
|
5624
|
+
log.info(TAG27, message);
|
|
5431
5625
|
},
|
|
5432
5626
|
warn(message) {
|
|
5433
|
-
log.warn(
|
|
5627
|
+
log.warn(TAG27, message);
|
|
5434
5628
|
},
|
|
5435
5629
|
fail() {
|
|
5436
5630
|
failed = true;
|
|
@@ -5438,7 +5632,7 @@ function jsonBanner(config, version) {
|
|
|
5438
5632
|
async ready(message) {
|
|
5439
5633
|
if (failed)
|
|
5440
5634
|
return;
|
|
5441
|
-
log.info(
|
|
5635
|
+
log.info(TAG27, message);
|
|
5442
5636
|
}
|
|
5443
5637
|
};
|
|
5444
5638
|
}
|
|
@@ -5505,7 +5699,7 @@ function cyan(s) {
|
|
|
5505
5699
|
function yellow(s) {
|
|
5506
5700
|
return `${ANSI.yellow}${s}${ANSI.reset}`;
|
|
5507
5701
|
}
|
|
5508
|
-
var
|
|
5702
|
+
var TAG27 = "daemon", RULE_WIDTH = 70, ANSI;
|
|
5509
5703
|
var init_startup_banner = __esm(() => {
|
|
5510
5704
|
init_log();
|
|
5511
5705
|
ANSI = {
|
|
@@ -5652,18 +5846,18 @@ class Watcher {
|
|
|
5652
5846
|
}
|
|
5653
5847
|
async start() {
|
|
5654
5848
|
if (!isPretty()) {
|
|
5655
|
-
log.info(
|
|
5849
|
+
log.info(TAG28, "Connecting to Supabase realtime (broadcast)...");
|
|
5656
5850
|
}
|
|
5657
5851
|
this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
|
|
5658
5852
|
const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
|
|
5659
5853
|
const channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
|
|
5660
|
-
log.debug(
|
|
5854
|
+
log.debug(TAG28, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
|
|
5661
5855
|
this.onCardBroadcast({
|
|
5662
5856
|
event: "card_update",
|
|
5663
5857
|
payload: msg.payload ?? {}
|
|
5664
5858
|
});
|
|
5665
5859
|
}).on("broadcast", { event: "card_created" }, (msg) => {
|
|
5666
|
-
log.debug(
|
|
5860
|
+
log.debug(TAG28, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
|
|
5667
5861
|
this.onCardBroadcast({
|
|
5668
5862
|
event: "card_created",
|
|
5669
5863
|
payload: msg.payload ?? {}
|
|
@@ -5673,29 +5867,29 @@ class Watcher {
|
|
|
5673
5867
|
const cardId = payload.card_id;
|
|
5674
5868
|
const command = payload.command;
|
|
5675
5869
|
if (cardId && command) {
|
|
5676
|
-
log.info(
|
|
5870
|
+
log.info(TAG28, `Broadcast: agent_command ${command} for ${cardId}`);
|
|
5677
5871
|
this.onAgentCommand?.({ cardId, command });
|
|
5678
5872
|
}
|
|
5679
5873
|
}).subscribe((status) => {
|
|
5680
5874
|
if (status === "SUBSCRIBED") {
|
|
5681
5875
|
this.connected = true;
|
|
5682
5876
|
if (!isPretty() || !this.suppressStartupLogs) {
|
|
5683
|
-
log.info(
|
|
5877
|
+
log.info(TAG28, "Broadcast subscription active");
|
|
5684
5878
|
}
|
|
5685
5879
|
this.maybeResolveReady();
|
|
5686
5880
|
} else if (status === "CHANNEL_ERROR") {
|
|
5687
5881
|
this.connected = false;
|
|
5688
|
-
log.error(
|
|
5882
|
+
log.error(TAG28, "Broadcast channel error — will rely on reconciliation");
|
|
5689
5883
|
} else if (status === "TIMED_OUT") {
|
|
5690
5884
|
this.connected = false;
|
|
5691
|
-
log.warn(
|
|
5885
|
+
log.warn(TAG28, "Broadcast subscription timed out — retrying...");
|
|
5692
5886
|
} else if (status === "CLOSED") {
|
|
5693
5887
|
this.connected = false;
|
|
5694
5888
|
}
|
|
5695
5889
|
});
|
|
5696
5890
|
this.channel = channel;
|
|
5697
5891
|
presenceChannel.on("presence", { event: "sync" }, () => {
|
|
5698
|
-
log.debug(
|
|
5892
|
+
log.debug(TAG28, "Presence sync");
|
|
5699
5893
|
}).subscribe(async (status) => {
|
|
5700
5894
|
if (status === "SUBSCRIBED") {
|
|
5701
5895
|
await presenceChannel.track({
|
|
@@ -5707,7 +5901,7 @@ class Watcher {
|
|
|
5707
5901
|
agentName: this.identity.agentName
|
|
5708
5902
|
});
|
|
5709
5903
|
if (!isPretty() || !this.suppressStartupLogs) {
|
|
5710
|
-
log.info(
|
|
5904
|
+
log.info(TAG28, "Presence tracked on board-presence channel");
|
|
5711
5905
|
}
|
|
5712
5906
|
this.presenceTracked = true;
|
|
5713
5907
|
this.maybeResolveReady();
|
|
@@ -5729,10 +5923,10 @@ class Watcher {
|
|
|
5729
5923
|
this.supabase = null;
|
|
5730
5924
|
}
|
|
5731
5925
|
this.connected = false;
|
|
5732
|
-
log.info(
|
|
5926
|
+
log.info(TAG28, "Broadcast subscription stopped");
|
|
5733
5927
|
}
|
|
5734
5928
|
}
|
|
5735
|
-
var
|
|
5929
|
+
var TAG28 = "watcher";
|
|
5736
5930
|
var init_watcher = __esm(() => {
|
|
5737
5931
|
init_log();
|
|
5738
5932
|
});
|
|
@@ -5807,10 +6001,10 @@ function runWorktreeGc(basePath, store, opts = {}) {
|
|
|
5807
6001
|
});
|
|
5808
6002
|
} catch {}
|
|
5809
6003
|
if (result.removed.length > 0) {
|
|
5810
|
-
log.info(
|
|
6004
|
+
log.info(TAG29, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
|
|
5811
6005
|
}
|
|
5812
6006
|
if (result.errors.length > 0) {
|
|
5813
|
-
log.warn(
|
|
6007
|
+
log.warn(TAG29, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
|
|
5814
6008
|
}
|
|
5815
6009
|
return result;
|
|
5816
6010
|
}
|
|
@@ -5887,10 +6081,10 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
5887
6081
|
}
|
|
5888
6082
|
}
|
|
5889
6083
|
if (result.removed.length > 0) {
|
|
5890
|
-
log.info(
|
|
6084
|
+
log.info(TAG29, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
|
|
5891
6085
|
}
|
|
5892
6086
|
if (result.errors.length > 0) {
|
|
5893
|
-
log.warn(
|
|
6087
|
+
log.warn(TAG29, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
|
|
5894
6088
|
}
|
|
5895
6089
|
return result;
|
|
5896
6090
|
}
|
|
@@ -5921,13 +6115,13 @@ class WorktreeGc {
|
|
|
5921
6115
|
try {
|
|
5922
6116
|
runWorktreeGc(this.basePath, this.store);
|
|
5923
6117
|
} catch (err) {
|
|
5924
|
-
log.warn(
|
|
6118
|
+
log.warn(TAG29, `GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
5925
6119
|
}
|
|
5926
6120
|
if (this.remoteOpts) {
|
|
5927
6121
|
try {
|
|
5928
6122
|
pruneFailedRemoteBranches(this.remoteOpts);
|
|
5929
6123
|
} catch (err) {
|
|
5930
|
-
log.warn(
|
|
6124
|
+
log.warn(TAG29, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
5931
6125
|
}
|
|
5932
6126
|
}
|
|
5933
6127
|
}
|
|
@@ -5941,7 +6135,7 @@ function getRepoRoot2() {
|
|
|
5941
6135
|
return null;
|
|
5942
6136
|
}
|
|
5943
6137
|
}
|
|
5944
|
-
var
|
|
6138
|
+
var TAG29 = "worktree-gc";
|
|
5945
6139
|
var init_worktree_gc = __esm(() => {
|
|
5946
6140
|
init_log();
|
|
5947
6141
|
init_worktree();
|
|
@@ -6023,7 +6217,7 @@ async function main() {
|
|
|
6023
6217
|
} catch (err) {
|
|
6024
6218
|
if (err instanceof ConfigValidationError) {
|
|
6025
6219
|
banner.fail();
|
|
6026
|
-
log.error(
|
|
6220
|
+
log.error(TAG30, err.message);
|
|
6027
6221
|
process.exit(1);
|
|
6028
6222
|
}
|
|
6029
6223
|
throw err;
|
|
@@ -6041,12 +6235,21 @@ async function main() {
|
|
|
6041
6235
|
const realtimeCreds = await fetchRealtimeCredentials(client);
|
|
6042
6236
|
banner.check("Realtime credentials");
|
|
6043
6237
|
const pool = new Pool(config.agent, client, config.userEmail, config.workspaceId, config.projectId, stateStore);
|
|
6238
|
+
const promoteSuccessors = async (completedCard) => {
|
|
6239
|
+
await promoteUnblockedSuccessors(completedCard, {
|
|
6240
|
+
client,
|
|
6241
|
+
agentUserId,
|
|
6242
|
+
enqueue: (cardId) => tryEnqueueCard(cardId, client, pool, config)
|
|
6243
|
+
});
|
|
6244
|
+
};
|
|
6245
|
+
pool.onCardCompleted = promoteSuccessors;
|
|
6044
6246
|
const reviewColumns = config.agent.review.enabled ? config.agent.review.pickupColumns : [];
|
|
6045
6247
|
const approvedLabel = config.agent.review.enabled ? config.agent.review.approvedLabel : "";
|
|
6046
6248
|
const reconciler = new Reconciler(client, pool, config.projectId, agentUserId, config.agent.pickupColumns, reviewColumns, approvedLabel, config.agent.timing.reconcileIntervalMs, stateStore, config.agent);
|
|
6047
6249
|
let mergeMonitor = null;
|
|
6048
6250
|
if (config.agent.review.enabled && config.agent.review.mergeMonitor) {
|
|
6049
6251
|
mergeMonitor = new MergeMonitor(client, config.projectId, config.agent);
|
|
6252
|
+
mergeMonitor.onCardCompleted = promoteSuccessors;
|
|
6050
6253
|
}
|
|
6051
6254
|
const worktreeGc = new WorktreeGc(config.agent.worktree.basePath, stateStore, config.agent.timing.worktreeGcIntervalMs, config.agent.worktree.failedAttemptRetentionDays > 0 ? {
|
|
6052
6255
|
prefix: config.agent.worktree.failedBranchPrefix,
|
|
@@ -6115,25 +6318,25 @@ async function main() {
|
|
|
6115
6318
|
if (shuttingDown)
|
|
6116
6319
|
return;
|
|
6117
6320
|
shuttingDown = true;
|
|
6118
|
-
log.info(
|
|
6321
|
+
log.info(TAG30, `Received ${signal}, shutting down gracefully...`);
|
|
6119
6322
|
reconciler.stop();
|
|
6120
6323
|
mergeMonitor?.stop();
|
|
6121
6324
|
worktreeGc.stop();
|
|
6122
6325
|
await httpServer?.stop();
|
|
6123
6326
|
await watcher.stop();
|
|
6124
6327
|
await pool.shutdown();
|
|
6125
|
-
log.info(
|
|
6328
|
+
log.info(TAG30, "Daemon stopped.");
|
|
6126
6329
|
process.exit(exitCode);
|
|
6127
6330
|
};
|
|
6128
6331
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
6129
6332
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
6130
6333
|
process.on("uncaughtException", (err) => {
|
|
6131
|
-
log.error(
|
|
6334
|
+
log.error(TAG30, `Uncaught exception: ${err.message}`);
|
|
6132
6335
|
exitCode = 1;
|
|
6133
6336
|
shutdown("uncaughtException");
|
|
6134
6337
|
});
|
|
6135
6338
|
process.on("unhandledRejection", (reason) => {
|
|
6136
|
-
log.error(
|
|
6339
|
+
log.error(TAG30, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
|
|
6137
6340
|
exitCode = 1;
|
|
6138
6341
|
shutdown("unhandledRejection");
|
|
6139
6342
|
});
|
|
@@ -6180,14 +6383,14 @@ async function handleBroadcast(event, client, pool, config, agentUserId) {
|
|
|
6180
6383
|
if (assigneeId === undefined)
|
|
6181
6384
|
return;
|
|
6182
6385
|
if (assigneeId === agentUserId) {
|
|
6183
|
-
log.info(
|
|
6386
|
+
log.info(TAG30, `Broadcast: card ${cardId} assigned to agent`);
|
|
6184
6387
|
try {
|
|
6185
6388
|
await tryEnqueueCard(cardId, client, pool, config);
|
|
6186
6389
|
} catch (err) {
|
|
6187
|
-
log.error(
|
|
6390
|
+
log.error(TAG30, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
|
|
6188
6391
|
}
|
|
6189
6392
|
} else if (pool.isCardKnown(cardId)) {
|
|
6190
|
-
log.info(
|
|
6393
|
+
log.info(TAG30, `Broadcast: card ${cardId} unassigned from agent`);
|
|
6191
6394
|
await pool.removeCard(cardId);
|
|
6192
6395
|
}
|
|
6193
6396
|
}
|
|
@@ -6197,13 +6400,13 @@ async function tryEnqueueCard(cardId, client, pool, config) {
|
|
|
6197
6400
|
const columns = board.columns;
|
|
6198
6401
|
const column = columns.find((c) => c.id === card.column_id);
|
|
6199
6402
|
if (!column) {
|
|
6200
|
-
log.warn(
|
|
6403
|
+
log.warn(TAG30, `Column not found for card ${cardId}`);
|
|
6201
6404
|
return;
|
|
6202
6405
|
}
|
|
6203
6406
|
const isPickupColumn = config.agent.pickupColumns.some((name) => name.toLowerCase() === column.name.toLowerCase());
|
|
6204
6407
|
const isReviewColumn = config.agent.review.enabled && config.agent.review.pickupColumns.some((name) => name.toLowerCase() === column.name.toLowerCase());
|
|
6205
6408
|
if (!isPickupColumn && !isReviewColumn) {
|
|
6206
|
-
log.info(
|
|
6409
|
+
log.info(TAG30, `Card #${card.short_id} is in "${column.name}", not a pickup/review column — skipping`);
|
|
6207
6410
|
return;
|
|
6208
6411
|
}
|
|
6209
6412
|
const mode = isReviewColumn ? "review" : "implement";
|
|
@@ -6211,16 +6414,16 @@ async function tryEnqueueCard(cardId, client, pool, config) {
|
|
|
6211
6414
|
const cardLabels = resolveCardLabels(card, labelMap);
|
|
6212
6415
|
const subtasks = card.subtasks ?? [];
|
|
6213
6416
|
if (mode === "review" && config.agent.review.approvedLabel && hasLabel(cardLabels, config.agent.review.approvedLabel)) {
|
|
6214
|
-
log.debug(
|
|
6417
|
+
log.debug(TAG30, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
|
|
6215
6418
|
return;
|
|
6216
6419
|
}
|
|
6217
6420
|
if (mode === "review" && !extractBranchFromDescription(card.description)) {
|
|
6218
|
-
log.info(
|
|
6421
|
+
log.info(TAG30, `Card #${card.short_id} has no branch reference — skipping auto-review`);
|
|
6219
6422
|
return;
|
|
6220
6423
|
}
|
|
6221
6424
|
await pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
6222
6425
|
}
|
|
6223
|
-
var
|
|
6426
|
+
var TAG30 = "daemon", PKG_VERSION;
|
|
6224
6427
|
var init_src = __esm(() => {
|
|
6225
6428
|
init_board_helpers();
|
|
6226
6429
|
init_config();
|
|
@@ -6237,6 +6440,7 @@ var init_src = __esm(() => {
|
|
|
6237
6440
|
init_state_store();
|
|
6238
6441
|
init_stream_parser_selftest();
|
|
6239
6442
|
init_types();
|
|
6443
|
+
init_unblock();
|
|
6240
6444
|
init_watcher();
|
|
6241
6445
|
init_worktree_gc();
|
|
6242
6446
|
({ version: PKG_VERSION } = createRequire2(import.meta.url)("../package.json"));
|