@gethmy/agent 1.8.1 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +104 -32
- package/dist/index.js +104 -32
- 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;
|
|
@@ -3759,9 +3779,10 @@ class ReviewWorker {
|
|
|
3759
3779
|
runId = null;
|
|
3760
3780
|
lastRunLogPath = null;
|
|
3761
3781
|
sessionId = null;
|
|
3762
|
-
constructor(id, config, client,
|
|
3782
|
+
constructor(id, config, client, agentId, onDone, stateStore, workspaceId, _projectId) {
|
|
3763
3783
|
this.config = config;
|
|
3764
3784
|
this.client = client;
|
|
3785
|
+
this.agentId = agentId;
|
|
3765
3786
|
this.onDone = onDone;
|
|
3766
3787
|
this.stateStore = stateStore;
|
|
3767
3788
|
this.workspaceId = workspaceId;
|
|
@@ -3860,6 +3881,7 @@ class ReviewWorker {
|
|
|
3860
3881
|
const { session: reviewSession } = await this.client.startAgentSession(card.id, {
|
|
3861
3882
|
agentIdentifier: agentIdentifier(this.id),
|
|
3862
3883
|
agentName: `${AGENT_NAME} (Review)`,
|
|
3884
|
+
agentId: this.agentId,
|
|
3863
3885
|
status: "working",
|
|
3864
3886
|
currentTask: localMode ? "Reviewing local changes" : "Setting up review worktree",
|
|
3865
3887
|
progressPercent: 5
|
|
@@ -3960,6 +3982,10 @@ class ReviewWorker {
|
|
|
3960
3982
|
const sessionStats = this.lastSessionStats;
|
|
3961
3983
|
if (this.aborted)
|
|
3962
3984
|
return;
|
|
3985
|
+
if (this.timeoutTimer) {
|
|
3986
|
+
clearTimeout(this.timeoutTimer);
|
|
3987
|
+
this.timeoutTimer = null;
|
|
3988
|
+
}
|
|
3963
3989
|
this.state = "completing";
|
|
3964
3990
|
await this.recordPhase("completing");
|
|
3965
3991
|
log.info(this.tag, `Claude review finished for #${card.short_id}`);
|
|
@@ -4311,14 +4337,21 @@ class SleepGuard {
|
|
|
4311
4337
|
const child = spawn3("caffeinate", ["-i", "-w", String(process.pid)], {
|
|
4312
4338
|
stdio: "ignore"
|
|
4313
4339
|
});
|
|
4340
|
+
let spawned = false;
|
|
4341
|
+
child.on("spawn", () => {
|
|
4342
|
+
spawned = true;
|
|
4343
|
+
});
|
|
4314
4344
|
child.on("error", (err) => {
|
|
4315
4345
|
log.warn(TAG20, `caffeinate unavailable: ${err.message}`);
|
|
4316
4346
|
if (this.child === child)
|
|
4317
4347
|
this.child = null;
|
|
4318
4348
|
});
|
|
4319
4349
|
child.on("exit", () => {
|
|
4320
|
-
if (this.child
|
|
4321
|
-
|
|
4350
|
+
if (this.child !== child)
|
|
4351
|
+
return;
|
|
4352
|
+
this.child = null;
|
|
4353
|
+
if (spawned && this.holds > 0)
|
|
4354
|
+
this.start();
|
|
4322
4355
|
});
|
|
4323
4356
|
child.unref();
|
|
4324
4357
|
this.child = child;
|
|
@@ -4376,7 +4409,7 @@ async function promoteUnblockedSuccessors(completedCard, deps) {
|
|
|
4376
4409
|
const successorId = link.target_card.id;
|
|
4377
4410
|
try {
|
|
4378
4411
|
const { card } = await deps.client.getCard(successorId);
|
|
4379
|
-
if (card.
|
|
4412
|
+
if (card.assigned_agent_id !== deps.agentId) {
|
|
4380
4413
|
log.debug(TAG21, `successor #${card.short_id} not assigned to agent — skipping promotion`);
|
|
4381
4414
|
continue;
|
|
4382
4415
|
}
|
|
@@ -4653,6 +4686,7 @@ var init_prompt = __esm(() => {
|
|
|
4653
4686
|
class Worker {
|
|
4654
4687
|
config;
|
|
4655
4688
|
client;
|
|
4689
|
+
agentId;
|
|
4656
4690
|
onDone;
|
|
4657
4691
|
workspaceId;
|
|
4658
4692
|
projectId;
|
|
@@ -4674,9 +4708,10 @@ class Worker {
|
|
|
4674
4708
|
verificationFailed = false;
|
|
4675
4709
|
sessionId = null;
|
|
4676
4710
|
runId = null;
|
|
4677
|
-
constructor(id, config, client,
|
|
4711
|
+
constructor(id, config, client, agentId, onDone, workspaceId, projectId, stateStore, onCardCompleted) {
|
|
4678
4712
|
this.config = config;
|
|
4679
4713
|
this.client = client;
|
|
4714
|
+
this.agentId = agentId;
|
|
4680
4715
|
this.onDone = onDone;
|
|
4681
4716
|
this.workspaceId = workspaceId;
|
|
4682
4717
|
this.projectId = projectId;
|
|
@@ -4757,6 +4792,7 @@ class Worker {
|
|
|
4757
4792
|
const { session } = await this.client.startAgentSession(card.id, {
|
|
4758
4793
|
agentIdentifier: agentIdentifier(this.id),
|
|
4759
4794
|
agentName: AGENT_NAME,
|
|
4795
|
+
agentId: this.agentId,
|
|
4760
4796
|
status: "working",
|
|
4761
4797
|
currentTask: "Setting up worktree",
|
|
4762
4798
|
progressPercent: 5
|
|
@@ -4801,6 +4837,10 @@ class Worker {
|
|
|
4801
4837
|
await this.spawnClaude(prompt, card, subtasks);
|
|
4802
4838
|
if (this.aborted)
|
|
4803
4839
|
return;
|
|
4840
|
+
if (this.timeoutTimer) {
|
|
4841
|
+
clearTimeout(this.timeoutTimer);
|
|
4842
|
+
this.timeoutTimer = null;
|
|
4843
|
+
}
|
|
4804
4844
|
this.state = "verifying";
|
|
4805
4845
|
await this.recordPhase("verifying");
|
|
4806
4846
|
log.info(this.tag, `Claude finished for #${card.short_id}, running verification & completion`);
|
|
@@ -4843,6 +4883,14 @@ class Worker {
|
|
|
4843
4883
|
} catch {}
|
|
4844
4884
|
await this.recordOutcome(card.id, "success");
|
|
4845
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
|
+
}
|
|
4846
4894
|
try {
|
|
4847
4895
|
await runTransition(this.client, card, {
|
|
4848
4896
|
move: { columnName: this.config.pickupColumns[0] ?? "To Do" },
|
|
@@ -5098,24 +5146,30 @@ class Pool {
|
|
|
5098
5146
|
client;
|
|
5099
5147
|
projectId;
|
|
5100
5148
|
stateStore;
|
|
5149
|
+
agentId;
|
|
5101
5150
|
implWorkers = [];
|
|
5102
5151
|
reviewWorkers = [];
|
|
5103
5152
|
implQueue;
|
|
5104
5153
|
reviewQueue;
|
|
5105
5154
|
budget;
|
|
5106
5155
|
sleepGuard = new SleepGuard;
|
|
5156
|
+
shuttingDown = false;
|
|
5107
5157
|
onCardCompleted = null;
|
|
5108
|
-
constructor(config, client,
|
|
5158
|
+
constructor(config, client, _userEmail, workspaceId, projectId, stateStore, agentId) {
|
|
5109
5159
|
this.client = client;
|
|
5110
5160
|
this.projectId = projectId;
|
|
5111
5161
|
this.stateStore = stateStore;
|
|
5162
|
+
this.agentId = agentId;
|
|
5112
5163
|
this.implQueue = new PriorityQueue(config);
|
|
5113
5164
|
this.reviewQueue = new PriorityQueue(config);
|
|
5114
5165
|
this.budget = new BudgetGuard(config.budget, this.stateStore);
|
|
5115
5166
|
for (let i = 0;i < config.poolSize; i++) {
|
|
5116
|
-
this.implWorkers.push(new Worker(i, config, client,
|
|
5117
|
-
|
|
5118
|
-
|
|
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
|
+
}
|
|
5119
5173
|
}, workspaceId, projectId, stateStore, async (completedCard) => {
|
|
5120
5174
|
await this.onCardCompleted?.(completedCard);
|
|
5121
5175
|
}));
|
|
@@ -5123,9 +5177,12 @@ class Pool {
|
|
|
5123
5177
|
if (config.review.enabled) {
|
|
5124
5178
|
for (let i = 0;i < config.review.poolSize; i++) {
|
|
5125
5179
|
const reviewWorkerId = config.poolSize + i;
|
|
5126
|
-
this.reviewWorkers.push(new ReviewWorker(reviewWorkerId, config, client,
|
|
5127
|
-
|
|
5128
|
-
|
|
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
|
+
}
|
|
5129
5186
|
}, stateStore, workspaceId, projectId));
|
|
5130
5187
|
}
|
|
5131
5188
|
}
|
|
@@ -5276,6 +5333,7 @@ class Pool {
|
|
|
5276
5333
|
}
|
|
5277
5334
|
async shutdown() {
|
|
5278
5335
|
log.info(TAG24, "Shutting down pool...");
|
|
5336
|
+
this.shuttingDown = true;
|
|
5279
5337
|
const active = [
|
|
5280
5338
|
...this.implWorkers.filter((w) => w.isActive),
|
|
5281
5339
|
...this.reviewWorkers.filter((w) => w.isActive)
|
|
@@ -5286,6 +5344,8 @@ class Pool {
|
|
|
5286
5344
|
}
|
|
5287
5345
|
cardDataCache = new Map;
|
|
5288
5346
|
tryDispatchFor(workers, queue, label) {
|
|
5347
|
+
if (this.shuttingDown)
|
|
5348
|
+
return false;
|
|
5289
5349
|
const idle = workers.find((w) => w.isIdle);
|
|
5290
5350
|
if (!idle) {
|
|
5291
5351
|
log.debug(TAG24, `No idle ${label} workers (queue: ${queue.length})`);
|
|
@@ -5433,7 +5493,7 @@ class Reconciler {
|
|
|
5433
5493
|
client;
|
|
5434
5494
|
pool;
|
|
5435
5495
|
projectId;
|
|
5436
|
-
|
|
5496
|
+
agentId;
|
|
5437
5497
|
pickupColumns;
|
|
5438
5498
|
reviewColumns;
|
|
5439
5499
|
approvedLabel;
|
|
@@ -5448,11 +5508,11 @@ class Reconciler {
|
|
|
5448
5508
|
get isRunning() {
|
|
5449
5509
|
return this.timer !== null;
|
|
5450
5510
|
}
|
|
5451
|
-
constructor(client, pool, projectId,
|
|
5511
|
+
constructor(client, pool, projectId, agentId, pickupColumns, reviewColumns, approvedLabel, intervalMs = 60000, stateStore, agentConfig) {
|
|
5452
5512
|
this.client = client;
|
|
5453
5513
|
this.pool = pool;
|
|
5454
5514
|
this.projectId = projectId;
|
|
5455
|
-
this.
|
|
5515
|
+
this.agentId = agentId;
|
|
5456
5516
|
this.pickupColumns = pickupColumns;
|
|
5457
5517
|
this.reviewColumns = reviewColumns;
|
|
5458
5518
|
this.approvedLabel = approvedLabel;
|
|
@@ -5510,9 +5570,9 @@ class Reconciler {
|
|
|
5510
5570
|
}
|
|
5511
5571
|
const pickupColumnIds = new Set(columns.filter((c) => this.pickupColumns.some((name) => name.toLowerCase() === c.name.toLowerCase())).map((c) => c.id));
|
|
5512
5572
|
const reviewColumnIds = new Set(columns.filter((c) => this.reviewColumns.some((name) => name.toLowerCase() === c.name.toLowerCase())).map((c) => c.id));
|
|
5513
|
-
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)));
|
|
5514
5574
|
const knownCardIds = this.pool.knownCardIds();
|
|
5515
|
-
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));
|
|
5516
5576
|
for (const card of assignedCards) {
|
|
5517
5577
|
if (!knownCardIds.has(card.id)) {
|
|
5518
5578
|
const column = columnMap.get(card.column_id);
|
|
@@ -5897,6 +5957,7 @@ class Watcher {
|
|
|
5897
5957
|
daemonId: this.daemonId,
|
|
5898
5958
|
startedAt: new Date().toISOString(),
|
|
5899
5959
|
userId: this.identity.userId,
|
|
5960
|
+
agentId: this.identity.agentId,
|
|
5900
5961
|
userEmail: this.identity.userEmail,
|
|
5901
5962
|
agentIdentifier: this.identity.agentIdentifier,
|
|
5902
5963
|
agentName: this.identity.agentName
|
|
@@ -6233,20 +6294,27 @@ async function main() {
|
|
|
6233
6294
|
const errored = outcomes.filter((o) => o.errors.length).length;
|
|
6234
6295
|
banner.check(`Recovery: ${outcomes.length} orphan(s) handled${errored > 0 ? `, ${errored} with errors` : ""}`);
|
|
6235
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})`);
|
|
6236
6304
|
const realtimeCreds = await fetchRealtimeCredentials(client);
|
|
6237
6305
|
banner.check("Realtime credentials");
|
|
6238
|
-
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);
|
|
6239
6307
|
const promoteSuccessors = async (completedCard) => {
|
|
6240
6308
|
await promoteUnblockedSuccessors(completedCard, {
|
|
6241
6309
|
client,
|
|
6242
|
-
|
|
6243
|
-
enqueue: (cardId) => tryEnqueueCard(cardId, client, pool, config)
|
|
6310
|
+
agentId,
|
|
6311
|
+
enqueue: (cardId) => tryEnqueueCard(cardId, client, pool, config, agentId)
|
|
6244
6312
|
});
|
|
6245
6313
|
};
|
|
6246
6314
|
pool.onCardCompleted = promoteSuccessors;
|
|
6247
6315
|
const reviewColumns = config.agent.review.enabled ? config.agent.review.pickupColumns : [];
|
|
6248
6316
|
const approvedLabel = config.agent.review.enabled ? config.agent.review.approvedLabel : "";
|
|
6249
|
-
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);
|
|
6250
6318
|
let mergeMonitor = null;
|
|
6251
6319
|
if (config.agent.review.enabled && config.agent.review.mergeMonitor) {
|
|
6252
6320
|
mergeMonitor = new MergeMonitor(client, config.projectId, config.agent);
|
|
@@ -6305,11 +6373,12 @@ async function main() {
|
|
|
6305
6373
|
}) : null;
|
|
6306
6374
|
const watcher = new Watcher(realtimeCreds, config.projectId, {
|
|
6307
6375
|
userId: agentUserId,
|
|
6376
|
+
agentId,
|
|
6308
6377
|
userEmail: config.userEmail,
|
|
6309
|
-
agentIdentifier:
|
|
6310
|
-
agentName:
|
|
6378
|
+
agentIdentifier: config.agentIdentifier,
|
|
6379
|
+
agentName: config.agentName
|
|
6311
6380
|
}, async (event) => {
|
|
6312
|
-
await handleBroadcast(event, client, pool, config,
|
|
6381
|
+
await handleBroadcast(event, client, pool, config, agentId);
|
|
6313
6382
|
}, async (command) => {
|
|
6314
6383
|
await pool.handleAgentCommand(command.cardId, command.command);
|
|
6315
6384
|
});
|
|
@@ -6375,18 +6444,18 @@ async function main() {
|
|
|
6375
6444
|
await banner.ready("watching for card assignments");
|
|
6376
6445
|
watcher.allowStartupLogs();
|
|
6377
6446
|
}
|
|
6378
|
-
async function handleBroadcast(event, client, pool, config,
|
|
6447
|
+
async function handleBroadcast(event, client, pool, config, agentId) {
|
|
6379
6448
|
const payload = event.payload;
|
|
6380
6449
|
const cardId = payload.card_id;
|
|
6381
6450
|
if (!cardId)
|
|
6382
6451
|
return;
|
|
6383
|
-
const
|
|
6384
|
-
if (
|
|
6452
|
+
const assignedAgentId = payload.assigned_agent_id;
|
|
6453
|
+
if (assignedAgentId === undefined)
|
|
6385
6454
|
return;
|
|
6386
|
-
if (
|
|
6455
|
+
if (assignedAgentId === agentId) {
|
|
6387
6456
|
log.info(TAG30, `Broadcast: card ${cardId} assigned to agent`);
|
|
6388
6457
|
try {
|
|
6389
|
-
await tryEnqueueCard(cardId, client, pool, config);
|
|
6458
|
+
await tryEnqueueCard(cardId, client, pool, config, agentId);
|
|
6390
6459
|
} catch (err) {
|
|
6391
6460
|
log.error(TAG30, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
|
|
6392
6461
|
}
|
|
@@ -6395,8 +6464,12 @@ async function handleBroadcast(event, client, pool, config, agentUserId) {
|
|
|
6395
6464
|
await pool.removeCard(cardId);
|
|
6396
6465
|
}
|
|
6397
6466
|
}
|
|
6398
|
-
async function tryEnqueueCard(cardId, client, pool, config) {
|
|
6467
|
+
async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
6399
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
|
+
}
|
|
6400
6473
|
const board = await client.getBoard(config.projectId, { summary: true });
|
|
6401
6474
|
const columns = board.columns;
|
|
6402
6475
|
const column = columns.find((c) => c.id === card.column_id);
|
|
@@ -6440,7 +6513,6 @@ var init_src = __esm(() => {
|
|
|
6440
6513
|
init_startup_banner();
|
|
6441
6514
|
init_state_store();
|
|
6442
6515
|
init_stream_parser_selftest();
|
|
6443
|
-
init_types();
|
|
6444
6516
|
init_unblock();
|
|
6445
6517
|
init_watcher();
|
|
6446
6518
|
init_worktree_gc();
|
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;
|
|
@@ -3758,9 +3778,10 @@ class ReviewWorker {
|
|
|
3758
3778
|
runId = null;
|
|
3759
3779
|
lastRunLogPath = null;
|
|
3760
3780
|
sessionId = null;
|
|
3761
|
-
constructor(id, config, client,
|
|
3781
|
+
constructor(id, config, client, agentId, onDone, stateStore, workspaceId, _projectId) {
|
|
3762
3782
|
this.config = config;
|
|
3763
3783
|
this.client = client;
|
|
3784
|
+
this.agentId = agentId;
|
|
3764
3785
|
this.onDone = onDone;
|
|
3765
3786
|
this.stateStore = stateStore;
|
|
3766
3787
|
this.workspaceId = workspaceId;
|
|
@@ -3859,6 +3880,7 @@ class ReviewWorker {
|
|
|
3859
3880
|
const { session: reviewSession } = await this.client.startAgentSession(card.id, {
|
|
3860
3881
|
agentIdentifier: agentIdentifier(this.id),
|
|
3861
3882
|
agentName: `${AGENT_NAME} (Review)`,
|
|
3883
|
+
agentId: this.agentId,
|
|
3862
3884
|
status: "working",
|
|
3863
3885
|
currentTask: localMode ? "Reviewing local changes" : "Setting up review worktree",
|
|
3864
3886
|
progressPercent: 5
|
|
@@ -3959,6 +3981,10 @@ class ReviewWorker {
|
|
|
3959
3981
|
const sessionStats = this.lastSessionStats;
|
|
3960
3982
|
if (this.aborted)
|
|
3961
3983
|
return;
|
|
3984
|
+
if (this.timeoutTimer) {
|
|
3985
|
+
clearTimeout(this.timeoutTimer);
|
|
3986
|
+
this.timeoutTimer = null;
|
|
3987
|
+
}
|
|
3962
3988
|
this.state = "completing";
|
|
3963
3989
|
await this.recordPhase("completing");
|
|
3964
3990
|
log.info(this.tag, `Claude review finished for #${card.short_id}`);
|
|
@@ -4310,14 +4336,21 @@ class SleepGuard {
|
|
|
4310
4336
|
const child = spawn3("caffeinate", ["-i", "-w", String(process.pid)], {
|
|
4311
4337
|
stdio: "ignore"
|
|
4312
4338
|
});
|
|
4339
|
+
let spawned = false;
|
|
4340
|
+
child.on("spawn", () => {
|
|
4341
|
+
spawned = true;
|
|
4342
|
+
});
|
|
4313
4343
|
child.on("error", (err) => {
|
|
4314
4344
|
log.warn(TAG20, `caffeinate unavailable: ${err.message}`);
|
|
4315
4345
|
if (this.child === child)
|
|
4316
4346
|
this.child = null;
|
|
4317
4347
|
});
|
|
4318
4348
|
child.on("exit", () => {
|
|
4319
|
-
if (this.child
|
|
4320
|
-
|
|
4349
|
+
if (this.child !== child)
|
|
4350
|
+
return;
|
|
4351
|
+
this.child = null;
|
|
4352
|
+
if (spawned && this.holds > 0)
|
|
4353
|
+
this.start();
|
|
4321
4354
|
});
|
|
4322
4355
|
child.unref();
|
|
4323
4356
|
this.child = child;
|
|
@@ -4375,7 +4408,7 @@ async function promoteUnblockedSuccessors(completedCard, deps) {
|
|
|
4375
4408
|
const successorId = link.target_card.id;
|
|
4376
4409
|
try {
|
|
4377
4410
|
const { card } = await deps.client.getCard(successorId);
|
|
4378
|
-
if (card.
|
|
4411
|
+
if (card.assigned_agent_id !== deps.agentId) {
|
|
4379
4412
|
log.debug(TAG21, `successor #${card.short_id} not assigned to agent — skipping promotion`);
|
|
4380
4413
|
continue;
|
|
4381
4414
|
}
|
|
@@ -4652,6 +4685,7 @@ var init_prompt = __esm(() => {
|
|
|
4652
4685
|
class Worker {
|
|
4653
4686
|
config;
|
|
4654
4687
|
client;
|
|
4688
|
+
agentId;
|
|
4655
4689
|
onDone;
|
|
4656
4690
|
workspaceId;
|
|
4657
4691
|
projectId;
|
|
@@ -4673,9 +4707,10 @@ class Worker {
|
|
|
4673
4707
|
verificationFailed = false;
|
|
4674
4708
|
sessionId = null;
|
|
4675
4709
|
runId = null;
|
|
4676
|
-
constructor(id, config, client,
|
|
4710
|
+
constructor(id, config, client, agentId, onDone, workspaceId, projectId, stateStore, onCardCompleted) {
|
|
4677
4711
|
this.config = config;
|
|
4678
4712
|
this.client = client;
|
|
4713
|
+
this.agentId = agentId;
|
|
4679
4714
|
this.onDone = onDone;
|
|
4680
4715
|
this.workspaceId = workspaceId;
|
|
4681
4716
|
this.projectId = projectId;
|
|
@@ -4756,6 +4791,7 @@ class Worker {
|
|
|
4756
4791
|
const { session } = await this.client.startAgentSession(card.id, {
|
|
4757
4792
|
agentIdentifier: agentIdentifier(this.id),
|
|
4758
4793
|
agentName: AGENT_NAME,
|
|
4794
|
+
agentId: this.agentId,
|
|
4759
4795
|
status: "working",
|
|
4760
4796
|
currentTask: "Setting up worktree",
|
|
4761
4797
|
progressPercent: 5
|
|
@@ -4800,6 +4836,10 @@ class Worker {
|
|
|
4800
4836
|
await this.spawnClaude(prompt, card, subtasks);
|
|
4801
4837
|
if (this.aborted)
|
|
4802
4838
|
return;
|
|
4839
|
+
if (this.timeoutTimer) {
|
|
4840
|
+
clearTimeout(this.timeoutTimer);
|
|
4841
|
+
this.timeoutTimer = null;
|
|
4842
|
+
}
|
|
4803
4843
|
this.state = "verifying";
|
|
4804
4844
|
await this.recordPhase("verifying");
|
|
4805
4845
|
log.info(this.tag, `Claude finished for #${card.short_id}, running verification & completion`);
|
|
@@ -4842,6 +4882,14 @@ class Worker {
|
|
|
4842
4882
|
} catch {}
|
|
4843
4883
|
await this.recordOutcome(card.id, "success");
|
|
4844
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
|
+
}
|
|
4845
4893
|
try {
|
|
4846
4894
|
await runTransition(this.client, card, {
|
|
4847
4895
|
move: { columnName: this.config.pickupColumns[0] ?? "To Do" },
|
|
@@ -5097,24 +5145,30 @@ class Pool {
|
|
|
5097
5145
|
client;
|
|
5098
5146
|
projectId;
|
|
5099
5147
|
stateStore;
|
|
5148
|
+
agentId;
|
|
5100
5149
|
implWorkers = [];
|
|
5101
5150
|
reviewWorkers = [];
|
|
5102
5151
|
implQueue;
|
|
5103
5152
|
reviewQueue;
|
|
5104
5153
|
budget;
|
|
5105
5154
|
sleepGuard = new SleepGuard;
|
|
5155
|
+
shuttingDown = false;
|
|
5106
5156
|
onCardCompleted = null;
|
|
5107
|
-
constructor(config, client,
|
|
5157
|
+
constructor(config, client, _userEmail, workspaceId, projectId, stateStore, agentId) {
|
|
5108
5158
|
this.client = client;
|
|
5109
5159
|
this.projectId = projectId;
|
|
5110
5160
|
this.stateStore = stateStore;
|
|
5161
|
+
this.agentId = agentId;
|
|
5111
5162
|
this.implQueue = new PriorityQueue(config);
|
|
5112
5163
|
this.reviewQueue = new PriorityQueue(config);
|
|
5113
5164
|
this.budget = new BudgetGuard(config.budget, this.stateStore);
|
|
5114
5165
|
for (let i = 0;i < config.poolSize; i++) {
|
|
5115
|
-
this.implWorkers.push(new Worker(i, config, client,
|
|
5116
|
-
|
|
5117
|
-
|
|
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
|
+
}
|
|
5118
5172
|
}, workspaceId, projectId, stateStore, async (completedCard) => {
|
|
5119
5173
|
await this.onCardCompleted?.(completedCard);
|
|
5120
5174
|
}));
|
|
@@ -5122,9 +5176,12 @@ class Pool {
|
|
|
5122
5176
|
if (config.review.enabled) {
|
|
5123
5177
|
for (let i = 0;i < config.review.poolSize; i++) {
|
|
5124
5178
|
const reviewWorkerId = config.poolSize + i;
|
|
5125
|
-
this.reviewWorkers.push(new ReviewWorker(reviewWorkerId, config, client,
|
|
5126
|
-
|
|
5127
|
-
|
|
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
|
+
}
|
|
5128
5185
|
}, stateStore, workspaceId, projectId));
|
|
5129
5186
|
}
|
|
5130
5187
|
}
|
|
@@ -5275,6 +5332,7 @@ class Pool {
|
|
|
5275
5332
|
}
|
|
5276
5333
|
async shutdown() {
|
|
5277
5334
|
log.info(TAG24, "Shutting down pool...");
|
|
5335
|
+
this.shuttingDown = true;
|
|
5278
5336
|
const active = [
|
|
5279
5337
|
...this.implWorkers.filter((w) => w.isActive),
|
|
5280
5338
|
...this.reviewWorkers.filter((w) => w.isActive)
|
|
@@ -5285,6 +5343,8 @@ class Pool {
|
|
|
5285
5343
|
}
|
|
5286
5344
|
cardDataCache = new Map;
|
|
5287
5345
|
tryDispatchFor(workers, queue, label) {
|
|
5346
|
+
if (this.shuttingDown)
|
|
5347
|
+
return false;
|
|
5288
5348
|
const idle = workers.find((w) => w.isIdle);
|
|
5289
5349
|
if (!idle) {
|
|
5290
5350
|
log.debug(TAG24, `No idle ${label} workers (queue: ${queue.length})`);
|
|
@@ -5432,7 +5492,7 @@ class Reconciler {
|
|
|
5432
5492
|
client;
|
|
5433
5493
|
pool;
|
|
5434
5494
|
projectId;
|
|
5435
|
-
|
|
5495
|
+
agentId;
|
|
5436
5496
|
pickupColumns;
|
|
5437
5497
|
reviewColumns;
|
|
5438
5498
|
approvedLabel;
|
|
@@ -5447,11 +5507,11 @@ class Reconciler {
|
|
|
5447
5507
|
get isRunning() {
|
|
5448
5508
|
return this.timer !== null;
|
|
5449
5509
|
}
|
|
5450
|
-
constructor(client, pool, projectId,
|
|
5510
|
+
constructor(client, pool, projectId, agentId, pickupColumns, reviewColumns, approvedLabel, intervalMs = 60000, stateStore, agentConfig) {
|
|
5451
5511
|
this.client = client;
|
|
5452
5512
|
this.pool = pool;
|
|
5453
5513
|
this.projectId = projectId;
|
|
5454
|
-
this.
|
|
5514
|
+
this.agentId = agentId;
|
|
5455
5515
|
this.pickupColumns = pickupColumns;
|
|
5456
5516
|
this.reviewColumns = reviewColumns;
|
|
5457
5517
|
this.approvedLabel = approvedLabel;
|
|
@@ -5509,9 +5569,9 @@ class Reconciler {
|
|
|
5509
5569
|
}
|
|
5510
5570
|
const pickupColumnIds = new Set(columns.filter((c) => this.pickupColumns.some((name) => name.toLowerCase() === c.name.toLowerCase())).map((c) => c.id));
|
|
5511
5571
|
const reviewColumnIds = new Set(columns.filter((c) => this.reviewColumns.some((name) => name.toLowerCase() === c.name.toLowerCase())).map((c) => c.id));
|
|
5512
|
-
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)));
|
|
5513
5573
|
const knownCardIds = this.pool.knownCardIds();
|
|
5514
|
-
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));
|
|
5515
5575
|
for (const card of assignedCards) {
|
|
5516
5576
|
if (!knownCardIds.has(card.id)) {
|
|
5517
5577
|
const column = columnMap.get(card.column_id);
|
|
@@ -5896,6 +5956,7 @@ class Watcher {
|
|
|
5896
5956
|
daemonId: this.daemonId,
|
|
5897
5957
|
startedAt: new Date().toISOString(),
|
|
5898
5958
|
userId: this.identity.userId,
|
|
5959
|
+
agentId: this.identity.agentId,
|
|
5899
5960
|
userEmail: this.identity.userEmail,
|
|
5900
5961
|
agentIdentifier: this.identity.agentIdentifier,
|
|
5901
5962
|
agentName: this.identity.agentName
|
|
@@ -6232,20 +6293,27 @@ async function main() {
|
|
|
6232
6293
|
const errored = outcomes.filter((o) => o.errors.length).length;
|
|
6233
6294
|
banner.check(`Recovery: ${outcomes.length} orphan(s) handled${errored > 0 ? `, ${errored} with errors` : ""}`);
|
|
6234
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})`);
|
|
6235
6303
|
const realtimeCreds = await fetchRealtimeCredentials(client);
|
|
6236
6304
|
banner.check("Realtime credentials");
|
|
6237
|
-
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);
|
|
6238
6306
|
const promoteSuccessors = async (completedCard) => {
|
|
6239
6307
|
await promoteUnblockedSuccessors(completedCard, {
|
|
6240
6308
|
client,
|
|
6241
|
-
|
|
6242
|
-
enqueue: (cardId) => tryEnqueueCard(cardId, client, pool, config)
|
|
6309
|
+
agentId,
|
|
6310
|
+
enqueue: (cardId) => tryEnqueueCard(cardId, client, pool, config, agentId)
|
|
6243
6311
|
});
|
|
6244
6312
|
};
|
|
6245
6313
|
pool.onCardCompleted = promoteSuccessors;
|
|
6246
6314
|
const reviewColumns = config.agent.review.enabled ? config.agent.review.pickupColumns : [];
|
|
6247
6315
|
const approvedLabel = config.agent.review.enabled ? config.agent.review.approvedLabel : "";
|
|
6248
|
-
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);
|
|
6249
6317
|
let mergeMonitor = null;
|
|
6250
6318
|
if (config.agent.review.enabled && config.agent.review.mergeMonitor) {
|
|
6251
6319
|
mergeMonitor = new MergeMonitor(client, config.projectId, config.agent);
|
|
@@ -6304,11 +6372,12 @@ async function main() {
|
|
|
6304
6372
|
}) : null;
|
|
6305
6373
|
const watcher = new Watcher(realtimeCreds, config.projectId, {
|
|
6306
6374
|
userId: agentUserId,
|
|
6375
|
+
agentId,
|
|
6307
6376
|
userEmail: config.userEmail,
|
|
6308
|
-
agentIdentifier:
|
|
6309
|
-
agentName:
|
|
6377
|
+
agentIdentifier: config.agentIdentifier,
|
|
6378
|
+
agentName: config.agentName
|
|
6310
6379
|
}, async (event) => {
|
|
6311
|
-
await handleBroadcast(event, client, pool, config,
|
|
6380
|
+
await handleBroadcast(event, client, pool, config, agentId);
|
|
6312
6381
|
}, async (command) => {
|
|
6313
6382
|
await pool.handleAgentCommand(command.cardId, command.command);
|
|
6314
6383
|
});
|
|
@@ -6374,18 +6443,18 @@ async function main() {
|
|
|
6374
6443
|
await banner.ready("watching for card assignments");
|
|
6375
6444
|
watcher.allowStartupLogs();
|
|
6376
6445
|
}
|
|
6377
|
-
async function handleBroadcast(event, client, pool, config,
|
|
6446
|
+
async function handleBroadcast(event, client, pool, config, agentId) {
|
|
6378
6447
|
const payload = event.payload;
|
|
6379
6448
|
const cardId = payload.card_id;
|
|
6380
6449
|
if (!cardId)
|
|
6381
6450
|
return;
|
|
6382
|
-
const
|
|
6383
|
-
if (
|
|
6451
|
+
const assignedAgentId = payload.assigned_agent_id;
|
|
6452
|
+
if (assignedAgentId === undefined)
|
|
6384
6453
|
return;
|
|
6385
|
-
if (
|
|
6454
|
+
if (assignedAgentId === agentId) {
|
|
6386
6455
|
log.info(TAG30, `Broadcast: card ${cardId} assigned to agent`);
|
|
6387
6456
|
try {
|
|
6388
|
-
await tryEnqueueCard(cardId, client, pool, config);
|
|
6457
|
+
await tryEnqueueCard(cardId, client, pool, config, agentId);
|
|
6389
6458
|
} catch (err) {
|
|
6390
6459
|
log.error(TAG30, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
|
|
6391
6460
|
}
|
|
@@ -6394,8 +6463,12 @@ async function handleBroadcast(event, client, pool, config, agentUserId) {
|
|
|
6394
6463
|
await pool.removeCard(cardId);
|
|
6395
6464
|
}
|
|
6396
6465
|
}
|
|
6397
|
-
async function tryEnqueueCard(cardId, client, pool, config) {
|
|
6466
|
+
async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
6398
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
|
+
}
|
|
6399
6472
|
const board = await client.getBoard(config.projectId, { summary: true });
|
|
6400
6473
|
const columns = board.columns;
|
|
6401
6474
|
const column = columns.find((c) => c.id === card.column_id);
|
|
@@ -6439,7 +6512,6 @@ var init_src = __esm(() => {
|
|
|
6439
6512
|
init_startup_banner();
|
|
6440
6513
|
init_state_store();
|
|
6441
6514
|
init_stream_parser_selftest();
|
|
6442
|
-
init_types();
|
|
6443
6515
|
init_unblock();
|
|
6444
6516
|
init_watcher();
|
|
6445
6517
|
init_worktree_gc();
|