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