@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/progress-tracker.js
CHANGED
|
@@ -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,
|
|
8
|
-
implementing: { min: 25, max: 55,
|
|
9
|
-
testing: { min: 55, max: 65,
|
|
10
|
-
committing: { min: 65, max: 70,
|
|
11
|
-
finishing: { min: 70, max: 75,
|
|
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.
|
|
77
|
-
if (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
|
|
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
|
-
|
|
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
|
-
//
|
|
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(
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
249
|
+
// Cleanup worktree (skip in local mode — no worktree to clean)
|
|
250
|
+
if (branchName) {
|
|
251
|
+
cleanupWorktree(worktreePath, branchName);
|
|
252
|
+
}
|
|
247
253
|
}
|
package/dist/review-prompt.d.ts
CHANGED
|
@@ -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;
|
package/dist/review-prompt.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
}
|
package/dist/review-worker.d.ts
CHANGED
|
@@ -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;
|