@gethmy/agent 1.8.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +287 -118
- package/dist/index.js +287 -118
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -336,6 +336,9 @@ function loadDaemonConfig() {
|
|
|
336
336
|
throw new Error("No user email configured. Run `npx @gethmy/mcp setup` first.");
|
|
337
337
|
}
|
|
338
338
|
let agentOverrides = {};
|
|
339
|
+
let agentName = "Harmony Agent";
|
|
340
|
+
let agentIdentifier2 = "harmony-daemon";
|
|
341
|
+
let agentColor = "#57b8a5";
|
|
339
342
|
try {
|
|
340
343
|
const configPath = join(homedir(), ".harmony-mcp", "config.json");
|
|
341
344
|
const raw = readFileSync(configPath, "utf-8");
|
|
@@ -343,6 +346,12 @@ function loadDaemonConfig() {
|
|
|
343
346
|
if (parsed.agent) {
|
|
344
347
|
agentOverrides = parsed.agent;
|
|
345
348
|
}
|
|
349
|
+
if (typeof parsed.agentName === "string" && parsed.agentName.trim())
|
|
350
|
+
agentName = parsed.agentName.trim();
|
|
351
|
+
if (typeof parsed.agentIdentifier === "string" && parsed.agentIdentifier.trim())
|
|
352
|
+
agentIdentifier2 = parsed.agentIdentifier.trim();
|
|
353
|
+
if (typeof parsed.agentColor === "string" && parsed.agentColor.trim())
|
|
354
|
+
agentColor = parsed.agentColor.trim();
|
|
346
355
|
} catch {}
|
|
347
356
|
const agent = {
|
|
348
357
|
...DEFAULT_AGENT_CONFIG,
|
|
@@ -380,7 +389,17 @@ function loadDaemonConfig() {
|
|
|
380
389
|
...agentOverrides.timing ?? {}
|
|
381
390
|
}
|
|
382
391
|
};
|
|
383
|
-
return {
|
|
392
|
+
return {
|
|
393
|
+
apiKey,
|
|
394
|
+
apiUrl,
|
|
395
|
+
workspaceId,
|
|
396
|
+
projectId,
|
|
397
|
+
userEmail,
|
|
398
|
+
agentName,
|
|
399
|
+
agentIdentifier: agentIdentifier2,
|
|
400
|
+
agentColor,
|
|
401
|
+
agent
|
|
402
|
+
};
|
|
384
403
|
}
|
|
385
404
|
async function fetchRealtimeCredentials(client) {
|
|
386
405
|
const result = await client.request("GET", "/config/realtime");
|
|
@@ -3738,6 +3757,7 @@ import { createHash } from "node:crypto";
|
|
|
3738
3757
|
class ReviewWorker {
|
|
3739
3758
|
config;
|
|
3740
3759
|
client;
|
|
3760
|
+
agentId;
|
|
3741
3761
|
onDone;
|
|
3742
3762
|
stateStore;
|
|
3743
3763
|
workspaceId;
|
|
@@ -3754,12 +3774,14 @@ class ReviewWorker {
|
|
|
3754
3774
|
progressTracker = null;
|
|
3755
3775
|
lastSessionStats = null;
|
|
3756
3776
|
aborted = false;
|
|
3777
|
+
timedOut = false;
|
|
3757
3778
|
runId = null;
|
|
3758
3779
|
lastRunLogPath = null;
|
|
3759
3780
|
sessionId = null;
|
|
3760
|
-
constructor(id, config, client,
|
|
3781
|
+
constructor(id, config, client, agentId, onDone, stateStore, workspaceId, _projectId) {
|
|
3761
3782
|
this.config = config;
|
|
3762
3783
|
this.client = client;
|
|
3784
|
+
this.agentId = agentId;
|
|
3763
3785
|
this.onDone = onDone;
|
|
3764
3786
|
this.stateStore = stateStore;
|
|
3765
3787
|
this.workspaceId = workspaceId;
|
|
@@ -3809,6 +3831,7 @@ class ReviewWorker {
|
|
|
3809
3831
|
}
|
|
3810
3832
|
async run(card, column, labels, subtasks) {
|
|
3811
3833
|
this.aborted = false;
|
|
3834
|
+
this.timedOut = false;
|
|
3812
3835
|
this.cardId = card.id;
|
|
3813
3836
|
this.startedAt = Date.now();
|
|
3814
3837
|
this.runId = newRunId();
|
|
@@ -3857,6 +3880,7 @@ class ReviewWorker {
|
|
|
3857
3880
|
const { session: reviewSession } = await this.client.startAgentSession(card.id, {
|
|
3858
3881
|
agentIdentifier: agentIdentifier(this.id),
|
|
3859
3882
|
agentName: `${AGENT_NAME} (Review)`,
|
|
3883
|
+
agentId: this.agentId,
|
|
3860
3884
|
status: "working",
|
|
3861
3885
|
currentTask: localMode ? "Reviewing local changes" : "Setting up review worktree",
|
|
3862
3886
|
progressPercent: 5
|
|
@@ -3946,6 +3970,7 @@ class ReviewWorker {
|
|
|
3946
3970
|
});
|
|
3947
3971
|
this.timeoutTimer = setTimeout(() => {
|
|
3948
3972
|
log.warn(this.tag, `Review timeout reached (${this.config.review.maxTimeout}ms), cancelling`);
|
|
3973
|
+
this.timedOut = true;
|
|
3949
3974
|
this.cancel();
|
|
3950
3975
|
}, this.config.review.maxTimeout);
|
|
3951
3976
|
this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks);
|
|
@@ -3956,6 +3981,10 @@ class ReviewWorker {
|
|
|
3956
3981
|
const sessionStats = this.lastSessionStats;
|
|
3957
3982
|
if (this.aborted)
|
|
3958
3983
|
return;
|
|
3984
|
+
if (this.timeoutTimer) {
|
|
3985
|
+
clearTimeout(this.timeoutTimer);
|
|
3986
|
+
this.timeoutTimer = null;
|
|
3987
|
+
}
|
|
3959
3988
|
this.state = "completing";
|
|
3960
3989
|
await this.recordPhase("completing");
|
|
3961
3990
|
log.info(this.tag, `Claude review finished for #${card.short_id}`);
|
|
@@ -4012,7 +4041,7 @@ class ReviewWorker {
|
|
|
4012
4041
|
try {
|
|
4013
4042
|
const run = this.stateStore.getRun(this.runId);
|
|
4014
4043
|
if (run && run.endedAt === null) {
|
|
4015
|
-
const status = this.state === "error" || this.aborted ? "paused" : "completed";
|
|
4044
|
+
const status = this.timedOut ? "failed" : this.state === "error" || this.aborted ? "paused" : "completed";
|
|
4016
4045
|
await this.stateStore.endRun(this.runId, status);
|
|
4017
4046
|
}
|
|
4018
4047
|
} catch {}
|
|
@@ -4050,6 +4079,7 @@ class ReviewWorker {
|
|
|
4050
4079
|
signalGroup(this.process, "SIGCONT");
|
|
4051
4080
|
this.timeoutTimer = setTimeout(() => {
|
|
4052
4081
|
log.warn(this.tag, `Timeout reached (${this.config.review.maxTimeout}ms), cancelling`);
|
|
4082
|
+
this.timedOut = true;
|
|
4053
4083
|
this.cancel();
|
|
4054
4084
|
}, this.config.review.maxTimeout);
|
|
4055
4085
|
if (this.cardId) {
|
|
@@ -4085,7 +4115,11 @@ class ReviewWorker {
|
|
|
4085
4115
|
if (this.cardId) {
|
|
4086
4116
|
try {
|
|
4087
4117
|
await this.client.endAgentSession(this.cardId, {
|
|
4088
|
-
status: "paused",
|
|
4118
|
+
status: this.timedOut ? "failed" : "paused",
|
|
4119
|
+
...this.timedOut ? {
|
|
4120
|
+
failureReason: "timeout",
|
|
4121
|
+
failureSummary: `Review exceeded the ${Math.round(this.config.review.maxTimeout / 60000)} min timeout`
|
|
4122
|
+
} : {},
|
|
4089
4123
|
...buildTokenPayload(snapshotStats)
|
|
4090
4124
|
});
|
|
4091
4125
|
} catch {}
|
|
@@ -4263,13 +4297,81 @@ var init_review_worker = __esm(() => {
|
|
|
4263
4297
|
init_worktree();
|
|
4264
4298
|
});
|
|
4265
4299
|
|
|
4300
|
+
// src/sleep-guard.ts
|
|
4301
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
4302
|
+
|
|
4303
|
+
class SleepGuard {
|
|
4304
|
+
platform;
|
|
4305
|
+
child = null;
|
|
4306
|
+
holds = 0;
|
|
4307
|
+
constructor(platform = process.platform) {
|
|
4308
|
+
this.platform = platform;
|
|
4309
|
+
}
|
|
4310
|
+
get active() {
|
|
4311
|
+
return this.child !== null;
|
|
4312
|
+
}
|
|
4313
|
+
acquire() {
|
|
4314
|
+
this.holds++;
|
|
4315
|
+
if (this.holds === 1)
|
|
4316
|
+
this.start();
|
|
4317
|
+
}
|
|
4318
|
+
release() {
|
|
4319
|
+
this.holds = Math.max(0, this.holds - 1);
|
|
4320
|
+
if (this.holds === 0)
|
|
4321
|
+
this.stop();
|
|
4322
|
+
}
|
|
4323
|
+
stop() {
|
|
4324
|
+
this.holds = 0;
|
|
4325
|
+
if (this.child) {
|
|
4326
|
+
if (!this.child.killed)
|
|
4327
|
+
this.child.kill("SIGTERM");
|
|
4328
|
+
this.child = null;
|
|
4329
|
+
log.info(TAG20, "sleep assertion released");
|
|
4330
|
+
}
|
|
4331
|
+
}
|
|
4332
|
+
start() {
|
|
4333
|
+
if (this.platform !== "darwin" || this.child)
|
|
4334
|
+
return;
|
|
4335
|
+
try {
|
|
4336
|
+
const child = spawn3("caffeinate", ["-i", "-w", String(process.pid)], {
|
|
4337
|
+
stdio: "ignore"
|
|
4338
|
+
});
|
|
4339
|
+
let spawned = false;
|
|
4340
|
+
child.on("spawn", () => {
|
|
4341
|
+
spawned = true;
|
|
4342
|
+
});
|
|
4343
|
+
child.on("error", (err) => {
|
|
4344
|
+
log.warn(TAG20, `caffeinate unavailable: ${err.message}`);
|
|
4345
|
+
if (this.child === child)
|
|
4346
|
+
this.child = null;
|
|
4347
|
+
});
|
|
4348
|
+
child.on("exit", () => {
|
|
4349
|
+
if (this.child !== child)
|
|
4350
|
+
return;
|
|
4351
|
+
this.child = null;
|
|
4352
|
+
if (spawned && this.holds > 0)
|
|
4353
|
+
this.start();
|
|
4354
|
+
});
|
|
4355
|
+
child.unref();
|
|
4356
|
+
this.child = child;
|
|
4357
|
+
log.info(TAG20, "sleep assertion acquired (caffeinate -i)");
|
|
4358
|
+
} catch (err) {
|
|
4359
|
+
log.warn(TAG20, `failed to spawn caffeinate: ${err instanceof Error ? err.message : err}`);
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
var TAG20 = "sleep-guard";
|
|
4364
|
+
var init_sleep_guard = __esm(() => {
|
|
4365
|
+
init_log();
|
|
4366
|
+
});
|
|
4367
|
+
|
|
4266
4368
|
// src/unblock.ts
|
|
4267
4369
|
async function fetchBlocksLinks(client, cardId) {
|
|
4268
4370
|
try {
|
|
4269
4371
|
const { links } = await client.getCardLinks(cardId);
|
|
4270
4372
|
return links.filter((l) => l.link_type === "blocks");
|
|
4271
4373
|
} catch (err) {
|
|
4272
|
-
log.warn(
|
|
4374
|
+
log.warn(TAG21, `link fetch failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
4273
4375
|
return null;
|
|
4274
4376
|
}
|
|
4275
4377
|
}
|
|
@@ -4301,22 +4403,22 @@ async function promoteUnblockedSuccessors(completedCard, deps) {
|
|
|
4301
4403
|
const successors = links.filter((l) => l.direction === "outgoing" && !l.target_card.done);
|
|
4302
4404
|
if (successors.length === 0)
|
|
4303
4405
|
return;
|
|
4304
|
-
log.info(
|
|
4406
|
+
log.info(TAG21, `#${completedCard.short_id} completed — checking ${successors.length} chained successor(s)`);
|
|
4305
4407
|
for (const link of successors) {
|
|
4306
4408
|
const successorId = link.target_card.id;
|
|
4307
4409
|
try {
|
|
4308
4410
|
const { card } = await deps.client.getCard(successorId);
|
|
4309
|
-
if (card.
|
|
4310
|
-
log.debug(
|
|
4411
|
+
if (card.assigned_agent_id !== deps.agentId) {
|
|
4412
|
+
log.debug(TAG21, `successor #${card.short_id} not assigned to agent — skipping promotion`);
|
|
4311
4413
|
continue;
|
|
4312
4414
|
}
|
|
4313
4415
|
await deps.enqueue(successorId);
|
|
4314
4416
|
} catch (err) {
|
|
4315
|
-
log.warn(
|
|
4417
|
+
log.warn(TAG21, `promotion failed for successor ${successorId}: ${err instanceof Error ? err.message : err}`);
|
|
4316
4418
|
}
|
|
4317
4419
|
}
|
|
4318
4420
|
}
|
|
4319
|
-
var
|
|
4421
|
+
var TAG21 = "unblock";
|
|
4320
4422
|
var init_unblock = __esm(() => {
|
|
4321
4423
|
init_log();
|
|
4322
4424
|
});
|
|
@@ -4460,11 +4562,11 @@ async function buildPrompt(enriched, branchName, worktreePath, client, workspace
|
|
|
4460
4562
|
Do NOT push to main. All your work stays on \`${branchName}\`.
|
|
4461
4563
|
When finished, call harmony_end_agent_session with status="completed".`
|
|
4462
4564
|
});
|
|
4463
|
-
log.info(
|
|
4565
|
+
log.info(TAG22, `Generated prompt for #${card.short_id} — ${result.contextSummary.memoryCount} memories, ${result.tokenEstimate} tokens`);
|
|
4464
4566
|
return result.prompt + pastEpisodesSection;
|
|
4465
4567
|
} catch (err) {
|
|
4466
4568
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4467
|
-
log.warn(
|
|
4569
|
+
log.warn(TAG22, `Failed to generate prompt via API, using fallback: ${msg}`);
|
|
4468
4570
|
const commentsSection = await renderCommentsSection(client, card.id);
|
|
4469
4571
|
return buildFallbackPrompt(enriched, branchName, worktreePath) + commentsSection + pastEpisodesSection;
|
|
4470
4572
|
}
|
|
@@ -4482,7 +4584,7 @@ async function renderCommentsSection(client, cardId) {
|
|
|
4482
4584
|
|
|
4483
4585
|
${section}` : "";
|
|
4484
4586
|
} catch (err) {
|
|
4485
|
-
log.warn(
|
|
4587
|
+
log.warn(TAG22, "comment-thread fetch failed", {
|
|
4486
4588
|
event: "comment_fetch_failed",
|
|
4487
4589
|
error: err instanceof Error ? err.message : String(err)
|
|
4488
4590
|
});
|
|
@@ -4532,7 +4634,7 @@ ${description}`.trim();
|
|
|
4532
4634
|
## Similar past tasks
|
|
4533
4635
|
${bullets}`;
|
|
4534
4636
|
} catch (err) {
|
|
4535
|
-
log.warn(
|
|
4637
|
+
log.warn(TAG22, "past-episodes recall failed", {
|
|
4536
4638
|
event: "episode_recall_failed",
|
|
4537
4639
|
error: err instanceof Error ? err.message : String(err)
|
|
4538
4640
|
});
|
|
@@ -4573,7 +4675,7 @@ ${subtaskStr}
|
|
|
4573
4675
|
You are working in a git worktree at \`${worktreePath}\` on branch \`${branchName}\`.
|
|
4574
4676
|
Do NOT push to main. All your work stays on \`${branchName}\`.`;
|
|
4575
4677
|
}
|
|
4576
|
-
var
|
|
4678
|
+
var TAG22 = "prompt";
|
|
4577
4679
|
var init_prompt = __esm(() => {
|
|
4578
4680
|
init_dist();
|
|
4579
4681
|
init_log();
|
|
@@ -4583,6 +4685,7 @@ var init_prompt = __esm(() => {
|
|
|
4583
4685
|
class Worker {
|
|
4584
4686
|
config;
|
|
4585
4687
|
client;
|
|
4688
|
+
agentId;
|
|
4586
4689
|
onDone;
|
|
4587
4690
|
workspaceId;
|
|
4588
4691
|
projectId;
|
|
@@ -4600,12 +4703,14 @@ class Worker {
|
|
|
4600
4703
|
progressTracker = null;
|
|
4601
4704
|
lastSessionStats;
|
|
4602
4705
|
aborted = false;
|
|
4706
|
+
timedOut = false;
|
|
4603
4707
|
verificationFailed = false;
|
|
4604
4708
|
sessionId = null;
|
|
4605
4709
|
runId = null;
|
|
4606
|
-
constructor(id, config, client,
|
|
4710
|
+
constructor(id, config, client, agentId, onDone, workspaceId, projectId, stateStore, onCardCompleted) {
|
|
4607
4711
|
this.config = config;
|
|
4608
4712
|
this.client = client;
|
|
4713
|
+
this.agentId = agentId;
|
|
4609
4714
|
this.onDone = onDone;
|
|
4610
4715
|
this.workspaceId = workspaceId;
|
|
4611
4716
|
this.projectId = projectId;
|
|
@@ -4645,7 +4750,7 @@ class Worker {
|
|
|
4645
4750
|
}
|
|
4646
4751
|
}
|
|
4647
4752
|
get tag() {
|
|
4648
|
-
return `${
|
|
4753
|
+
return `${TAG23}:${this.id}`;
|
|
4649
4754
|
}
|
|
4650
4755
|
get isIdle() {
|
|
4651
4756
|
return this.state === "idle";
|
|
@@ -4655,6 +4760,7 @@ class Worker {
|
|
|
4655
4760
|
}
|
|
4656
4761
|
async run(card, column, labels, subtasks) {
|
|
4657
4762
|
this.aborted = false;
|
|
4763
|
+
this.timedOut = false;
|
|
4658
4764
|
this.verificationFailed = false;
|
|
4659
4765
|
this.cardId = card.id;
|
|
4660
4766
|
this.startedAt = Date.now();
|
|
@@ -4685,13 +4791,14 @@ class Worker {
|
|
|
4685
4791
|
const { session } = await this.client.startAgentSession(card.id, {
|
|
4686
4792
|
agentIdentifier: agentIdentifier(this.id),
|
|
4687
4793
|
agentName: AGENT_NAME,
|
|
4794
|
+
agentId: this.agentId,
|
|
4688
4795
|
status: "working",
|
|
4689
4796
|
currentTask: "Setting up worktree",
|
|
4690
4797
|
progressPercent: 5
|
|
4691
4798
|
});
|
|
4692
4799
|
const sid = session && typeof session === "object" && "id" in session ? session.id : null;
|
|
4693
4800
|
if (!sid) {
|
|
4694
|
-
log.warn(
|
|
4801
|
+
log.warn(TAG23, "startAgentSession returned no session id");
|
|
4695
4802
|
}
|
|
4696
4803
|
this.sessionId = sid;
|
|
4697
4804
|
await this.recordPhase("preparing");
|
|
@@ -4723,11 +4830,16 @@ class Worker {
|
|
|
4723
4830
|
});
|
|
4724
4831
|
this.timeoutTimer = setTimeout(() => {
|
|
4725
4832
|
log.warn(this.tag, `Timeout reached (${this.config.maxTimeout}ms), cancelling`);
|
|
4833
|
+
this.timedOut = true;
|
|
4726
4834
|
this.cancel();
|
|
4727
4835
|
}, this.config.maxTimeout);
|
|
4728
4836
|
await this.spawnClaude(prompt, card, subtasks);
|
|
4729
4837
|
if (this.aborted)
|
|
4730
4838
|
return;
|
|
4839
|
+
if (this.timeoutTimer) {
|
|
4840
|
+
clearTimeout(this.timeoutTimer);
|
|
4841
|
+
this.timeoutTimer = null;
|
|
4842
|
+
}
|
|
4731
4843
|
this.state = "verifying";
|
|
4732
4844
|
await this.recordPhase("verifying");
|
|
4733
4845
|
log.info(this.tag, `Claude finished for #${card.short_id}, running verification & completion`);
|
|
@@ -4769,6 +4881,32 @@ class Worker {
|
|
|
4769
4881
|
await this.stateStore.endRun(this.runId, "completed");
|
|
4770
4882
|
} catch {}
|
|
4771
4883
|
await this.recordOutcome(card.id, "success");
|
|
4884
|
+
} else if (this.runId && this.timedOut) {
|
|
4885
|
+
if (this.worktreePath) {
|
|
4886
|
+
try {
|
|
4887
|
+
cleanupWorktree(this.worktreePath, this.branchName ?? undefined);
|
|
4888
|
+
} catch {
|
|
4889
|
+
log.warn(this.tag, "Failed to cleanup worktree before requeue");
|
|
4890
|
+
}
|
|
4891
|
+
this.worktreePath = null;
|
|
4892
|
+
}
|
|
4893
|
+
try {
|
|
4894
|
+
await runTransition(this.client, card, {
|
|
4895
|
+
move: { columnName: this.config.pickupColumns[0] ?? "To Do" },
|
|
4896
|
+
endSession: {
|
|
4897
|
+
status: "failed",
|
|
4898
|
+
failureReason: "timeout",
|
|
4899
|
+
failureSummary: `Run exceeded the ${Math.round(this.config.maxTimeout / 60000)} min timeout`,
|
|
4900
|
+
...buildTokenPayload(this.lastSessionStats)
|
|
4901
|
+
}
|
|
4902
|
+
});
|
|
4903
|
+
} catch (tErr) {
|
|
4904
|
+
log.error(this.tag, `timeout transition failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
|
|
4905
|
+
}
|
|
4906
|
+
try {
|
|
4907
|
+
await this.stateStore.endRun(this.runId, "failed", "timeout");
|
|
4908
|
+
} catch {}
|
|
4909
|
+
await this.recordOutcome(card.id, "failure");
|
|
4772
4910
|
} else if (this.runId && this.aborted) {
|
|
4773
4911
|
try {
|
|
4774
4912
|
await this.stateStore.endRun(this.runId, "paused", "cancelled");
|
|
@@ -4839,6 +4977,7 @@ class Worker {
|
|
|
4839
4977
|
signalGroup(this.process, "SIGCONT");
|
|
4840
4978
|
this.timeoutTimer = setTimeout(() => {
|
|
4841
4979
|
log.warn(this.tag, `Timeout reached (${this.config.maxTimeout}ms), cancelling`);
|
|
4980
|
+
this.timedOut = true;
|
|
4842
4981
|
this.cancel();
|
|
4843
4982
|
}, this.config.maxTimeout);
|
|
4844
4983
|
if (this.cardId) {
|
|
@@ -4865,7 +5004,7 @@ class Worker {
|
|
|
4865
5004
|
sigtermTimeoutMs: CANCEL_SIGTERM_TIMEOUT2
|
|
4866
5005
|
});
|
|
4867
5006
|
}
|
|
4868
|
-
if (this.cardId) {
|
|
5007
|
+
if (this.cardId && !this.timedOut) {
|
|
4869
5008
|
try {
|
|
4870
5009
|
const stats = this.lastSessionStats ?? this.progressTracker?.stats;
|
|
4871
5010
|
await this.client.endAgentSession(this.cardId, {
|
|
@@ -4969,7 +5108,7 @@ class Worker {
|
|
|
4969
5108
|
clearTimeout(this.timeoutTimer);
|
|
4970
5109
|
this.timeoutTimer = null;
|
|
4971
5110
|
}
|
|
4972
|
-
if (this.worktreePath && this.state === "error") {
|
|
5111
|
+
if (this.worktreePath && (this.state === "error" || this.timedOut)) {
|
|
4973
5112
|
try {
|
|
4974
5113
|
cleanupWorktree(this.worktreePath, this.branchName ?? undefined);
|
|
4975
5114
|
} catch {
|
|
@@ -4985,7 +5124,7 @@ class Worker {
|
|
|
4985
5124
|
this.sessionId = null;
|
|
4986
5125
|
}
|
|
4987
5126
|
}
|
|
4988
|
-
var
|
|
5127
|
+
var TAG23 = "worker", CANCEL_SIGINT_TIMEOUT2 = 30000, CANCEL_SIGTERM_TIMEOUT2 = 1e4;
|
|
4989
5128
|
var init_worker = __esm(() => {
|
|
4990
5129
|
init_board_helpers();
|
|
4991
5130
|
init_completion();
|
|
@@ -5006,22 +5145,30 @@ class Pool {
|
|
|
5006
5145
|
client;
|
|
5007
5146
|
projectId;
|
|
5008
5147
|
stateStore;
|
|
5148
|
+
agentId;
|
|
5009
5149
|
implWorkers = [];
|
|
5010
5150
|
reviewWorkers = [];
|
|
5011
5151
|
implQueue;
|
|
5012
5152
|
reviewQueue;
|
|
5013
5153
|
budget;
|
|
5154
|
+
sleepGuard = new SleepGuard;
|
|
5155
|
+
shuttingDown = false;
|
|
5014
5156
|
onCardCompleted = null;
|
|
5015
|
-
constructor(config, client,
|
|
5157
|
+
constructor(config, client, _userEmail, workspaceId, projectId, stateStore, agentId) {
|
|
5016
5158
|
this.client = client;
|
|
5017
5159
|
this.projectId = projectId;
|
|
5018
5160
|
this.stateStore = stateStore;
|
|
5161
|
+
this.agentId = agentId;
|
|
5019
5162
|
this.implQueue = new PriorityQueue(config);
|
|
5020
5163
|
this.reviewQueue = new PriorityQueue(config);
|
|
5021
5164
|
this.budget = new BudgetGuard(config.budget, this.stateStore);
|
|
5022
5165
|
for (let i = 0;i < config.poolSize; i++) {
|
|
5023
|
-
this.implWorkers.push(new Worker(i, config, client,
|
|
5024
|
-
|
|
5166
|
+
this.implWorkers.push(new Worker(i, config, client, this.agentId, () => {
|
|
5167
|
+
try {
|
|
5168
|
+
this.tryDispatchFor(this.implWorkers, this.implQueue, "impl");
|
|
5169
|
+
} finally {
|
|
5170
|
+
this.sleepGuard.release();
|
|
5171
|
+
}
|
|
5025
5172
|
}, workspaceId, projectId, stateStore, async (completedCard) => {
|
|
5026
5173
|
await this.onCardCompleted?.(completedCard);
|
|
5027
5174
|
}));
|
|
@@ -5029,36 +5176,40 @@ class Pool {
|
|
|
5029
5176
|
if (config.review.enabled) {
|
|
5030
5177
|
for (let i = 0;i < config.review.poolSize; i++) {
|
|
5031
5178
|
const reviewWorkerId = config.poolSize + i;
|
|
5032
|
-
this.reviewWorkers.push(new ReviewWorker(reviewWorkerId, config, client,
|
|
5033
|
-
|
|
5179
|
+
this.reviewWorkers.push(new ReviewWorker(reviewWorkerId, config, client, this.agentId, () => {
|
|
5180
|
+
try {
|
|
5181
|
+
this.tryDispatchFor(this.reviewWorkers, this.reviewQueue, "review");
|
|
5182
|
+
} finally {
|
|
5183
|
+
this.sleepGuard.release();
|
|
5184
|
+
}
|
|
5034
5185
|
}, stateStore, workspaceId, projectId));
|
|
5035
5186
|
}
|
|
5036
5187
|
}
|
|
5037
5188
|
}
|
|
5038
5189
|
async enqueue(card, column, labels, subtasks, mode = "implement") {
|
|
5039
5190
|
if (this.implQueue.has(card.id) || this.reviewQueue.has(card.id) || this.isCardActive(card.id)) {
|
|
5040
|
-
log.debug(
|
|
5191
|
+
log.debug(TAG24, `Card ${card.id} already queued or active, skipping`);
|
|
5041
5192
|
return;
|
|
5042
5193
|
}
|
|
5043
5194
|
if (mode === "implement") {
|
|
5044
5195
|
const decision = this.budget.check(card.id);
|
|
5045
5196
|
if (!decision.allow) {
|
|
5046
5197
|
if (decision.reason === "daily_budget") {
|
|
5047
|
-
log.warn(
|
|
5198
|
+
log.warn(TAG24, `#${card.short_id} skipped (daily_budget): ${decision.detail}`);
|
|
5048
5199
|
await this.emitWaiting(card.id, `Daily budget reached — waiting for reset (${decision.detail})`);
|
|
5049
5200
|
} else {
|
|
5050
|
-
log.debug(
|
|
5201
|
+
log.debug(TAG24, `#${card.short_id} gave up: ${decision.detail}`);
|
|
5051
5202
|
}
|
|
5052
5203
|
return;
|
|
5053
5204
|
}
|
|
5054
5205
|
const blockers = await getUnresolvedBlockers(this.client, card, this.projectId);
|
|
5055
5206
|
if (blockers === null) {
|
|
5056
|
-
log.warn(
|
|
5207
|
+
log.warn(TAG24, `#${card.short_id} blocker check failed — deferring to next tick`);
|
|
5057
5208
|
return;
|
|
5058
5209
|
}
|
|
5059
5210
|
if (blockers.length > 0) {
|
|
5060
5211
|
const list = blockers.map((b) => `#${b.shortId}`).join(", ");
|
|
5061
|
-
log.info(
|
|
5212
|
+
log.info(TAG24, `#${card.short_id} blocked by ${list} — waiting`);
|
|
5062
5213
|
await this.emitWaiting(card.id, `Blocked by ${list} — waiting for chain`);
|
|
5063
5214
|
return;
|
|
5064
5215
|
}
|
|
@@ -5087,7 +5238,7 @@ class Pool {
|
|
|
5087
5238
|
});
|
|
5088
5239
|
this.lastWaitingEmit.set(cardId, currentTask);
|
|
5089
5240
|
} catch (err) {
|
|
5090
|
-
log.debug(
|
|
5241
|
+
log.debug(TAG24, `waiting emit failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
5091
5242
|
}
|
|
5092
5243
|
}
|
|
5093
5244
|
async removeCard(cardId) {
|
|
@@ -5097,13 +5248,13 @@ class Pool {
|
|
|
5097
5248
|
const removed = queue.remove(cardId);
|
|
5098
5249
|
if (removed) {
|
|
5099
5250
|
this.cardDataCache.delete(cardId);
|
|
5100
|
-
log.info(
|
|
5251
|
+
log.info(TAG24, `Removed #${removed.shortId} from ${removed.mode} queue`);
|
|
5101
5252
|
return;
|
|
5102
5253
|
}
|
|
5103
5254
|
}
|
|
5104
5255
|
const worker = this.implWorkers.find((w) => w.cardId === cardId) ?? this.reviewWorkers.find((w) => w.cardId === cardId);
|
|
5105
5256
|
if (worker) {
|
|
5106
|
-
log.info(
|
|
5257
|
+
log.info(TAG24, `Cancelling worker ${worker.id} for card ${cardId}`);
|
|
5107
5258
|
await worker.cancel();
|
|
5108
5259
|
}
|
|
5109
5260
|
}
|
|
@@ -5131,10 +5282,10 @@ class Pool {
|
|
|
5131
5282
|
async handleAgentCommand(cardId, command) {
|
|
5132
5283
|
const worker = this.implWorkers.find((w) => w.cardId === cardId && w.isActive) ?? this.reviewWorkers.find((w) => w.cardId === cardId && w.isActive);
|
|
5133
5284
|
if (!worker) {
|
|
5134
|
-
log.debug(
|
|
5285
|
+
log.debug(TAG24, `No active worker for card ${cardId}, ignoring ${command}`);
|
|
5135
5286
|
return;
|
|
5136
5287
|
}
|
|
5137
|
-
log.info(
|
|
5288
|
+
log.info(TAG24, `Agent command: ${command} → worker ${worker.id} (card ${cardId})`);
|
|
5138
5289
|
switch (command) {
|
|
5139
5290
|
case "pause":
|
|
5140
5291
|
await worker.pause();
|
|
@@ -5180,19 +5331,23 @@ class Pool {
|
|
|
5180
5331
|
};
|
|
5181
5332
|
}
|
|
5182
5333
|
async shutdown() {
|
|
5183
|
-
log.info(
|
|
5334
|
+
log.info(TAG24, "Shutting down pool...");
|
|
5335
|
+
this.shuttingDown = true;
|
|
5184
5336
|
const active = [
|
|
5185
5337
|
...this.implWorkers.filter((w) => w.isActive),
|
|
5186
5338
|
...this.reviewWorkers.filter((w) => w.isActive)
|
|
5187
5339
|
];
|
|
5188
5340
|
await Promise.all(active.map((w) => w.cancel()));
|
|
5189
|
-
|
|
5341
|
+
this.sleepGuard.stop();
|
|
5342
|
+
log.info(TAG24, "Pool shutdown complete");
|
|
5190
5343
|
}
|
|
5191
5344
|
cardDataCache = new Map;
|
|
5192
5345
|
tryDispatchFor(workers, queue, label) {
|
|
5346
|
+
if (this.shuttingDown)
|
|
5347
|
+
return false;
|
|
5193
5348
|
const idle = workers.find((w) => w.isIdle);
|
|
5194
5349
|
if (!idle) {
|
|
5195
|
-
log.debug(
|
|
5350
|
+
log.debug(TAG24, `No idle ${label} workers (queue: ${queue.length})`);
|
|
5196
5351
|
return false;
|
|
5197
5352
|
}
|
|
5198
5353
|
const next = queue.dequeue();
|
|
@@ -5200,21 +5355,23 @@ class Pool {
|
|
|
5200
5355
|
return false;
|
|
5201
5356
|
const data = this.cardDataCache.get(next.cardId);
|
|
5202
5357
|
if (!data) {
|
|
5203
|
-
log.warn(
|
|
5358
|
+
log.warn(TAG24, `No cached data for card ${next.cardId}, skipping`);
|
|
5204
5359
|
return false;
|
|
5205
5360
|
}
|
|
5206
5361
|
this.cardDataCache.delete(next.cardId);
|
|
5207
5362
|
this.lastWaitingEmit.delete(next.cardId);
|
|
5208
|
-
log.info(
|
|
5363
|
+
log.info(TAG24, `Dispatching #${next.shortId} to ${label} worker ${idle.id}`);
|
|
5364
|
+
this.sleepGuard.acquire();
|
|
5209
5365
|
idle.run(data.card, data.column, data.labels, data.subtasks);
|
|
5210
5366
|
return true;
|
|
5211
5367
|
}
|
|
5212
5368
|
}
|
|
5213
|
-
var
|
|
5369
|
+
var TAG24 = "pool";
|
|
5214
5370
|
var init_pool = __esm(() => {
|
|
5215
5371
|
init_log();
|
|
5216
5372
|
init_queue();
|
|
5217
5373
|
init_review_worker();
|
|
5374
|
+
init_sleep_guard();
|
|
5218
5375
|
init_types();
|
|
5219
5376
|
init_unblock();
|
|
5220
5377
|
init_worker();
|
|
@@ -5236,7 +5393,7 @@ async function fetchCardSafely(client, cardId) {
|
|
|
5236
5393
|
const { card } = await client.getCard(cardId);
|
|
5237
5394
|
return card;
|
|
5238
5395
|
} catch (err) {
|
|
5239
|
-
log.warn(
|
|
5396
|
+
log.warn(TAG25, `cannot fetch card ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
5240
5397
|
return null;
|
|
5241
5398
|
}
|
|
5242
5399
|
}
|
|
@@ -5246,7 +5403,7 @@ async function recoverOrphans(store, client, config) {
|
|
|
5246
5403
|
return [];
|
|
5247
5404
|
}
|
|
5248
5405
|
const outcomes = [];
|
|
5249
|
-
log.info(
|
|
5406
|
+
log.info(TAG25, `recovering ${active.length} orphan run(s) from prior daemon`);
|
|
5250
5407
|
for (const run of active) {
|
|
5251
5408
|
const outcome = {
|
|
5252
5409
|
runId: run.runId,
|
|
@@ -5258,11 +5415,11 @@ async function recoverOrphans(store, client, config) {
|
|
|
5258
5415
|
};
|
|
5259
5416
|
outcomes.push(outcome);
|
|
5260
5417
|
if (isProcessAlive(run.daemonPid, process.pid)) {
|
|
5261
|
-
log.warn(
|
|
5418
|
+
log.warn(TAG25, `run ${run.runId} claims live daemon pid ${run.daemonPid} — skipping`);
|
|
5262
5419
|
outcome.actions.push("skipped: daemon pid still alive");
|
|
5263
5420
|
continue;
|
|
5264
5421
|
}
|
|
5265
|
-
log.info(
|
|
5422
|
+
log.info(TAG25, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
|
|
5266
5423
|
await recoverRun(run, store, client, config, outcome);
|
|
5267
5424
|
}
|
|
5268
5425
|
return outcomes;
|
|
@@ -5280,7 +5437,7 @@ async function recoverRun(run, store, client, config, outcome) {
|
|
|
5280
5437
|
} catch (err) {
|
|
5281
5438
|
const msg = err instanceof Error ? err.message : String(err);
|
|
5282
5439
|
outcome.errors.push(`endAgentSession: ${msg}`);
|
|
5283
|
-
log.warn(
|
|
5440
|
+
log.warn(TAG25, `endAgentSession failed for ${run.cardId}: ${msg}`);
|
|
5284
5441
|
}
|
|
5285
5442
|
const card = await fetchCardSafely(client, run.cardId);
|
|
5286
5443
|
if (card) {
|
|
@@ -5321,9 +5478,9 @@ async function recoverRun(run, store, client, config, outcome) {
|
|
|
5321
5478
|
const msg = err instanceof Error ? err.message : String(err);
|
|
5322
5479
|
outcome.errors.push(`endRun: ${msg}`);
|
|
5323
5480
|
}
|
|
5324
|
-
log.info(
|
|
5481
|
+
log.info(TAG25, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
|
|
5325
5482
|
}
|
|
5326
|
-
var
|
|
5483
|
+
var TAG25 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
|
|
5327
5484
|
var init_recovery = __esm(() => {
|
|
5328
5485
|
init_board_helpers();
|
|
5329
5486
|
init_log();
|
|
@@ -5335,7 +5492,7 @@ class Reconciler {
|
|
|
5335
5492
|
client;
|
|
5336
5493
|
pool;
|
|
5337
5494
|
projectId;
|
|
5338
|
-
|
|
5495
|
+
agentId;
|
|
5339
5496
|
pickupColumns;
|
|
5340
5497
|
reviewColumns;
|
|
5341
5498
|
approvedLabel;
|
|
@@ -5350,11 +5507,11 @@ class Reconciler {
|
|
|
5350
5507
|
get isRunning() {
|
|
5351
5508
|
return this.timer !== null;
|
|
5352
5509
|
}
|
|
5353
|
-
constructor(client, pool, projectId,
|
|
5510
|
+
constructor(client, pool, projectId, agentId, pickupColumns, reviewColumns, approvedLabel, intervalMs = 60000, stateStore, agentConfig) {
|
|
5354
5511
|
this.client = client;
|
|
5355
5512
|
this.pool = pool;
|
|
5356
5513
|
this.projectId = projectId;
|
|
5357
|
-
this.
|
|
5514
|
+
this.agentId = agentId;
|
|
5358
5515
|
this.pickupColumns = pickupColumns;
|
|
5359
5516
|
this.reviewColumns = reviewColumns;
|
|
5360
5517
|
this.approvedLabel = approvedLabel;
|
|
@@ -5371,7 +5528,7 @@ class Reconciler {
|
|
|
5371
5528
|
clearInterval(this.timer);
|
|
5372
5529
|
this.timer = null;
|
|
5373
5530
|
}
|
|
5374
|
-
log.info(
|
|
5531
|
+
log.info(TAG26, "Heartbeat stopped");
|
|
5375
5532
|
}
|
|
5376
5533
|
async recoverStaleRuns() {
|
|
5377
5534
|
if (!this.stateStore || !this.agentConfig)
|
|
@@ -5388,7 +5545,7 @@ class Reconciler {
|
|
|
5388
5545
|
if (!daemonDead && !(heartbeatStale && ourZombie))
|
|
5389
5546
|
continue;
|
|
5390
5547
|
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`;
|
|
5391
|
-
log.warn(
|
|
5548
|
+
log.warn(TAG26, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
|
|
5392
5549
|
await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
|
|
5393
5550
|
runId: run.runId,
|
|
5394
5551
|
cardId: run.cardId,
|
|
@@ -5412,9 +5569,9 @@ class Reconciler {
|
|
|
5412
5569
|
}
|
|
5413
5570
|
const pickupColumnIds = new Set(columns.filter((c) => this.pickupColumns.some((name) => name.toLowerCase() === c.name.toLowerCase())).map((c) => c.id));
|
|
5414
5571
|
const reviewColumnIds = new Set(columns.filter((c) => this.reviewColumns.some((name) => name.toLowerCase() === c.name.toLowerCase())).map((c) => c.id));
|
|
5415
|
-
const assignedCards = cards.filter((c) => c.
|
|
5572
|
+
const assignedCards = cards.filter((c) => c.assigned_agent_id === this.agentId && !c.archived_at && (pickupColumnIds.has(c.column_id) || reviewColumnIds.has(c.column_id)));
|
|
5416
5573
|
const knownCardIds = this.pool.knownCardIds();
|
|
5417
|
-
const allAgentCardIds = new Set(cards.filter((c) => c.
|
|
5574
|
+
const allAgentCardIds = new Set(cards.filter((c) => c.assigned_agent_id === this.agentId && !c.archived_at).map((c) => c.id));
|
|
5418
5575
|
for (const card of assignedCards) {
|
|
5419
5576
|
if (!knownCardIds.has(card.id)) {
|
|
5420
5577
|
const column = columnMap.get(card.column_id);
|
|
@@ -5424,18 +5581,18 @@ class Reconciler {
|
|
|
5424
5581
|
const subtasks = card.subtasks ?? [];
|
|
5425
5582
|
const mode = reviewColumnIds.has(card.column_id) ? "review" : "implement";
|
|
5426
5583
|
if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
|
|
5427
|
-
log.debug(
|
|
5584
|
+
log.debug(TAG26, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
|
|
5428
5585
|
continue;
|
|
5429
5586
|
}
|
|
5430
5587
|
if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
|
|
5431
|
-
log.debug(
|
|
5588
|
+
log.debug(TAG26, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
|
|
5432
5589
|
continue;
|
|
5433
5590
|
}
|
|
5434
5591
|
if (mode === "review" && !extractBranchFromDescription(card.description)) {
|
|
5435
|
-
log.debug(
|
|
5592
|
+
log.debug(TAG26, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
|
|
5436
5593
|
continue;
|
|
5437
5594
|
}
|
|
5438
|
-
log.info(
|
|
5595
|
+
log.info(TAG26, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
|
|
5439
5596
|
await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
5440
5597
|
}
|
|
5441
5598
|
}
|
|
@@ -5444,17 +5601,17 @@ class Reconciler {
|
|
|
5444
5601
|
}
|
|
5445
5602
|
for (const knownId of knownCardIds) {
|
|
5446
5603
|
if (!allAgentCardIds.has(knownId)) {
|
|
5447
|
-
log.info(
|
|
5604
|
+
log.info(TAG26, `Missed unassign: ${knownId} — removing`);
|
|
5448
5605
|
await this.pool.removeCard(knownId);
|
|
5449
5606
|
}
|
|
5450
5607
|
}
|
|
5451
|
-
log.debug(
|
|
5608
|
+
log.debug(TAG26, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
|
|
5452
5609
|
} catch (err) {
|
|
5453
|
-
log.error(
|
|
5610
|
+
log.error(TAG26, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
|
|
5454
5611
|
}
|
|
5455
5612
|
}
|
|
5456
5613
|
}
|
|
5457
|
-
var
|
|
5614
|
+
var TAG26 = "reconcile";
|
|
5458
5615
|
var init_reconcile = __esm(() => {
|
|
5459
5616
|
init_board_helpers();
|
|
5460
5617
|
init_log();
|
|
@@ -5488,7 +5645,7 @@ function prettyBanner(config, version) {
|
|
|
5488
5645
|
checks.push({ kind: "ok", message });
|
|
5489
5646
|
},
|
|
5490
5647
|
warn(message) {
|
|
5491
|
-
log.warn(
|
|
5648
|
+
log.warn(TAG27, message);
|
|
5492
5649
|
checks.push({ kind: "warn", message: message.split(`
|
|
5493
5650
|
`, 1)[0] });
|
|
5494
5651
|
},
|
|
@@ -5512,22 +5669,22 @@ function prettyBanner(config, version) {
|
|
|
5512
5669
|
};
|
|
5513
5670
|
}
|
|
5514
5671
|
function jsonBanner(config, version) {
|
|
5515
|
-
log.info(
|
|
5516
|
-
log.info(
|
|
5672
|
+
log.info(TAG27, `Harmony Agent Daemon v${version} starting...`);
|
|
5673
|
+
log.info(TAG27, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
|
|
5517
5674
|
if (config.agent.review.enabled) {
|
|
5518
|
-
log.info(
|
|
5675
|
+
log.info(TAG27, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
|
|
5519
5676
|
}
|
|
5520
5677
|
let failed = false;
|
|
5521
5678
|
return {
|
|
5522
5679
|
setProjectName(_name) {},
|
|
5523
5680
|
setGitProvider(provider) {
|
|
5524
|
-
log.info(
|
|
5681
|
+
log.info(TAG27, `Git provider: ${provider}`);
|
|
5525
5682
|
},
|
|
5526
5683
|
check(message) {
|
|
5527
|
-
log.info(
|
|
5684
|
+
log.info(TAG27, message);
|
|
5528
5685
|
},
|
|
5529
5686
|
warn(message) {
|
|
5530
|
-
log.warn(
|
|
5687
|
+
log.warn(TAG27, message);
|
|
5531
5688
|
},
|
|
5532
5689
|
fail() {
|
|
5533
5690
|
failed = true;
|
|
@@ -5535,7 +5692,7 @@ function jsonBanner(config, version) {
|
|
|
5535
5692
|
async ready(message) {
|
|
5536
5693
|
if (failed)
|
|
5537
5694
|
return;
|
|
5538
|
-
log.info(
|
|
5695
|
+
log.info(TAG27, message);
|
|
5539
5696
|
}
|
|
5540
5697
|
};
|
|
5541
5698
|
}
|
|
@@ -5602,7 +5759,7 @@ function cyan(s) {
|
|
|
5602
5759
|
function yellow(s) {
|
|
5603
5760
|
return `${ANSI.yellow}${s}${ANSI.reset}`;
|
|
5604
5761
|
}
|
|
5605
|
-
var
|
|
5762
|
+
var TAG27 = "daemon", RULE_WIDTH = 70, ANSI;
|
|
5606
5763
|
var init_startup_banner = __esm(() => {
|
|
5607
5764
|
init_log();
|
|
5608
5765
|
ANSI = {
|
|
@@ -5749,18 +5906,18 @@ class Watcher {
|
|
|
5749
5906
|
}
|
|
5750
5907
|
async start() {
|
|
5751
5908
|
if (!isPretty()) {
|
|
5752
|
-
log.info(
|
|
5909
|
+
log.info(TAG28, "Connecting to Supabase realtime (broadcast)...");
|
|
5753
5910
|
}
|
|
5754
5911
|
this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
|
|
5755
5912
|
const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
|
|
5756
5913
|
const channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
|
|
5757
|
-
log.debug(
|
|
5914
|
+
log.debug(TAG28, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
|
|
5758
5915
|
this.onCardBroadcast({
|
|
5759
5916
|
event: "card_update",
|
|
5760
5917
|
payload: msg.payload ?? {}
|
|
5761
5918
|
});
|
|
5762
5919
|
}).on("broadcast", { event: "card_created" }, (msg) => {
|
|
5763
|
-
log.debug(
|
|
5920
|
+
log.debug(TAG28, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
|
|
5764
5921
|
this.onCardBroadcast({
|
|
5765
5922
|
event: "card_created",
|
|
5766
5923
|
payload: msg.payload ?? {}
|
|
@@ -5770,41 +5927,42 @@ class Watcher {
|
|
|
5770
5927
|
const cardId = payload.card_id;
|
|
5771
5928
|
const command = payload.command;
|
|
5772
5929
|
if (cardId && command) {
|
|
5773
|
-
log.info(
|
|
5930
|
+
log.info(TAG28, `Broadcast: agent_command ${command} for ${cardId}`);
|
|
5774
5931
|
this.onAgentCommand?.({ cardId, command });
|
|
5775
5932
|
}
|
|
5776
5933
|
}).subscribe((status) => {
|
|
5777
5934
|
if (status === "SUBSCRIBED") {
|
|
5778
5935
|
this.connected = true;
|
|
5779
5936
|
if (!isPretty() || !this.suppressStartupLogs) {
|
|
5780
|
-
log.info(
|
|
5937
|
+
log.info(TAG28, "Broadcast subscription active");
|
|
5781
5938
|
}
|
|
5782
5939
|
this.maybeResolveReady();
|
|
5783
5940
|
} else if (status === "CHANNEL_ERROR") {
|
|
5784
5941
|
this.connected = false;
|
|
5785
|
-
log.error(
|
|
5942
|
+
log.error(TAG28, "Broadcast channel error — will rely on reconciliation");
|
|
5786
5943
|
} else if (status === "TIMED_OUT") {
|
|
5787
5944
|
this.connected = false;
|
|
5788
|
-
log.warn(
|
|
5945
|
+
log.warn(TAG28, "Broadcast subscription timed out — retrying...");
|
|
5789
5946
|
} else if (status === "CLOSED") {
|
|
5790
5947
|
this.connected = false;
|
|
5791
5948
|
}
|
|
5792
5949
|
});
|
|
5793
5950
|
this.channel = channel;
|
|
5794
5951
|
presenceChannel.on("presence", { event: "sync" }, () => {
|
|
5795
|
-
log.debug(
|
|
5952
|
+
log.debug(TAG28, "Presence sync");
|
|
5796
5953
|
}).subscribe(async (status) => {
|
|
5797
5954
|
if (status === "SUBSCRIBED") {
|
|
5798
5955
|
await presenceChannel.track({
|
|
5799
5956
|
daemonId: this.daemonId,
|
|
5800
5957
|
startedAt: new Date().toISOString(),
|
|
5801
5958
|
userId: this.identity.userId,
|
|
5959
|
+
agentId: this.identity.agentId,
|
|
5802
5960
|
userEmail: this.identity.userEmail,
|
|
5803
5961
|
agentIdentifier: this.identity.agentIdentifier,
|
|
5804
5962
|
agentName: this.identity.agentName
|
|
5805
5963
|
});
|
|
5806
5964
|
if (!isPretty() || !this.suppressStartupLogs) {
|
|
5807
|
-
log.info(
|
|
5965
|
+
log.info(TAG28, "Presence tracked on board-presence channel");
|
|
5808
5966
|
}
|
|
5809
5967
|
this.presenceTracked = true;
|
|
5810
5968
|
this.maybeResolveReady();
|
|
@@ -5826,10 +5984,10 @@ class Watcher {
|
|
|
5826
5984
|
this.supabase = null;
|
|
5827
5985
|
}
|
|
5828
5986
|
this.connected = false;
|
|
5829
|
-
log.info(
|
|
5987
|
+
log.info(TAG28, "Broadcast subscription stopped");
|
|
5830
5988
|
}
|
|
5831
5989
|
}
|
|
5832
|
-
var
|
|
5990
|
+
var TAG28 = "watcher";
|
|
5833
5991
|
var init_watcher = __esm(() => {
|
|
5834
5992
|
init_log();
|
|
5835
5993
|
});
|
|
@@ -5904,10 +6062,10 @@ function runWorktreeGc(basePath, store, opts = {}) {
|
|
|
5904
6062
|
});
|
|
5905
6063
|
} catch {}
|
|
5906
6064
|
if (result.removed.length > 0) {
|
|
5907
|
-
log.info(
|
|
6065
|
+
log.info(TAG29, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
|
|
5908
6066
|
}
|
|
5909
6067
|
if (result.errors.length > 0) {
|
|
5910
|
-
log.warn(
|
|
6068
|
+
log.warn(TAG29, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
|
|
5911
6069
|
}
|
|
5912
6070
|
return result;
|
|
5913
6071
|
}
|
|
@@ -5984,10 +6142,10 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
5984
6142
|
}
|
|
5985
6143
|
}
|
|
5986
6144
|
if (result.removed.length > 0) {
|
|
5987
|
-
log.info(
|
|
6145
|
+
log.info(TAG29, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
|
|
5988
6146
|
}
|
|
5989
6147
|
if (result.errors.length > 0) {
|
|
5990
|
-
log.warn(
|
|
6148
|
+
log.warn(TAG29, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
|
|
5991
6149
|
}
|
|
5992
6150
|
return result;
|
|
5993
6151
|
}
|
|
@@ -6018,13 +6176,13 @@ class WorktreeGc {
|
|
|
6018
6176
|
try {
|
|
6019
6177
|
runWorktreeGc(this.basePath, this.store);
|
|
6020
6178
|
} catch (err) {
|
|
6021
|
-
log.warn(
|
|
6179
|
+
log.warn(TAG29, `GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
6022
6180
|
}
|
|
6023
6181
|
if (this.remoteOpts) {
|
|
6024
6182
|
try {
|
|
6025
6183
|
pruneFailedRemoteBranches(this.remoteOpts);
|
|
6026
6184
|
} catch (err) {
|
|
6027
|
-
log.warn(
|
|
6185
|
+
log.warn(TAG29, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
6028
6186
|
}
|
|
6029
6187
|
}
|
|
6030
6188
|
}
|
|
@@ -6038,7 +6196,7 @@ function getRepoRoot2() {
|
|
|
6038
6196
|
return null;
|
|
6039
6197
|
}
|
|
6040
6198
|
}
|
|
6041
|
-
var
|
|
6199
|
+
var TAG29 = "worktree-gc";
|
|
6042
6200
|
var init_worktree_gc = __esm(() => {
|
|
6043
6201
|
init_log();
|
|
6044
6202
|
init_worktree();
|
|
@@ -6120,7 +6278,7 @@ async function main() {
|
|
|
6120
6278
|
} catch (err) {
|
|
6121
6279
|
if (err instanceof ConfigValidationError) {
|
|
6122
6280
|
banner.fail();
|
|
6123
|
-
log.error(
|
|
6281
|
+
log.error(TAG30, err.message);
|
|
6124
6282
|
process.exit(1);
|
|
6125
6283
|
}
|
|
6126
6284
|
throw err;
|
|
@@ -6135,20 +6293,27 @@ async function main() {
|
|
|
6135
6293
|
const errored = outcomes.filter((o) => o.errors.length).length;
|
|
6136
6294
|
banner.check(`Recovery: ${outcomes.length} orphan(s) handled${errored > 0 ? `, ${errored} with errors` : ""}`);
|
|
6137
6295
|
}
|
|
6296
|
+
const { agent: registeredAgent } = await client.registerWorkspaceAgent(config.workspaceId, {
|
|
6297
|
+
identifier: config.agentIdentifier,
|
|
6298
|
+
name: config.agentName,
|
|
6299
|
+
color: config.agentColor
|
|
6300
|
+
});
|
|
6301
|
+
const agentId = registeredAgent.id;
|
|
6302
|
+
banner.check(`Agent registered (${config.agentName})`);
|
|
6138
6303
|
const realtimeCreds = await fetchRealtimeCredentials(client);
|
|
6139
6304
|
banner.check("Realtime credentials");
|
|
6140
|
-
const pool = new Pool(config.agent, client, config.userEmail, config.workspaceId, config.projectId, stateStore);
|
|
6305
|
+
const pool = new Pool(config.agent, client, config.userEmail, config.workspaceId, config.projectId, stateStore, agentId);
|
|
6141
6306
|
const promoteSuccessors = async (completedCard) => {
|
|
6142
6307
|
await promoteUnblockedSuccessors(completedCard, {
|
|
6143
6308
|
client,
|
|
6144
|
-
|
|
6145
|
-
enqueue: (cardId) => tryEnqueueCard(cardId, client, pool, config)
|
|
6309
|
+
agentId,
|
|
6310
|
+
enqueue: (cardId) => tryEnqueueCard(cardId, client, pool, config, agentId)
|
|
6146
6311
|
});
|
|
6147
6312
|
};
|
|
6148
6313
|
pool.onCardCompleted = promoteSuccessors;
|
|
6149
6314
|
const reviewColumns = config.agent.review.enabled ? config.agent.review.pickupColumns : [];
|
|
6150
6315
|
const approvedLabel = config.agent.review.enabled ? config.agent.review.approvedLabel : "";
|
|
6151
|
-
const reconciler = new Reconciler(client, pool, config.projectId,
|
|
6316
|
+
const reconciler = new Reconciler(client, pool, config.projectId, agentId, config.agent.pickupColumns, reviewColumns, approvedLabel, config.agent.timing.reconcileIntervalMs, stateStore, config.agent);
|
|
6152
6317
|
let mergeMonitor = null;
|
|
6153
6318
|
if (config.agent.review.enabled && config.agent.review.mergeMonitor) {
|
|
6154
6319
|
mergeMonitor = new MergeMonitor(client, config.projectId, config.agent);
|
|
@@ -6207,11 +6372,12 @@ async function main() {
|
|
|
6207
6372
|
}) : null;
|
|
6208
6373
|
const watcher = new Watcher(realtimeCreds, config.projectId, {
|
|
6209
6374
|
userId: agentUserId,
|
|
6375
|
+
agentId,
|
|
6210
6376
|
userEmail: config.userEmail,
|
|
6211
|
-
agentIdentifier:
|
|
6212
|
-
agentName:
|
|
6377
|
+
agentIdentifier: config.agentIdentifier,
|
|
6378
|
+
agentName: config.agentName
|
|
6213
6379
|
}, async (event) => {
|
|
6214
|
-
await handleBroadcast(event, client, pool, config,
|
|
6380
|
+
await handleBroadcast(event, client, pool, config, agentId);
|
|
6215
6381
|
}, async (command) => {
|
|
6216
6382
|
await pool.handleAgentCommand(command.cardId, command.command);
|
|
6217
6383
|
});
|
|
@@ -6221,25 +6387,25 @@ async function main() {
|
|
|
6221
6387
|
if (shuttingDown)
|
|
6222
6388
|
return;
|
|
6223
6389
|
shuttingDown = true;
|
|
6224
|
-
log.info(
|
|
6390
|
+
log.info(TAG30, `Received ${signal}, shutting down gracefully...`);
|
|
6225
6391
|
reconciler.stop();
|
|
6226
6392
|
mergeMonitor?.stop();
|
|
6227
6393
|
worktreeGc.stop();
|
|
6228
6394
|
await httpServer?.stop();
|
|
6229
6395
|
await watcher.stop();
|
|
6230
6396
|
await pool.shutdown();
|
|
6231
|
-
log.info(
|
|
6397
|
+
log.info(TAG30, "Daemon stopped.");
|
|
6232
6398
|
process.exit(exitCode);
|
|
6233
6399
|
};
|
|
6234
6400
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
6235
6401
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
6236
6402
|
process.on("uncaughtException", (err) => {
|
|
6237
|
-
log.error(
|
|
6403
|
+
log.error(TAG30, `Uncaught exception: ${err.message}`);
|
|
6238
6404
|
exitCode = 1;
|
|
6239
6405
|
shutdown("uncaughtException");
|
|
6240
6406
|
});
|
|
6241
6407
|
process.on("unhandledRejection", (reason) => {
|
|
6242
|
-
log.error(
|
|
6408
|
+
log.error(TAG30, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
|
|
6243
6409
|
exitCode = 1;
|
|
6244
6410
|
shutdown("unhandledRejection");
|
|
6245
6411
|
});
|
|
@@ -6277,39 +6443,43 @@ async function main() {
|
|
|
6277
6443
|
await banner.ready("watching for card assignments");
|
|
6278
6444
|
watcher.allowStartupLogs();
|
|
6279
6445
|
}
|
|
6280
|
-
async function handleBroadcast(event, client, pool, config,
|
|
6446
|
+
async function handleBroadcast(event, client, pool, config, agentId) {
|
|
6281
6447
|
const payload = event.payload;
|
|
6282
6448
|
const cardId = payload.card_id;
|
|
6283
6449
|
if (!cardId)
|
|
6284
6450
|
return;
|
|
6285
|
-
const
|
|
6286
|
-
if (
|
|
6451
|
+
const assignedAgentId = payload.assigned_agent_id;
|
|
6452
|
+
if (assignedAgentId === undefined)
|
|
6287
6453
|
return;
|
|
6288
|
-
if (
|
|
6289
|
-
log.info(
|
|
6454
|
+
if (assignedAgentId === agentId) {
|
|
6455
|
+
log.info(TAG30, `Broadcast: card ${cardId} assigned to agent`);
|
|
6290
6456
|
try {
|
|
6291
|
-
await tryEnqueueCard(cardId, client, pool, config);
|
|
6457
|
+
await tryEnqueueCard(cardId, client, pool, config, agentId);
|
|
6292
6458
|
} catch (err) {
|
|
6293
|
-
log.error(
|
|
6459
|
+
log.error(TAG30, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
|
|
6294
6460
|
}
|
|
6295
6461
|
} else if (pool.isCardKnown(cardId)) {
|
|
6296
|
-
log.info(
|
|
6462
|
+
log.info(TAG30, `Broadcast: card ${cardId} unassigned from agent`);
|
|
6297
6463
|
await pool.removeCard(cardId);
|
|
6298
6464
|
}
|
|
6299
6465
|
}
|
|
6300
|
-
async function tryEnqueueCard(cardId, client, pool, config) {
|
|
6466
|
+
async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
6301
6467
|
const { card } = await client.getCard(cardId);
|
|
6468
|
+
if (card.assigned_agent_id !== agentId) {
|
|
6469
|
+
log.debug(TAG30, `Card ${cardId} no longer assigned to agent — skipping`);
|
|
6470
|
+
return;
|
|
6471
|
+
}
|
|
6302
6472
|
const board = await client.getBoard(config.projectId, { summary: true });
|
|
6303
6473
|
const columns = board.columns;
|
|
6304
6474
|
const column = columns.find((c) => c.id === card.column_id);
|
|
6305
6475
|
if (!column) {
|
|
6306
|
-
log.warn(
|
|
6476
|
+
log.warn(TAG30, `Column not found for card ${cardId}`);
|
|
6307
6477
|
return;
|
|
6308
6478
|
}
|
|
6309
6479
|
const isPickupColumn = config.agent.pickupColumns.some((name) => name.toLowerCase() === column.name.toLowerCase());
|
|
6310
6480
|
const isReviewColumn = config.agent.review.enabled && config.agent.review.pickupColumns.some((name) => name.toLowerCase() === column.name.toLowerCase());
|
|
6311
6481
|
if (!isPickupColumn && !isReviewColumn) {
|
|
6312
|
-
log.info(
|
|
6482
|
+
log.info(TAG30, `Card #${card.short_id} is in "${column.name}", not a pickup/review column — skipping`);
|
|
6313
6483
|
return;
|
|
6314
6484
|
}
|
|
6315
6485
|
const mode = isReviewColumn ? "review" : "implement";
|
|
@@ -6317,16 +6487,16 @@ async function tryEnqueueCard(cardId, client, pool, config) {
|
|
|
6317
6487
|
const cardLabels = resolveCardLabels(card, labelMap);
|
|
6318
6488
|
const subtasks = card.subtasks ?? [];
|
|
6319
6489
|
if (mode === "review" && config.agent.review.approvedLabel && hasLabel(cardLabels, config.agent.review.approvedLabel)) {
|
|
6320
|
-
log.debug(
|
|
6490
|
+
log.debug(TAG30, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
|
|
6321
6491
|
return;
|
|
6322
6492
|
}
|
|
6323
6493
|
if (mode === "review" && !extractBranchFromDescription(card.description)) {
|
|
6324
|
-
log.info(
|
|
6494
|
+
log.info(TAG30, `Card #${card.short_id} has no branch reference — skipping auto-review`);
|
|
6325
6495
|
return;
|
|
6326
6496
|
}
|
|
6327
6497
|
await pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
6328
6498
|
}
|
|
6329
|
-
var
|
|
6499
|
+
var TAG30 = "daemon", PKG_VERSION;
|
|
6330
6500
|
var init_src = __esm(() => {
|
|
6331
6501
|
init_board_helpers();
|
|
6332
6502
|
init_config();
|
|
@@ -6342,7 +6512,6 @@ var init_src = __esm(() => {
|
|
|
6342
6512
|
init_startup_banner();
|
|
6343
6513
|
init_state_store();
|
|
6344
6514
|
init_stream_parser_selftest();
|
|
6345
|
-
init_types();
|
|
6346
6515
|
init_unblock();
|
|
6347
6516
|
init_watcher();
|
|
6348
6517
|
init_worktree_gc();
|