@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.
@@ -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 { AGENT_NAME, agentIdentifier, } from "./types.js";
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
- if (!this.branchName) {
60
- throw new Error(`No branch name found in card #${card.short_id} description. ` +
61
- "Expected: Branch: `agent/...`");
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: "Setting up review worktree",
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
- // Checkout existing branch into worktree (sync)
75
- this.worktreePath = checkoutExistingBranch(this.config.worktree.basePath, this.branchName);
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
- // Start dev server
82
- // Use devServerPort directly review worker IDs are offset by poolSize
83
- // so we subtract to get a 0-based index for port allocation
84
- const port = this.config.review.devServerPort + (this.id - this.config.poolSize);
85
- log.info(this.tag, `Starting dev server on port ${port}...`);
86
- this.devServerProcess = spawn("bun", ["run", "dev", "--", "--port", String(port)], {
87
- cwd: this.worktreePath,
88
- stdio: ["ignore", "pipe", "pipe"],
89
- });
90
- await waitForDevServer(this.devServerProcess, 30_000);
91
- log.info(this.tag, `Dev server ready on port ${port}`);
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
- diff = execFileSync("git", ["diff", `origin/${this.config.worktree.baseBranch}..HEAD`], { cwd: this.worktreePath, encoding: "utf-8", timeout: 30_000 });
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, this.worktreePath, previewUrl, diff);
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
- // Spawn Claude CLI for review
125
- const stdout = await this.spawnClaude(userPrompt, systemPrompt);
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
- this.killDevServer();
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, this.worktreePath, this.branchName);
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
- spawnClaude(prompt, systemPrompt) {
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
- "--print",
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, 3).join(" ")} ...`);
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
- let stdout = "";
229
- let stderr = "";
230
- this.process.stdout?.on("data", (data) => {
231
- const text = data.toString();
232
- stdout += text;
233
- for (const line of text.split("\n")) {
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 ?? undefined);
425
+ cleanupWorktree(this.worktreePath, this.branchName);
291
426
  }
292
427
  catch {
293
428
  log.warn(this.tag, "Failed to cleanup review worktree");
@@ -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
- error: [msg: string];
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 finalText;
13
- get outputText(): string;
21
+ private attached;
14
22
  /**
15
- * Pipe a readable stream (Claude CLI stdout) through the parser.
16
- * Returns when the stream ends.
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
- pipe(stream: Readable): void;
27
+ attach(stream: Readable): void;
19
28
  private flush;
20
29
  private parseLine;
21
30
  private handleMessage;
@@ -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
- finalText = "";
7
- get outputText() {
8
- return this.finalText;
9
- }
6
+ attached = false;
10
7
  /**
11
- * Pipe a readable stream (Claude CLI stdout) through the parser.
12
- * Returns when the stream ends.
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
- pipe(stream) {
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("error", errMsg);
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
- // Ignore system, ping, cost_update, etc.
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
- this.channel = this.supabase
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
  */