@gethmy/agent 1.0.1 → 1.0.2

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.
@@ -3,17 +3,40 @@ import { AGENT_NAME, agentIdentifier } from "./types.js";
3
3
  const TAG = "progress-tracker";
4
4
  const THROTTLE_MS = 5_000;
5
5
  const HEARTBEAT_MS = 60_000;
6
+ const MAX_TASK_LENGTH = 120;
7
+ const MAX_RECENT_ACTIONS = 5;
8
+ // Hoisted regexes — avoids recompilation on every call
9
+ const SENTENCE_SPLIT = /\.\s|\n/;
10
+ const ACTION_PREFIX = /^(Let me|I'll|I need to|Now|First|Next|Looking|Checking|Creating|Adding|Updating|Fixing|Refactoring|Moving|The |This )/i;
11
+ const GIT_COMMIT_RE = /\bgit\s+commit\b/;
12
+ const BUILD_CMD_RE = /\b(test|build|lint|check|tsc|vitest|jest|(?:bun|npm|pnpm|yarn) run (?:build|lint))\b/;
6
13
  const PHASES = {
7
- exploring: { min: 10, max: 25, task: "Reading codebase..." },
8
- implementing: { min: 25, max: 55, task: "Implementing changes..." },
9
- testing: { min: 55, max: 65, task: "Running tests..." },
10
- committing: { min: 65, max: 70, task: "Committing changes..." },
11
- finishing: { min: 70, max: 75, task: "Finalizing..." },
14
+ exploring: { min: 10, max: 25, label: "Exploring" },
15
+ implementing: { min: 25, max: 55, label: "Implementing" },
16
+ testing: { min: 55, max: 65, label: "Testing" },
17
+ committing: { min: 65, max: 70, label: "Committing" },
18
+ finishing: { min: 70, max: 75, label: "Finalizing" },
19
+ };
20
+ const PHASE_ORDER = {
21
+ exploring: 0,
22
+ implementing: 1,
23
+ testing: 2,
24
+ committing: 3,
25
+ finishing: 4,
12
26
  };
13
- // Tools that indicate exploring phase
14
- const _EXPLORE_TOOLS = new Set(["Read", "Glob", "Grep", "Agent"]);
15
27
  // Tools that indicate implementation
16
28
  const EDIT_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
29
+ function truncate(str, max) {
30
+ return str.length > max ? `${str.slice(0, max - 3)}...` : str;
31
+ }
32
+ // Map file-based tools to their display verbs
33
+ const FILE_TOOL_VERBS = {
34
+ Read: ["Reading", "file_path"],
35
+ Edit: ["Editing", "file_path"],
36
+ MultiEdit: ["Editing", "file_path"],
37
+ Write: ["Writing", "file_path"],
38
+ NotebookEdit: ["Editing notebook", "notebook_path"],
39
+ };
17
40
  export class ProgressTracker {
18
41
  client;
19
42
  cardId;
@@ -24,12 +47,20 @@ export class ProgressTracker {
24
47
  hasEdited = false;
25
48
  lastUpdateAt = 0;
26
49
  pendingUpdate = null;
50
+ pendingTask = "";
27
51
  heartbeatTimer = null;
28
52
  stopped = false;
53
+ // Rich task tracking
54
+ lastAction = "";
29
55
  // Subtask tracking
30
56
  subtaskTotal;
31
57
  subtaskCompleted;
32
58
  subtaskMode;
59
+ // Rich activity tracking
60
+ filesEdited = new Set();
61
+ filesRead = new Set();
62
+ lastCost = null;
63
+ recentActions = [];
33
64
  constructor(client, cardId, workerId, subtasks) {
34
65
  this.client = client;
35
66
  this.cardId = cardId;
@@ -45,8 +76,14 @@ export class ProgressTracker {
45
76
  parser.on("tool_start", (name, input) => {
46
77
  this.onToolStart(name, input);
47
78
  });
48
- parser.on("tool_end", (name) => {
49
- this.onToolEnd(name);
79
+ parser.on("tool_end", (name, _id, content) => {
80
+ this.onToolEnd(name, content);
81
+ });
82
+ parser.on("text", (content) => {
83
+ this.onText(content);
84
+ });
85
+ parser.on("cost_update", (cost) => {
86
+ this.lastCost = cost;
50
87
  });
51
88
  this.startHeartbeat();
52
89
  }
@@ -64,54 +101,112 @@ export class ProgressTracker {
64
101
  this.heartbeatTimer = null;
65
102
  }
66
103
  }
104
+ /** Get a summary of the session stats. */
105
+ get stats() {
106
+ return {
107
+ filesEdited: this.filesEdited.size,
108
+ filesRead: this.filesRead.size,
109
+ toolCalls: this.toolCallCount,
110
+ cost: this.lastCost,
111
+ };
112
+ }
67
113
  onToolStart(name, input) {
68
114
  this.toolCallCount++;
69
115
  log.debug(TAG, `Tool: ${name} (count: ${this.toolCallCount}, phase: ${this.phase})`);
116
+ // Track files
117
+ const filePath = this.extractString(input, "file_path");
118
+ if (filePath) {
119
+ if (EDIT_TOOLS.has(name)) {
120
+ this.filesEdited.add(filePath);
121
+ }
122
+ else if (name === "Read" || name === "Glob" || name === "Grep") {
123
+ this.filesRead.add(filePath);
124
+ }
125
+ }
70
126
  // Detect phase transitions
71
127
  if (!this.hasEdited && EDIT_TOOLS.has(name)) {
72
128
  this.hasEdited = true;
73
129
  this.transitionTo("implementing");
74
130
  }
75
131
  else if (this.hasEdited && name === "Bash") {
76
- const cmd = this.extractBashCommand(input);
77
- if (cmd && /\bgit\s+commit\b/.test(cmd)) {
132
+ const cmd = this.extractString(input, "command");
133
+ if (cmd && GIT_COMMIT_RE.test(cmd)) {
78
134
  this.transitionTo("committing");
79
135
  }
80
- else if (cmd &&
81
- /\b(test|build|lint|check|tsc|vitest|jest|(?:bun|npm|pnpm|yarn) run (?:build|lint))\b/.test(cmd)) {
136
+ else if (cmd && BUILD_CMD_RE.test(cmd)) {
82
137
  this.transitionTo("testing");
83
138
  }
84
139
  }
85
- else if (name.startsWith("mcp__harmony__harmony_end_agent_session")) {
140
+ else if (name === "mcp__harmony__harmony_end_agent_session") {
86
141
  this.transitionTo("finishing");
87
142
  }
88
143
  // Handle subtask toggling — override heuristic progress
89
144
  if (name === "mcp__harmony__harmony_toggle_subtask" && this.subtaskMode) {
90
- this.subtaskCompleted = Math.min(this.subtaskCompleted + 1, this.subtaskTotal);
145
+ const val = this.extractString(input, "completed");
146
+ const completing = val === null || val === "true";
147
+ if (completing) {
148
+ this.subtaskCompleted = Math.min(this.subtaskCompleted + 1, this.subtaskTotal);
149
+ }
150
+ else {
151
+ this.subtaskCompleted = Math.max(this.subtaskCompleted - 1, 0);
152
+ }
91
153
  const subtaskProgress = Math.round(10 + (this.subtaskCompleted / this.subtaskTotal) * 60);
92
154
  this.progress = Math.max(this.progress, subtaskProgress);
93
155
  this.scheduleUpdate(`Completed subtask ${this.subtaskCompleted}/${this.subtaskTotal}`);
94
156
  return;
95
157
  }
96
- // Increment progress within current phase bounds
158
+ // Build rich action description and increment progress
159
+ const action = this.describeToolAction(name, input);
160
+ if (action) {
161
+ this.lastAction = action;
162
+ this.pushRecentAction(action);
163
+ }
97
164
  this.incrementProgress();
98
165
  }
99
- onToolEnd(_name) {
166
+ onToolEnd(name, content) {
167
+ // Detect build/test failures from Bash results
168
+ if (name === "Bash" && content && this.phase === "testing") {
169
+ const lower = content.slice(-500).toLowerCase();
170
+ if (lower.includes("error") &&
171
+ (lower.includes("build failed") ||
172
+ lower.includes("failed to compile") ||
173
+ lower.includes("exit code 1"))) {
174
+ this.lastAction = "Build failed — fixing errors";
175
+ this.scheduleUpdate(this.lastAction);
176
+ }
177
+ }
100
178
  // Reset heartbeat on any activity
101
179
  this.startHeartbeat();
102
180
  }
181
+ onText(content) {
182
+ // Capture brief reasoning snippets from Claude's text output.
183
+ // Only update if the text looks like a meaningful status line
184
+ // (skip very short fragments from streaming).
185
+ const trimmed = content.trim();
186
+ if (trimmed.length < 10)
187
+ return;
188
+ // Extract first sentence or line as a brief description
189
+ const end = trimmed.search(SENTENCE_SPLIT);
190
+ const firstLine = (end === -1 ? trimmed : trimmed.slice(0, end)).trim();
191
+ if (firstLine.length >= 10 && firstLine.length <= 200) {
192
+ if (ACTION_PREFIX.test(firstLine)) {
193
+ this.lastAction = truncate(firstLine, MAX_TASK_LENGTH);
194
+ }
195
+ }
196
+ }
103
197
  transitionTo(newPhase) {
104
- if (this.phaseOrder(newPhase) <= this.phaseOrder(this.phase))
198
+ if (PHASE_ORDER[newPhase] <= PHASE_ORDER[this.phase])
105
199
  return;
106
200
  log.info(TAG, `Phase: ${this.phase} → ${newPhase}`);
107
201
  this.phase = newPhase;
108
202
  this.progress = Math.max(this.progress, PHASES[newPhase].min);
109
- this.scheduleUpdate(PHASES[newPhase].task);
203
+ // Reset stale action from prior phase; new phase starts with its own label
204
+ this.lastAction = "";
205
+ this.scheduleUpdate(PHASES[newPhase].label);
110
206
  }
111
207
  incrementProgress() {
112
208
  const config = PHASES[this.phase];
113
209
  const range = config.max - config.min;
114
- // Each tool call moves ~2% within the phase, so roughly 7-15 calls fill a phase
115
210
  const step = Math.max(1, Math.round(range / 12));
116
211
  const newProgress = Math.min(this.progress + step, config.max);
117
212
  if (newProgress > this.progress) {
@@ -120,29 +215,106 @@ export class ProgressTracker {
120
215
  }
121
216
  }
122
217
  currentTaskLabel() {
123
- const config = PHASES[this.phase];
124
- return config.task;
218
+ if (this.lastAction)
219
+ return this.lastAction;
220
+ // Include file count context when available
221
+ const edited = this.filesEdited.size;
222
+ if (edited > 0) {
223
+ return `${PHASES[this.phase].label} (${edited} file${edited > 1 ? "s" : ""} modified)`;
224
+ }
225
+ return PHASES[this.phase].label;
226
+ }
227
+ /**
228
+ * Build a human-readable description of what a tool call is doing.
229
+ */
230
+ describeToolAction(name, input) {
231
+ // File-based tools: Read, Edit, MultiEdit, Write, NotebookEdit
232
+ const fileTool = FILE_TOOL_VERBS[name];
233
+ if (fileTool) {
234
+ const [verb, key] = fileTool;
235
+ const fp = this.extractString(input, key);
236
+ return fp ? `${verb} ${this.shortPath(fp)}` : verb;
237
+ }
238
+ switch (name) {
239
+ case "Glob": {
240
+ const pattern = this.extractString(input, "pattern");
241
+ return pattern ? `Searching for ${pattern}` : "Searching files";
242
+ }
243
+ case "Grep": {
244
+ const pattern = this.extractString(input, "pattern");
245
+ return pattern
246
+ ? `Searching for "${truncate(pattern, 40)}"`
247
+ : "Searching code";
248
+ }
249
+ case "Bash": {
250
+ const cmd = this.extractString(input, "command");
251
+ return cmd
252
+ ? `Running: ${truncate(cmd.split("\n")[0], 80)}`
253
+ : "Running command";
254
+ }
255
+ case "Agent": {
256
+ const desc = this.extractString(input, "description");
257
+ return desc
258
+ ? `Sub-agent: ${truncate(desc, 60)}`
259
+ : "Delegating to sub-agent";
260
+ }
261
+ default: {
262
+ if (name.startsWith("mcp__harmony__harmony_")) {
263
+ const toolName = name
264
+ .replace("mcp__harmony__harmony_", "")
265
+ .replace(/_/g, " ");
266
+ return `Harmony: ${toolName}`;
267
+ }
268
+ if (name.startsWith("mcp__")) {
269
+ return `Tool: ${name.split("__").pop()?.replace(/_/g, " ") ?? name}`;
270
+ }
271
+ return null;
272
+ }
273
+ }
274
+ }
275
+ /**
276
+ * Strip absolute paths to show only meaningful segments from src/ or packages/.
277
+ */
278
+ shortPath(filePath) {
279
+ const parts = filePath.split("/");
280
+ const srcIdx = parts.lastIndexOf("src");
281
+ const pkgIdx = parts.lastIndexOf("packages");
282
+ const anchor = Math.max(srcIdx, pkgIdx);
283
+ if (anchor >= 0) {
284
+ return parts.slice(anchor).join("/");
285
+ }
286
+ return parts.slice(-3).join("/");
125
287
  }
126
288
  scheduleUpdate(currentTask) {
127
289
  if (this.stopped)
128
290
  return;
129
291
  const now = Date.now();
130
292
  const elapsed = now - this.lastUpdateAt;
293
+ // Always track latest task so throttled sends use the freshest label
294
+ this.pendingTask = currentTask;
131
295
  if (elapsed >= THROTTLE_MS) {
132
- // Can send immediately
133
- this.sendUpdate(currentTask);
296
+ this.sendUpdate(this.pendingTask);
134
297
  }
135
298
  else if (!this.pendingUpdate) {
136
- // Schedule for after throttle window
137
299
  const delay = THROTTLE_MS - elapsed;
138
300
  this.pendingUpdate = setTimeout(() => {
139
301
  this.pendingUpdate = null;
140
302
  if (!this.stopped) {
141
- this.sendUpdate(currentTask);
303
+ // Send the latest task, not the one captured at schedule time
304
+ this.sendUpdate(this.pendingTask);
142
305
  }
143
306
  }, delay);
144
307
  }
145
- // If there's already a pending update, it will fire soon skip
308
+ // If there's already a pending update, pendingTask is now updated — it will use the fresh value
309
+ }
310
+ pushRecentAction(action) {
311
+ this.recentActions.push({
312
+ action,
313
+ ts: new Date().toISOString(),
314
+ });
315
+ if (this.recentActions.length > MAX_RECENT_ACTIONS) {
316
+ this.recentActions.shift();
317
+ }
146
318
  }
147
319
  sendUpdate(currentTask) {
148
320
  this.lastUpdateAt = Date.now();
@@ -152,8 +324,12 @@ export class ProgressTracker {
152
324
  agentIdentifier: agentIdentifier(this.workerId),
153
325
  agentName: AGENT_NAME,
154
326
  status: "working",
155
- currentTask,
327
+ currentTask: truncate(currentTask, MAX_TASK_LENGTH),
156
328
  progressPercent: this.progress,
329
+ phase: this.phase,
330
+ filesChanged: this.filesEdited.size,
331
+ costCents: Math.round((this.lastCost?.totalCostUsd ?? 0) * 100),
332
+ recentActions: this.recentActions,
157
333
  })
158
334
  .catch((err) => {
159
335
  log.warn(TAG, `Failed to send progress update: ${err}`);
@@ -165,24 +341,20 @@ export class ProgressTracker {
165
341
  }
166
342
  this.heartbeatTimer = setTimeout(() => {
167
343
  if (!this.stopped) {
168
- this.sendUpdate("Still working...");
344
+ const task = this.lastAction
345
+ ? `Still working — ${this.lastAction}`
346
+ : "Still working...";
347
+ this.sendUpdate(truncate(task, MAX_TASK_LENGTH));
169
348
  this.startHeartbeat();
170
349
  }
171
350
  }, HEARTBEAT_MS);
172
351
  }
173
- phaseOrder(phase) {
174
- const order = [
175
- "exploring",
176
- "implementing",
177
- "testing",
178
- "committing",
179
- "finishing",
180
- ];
181
- return order.indexOf(phase);
182
- }
183
- extractBashCommand(input) {
184
- if (typeof input === "object" && input !== null && "command" in input) {
185
- return String(input.command);
352
+ /**
353
+ * Safely extract a string property from an unknown tool input.
354
+ */
355
+ extractString(input, key) {
356
+ if (typeof input === "object" && input !== null && key in input) {
357
+ return String(input[key]);
186
358
  }
187
359
  return null;
188
360
  }
package/dist/reconcile.js CHANGED
@@ -1,5 +1,6 @@
1
- import { hasApprovedLabel } from "./board-helpers.js";
1
+ import { buildLabelMap, hasLabel, resolveCardLabels } from "./board-helpers.js";
2
2
  import { log } from "./log.js";
3
+ import { NEED_REVIEW_LABEL } from "./types.js";
3
4
  const TAG = "reconcile";
4
5
  /**
5
6
  * Reconciliation heartbeat: polls the board every `intervalMs` to catch
@@ -42,8 +43,9 @@ export class Reconciler {
42
43
  try {
43
44
  const board = await this.client.getBoard(this.projectId);
44
45
  const cards = (board.cards ?? []);
45
- const columns = board.columns;
46
- const _labels = (board.labels ?? []);
46
+ const columns = (board.columns ?? []);
47
+ // Build label lookup (id Label) to resolve card.labelIds
48
+ const labelMap = buildLabelMap((board.labels ?? []));
47
49
  // Build a lookup of columns by ID
48
50
  const columnMap = new Map();
49
51
  for (const col of columns) {
@@ -74,7 +76,7 @@ export class Reconciler {
74
76
  const column = columnMap.get(card.column_id);
75
77
  if (!column)
76
78
  continue;
77
- const cardLabels = card.labels ?? [];
79
+ const cardLabels = resolveCardLabels(card, labelMap);
78
80
  const subtasks = card.subtasks ?? [];
79
81
  // Determine mode based on which column set the card is in
80
82
  const mode = reviewColumnIds.has(card.column_id)
@@ -83,10 +85,15 @@ export class Reconciler {
83
85
  // Skip already-approved cards in review mode
84
86
  if (mode === "review" &&
85
87
  this.approvedLabel &&
86
- hasApprovedLabel(cardLabels, this.approvedLabel)) {
88
+ hasLabel(cardLabels, this.approvedLabel)) {
87
89
  log.debug(TAG, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
88
90
  continue;
89
91
  }
92
+ // Skip cards with "Need Review" label (awaiting human review)
93
+ if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
94
+ log.debug(TAG, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
95
+ continue;
96
+ }
90
97
  log.info(TAG, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
91
98
  this.pool.enqueue(card, column, cardLabels, subtasks, mode);
92
99
  }
@@ -28,4 +28,4 @@ export declare function parseReviewOutput(stdout: string): ReviewResult;
28
28
  * Handles approved/rejected verdicts, creates subtasks for findings,
29
29
  * and moves the card to the appropriate column.
30
30
  */
31
- export declare function runReviewCompletion(client: HarmonyApiClient, card: Card, result: ReviewResult, config: AgentConfig, worktreePath: string, branchName: string): Promise<void>;
31
+ export declare function runReviewCompletion(client: HarmonyApiClient, card: Card, result: ReviewResult, config: AgentConfig, worktreePath: string, branchName: string | null): Promise<void>;
@@ -110,13 +110,15 @@ export async function runReviewCompletion(client, card, result, config, worktree
110
110
  const currentCycle = getReviewCycle(freshDesc) + 1;
111
111
  const maxCycles = config.review.maxReviewCycles;
112
112
  if (result.verdict === "approved") {
113
- // Ensure branch is pushed (may need force-push after rework)
114
- pushBranch(branchName, worktreePath);
115
- // Create PR if configured
113
+ // Ensure branch is pushed (skip in local mode — no branch to push)
116
114
  let prUrl = null;
117
- if (config.review.createPR) {
118
- const provider = detectGitProvider(worktreePath);
119
- prUrl = createPullRequest(card, branchName, worktreePath, config, provider);
115
+ if (branchName) {
116
+ pushBranch(branchName, worktreePath);
117
+ // Create PR if configured
118
+ if (config.review.createPR) {
119
+ const provider = detectGitProvider(worktreePath);
120
+ prUrl = createPullRequest(card, branchName, worktreePath, config, provider);
121
+ }
120
122
  }
121
123
  // Add "Ready to Merge" label
122
124
  await addLabelByName(client, card, config.review.approvedLabel, config.review.approvedLabelColor);
@@ -180,7 +182,9 @@ export async function runReviewCompletion(client, card, result, config, worktree
180
182
  log.error(TAG, `Failed to update description: ${err instanceof Error ? err.message : err}`);
181
183
  }
182
184
  await client.endAgentSession(card.id, { status: "completed" });
183
- cleanupWorktree(worktreePath, branchName);
185
+ if (branchName) {
186
+ cleanupWorktree(worktreePath, branchName);
187
+ }
184
188
  return;
185
189
  }
186
190
  // Post findings
@@ -242,6 +246,8 @@ export async function runReviewCompletion(client, card, result, config, worktree
242
246
  await client.endAgentSession(card.id, { status: "paused" });
243
247
  log.info(TAG, `#${card.short_id} rejected (cycle ${currentCycle}/${maxCycles}) — moved to "${config.review.failColumn}"`);
244
248
  }
245
- // Cleanup worktree
246
- cleanupWorktree(worktreePath, branchName);
249
+ // Cleanup worktree (skip in local mode — no worktree to clean)
250
+ if (branchName) {
251
+ cleanupWorktree(worktreePath, branchName);
252
+ }
247
253
  }
@@ -9,4 +9,4 @@ export declare function buildReviewSystemPrompt(): string;
9
9
  * Build the card-specific user prompt for the review agent.
10
10
  * Contains the diff, requirements, and structured review steps.
11
11
  */
12
- export declare function buildReviewUserPrompt(enriched: EnrichedCard, branchName: string, worktreePath: string, previewUrl: string, diff: string): string;
12
+ export declare function buildReviewUserPrompt(enriched: EnrichedCard, branchName: string | null, worktreePath: string, previewUrl: string, diff: string, baseBranch: string): string;
@@ -16,7 +16,7 @@ ${QA_VISUAL_CHECKLIST}`;
16
16
  * Build the card-specific user prompt for the review agent.
17
17
  * Contains the diff, requirements, and structured review steps.
18
18
  */
19
- export function buildReviewUserPrompt(enriched, branchName, worktreePath, previewUrl, diff) {
19
+ export function buildReviewUserPrompt(enriched, branchName, worktreePath, previewUrl, diff, baseBranch) {
20
20
  const { card, labels, subtasks } = enriched;
21
21
  const labelStr = labels.length > 0 ? labels.map((l) => l.name).join(", ") : "none";
22
22
  const subtaskStr = subtasks.length > 0
@@ -28,9 +28,12 @@ export function buildReviewUserPrompt(enriched, branchName, worktreePath, previe
28
28
  const truncatedDiff = diff.length > 80_000
29
29
  ? `${diff.slice(0, 80_000)}\n\n... (diff truncated at 80K characters)`
30
30
  : diff;
31
+ const branchLine = branchName
32
+ ? `**Branch**: ${branchName}`
33
+ : `**Mode**: Local review (no branch — reviewing working tree changes)`;
31
34
  return `## Card: #${card.short_id} - ${card.title}
32
35
  **Labels**: ${labelStr}
33
- **Branch**: ${branchName}
36
+ ${branchLine}
34
37
 
35
38
  ## Original Requirements
36
39
  ${description}
@@ -38,7 +41,7 @@ ${description}
38
41
  ## Subtasks (Acceptance Criteria)
39
42
  ${subtaskStr}
40
43
 
41
- ## Diff (origin/main..HEAD)
44
+ ## Diff ${branchName ? `(origin/${baseBranch}..HEAD)` : "(local changes)"}
42
45
  \`\`\`diff
43
46
  ${truncatedDiff}
44
47
  \`\`\`
@@ -96,5 +99,5 @@ After completing all steps, output EXACTLY one JSON block (and nothing else afte
96
99
  - **approved**: No critical findings, at most 1 major finding with minor findings OK.
97
100
 
98
101
  **Do NOT modify any code.** This is a read-only review.
99
- You are reviewing code in a git worktree at \`${worktreePath}\` on branch \`${branchName}\`.`;
102
+ ${branchName ? `You are reviewing code in a git worktree at \`${worktreePath}\` on branch \`${branchName}\`.` : `You are reviewing local changes in the repository at \`${worktreePath}\`.`}`;
100
103
  }
@@ -14,20 +14,31 @@ export declare class ReviewWorker {
14
14
  private process;
15
15
  private devServerProcess;
16
16
  private timeoutTimer;
17
+ private progressTracker;
17
18
  private aborted;
18
19
  constructor(id: number, config: AgentConfig, client: HarmonyApiClient, _userEmail: string, onDone: (worker: ReviewWorker) => void);
19
20
  get tag(): string;
20
21
  get isIdle(): boolean;
22
+ private get reviewPort();
21
23
  get isActive(): boolean;
22
24
  /**
23
25
  * Start reviewing a card. Runs the full lifecycle:
24
26
  * PREPARING → REVIEWING → COMPLETING → IDLE
25
27
  */
26
28
  run(card: Card, column: Column, labels: Label[], subtasks: Subtask[]): Promise<void>;
29
+ /**
30
+ * Pause the current review by suspending the Claude process (SIGSTOP).
31
+ */
32
+ pause(): Promise<void>;
33
+ /**
34
+ * Resume the Claude process after a pause (SIGCONT).
35
+ */
36
+ resume(): Promise<void>;
27
37
  /**
28
38
  * Cancel the current review. Sends escalating signals to both processes.
29
39
  */
30
40
  cancel(): Promise<void>;
41
+ private checkDevServer;
31
42
  private spawnClaude;
32
43
  private waitForExit;
33
44
  private killDevServer;