@gethmy/agent 1.0.1 → 1.0.3
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/README.md +3 -2
- package/dist/board-helpers.d.ts +10 -2
- package/dist/board-helpers.js +22 -3
- package/dist/completion.d.ts +8 -1
- package/dist/completion.js +14 -4
- package/dist/index.js +9 -5
- package/dist/merge-monitor.js +34 -22
- package/dist/pool.d.ts +4 -0
- package/dist/pool.js +23 -0
- package/dist/progress-tracker.d.ts +28 -3
- package/dist/progress-tracker.js +214 -42
- package/dist/reconcile.js +12 -5
- package/dist/review-completion.d.ts +1 -1
- package/dist/review-completion.js +15 -9
- package/dist/review-prompt.d.ts +1 -1
- package/dist/review-prompt.js +7 -4
- package/dist/review-worker.d.ts +11 -0
- package/dist/review-worker.js +177 -42
- package/dist/stream-parser.d.ts +16 -7
- package/dist/stream-parser.js +25 -11
- package/dist/types.d.ts +2 -0
- package/dist/types.js +3 -0
- package/dist/watcher.d.ts +7 -1
- package/dist/watcher.js +14 -2
- package/dist/worker.d.ts +9 -0
- package/dist/worker.js +63 -4
- package/package.json +4 -4
package/dist/review-worker.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { execFileSync, spawn } from "node:child_process";
|
|
2
2
|
import { addLabelByName, moveCardToColumn } from "./board-helpers.js";
|
|
3
3
|
import { log } from "./log.js";
|
|
4
|
+
import { ProgressTracker } from "./progress-tracker.js";
|
|
4
5
|
import { parseReviewOutput, runReviewCompletion } from "./review-completion.js";
|
|
5
6
|
import { buildReviewSystemPrompt, buildReviewUserPrompt, } from "./review-prompt.js";
|
|
6
7
|
import { checkoutExistingBranch, extractBranchFromDescription, } from "./review-worktree.js";
|
|
7
|
-
import {
|
|
8
|
+
import { StreamParser } from "./stream-parser.js";
|
|
9
|
+
import { AGENT_NAME, agentIdentifier, NEED_REVIEW_LABEL, NEED_REVIEW_LABEL_COLOR, } from "./types.js";
|
|
8
10
|
import { waitForDevServer } from "./verification.js";
|
|
9
11
|
import { cleanupWorktree } from "./worktree.js";
|
|
10
12
|
const TAG = "review-worker";
|
|
@@ -23,6 +25,7 @@ export class ReviewWorker {
|
|
|
23
25
|
process = null;
|
|
24
26
|
devServerProcess = null;
|
|
25
27
|
timeoutTimer = null;
|
|
28
|
+
progressTracker = null;
|
|
26
29
|
aborted = false;
|
|
27
30
|
constructor(id, config, client, _userEmail, onDone) {
|
|
28
31
|
this.config = config;
|
|
@@ -36,6 +39,9 @@ export class ReviewWorker {
|
|
|
36
39
|
get isIdle() {
|
|
37
40
|
return this.state === "idle";
|
|
38
41
|
}
|
|
42
|
+
get reviewPort() {
|
|
43
|
+
return this.config.review.devServerPort + (this.id - this.config.poolSize);
|
|
44
|
+
}
|
|
39
45
|
get isActive() {
|
|
40
46
|
return (this.state === "preparing" ||
|
|
41
47
|
this.state === "running" ||
|
|
@@ -56,45 +62,93 @@ export class ReviewWorker {
|
|
|
56
62
|
log.info(this.tag, `Preparing review for #${card.short_id} "${card.title}"`);
|
|
57
63
|
// Extract branch name from card description
|
|
58
64
|
this.branchName = extractBranchFromDescription(card.description);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
const localMode = !this.branchName;
|
|
66
|
+
if (localMode) {
|
|
67
|
+
// LOCAL FALLBACK: no branch → review local working tree
|
|
68
|
+
log.info(this.tag, `No branch found for #${card.short_id}, attempting local review`);
|
|
69
|
+
// Use repo root as working directory
|
|
70
|
+
this.worktreePath = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
71
|
+
cwd: this.config.worktree.basePath,
|
|
72
|
+
encoding: "utf-8",
|
|
73
|
+
timeout: 5_000,
|
|
74
|
+
}).trim();
|
|
75
|
+
// Check if dev server is already running on the configured port
|
|
76
|
+
const serverRunning = await this.checkDevServer(this.reviewPort);
|
|
77
|
+
if (!serverRunning) {
|
|
78
|
+
log.info(this.tag, `No dev server on port ${this.reviewPort} — adding "${NEED_REVIEW_LABEL}" label and moving to "${this.config.review.failColumn}"`);
|
|
79
|
+
await Promise.all([
|
|
80
|
+
addLabelByName(this.client, card, NEED_REVIEW_LABEL, NEED_REVIEW_LABEL_COLOR),
|
|
81
|
+
moveCardToColumn(this.client, card, this.config.review.failColumn),
|
|
82
|
+
]);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (this.branchName) {
|
|
87
|
+
log.info(this.tag, `Review branch: ${this.branchName}`);
|
|
62
88
|
}
|
|
63
|
-
log.info(this.tag, `Review branch: ${this.branchName}`);
|
|
64
89
|
// Start agent session and make it visible on the board
|
|
65
90
|
await this.client.startAgentSession(card.id, {
|
|
66
91
|
agentIdentifier: agentIdentifier(this.id),
|
|
67
92
|
agentName: `${AGENT_NAME} (Review)`,
|
|
68
93
|
status: "working",
|
|
69
|
-
currentTask:
|
|
94
|
+
currentTask: localMode
|
|
95
|
+
? "Reviewing local changes"
|
|
96
|
+
: "Setting up review worktree",
|
|
70
97
|
progressPercent: 5,
|
|
71
98
|
});
|
|
72
99
|
// Fire label addition concurrently with sync worktree checkout
|
|
73
100
|
const labelPromise = addLabelByName(this.client, card, "agent", "#8b5cf6");
|
|
74
|
-
|
|
75
|
-
|
|
101
|
+
if (!localMode) {
|
|
102
|
+
// Checkout existing branch into worktree (sync)
|
|
103
|
+
this.worktreePath = checkoutExistingBranch(this.config.worktree.basePath, this.branchName);
|
|
104
|
+
}
|
|
76
105
|
await labelPromise;
|
|
77
106
|
if (this.aborted)
|
|
78
107
|
return;
|
|
79
108
|
// --- REVIEWING ---
|
|
80
109
|
this.state = "running";
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
110
|
+
if (!this.worktreePath) {
|
|
111
|
+
throw new Error("worktreePath not set before review phase");
|
|
112
|
+
}
|
|
113
|
+
// Start dev server (only in branch mode — local mode uses existing server)
|
|
114
|
+
const port = this.reviewPort;
|
|
115
|
+
const cwd = this.worktreePath;
|
|
116
|
+
if (!localMode) {
|
|
117
|
+
log.info(this.tag, `Starting dev server on port ${port}...`);
|
|
118
|
+
this.devServerProcess = spawn("bun", ["run", "dev", "--", "--port", String(port)], { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
119
|
+
await waitForDevServer(this.devServerProcess, 30_000);
|
|
120
|
+
log.info(this.tag, `Dev server ready on port ${port}`);
|
|
121
|
+
}
|
|
92
122
|
if (this.aborted)
|
|
93
123
|
return;
|
|
94
124
|
// Get diff
|
|
95
125
|
let diff = "";
|
|
96
126
|
try {
|
|
97
|
-
|
|
127
|
+
if (localMode) {
|
|
128
|
+
// Local mode: show uncommitted changes or last commit's diff
|
|
129
|
+
const uncommitted = execFileSync("git", ["diff"], {
|
|
130
|
+
cwd,
|
|
131
|
+
encoding: "utf-8",
|
|
132
|
+
timeout: 30_000,
|
|
133
|
+
});
|
|
134
|
+
const staged = execFileSync("git", ["diff", "--cached"], {
|
|
135
|
+
cwd,
|
|
136
|
+
encoding: "utf-8",
|
|
137
|
+
timeout: 30_000,
|
|
138
|
+
});
|
|
139
|
+
diff = [staged, uncommitted].filter(Boolean).join("\n");
|
|
140
|
+
if (!diff) {
|
|
141
|
+
// No uncommitted changes — get last commit's diff
|
|
142
|
+
diff = execFileSync("git", ["diff", "HEAD~1..HEAD"], {
|
|
143
|
+
cwd,
|
|
144
|
+
encoding: "utf-8",
|
|
145
|
+
timeout: 30_000,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
diff = execFileSync("git", ["diff", `origin/${this.config.worktree.baseBranch}..HEAD`], { cwd, encoding: "utf-8", timeout: 30_000 });
|
|
151
|
+
}
|
|
98
152
|
}
|
|
99
153
|
catch {
|
|
100
154
|
diff = "(unable to retrieve diff)";
|
|
@@ -108,7 +162,7 @@ export class ReviewWorker {
|
|
|
108
162
|
mode: "review",
|
|
109
163
|
};
|
|
110
164
|
const systemPrompt = buildReviewSystemPrompt();
|
|
111
|
-
const userPrompt = buildReviewUserPrompt(enriched, this.branchName,
|
|
165
|
+
const userPrompt = buildReviewUserPrompt(enriched, this.branchName, cwd, previewUrl, diff, this.config.worktree.baseBranch);
|
|
112
166
|
await this.client.updateAgentProgress(card.id, {
|
|
113
167
|
agentIdentifier: agentIdentifier(this.id),
|
|
114
168
|
agentName: `${AGENT_NAME} (Review)`,
|
|
@@ -121,15 +175,21 @@ export class ReviewWorker {
|
|
|
121
175
|
log.warn(this.tag, `Review timeout reached (${this.config.review.maxTimeout}ms), cancelling`);
|
|
122
176
|
this.cancel();
|
|
123
177
|
}, this.config.review.maxTimeout);
|
|
124
|
-
//
|
|
125
|
-
|
|
178
|
+
// Set up progress tracking
|
|
179
|
+
this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks);
|
|
180
|
+
// Spawn Claude CLI for review with streaming
|
|
181
|
+
const stdout = await this.spawnClaude(userPrompt, systemPrompt, this.progressTracker);
|
|
182
|
+
this.progressTracker.stop();
|
|
183
|
+
this.progressTracker = null;
|
|
126
184
|
if (this.aborted)
|
|
127
185
|
return;
|
|
128
186
|
// --- COMPLETING ---
|
|
129
187
|
this.state = "completing";
|
|
130
188
|
log.info(this.tag, `Claude review finished for #${card.short_id}`);
|
|
131
|
-
// Kill dev server
|
|
132
|
-
|
|
189
|
+
// Kill dev server (only if we started one)
|
|
190
|
+
if (!localMode) {
|
|
191
|
+
this.killDevServer();
|
|
192
|
+
}
|
|
133
193
|
// Parse findings
|
|
134
194
|
const result = parseReviewOutput(stdout);
|
|
135
195
|
log.info(this.tag, `Review verdict: ${result.verdict} (${result.findings.length} finding(s))`);
|
|
@@ -141,7 +201,7 @@ export class ReviewWorker {
|
|
|
141
201
|
progressPercent: 80,
|
|
142
202
|
});
|
|
143
203
|
// Run review completion pipeline
|
|
144
|
-
await runReviewCompletion(this.client, card, result, this.config,
|
|
204
|
+
await runReviewCompletion(this.client, card, result, this.config, cwd, this.branchName);
|
|
145
205
|
}
|
|
146
206
|
catch (err) {
|
|
147
207
|
this.state = "error";
|
|
@@ -169,6 +229,58 @@ export class ReviewWorker {
|
|
|
169
229
|
this.onDone(this);
|
|
170
230
|
}
|
|
171
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Pause the current review by suspending the Claude process (SIGSTOP).
|
|
234
|
+
*/
|
|
235
|
+
async pause() {
|
|
236
|
+
if (!this.isActive || !this.process || this.process.killed)
|
|
237
|
+
return;
|
|
238
|
+
log.info(this.tag, `Pausing review on ${this.cardId}`);
|
|
239
|
+
this.process.kill("SIGSTOP");
|
|
240
|
+
if (this.timeoutTimer) {
|
|
241
|
+
clearTimeout(this.timeoutTimer);
|
|
242
|
+
this.timeoutTimer = null;
|
|
243
|
+
}
|
|
244
|
+
// Update agent session so the UI reflects the paused state
|
|
245
|
+
if (this.cardId) {
|
|
246
|
+
try {
|
|
247
|
+
await this.client.updateAgentProgress(this.cardId, {
|
|
248
|
+
agentIdentifier: agentIdentifier(this.id),
|
|
249
|
+
agentName: AGENT_NAME,
|
|
250
|
+
status: "paused",
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
log.warn(this.tag, "Failed to update agent session to paused");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Resume the Claude process after a pause (SIGCONT).
|
|
260
|
+
*/
|
|
261
|
+
async resume() {
|
|
262
|
+
if (!this.isActive || !this.process || this.process.killed)
|
|
263
|
+
return;
|
|
264
|
+
log.info(this.tag, `Resuming review on ${this.cardId}`);
|
|
265
|
+
this.process.kill("SIGCONT");
|
|
266
|
+
this.timeoutTimer = setTimeout(() => {
|
|
267
|
+
log.warn(this.tag, `Timeout reached (${this.config.review.maxTimeout}ms), cancelling`);
|
|
268
|
+
this.cancel();
|
|
269
|
+
}, this.config.review.maxTimeout);
|
|
270
|
+
// Update agent session so the UI reflects the resumed state
|
|
271
|
+
if (this.cardId) {
|
|
272
|
+
try {
|
|
273
|
+
await this.client.updateAgentProgress(this.cardId, {
|
|
274
|
+
agentIdentifier: agentIdentifier(this.id),
|
|
275
|
+
agentName: AGENT_NAME,
|
|
276
|
+
status: "working",
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
log.warn(this.tag, "Failed to update agent session to working");
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
172
284
|
/**
|
|
173
285
|
* Cancel the current review. Sends escalating signals to both processes.
|
|
174
286
|
*/
|
|
@@ -178,10 +290,17 @@ export class ReviewWorker {
|
|
|
178
290
|
this.aborted = true;
|
|
179
291
|
this.state = "cancelling";
|
|
180
292
|
log.info(this.tag, `Cancelling review on ${this.cardId}`);
|
|
293
|
+
// Stop progress tracking
|
|
294
|
+
if (this.progressTracker) {
|
|
295
|
+
this.progressTracker.stop();
|
|
296
|
+
this.progressTracker = null;
|
|
297
|
+
}
|
|
181
298
|
// Kill dev server first
|
|
182
299
|
this.killDevServer();
|
|
183
300
|
// Then kill Claude process with escalating signals
|
|
184
301
|
if (this.process && !this.process.killed) {
|
|
302
|
+
// Resume first in case the process is suspended
|
|
303
|
+
this.process.kill("SIGCONT");
|
|
185
304
|
this.process.kill("SIGINT");
|
|
186
305
|
log.debug(this.tag, "Sent SIGINT to Claude");
|
|
187
306
|
const sigintDead = await this.waitForExit(CANCEL_SIGINT_TIMEOUT);
|
|
@@ -205,10 +324,23 @@ export class ReviewWorker {
|
|
|
205
324
|
}
|
|
206
325
|
}
|
|
207
326
|
}
|
|
208
|
-
|
|
327
|
+
async checkDevServer(port) {
|
|
328
|
+
try {
|
|
329
|
+
const resp = await fetch(`http://localhost:${port}`, {
|
|
330
|
+
signal: AbortSignal.timeout(3_000),
|
|
331
|
+
});
|
|
332
|
+
return resp.ok;
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
spawnClaude(prompt, systemPrompt, tracker) {
|
|
209
339
|
return new Promise((resolve, reject) => {
|
|
210
340
|
const args = [
|
|
211
|
-
"--
|
|
341
|
+
"--output-format",
|
|
342
|
+
"stream-json",
|
|
343
|
+
"--verbose",
|
|
212
344
|
"--model",
|
|
213
345
|
this.config.claude.model,
|
|
214
346
|
"--max-turns",
|
|
@@ -220,22 +352,23 @@ export class ReviewWorker {
|
|
|
220
352
|
"--",
|
|
221
353
|
prompt,
|
|
222
354
|
];
|
|
223
|
-
log.info(this.tag, `Spawning review: claude ${args.slice(0,
|
|
355
|
+
log.info(this.tag, `Spawning review: claude ${args.slice(0, 5).join(" ")} ...`);
|
|
224
356
|
this.process = spawn("claude", args, {
|
|
225
357
|
cwd: this.worktreePath,
|
|
226
358
|
stdio: ["ignore", "pipe", "pipe"],
|
|
227
359
|
});
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (line.trim()) {
|
|
235
|
-
log.debug(this.tag, `claude> ${line.slice(0, 200)}`);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
360
|
+
// Stream parser for progress tracking + text reconstruction for verdict
|
|
361
|
+
const parser = new StreamParser();
|
|
362
|
+
tracker.attach(parser);
|
|
363
|
+
const textChunks = [];
|
|
364
|
+
parser.on("text", (content) => {
|
|
365
|
+
textChunks.push(content);
|
|
238
366
|
});
|
|
367
|
+
// Attach parser to stdout (single consumer)
|
|
368
|
+
if (this.process.stdout) {
|
|
369
|
+
parser.attach(this.process.stdout);
|
|
370
|
+
}
|
|
371
|
+
let stderr = "";
|
|
239
372
|
this.process.stderr?.on("data", (data) => {
|
|
240
373
|
stderr += data.toString();
|
|
241
374
|
});
|
|
@@ -244,6 +377,8 @@ export class ReviewWorker {
|
|
|
244
377
|
});
|
|
245
378
|
this.process.on("close", (code) => {
|
|
246
379
|
this.process = null;
|
|
380
|
+
// Reconstruct text from stream parser text events
|
|
381
|
+
const stdout = textChunks.join("");
|
|
247
382
|
if (this.aborted) {
|
|
248
383
|
resolve(stdout);
|
|
249
384
|
}
|
|
@@ -284,10 +419,10 @@ export class ReviewWorker {
|
|
|
284
419
|
this.timeoutTimer = null;
|
|
285
420
|
}
|
|
286
421
|
this.killDevServer();
|
|
287
|
-
// Clean up worktree on error
|
|
288
|
-
if (this.worktreePath && this.state === "error") {
|
|
422
|
+
// Clean up worktree on error (only if we created one — skip in local mode)
|
|
423
|
+
if (this.worktreePath && this.state === "error" && this.branchName) {
|
|
289
424
|
try {
|
|
290
|
-
cleanupWorktree(this.worktreePath, this.branchName
|
|
425
|
+
cleanupWorktree(this.worktreePath, this.branchName);
|
|
291
426
|
}
|
|
292
427
|
catch {
|
|
293
428
|
log.warn(this.tag, "Failed to cleanup review worktree");
|
package/dist/stream-parser.d.ts
CHANGED
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
import type { Readable } from "node:stream";
|
|
3
|
+
export interface CostUpdate {
|
|
4
|
+
totalCostUsd: number;
|
|
5
|
+
totalInputTokens: number;
|
|
6
|
+
totalOutputTokens: number;
|
|
7
|
+
durationMs: number;
|
|
8
|
+
durationApiMs: number;
|
|
9
|
+
numTurns: number;
|
|
10
|
+
}
|
|
3
11
|
export interface StreamParserEvents {
|
|
4
12
|
tool_start: [name: string, input: unknown];
|
|
5
|
-
tool_end: [name: string, toolUseId: string];
|
|
13
|
+
tool_end: [name: string, toolUseId: string, content: string | undefined];
|
|
6
14
|
text: [content: string];
|
|
7
15
|
result: [stopReason: string];
|
|
8
|
-
|
|
16
|
+
cost_update: [cost: CostUpdate];
|
|
17
|
+
parse_error: [msg: string];
|
|
9
18
|
}
|
|
10
19
|
export declare class StreamParser extends EventEmitter<StreamParserEvents> {
|
|
11
20
|
private buffer;
|
|
12
|
-
private
|
|
13
|
-
get outputText(): string;
|
|
21
|
+
private attached;
|
|
14
22
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
23
|
+
* Attach a readable stream (Claude CLI stdout) to the parser.
|
|
24
|
+
* Parses NDJSON lines and emits typed events.
|
|
25
|
+
* Each instance must only be attached once.
|
|
17
26
|
*/
|
|
18
|
-
|
|
27
|
+
attach(stream: Readable): void;
|
|
19
28
|
private flush;
|
|
20
29
|
private parseLine;
|
|
21
30
|
private handleMessage;
|
package/dist/stream-parser.js
CHANGED
|
@@ -3,15 +3,17 @@ import { log } from "./log.js";
|
|
|
3
3
|
const TAG = "stream-parser";
|
|
4
4
|
export class StreamParser extends EventEmitter {
|
|
5
5
|
buffer = "";
|
|
6
|
-
|
|
7
|
-
get outputText() {
|
|
8
|
-
return this.finalText;
|
|
9
|
-
}
|
|
6
|
+
attached = false;
|
|
10
7
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* Attach a readable stream (Claude CLI stdout) to the parser.
|
|
9
|
+
* Parses NDJSON lines and emits typed events.
|
|
10
|
+
* Each instance must only be attached once.
|
|
13
11
|
*/
|
|
14
|
-
|
|
12
|
+
attach(stream) {
|
|
13
|
+
if (this.attached) {
|
|
14
|
+
throw new Error("StreamParser already attached to a stream");
|
|
15
|
+
}
|
|
16
|
+
this.attached = true;
|
|
15
17
|
stream.on("data", (chunk) => {
|
|
16
18
|
this.buffer += chunk.toString();
|
|
17
19
|
this.flush();
|
|
@@ -50,7 +52,7 @@ export class StreamParser extends EventEmitter {
|
|
|
50
52
|
catch (err) {
|
|
51
53
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
52
54
|
log.warn(TAG, `Error handling stream event: ${errMsg}`);
|
|
53
|
-
this.emit("
|
|
55
|
+
this.emit("parse_error", errMsg);
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
handleMessage(msg) {
|
|
@@ -60,14 +62,13 @@ export class StreamParser extends EventEmitter {
|
|
|
60
62
|
this.emit("tool_start", msg.tool_name, msg.input);
|
|
61
63
|
}
|
|
62
64
|
else if (msg.subtype === "text" && msg.content) {
|
|
63
|
-
this.finalText += msg.content;
|
|
64
65
|
this.emit("text", msg.content);
|
|
65
66
|
}
|
|
66
67
|
break;
|
|
67
68
|
}
|
|
68
69
|
case "tool_result": {
|
|
69
70
|
if (msg.tool_use_id && msg.tool_name) {
|
|
70
|
-
this.emit("tool_end", msg.tool_name, msg.tool_use_id);
|
|
71
|
+
this.emit("tool_end", msg.tool_name, msg.tool_use_id, msg.content);
|
|
71
72
|
}
|
|
72
73
|
break;
|
|
73
74
|
}
|
|
@@ -75,7 +76,20 @@ export class StreamParser extends EventEmitter {
|
|
|
75
76
|
this.emit("result", msg.stop_reason ?? "unknown");
|
|
76
77
|
break;
|
|
77
78
|
}
|
|
78
|
-
|
|
79
|
+
case "cost_update": {
|
|
80
|
+
if (msg.total_cost_usd != null) {
|
|
81
|
+
this.emit("cost_update", {
|
|
82
|
+
totalCostUsd: msg.total_cost_usd,
|
|
83
|
+
totalInputTokens: msg.total_input_tokens ?? 0,
|
|
84
|
+
totalOutputTokens: msg.total_output_tokens ?? 0,
|
|
85
|
+
durationMs: msg.duration_ms ?? 0,
|
|
86
|
+
durationApiMs: msg.duration_api_ms ?? 0,
|
|
87
|
+
numTurns: msg.num_turns ?? 0,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
// Ignore system, ping, etc.
|
|
79
93
|
}
|
|
80
94
|
}
|
|
81
95
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -49,6 +49,8 @@ export interface AgentConfig {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
export declare const DEFAULT_AGENT_CONFIG: AgentConfig;
|
|
52
|
+
export declare const NEED_REVIEW_LABEL = "Need Review";
|
|
53
|
+
export declare const NEED_REVIEW_LABEL_COLOR = "#f59e0b";
|
|
52
54
|
export declare const AGENT_NAME = "Harmony Agent";
|
|
53
55
|
export declare function agentIdentifier(workerId: number): string;
|
|
54
56
|
export type ProgressPhase = "exploring" | "implementing" | "testing" | "committing" | "finishing";
|
package/dist/types.js
CHANGED
|
@@ -46,6 +46,9 @@ export const DEFAULT_AGENT_CONFIG = {
|
|
|
46
46
|
mergedLabelColor: "#6366f1",
|
|
47
47
|
},
|
|
48
48
|
};
|
|
49
|
+
// ============ LABELS ============
|
|
50
|
+
export const NEED_REVIEW_LABEL = "Need Review";
|
|
51
|
+
export const NEED_REVIEW_LABEL_COLOR = "#f59e0b";
|
|
49
52
|
// ============ AGENT IDENTITY ============
|
|
50
53
|
export const AGENT_NAME = "Harmony Agent";
|
|
51
54
|
export function agentIdentifier(workerId) {
|
package/dist/watcher.d.ts
CHANGED
|
@@ -3,7 +3,12 @@ export interface CardBroadcastEvent {
|
|
|
3
3
|
event: "card_update" | "card_created";
|
|
4
4
|
payload: Record<string, unknown>;
|
|
5
5
|
}
|
|
6
|
+
export interface AgentCommandEvent {
|
|
7
|
+
cardId: string;
|
|
8
|
+
command: "pause" | "resume" | "stop";
|
|
9
|
+
}
|
|
6
10
|
export type CardBroadcastHandler = (event: CardBroadcastEvent) => void;
|
|
11
|
+
export type AgentCommandHandler = (event: AgentCommandEvent) => void;
|
|
7
12
|
/**
|
|
8
13
|
* Subscribes to Supabase broadcast events on the board channel.
|
|
9
14
|
* The harmony-api broadcasts card_update and card_created events
|
|
@@ -13,9 +18,10 @@ export declare class Watcher {
|
|
|
13
18
|
private credentials;
|
|
14
19
|
private projectId;
|
|
15
20
|
private onCardBroadcast;
|
|
21
|
+
private onAgentCommand?;
|
|
16
22
|
private channel;
|
|
17
23
|
private supabase;
|
|
18
|
-
constructor(credentials: RealtimeCredentials, projectId: string, onCardBroadcast: CardBroadcastHandler);
|
|
24
|
+
constructor(credentials: RealtimeCredentials, projectId: string, onCardBroadcast: CardBroadcastHandler, onAgentCommand?: AgentCommandHandler | undefined);
|
|
19
25
|
start(): Promise<void>;
|
|
20
26
|
stop(): Promise<void>;
|
|
21
27
|
}
|
package/dist/watcher.js
CHANGED
|
@@ -10,17 +10,19 @@ export class Watcher {
|
|
|
10
10
|
credentials;
|
|
11
11
|
projectId;
|
|
12
12
|
onCardBroadcast;
|
|
13
|
+
onAgentCommand;
|
|
13
14
|
channel = null;
|
|
14
15
|
supabase = null;
|
|
15
|
-
constructor(credentials, projectId, onCardBroadcast) {
|
|
16
|
+
constructor(credentials, projectId, onCardBroadcast, onAgentCommand) {
|
|
16
17
|
this.credentials = credentials;
|
|
17
18
|
this.projectId = projectId;
|
|
18
19
|
this.onCardBroadcast = onCardBroadcast;
|
|
20
|
+
this.onAgentCommand = onAgentCommand;
|
|
19
21
|
}
|
|
20
22
|
async start() {
|
|
21
23
|
log.info(TAG, "Connecting to Supabase realtime (broadcast)...");
|
|
22
24
|
this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
|
|
23
|
-
|
|
25
|
+
const channel = this.supabase
|
|
24
26
|
.channel(`board-${this.projectId}`)
|
|
25
27
|
.on("broadcast", { event: "card_update" }, (msg) => {
|
|
26
28
|
log.debug(TAG, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
|
|
@@ -35,6 +37,15 @@ export class Watcher {
|
|
|
35
37
|
event: "card_created",
|
|
36
38
|
payload: msg.payload ?? {},
|
|
37
39
|
});
|
|
40
|
+
})
|
|
41
|
+
.on("broadcast", { event: "agent_command" }, (msg) => {
|
|
42
|
+
const payload = msg.payload ?? {};
|
|
43
|
+
const cardId = payload.card_id;
|
|
44
|
+
const command = payload.command;
|
|
45
|
+
if (cardId && command) {
|
|
46
|
+
log.info(TAG, `Broadcast: agent_command ${command} for ${cardId}`);
|
|
47
|
+
this.onAgentCommand?.({ cardId, command });
|
|
48
|
+
}
|
|
38
49
|
})
|
|
39
50
|
.subscribe((status) => {
|
|
40
51
|
if (status === "SUBSCRIBED") {
|
|
@@ -47,6 +58,7 @@ export class Watcher {
|
|
|
47
58
|
log.warn(TAG, "Broadcast subscription timed out — retrying...");
|
|
48
59
|
}
|
|
49
60
|
});
|
|
61
|
+
this.channel = channel;
|
|
50
62
|
}
|
|
51
63
|
async stop() {
|
|
52
64
|
if (this.channel) {
|
package/dist/worker.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export declare class Worker {
|
|
|
14
14
|
private process;
|
|
15
15
|
private timeoutTimer;
|
|
16
16
|
private progressTracker;
|
|
17
|
+
private lastSessionStats;
|
|
17
18
|
private aborted;
|
|
18
19
|
constructor(id: number, config: AgentConfig, client: HarmonyApiClient, _userEmail: string, onDone: (worker: Worker) => void);
|
|
19
20
|
get tag(): string;
|
|
@@ -24,6 +25,14 @@ export declare class Worker {
|
|
|
24
25
|
* PREPARING → RUNNING → COMPLETING → IDLE
|
|
25
26
|
*/
|
|
26
27
|
run(card: Card, column: Column, labels: Label[], subtasks: Subtask[]): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Pause the current work by suspending the Claude process (SIGSTOP).
|
|
30
|
+
*/
|
|
31
|
+
pause(): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Resume the Claude process after a pause (SIGCONT).
|
|
34
|
+
*/
|
|
35
|
+
resume(): Promise<void>;
|
|
27
36
|
/**
|
|
28
37
|
* Cancel the current work. Sends escalating signals to the Claude process.
|
|
29
38
|
*/
|