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