@gethmy/agent 1.7.0 → 1.7.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.
- package/README.md +8 -1
- package/dist/cli.js +6376 -205
- package/dist/index.js +6206 -341
- package/package.json +2 -2
- package/dist/board-helpers.d.ts +0 -31
- package/dist/board-helpers.js +0 -150
- package/dist/budget.d.ts +0 -47
- package/dist/budget.js +0 -161
- package/dist/cli.d.ts +0 -16
- package/dist/completion.d.ts +0 -32
- package/dist/completion.js +0 -304
- package/dist/config-validation.d.ts +0 -23
- package/dist/config-validation.js +0 -77
- package/dist/config.d.ts +0 -23
- package/dist/config.js +0 -103
- package/dist/episode-writer.d.ts +0 -84
- package/dist/episode-writer.js +0 -232
- package/dist/git-pr.d.ts +0 -38
- package/dist/git-pr.js +0 -399
- package/dist/http-server.d.ts +0 -79
- package/dist/http-server.js +0 -114
- package/dist/index.d.ts +0 -5
- package/dist/log.d.ts +0 -34
- package/dist/log.js +0 -100
- package/dist/merge-monitor.d.ts +0 -23
- package/dist/merge-monitor.js +0 -169
- package/dist/pm.d.ts +0 -14
- package/dist/pm.js +0 -63
- package/dist/pool.d.ts +0 -70
- package/dist/pool.js +0 -258
- package/dist/process-group.d.ts +0 -26
- package/dist/process-group.js +0 -72
- package/dist/progress-tracker.d.ts +0 -79
- package/dist/progress-tracker.js +0 -442
- package/dist/prompt.d.ts +0 -18
- package/dist/prompt.js +0 -117
- package/dist/queue.d.ts +0 -39
- package/dist/queue.js +0 -100
- package/dist/reconcile.d.ts +0 -35
- package/dist/reconcile.js +0 -174
- package/dist/recovery.d.ts +0 -30
- package/dist/recovery.js +0 -141
- package/dist/review-completion.d.ts +0 -40
- package/dist/review-completion.js +0 -474
- package/dist/review-knowledge.d.ts +0 -14
- package/dist/review-knowledge.js +0 -89
- package/dist/review-prompt.d.ts +0 -12
- package/dist/review-prompt.js +0 -103
- package/dist/review-worker.d.ts +0 -56
- package/dist/review-worker.js +0 -638
- package/dist/review-worktree.d.ts +0 -12
- package/dist/review-worktree.js +0 -95
- package/dist/run-log.d.ts +0 -6
- package/dist/run-log.js +0 -19
- package/dist/startup-banner.d.ts +0 -29
- package/dist/startup-banner.js +0 -143
- package/dist/state-store.d.ts +0 -88
- package/dist/state-store.js +0 -239
- package/dist/stream-parser-selftest.d.ts +0 -9
- package/dist/stream-parser-selftest.js +0 -97
- package/dist/stream-parser.d.ts +0 -43
- package/dist/stream-parser.js +0 -174
- package/dist/transitions.d.ts +0 -57
- package/dist/transitions.js +0 -131
- package/dist/types.d.ts +0 -140
- package/dist/types.js +0 -79
- package/dist/verification.d.ts +0 -39
- package/dist/verification.js +0 -317
- package/dist/watcher.d.ts +0 -53
- package/dist/watcher.js +0 -153
- package/dist/worker.d.ts +0 -53
- package/dist/worker.js +0 -464
- package/dist/worktree-gc.d.ts +0 -67
- package/dist/worktree-gc.js +0 -245
- package/dist/worktree.d.ts +0 -18
- package/dist/worktree.js +0 -177
package/dist/progress-tracker.js
DELETED
|
@@ -1,442 +0,0 @@
|
|
|
1
|
-
import { log } from "./log.js";
|
|
2
|
-
import { AGENT_NAME, agentIdentifier } from "./types.js";
|
|
3
|
-
const TAG = "progress-tracker";
|
|
4
|
-
const THROTTLE_MS = 5_000;
|
|
5
|
-
const HEARTBEAT_MS = 60_000;
|
|
6
|
-
const MAX_TASK_LENGTH = 120;
|
|
7
|
-
const MAX_LOG_BUFFER = 500;
|
|
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/;
|
|
13
|
-
const PHASES = {
|
|
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,
|
|
26
|
-
};
|
|
27
|
-
// Tools that indicate implementation
|
|
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
|
-
};
|
|
40
|
-
export class ProgressTracker {
|
|
41
|
-
client;
|
|
42
|
-
cardId;
|
|
43
|
-
workerId;
|
|
44
|
-
phase = "exploring";
|
|
45
|
-
progress = 10;
|
|
46
|
-
toolCallCount = 0;
|
|
47
|
-
hasEdited = false;
|
|
48
|
-
lastUpdateAt = 0;
|
|
49
|
-
pendingUpdate = null;
|
|
50
|
-
pendingTask = "";
|
|
51
|
-
heartbeatTimer = null;
|
|
52
|
-
stopped = false;
|
|
53
|
-
// Rich task tracking
|
|
54
|
-
lastAction = "";
|
|
55
|
-
// Subtask tracking
|
|
56
|
-
subtaskTotal;
|
|
57
|
-
subtaskCompleted;
|
|
58
|
-
subtaskMode;
|
|
59
|
-
// Rich activity tracking
|
|
60
|
-
filesEdited = new Set();
|
|
61
|
-
filesRead = new Set();
|
|
62
|
-
lastCost = null;
|
|
63
|
-
logBuffer = [];
|
|
64
|
-
sessionId = null;
|
|
65
|
-
// Last assistant text block — used by the episode write hook to
|
|
66
|
-
// capture an approach summary without re-running an LLM (plan §"Write hook").
|
|
67
|
-
lastAssistantText = "";
|
|
68
|
-
constructor(client, cardId, workerId, subtasks) {
|
|
69
|
-
this.client = client;
|
|
70
|
-
this.cardId = cardId;
|
|
71
|
-
this.workerId = workerId;
|
|
72
|
-
this.subtaskTotal = subtasks.length;
|
|
73
|
-
this.subtaskCompleted = subtasks.filter((s) => s.completed).length;
|
|
74
|
-
this.subtaskMode = subtasks.length > 0;
|
|
75
|
-
}
|
|
76
|
-
setSessionId(id) {
|
|
77
|
-
this.sessionId = id;
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Wire up the parser events and start the heartbeat.
|
|
81
|
-
*/
|
|
82
|
-
attach(parser) {
|
|
83
|
-
parser.on("tool_start", (name, input) => {
|
|
84
|
-
this.onToolStart(name, input);
|
|
85
|
-
const desc = this.describeToolAction(name, input);
|
|
86
|
-
if (desc) {
|
|
87
|
-
this.pushLogEntry({
|
|
88
|
-
phase: this.phase,
|
|
89
|
-
eventType: "tool_start",
|
|
90
|
-
toolName: name,
|
|
91
|
-
description: desc,
|
|
92
|
-
metadata: this.extractToolMetadata(name, input),
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
parser.on("tool_end", (name, _id, content) => {
|
|
97
|
-
this.onToolEnd(name, content);
|
|
98
|
-
this.pushLogEntry({
|
|
99
|
-
phase: this.phase,
|
|
100
|
-
eventType: "tool_end",
|
|
101
|
-
toolName: name,
|
|
102
|
-
description: `Completed: ${name}`,
|
|
103
|
-
metadata: {},
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
parser.on("text", (content) => {
|
|
107
|
-
this.onText(content);
|
|
108
|
-
});
|
|
109
|
-
parser.on("cost_update", (cost) => {
|
|
110
|
-
this.lastCost = cost;
|
|
111
|
-
});
|
|
112
|
-
this.startHeartbeat();
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* Stop all timers and flush any pending update.
|
|
116
|
-
*/
|
|
117
|
-
stop() {
|
|
118
|
-
this.stopped = true;
|
|
119
|
-
if (this.pendingUpdate) {
|
|
120
|
-
clearTimeout(this.pendingUpdate);
|
|
121
|
-
this.pendingUpdate = null;
|
|
122
|
-
}
|
|
123
|
-
if (this.heartbeatTimer) {
|
|
124
|
-
clearTimeout(this.heartbeatTimer);
|
|
125
|
-
this.heartbeatTimer = null;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
/** Get a summary of the session stats. */
|
|
129
|
-
get stats() {
|
|
130
|
-
return {
|
|
131
|
-
filesEdited: this.filesEdited.size,
|
|
132
|
-
filesRead: this.filesRead.size,
|
|
133
|
-
toolCalls: this.toolCallCount,
|
|
134
|
-
cost: this.lastCost,
|
|
135
|
-
lastAssistantText: this.lastAssistantText,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
onToolStart(name, input) {
|
|
139
|
-
this.toolCallCount++;
|
|
140
|
-
log.debug(TAG, `Tool: ${name} (count: ${this.toolCallCount}, phase: ${this.phase})`);
|
|
141
|
-
// Track files
|
|
142
|
-
const filePath = this.extractString(input, "file_path");
|
|
143
|
-
if (filePath) {
|
|
144
|
-
if (EDIT_TOOLS.has(name)) {
|
|
145
|
-
this.filesEdited.add(filePath);
|
|
146
|
-
}
|
|
147
|
-
else if (name === "Read" || name === "Glob" || name === "Grep") {
|
|
148
|
-
this.filesRead.add(filePath);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
// Detect phase transitions
|
|
152
|
-
if (!this.hasEdited && EDIT_TOOLS.has(name)) {
|
|
153
|
-
this.hasEdited = true;
|
|
154
|
-
this.transitionTo("implementing");
|
|
155
|
-
}
|
|
156
|
-
else if (this.hasEdited && name === "Bash") {
|
|
157
|
-
const cmd = this.extractString(input, "command");
|
|
158
|
-
if (cmd && GIT_COMMIT_RE.test(cmd)) {
|
|
159
|
-
this.transitionTo("committing");
|
|
160
|
-
}
|
|
161
|
-
else if (cmd && BUILD_CMD_RE.test(cmd)) {
|
|
162
|
-
this.transitionTo("testing");
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
else if (name === "mcp__harmony__harmony_end_agent_session") {
|
|
166
|
-
this.transitionTo("finishing");
|
|
167
|
-
}
|
|
168
|
-
// Handle subtask toggling — override heuristic progress
|
|
169
|
-
if (name === "mcp__harmony__harmony_toggle_subtask" && this.subtaskMode) {
|
|
170
|
-
const val = this.extractString(input, "completed");
|
|
171
|
-
const completing = val === null || val === "true";
|
|
172
|
-
if (completing) {
|
|
173
|
-
this.subtaskCompleted = Math.min(this.subtaskCompleted + 1, this.subtaskTotal);
|
|
174
|
-
}
|
|
175
|
-
else {
|
|
176
|
-
this.subtaskCompleted = Math.max(this.subtaskCompleted - 1, 0);
|
|
177
|
-
}
|
|
178
|
-
const subtaskProgress = Math.round(10 + (this.subtaskCompleted / this.subtaskTotal) * 60);
|
|
179
|
-
this.progress = Math.max(this.progress, subtaskProgress);
|
|
180
|
-
this.scheduleUpdate(`Completed subtask ${this.subtaskCompleted}/${this.subtaskTotal}`);
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
// Build rich action description and increment progress
|
|
184
|
-
const action = this.describeToolAction(name, input);
|
|
185
|
-
if (action) {
|
|
186
|
-
this.lastAction = action;
|
|
187
|
-
}
|
|
188
|
-
this.incrementProgress();
|
|
189
|
-
}
|
|
190
|
-
onToolEnd(name, content) {
|
|
191
|
-
// Detect build/test failures from Bash results
|
|
192
|
-
if (name === "Bash" && content && this.phase === "testing") {
|
|
193
|
-
const lower = content.slice(-500).toLowerCase();
|
|
194
|
-
if (lower.includes("error") &&
|
|
195
|
-
(lower.includes("build failed") ||
|
|
196
|
-
lower.includes("failed to compile") ||
|
|
197
|
-
lower.includes("exit code 1"))) {
|
|
198
|
-
this.lastAction = "Build failed — fixing errors";
|
|
199
|
-
this.scheduleUpdate(this.lastAction);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
// Reset heartbeat on any activity
|
|
203
|
-
this.startHeartbeat();
|
|
204
|
-
}
|
|
205
|
-
onText(content) {
|
|
206
|
-
// Capture brief reasoning snippets from Claude's text output.
|
|
207
|
-
// Only update if the text looks like a meaningful status line
|
|
208
|
-
// (skip very short fragments from streaming).
|
|
209
|
-
const trimmed = content.trim();
|
|
210
|
-
if (trimmed.length < 10)
|
|
211
|
-
return;
|
|
212
|
-
// Always remember the latest non-trivial assistant turn for the episode
|
|
213
|
-
// write hook — last-turn trim, no LLM rewrite (plan §"Write hook").
|
|
214
|
-
this.lastAssistantText = trimmed;
|
|
215
|
-
// Extract first sentence or line as a brief description
|
|
216
|
-
const end = trimmed.search(SENTENCE_SPLIT);
|
|
217
|
-
const firstLine = (end === -1 ? trimmed : trimmed.slice(0, end)).trim();
|
|
218
|
-
if (firstLine.length >= 10 && firstLine.length <= 200) {
|
|
219
|
-
if (ACTION_PREFIX.test(firstLine)) {
|
|
220
|
-
this.lastAction = truncate(firstLine, MAX_TASK_LENGTH);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
transitionTo(newPhase) {
|
|
225
|
-
if (PHASE_ORDER[newPhase] <= PHASE_ORDER[this.phase])
|
|
226
|
-
return;
|
|
227
|
-
log.info(TAG, `Phase: ${this.phase} → ${newPhase}`);
|
|
228
|
-
this.phase = newPhase;
|
|
229
|
-
this.progress = Math.max(this.progress, PHASES[newPhase].min);
|
|
230
|
-
// Reset stale action from prior phase; new phase starts with its own label
|
|
231
|
-
this.lastAction = "";
|
|
232
|
-
this.pushLogEntry({
|
|
233
|
-
phase: newPhase,
|
|
234
|
-
eventType: "phase_change",
|
|
235
|
-
toolName: null,
|
|
236
|
-
description: `Entering ${newPhase} phase`,
|
|
237
|
-
metadata: {},
|
|
238
|
-
});
|
|
239
|
-
this.scheduleUpdate(PHASES[newPhase].label);
|
|
240
|
-
}
|
|
241
|
-
incrementProgress() {
|
|
242
|
-
const config = PHASES[this.phase];
|
|
243
|
-
const range = config.max - config.min;
|
|
244
|
-
const step = Math.max(1, Math.round(range / 12));
|
|
245
|
-
const newProgress = Math.min(this.progress + step, config.max);
|
|
246
|
-
if (newProgress > this.progress) {
|
|
247
|
-
this.progress = newProgress;
|
|
248
|
-
this.scheduleUpdate(this.currentTaskLabel());
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
currentTaskLabel() {
|
|
252
|
-
if (this.lastAction)
|
|
253
|
-
return this.lastAction;
|
|
254
|
-
// Include file count context when available
|
|
255
|
-
const edited = this.filesEdited.size;
|
|
256
|
-
if (edited > 0) {
|
|
257
|
-
return `${PHASES[this.phase].label} (${edited} file${edited > 1 ? "s" : ""} modified)`;
|
|
258
|
-
}
|
|
259
|
-
return PHASES[this.phase].label;
|
|
260
|
-
}
|
|
261
|
-
/**
|
|
262
|
-
* Build a human-readable description of what a tool call is doing.
|
|
263
|
-
*/
|
|
264
|
-
describeToolAction(name, input) {
|
|
265
|
-
// File-based tools: Read, Edit, MultiEdit, Write, NotebookEdit
|
|
266
|
-
const fileTool = FILE_TOOL_VERBS[name];
|
|
267
|
-
if (fileTool) {
|
|
268
|
-
const [verb, key] = fileTool;
|
|
269
|
-
const fp = this.extractString(input, key);
|
|
270
|
-
return fp ? `${verb} ${this.shortPath(fp)}` : verb;
|
|
271
|
-
}
|
|
272
|
-
switch (name) {
|
|
273
|
-
case "Glob": {
|
|
274
|
-
const pattern = this.extractString(input, "pattern");
|
|
275
|
-
return pattern ? `Searching for ${pattern}` : "Searching files";
|
|
276
|
-
}
|
|
277
|
-
case "Grep": {
|
|
278
|
-
const pattern = this.extractString(input, "pattern");
|
|
279
|
-
return pattern
|
|
280
|
-
? `Searching for "${truncate(pattern, 40)}"`
|
|
281
|
-
: "Searching code";
|
|
282
|
-
}
|
|
283
|
-
case "Bash": {
|
|
284
|
-
const cmd = this.extractString(input, "command");
|
|
285
|
-
return cmd
|
|
286
|
-
? `Running: ${truncate(cmd.split("\n")[0], 80)}`
|
|
287
|
-
: "Running command";
|
|
288
|
-
}
|
|
289
|
-
case "Agent": {
|
|
290
|
-
const desc = this.extractString(input, "description");
|
|
291
|
-
return desc
|
|
292
|
-
? `Sub-agent: ${truncate(desc, 60)}`
|
|
293
|
-
: "Delegating to sub-agent";
|
|
294
|
-
}
|
|
295
|
-
default: {
|
|
296
|
-
if (name.startsWith("mcp__harmony__harmony_")) {
|
|
297
|
-
const toolName = name
|
|
298
|
-
.replace("mcp__harmony__harmony_", "")
|
|
299
|
-
.replace(/_/g, " ");
|
|
300
|
-
return `Harmony: ${toolName}`;
|
|
301
|
-
}
|
|
302
|
-
if (name.startsWith("mcp__")) {
|
|
303
|
-
return `Tool: ${name.split("__").pop()?.replace(/_/g, " ") ?? name}`;
|
|
304
|
-
}
|
|
305
|
-
return null;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* Strip absolute paths to show only meaningful segments from src/ or packages/.
|
|
311
|
-
*/
|
|
312
|
-
shortPath(filePath) {
|
|
313
|
-
const parts = filePath.split("/");
|
|
314
|
-
const srcIdx = parts.lastIndexOf("src");
|
|
315
|
-
const pkgIdx = parts.lastIndexOf("packages");
|
|
316
|
-
const anchor = Math.max(srcIdx, pkgIdx);
|
|
317
|
-
if (anchor >= 0) {
|
|
318
|
-
return parts.slice(anchor).join("/");
|
|
319
|
-
}
|
|
320
|
-
return parts.slice(-3).join("/");
|
|
321
|
-
}
|
|
322
|
-
scheduleUpdate(currentTask) {
|
|
323
|
-
if (this.stopped)
|
|
324
|
-
return;
|
|
325
|
-
const now = Date.now();
|
|
326
|
-
const elapsed = now - this.lastUpdateAt;
|
|
327
|
-
// Always track latest task so throttled sends use the freshest label
|
|
328
|
-
this.pendingTask = currentTask;
|
|
329
|
-
if (elapsed >= THROTTLE_MS) {
|
|
330
|
-
this.sendUpdate(this.pendingTask);
|
|
331
|
-
}
|
|
332
|
-
else if (!this.pendingUpdate) {
|
|
333
|
-
const delay = THROTTLE_MS - elapsed;
|
|
334
|
-
this.pendingUpdate = setTimeout(() => {
|
|
335
|
-
this.pendingUpdate = null;
|
|
336
|
-
if (!this.stopped) {
|
|
337
|
-
// Send the latest task, not the one captured at schedule time
|
|
338
|
-
this.sendUpdate(this.pendingTask);
|
|
339
|
-
}
|
|
340
|
-
}, delay);
|
|
341
|
-
}
|
|
342
|
-
// If there's already a pending update, pendingTask is now updated — it will use the fresh value
|
|
343
|
-
}
|
|
344
|
-
sendUpdate(currentTask) {
|
|
345
|
-
this.lastUpdateAt = Date.now();
|
|
346
|
-
log.debug(TAG, `Progress: ${this.progress}% — ${currentTask}`);
|
|
347
|
-
this.client
|
|
348
|
-
.updateAgentProgress(this.cardId, {
|
|
349
|
-
agentIdentifier: agentIdentifier(this.workerId),
|
|
350
|
-
agentName: AGENT_NAME,
|
|
351
|
-
status: "working",
|
|
352
|
-
currentTask: truncate(currentTask, MAX_TASK_LENGTH),
|
|
353
|
-
progressPercent: this.progress,
|
|
354
|
-
phase: this.phase,
|
|
355
|
-
filesChanged: this.filesEdited.size,
|
|
356
|
-
costCents: Math.round((this.lastCost?.totalCostUsd ?? 0) * 100),
|
|
357
|
-
inputTokens: this.lastCost?.totalInputTokens ?? 0,
|
|
358
|
-
outputTokens: this.lastCost?.totalOutputTokens ?? 0,
|
|
359
|
-
cacheCreationInputTokens: this.lastCost?.totalCacheCreationInputTokens ?? 0,
|
|
360
|
-
cacheReadInputTokens: this.lastCost?.totalCacheReadInputTokens ?? 0,
|
|
361
|
-
modelName: this.lastCost?.modelName,
|
|
362
|
-
})
|
|
363
|
-
.catch((err) => {
|
|
364
|
-
log.warn(TAG, `Failed to send progress update: ${err}`);
|
|
365
|
-
});
|
|
366
|
-
this.flushActivityLog();
|
|
367
|
-
}
|
|
368
|
-
startHeartbeat() {
|
|
369
|
-
if (this.heartbeatTimer) {
|
|
370
|
-
clearTimeout(this.heartbeatTimer);
|
|
371
|
-
}
|
|
372
|
-
this.heartbeatTimer = setTimeout(() => {
|
|
373
|
-
if (!this.stopped) {
|
|
374
|
-
const task = this.lastAction
|
|
375
|
-
? `Still working — ${this.lastAction}`
|
|
376
|
-
: "Still working...";
|
|
377
|
-
this.sendUpdate(truncate(task, MAX_TASK_LENGTH));
|
|
378
|
-
this.startHeartbeat();
|
|
379
|
-
}
|
|
380
|
-
}, HEARTBEAT_MS);
|
|
381
|
-
}
|
|
382
|
-
flushFinal() {
|
|
383
|
-
this.flushActivityLog();
|
|
384
|
-
}
|
|
385
|
-
pushLogEntry(entry) {
|
|
386
|
-
this.logBuffer.push({
|
|
387
|
-
...entry,
|
|
388
|
-
createdAt: new Date().toISOString(),
|
|
389
|
-
});
|
|
390
|
-
if (this.logBuffer.length > MAX_LOG_BUFFER) {
|
|
391
|
-
this.logBuffer.shift();
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
flushActivityLog() {
|
|
395
|
-
if (!this.sessionId || this.logBuffer.length === 0)
|
|
396
|
-
return;
|
|
397
|
-
const raw = [...this.logBuffer];
|
|
398
|
-
this.logBuffer = [];
|
|
399
|
-
this.client
|
|
400
|
-
.flushActivityLog(this.cardId, {
|
|
401
|
-
sessionId: this.sessionId,
|
|
402
|
-
entries: raw.map((e) => ({
|
|
403
|
-
...e,
|
|
404
|
-
phase: e.phase ?? undefined,
|
|
405
|
-
toolName: e.toolName ?? undefined,
|
|
406
|
-
})),
|
|
407
|
-
})
|
|
408
|
-
.catch((err) => {
|
|
409
|
-
log.warn(TAG, `Failed to flush activity log: ${err}`);
|
|
410
|
-
// Put entries back at the front of the buffer for retry
|
|
411
|
-
this.logBuffer.unshift(...raw);
|
|
412
|
-
if (this.logBuffer.length > MAX_LOG_BUFFER) {
|
|
413
|
-
this.logBuffer.length = MAX_LOG_BUFFER;
|
|
414
|
-
}
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
extractToolMetadata(_name, input) {
|
|
418
|
-
const meta = {};
|
|
419
|
-
const fp = this.extractString(input, "file_path");
|
|
420
|
-
if (fp)
|
|
421
|
-
meta.file_path = fp;
|
|
422
|
-
const cmd = this.extractString(input, "command");
|
|
423
|
-
if (cmd)
|
|
424
|
-
meta.command = cmd.split("\n")[0].slice(0, 200);
|
|
425
|
-
const pattern = this.extractString(input, "pattern");
|
|
426
|
-
if (pattern)
|
|
427
|
-
meta.pattern = pattern;
|
|
428
|
-
const desc = this.extractString(input, "description");
|
|
429
|
-
if (desc)
|
|
430
|
-
meta.description = desc;
|
|
431
|
-
return meta;
|
|
432
|
-
}
|
|
433
|
-
/**
|
|
434
|
-
* Safely extract a string property from an unknown tool input.
|
|
435
|
-
*/
|
|
436
|
-
extractString(input, key) {
|
|
437
|
-
if (typeof input === "object" && input !== null && key in input) {
|
|
438
|
-
return String(input[key]);
|
|
439
|
-
}
|
|
440
|
-
return null;
|
|
441
|
-
}
|
|
442
|
-
}
|
package/dist/prompt.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { HarmonyApiClient } from "@gethmy/mcp/src/api-client.js";
|
|
2
|
-
import type { EnrichedCard } from "./types.js";
|
|
3
|
-
/**
|
|
4
|
-
* Build the prompt for a card using the shared prompt generation pipeline.
|
|
5
|
-
*
|
|
6
|
-
* This calls `client.generateCardPrompt()` which assembles relevant memories
|
|
7
|
-
* from the knowledge graph, applies role-based framing, and produces a
|
|
8
|
-
* context-rich prompt — the same pipeline used by the MCP server.
|
|
9
|
-
*
|
|
10
|
-
* Falls back to a minimal local prompt if the API call fails.
|
|
11
|
-
*/
|
|
12
|
-
export declare function buildPrompt(enriched: EnrichedCard, branchName: string, worktreePath: string, client: HarmonyApiClient, workspaceId: string, projectId?: string): Promise<string>;
|
|
13
|
-
/**
|
|
14
|
-
* Recall similar past episodes (implement solution/error type) and render them
|
|
15
|
-
* as a "Similar past tasks" section. Returns the empty string on no hits or
|
|
16
|
-
* recall failure — never throws.
|
|
17
|
-
*/
|
|
18
|
-
export declare function renderPastEpisodesSection(client: HarmonyApiClient, title: string, description: string, workspaceId: string, projectId?: string): Promise<string>;
|
package/dist/prompt.js
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { log } from "./log.js";
|
|
2
|
-
const TAG = "prompt";
|
|
3
|
-
/**
|
|
4
|
-
* Build the prompt for a card using the shared prompt generation pipeline.
|
|
5
|
-
*
|
|
6
|
-
* This calls `client.generateCardPrompt()` which assembles relevant memories
|
|
7
|
-
* from the knowledge graph, applies role-based framing, and produces a
|
|
8
|
-
* context-rich prompt — the same pipeline used by the MCP server.
|
|
9
|
-
*
|
|
10
|
-
* Falls back to a minimal local prompt if the API call fails.
|
|
11
|
-
*/
|
|
12
|
-
export async function buildPrompt(enriched, branchName, worktreePath, client, workspaceId, projectId) {
|
|
13
|
-
const { card } = enriched;
|
|
14
|
-
// Phase 1.5 read hook: surface similar past episodes for this card. Block
|
|
15
|
-
// on recall — v2 §6.3 budget already caps latency. Errors degrade silently
|
|
16
|
-
// so prompt build always succeeds (plan §"Read hook").
|
|
17
|
-
const pastEpisodesSection = await renderPastEpisodesSection(client, card.title, card.description ?? "", workspaceId, projectId);
|
|
18
|
-
try {
|
|
19
|
-
const result = await client.generateCardPrompt({
|
|
20
|
-
cardId: card.id,
|
|
21
|
-
workspaceId,
|
|
22
|
-
projectId,
|
|
23
|
-
variant: "execute",
|
|
24
|
-
customConstraints: `You are working in a git worktree at \`${worktreePath}\` on branch \`${branchName}\`.
|
|
25
|
-
Do NOT push to main. All your work stays on \`${branchName}\`.
|
|
26
|
-
When finished, call harmony_end_agent_session with status="completed".`,
|
|
27
|
-
});
|
|
28
|
-
log.info(TAG, `Generated prompt for #${card.short_id} — ${result.contextSummary.memoryCount} memories, ${result.tokenEstimate} tokens`);
|
|
29
|
-
return result.prompt + pastEpisodesSection;
|
|
30
|
-
}
|
|
31
|
-
catch (err) {
|
|
32
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
33
|
-
log.warn(TAG, `Failed to generate prompt via API, using fallback: ${msg}`);
|
|
34
|
-
return (buildFallbackPrompt(enriched, branchName, worktreePath) +
|
|
35
|
-
pastEpisodesSection);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Recall similar past episodes (implement solution/error type) and render them
|
|
40
|
-
* as a "Similar past tasks" section. Returns the empty string on no hits or
|
|
41
|
-
* recall failure — never throws.
|
|
42
|
-
*/
|
|
43
|
-
export async function renderPastEpisodesSection(client, title, description, workspaceId, projectId) {
|
|
44
|
-
if (!projectId)
|
|
45
|
-
return "";
|
|
46
|
-
try {
|
|
47
|
-
const query = `${title}\n${description}`.trim();
|
|
48
|
-
const { entities } = await client.harmonyRecall({
|
|
49
|
-
workspaceId,
|
|
50
|
-
projectId,
|
|
51
|
-
query,
|
|
52
|
-
type: ["solution", "error"],
|
|
53
|
-
memory_tier: "episode",
|
|
54
|
-
scope: "project",
|
|
55
|
-
topK: 3,
|
|
56
|
-
});
|
|
57
|
-
if (entities.length === 0)
|
|
58
|
-
return "";
|
|
59
|
-
const bullets = entities
|
|
60
|
-
.map((entity) => {
|
|
61
|
-
const e = entity;
|
|
62
|
-
const meta = e.metadata ?? {};
|
|
63
|
-
const outcomeTag = meta.outcome ? `[${meta.outcome}]` : "[?]";
|
|
64
|
-
const approach = meta.approach_summary ?? "";
|
|
65
|
-
return `- ${outcomeTag} ${e.title ?? "(untitled episode)"}\n Approach: ${approach}`;
|
|
66
|
-
})
|
|
67
|
-
.join("\n");
|
|
68
|
-
return `\n\n## Similar past tasks\n${bullets}`;
|
|
69
|
-
}
|
|
70
|
-
catch (err) {
|
|
71
|
-
log.warn(TAG, "past-episodes recall failed", {
|
|
72
|
-
event: "episode_recall_failed",
|
|
73
|
-
error: err instanceof Error ? err.message : String(err),
|
|
74
|
-
});
|
|
75
|
-
return "";
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Minimal fallback prompt when the API-based generation is unavailable.
|
|
80
|
-
*/
|
|
81
|
-
function buildFallbackPrompt(enriched, branchName, worktreePath) {
|
|
82
|
-
const { card, column, labels, subtasks } = enriched;
|
|
83
|
-
const labelStr = labels.length > 0 ? labels.map((l) => l.name).join(", ") : "none";
|
|
84
|
-
const subtaskStr = subtasks.length > 0
|
|
85
|
-
? subtasks
|
|
86
|
-
.map((s) => `- [${s.completed ? "x" : " "}] ${s.title}`)
|
|
87
|
-
.join("\n")
|
|
88
|
-
: "No subtasks defined.";
|
|
89
|
-
const description = card.description?.trim() || "No description provided.";
|
|
90
|
-
return `You are an AI agent working on a task from the Harmony project board.
|
|
91
|
-
|
|
92
|
-
## Card: #${card.short_id} - ${card.title}
|
|
93
|
-
**Labels**: ${labelStr}
|
|
94
|
-
**Column**: ${column.name}
|
|
95
|
-
**Priority**: ${card.priority}
|
|
96
|
-
|
|
97
|
-
## Description
|
|
98
|
-
${description}
|
|
99
|
-
|
|
100
|
-
## Subtasks
|
|
101
|
-
${subtaskStr}
|
|
102
|
-
|
|
103
|
-
## Instructions
|
|
104
|
-
1. Read the codebase and understand the context needed for this task
|
|
105
|
-
2. Report progress via harmony_update_agent_progress at key milestones:
|
|
106
|
-
- After reading codebase and forming a plan (~20%)
|
|
107
|
-
- After each major implementation step (~30-60%)
|
|
108
|
-
- After completing each subtask (also toggle via harmony_toggle_subtask)
|
|
109
|
-
- Before committing (~65%)
|
|
110
|
-
Include a brief currentTask description.
|
|
111
|
-
3. Implement the changes on branch \`${branchName}\`
|
|
112
|
-
4. Commit your work with clear, descriptive commit messages
|
|
113
|
-
5. When finished, call harmony_end_agent_session with status="completed"
|
|
114
|
-
|
|
115
|
-
You are working in a git worktree at \`${worktreePath}\` on branch \`${branchName}\`.
|
|
116
|
-
Do NOT push to main. All your work stays on \`${branchName}\`.`;
|
|
117
|
-
}
|
package/dist/queue.d.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import type { Card, Column, Label } from "@harmony/shared";
|
|
2
|
-
import type { AgentConfig, QueueItem, WorkMode } from "./types.js";
|
|
3
|
-
/**
|
|
4
|
-
* Priority queue for cards waiting to be worked on.
|
|
5
|
-
* Sorted by: label priority boost > column position boost > enqueue time (FIFO).
|
|
6
|
-
*/
|
|
7
|
-
export declare class PriorityQueue {
|
|
8
|
-
private config;
|
|
9
|
-
private items;
|
|
10
|
-
constructor(config: AgentConfig);
|
|
11
|
-
/**
|
|
12
|
-
* Calculate priority score for a card.
|
|
13
|
-
*/
|
|
14
|
-
scoreCard(_card: Card, column: Column, labels: Label[]): number;
|
|
15
|
-
/**
|
|
16
|
-
* Add a card to the queue. If already present, update its priority.
|
|
17
|
-
*/
|
|
18
|
-
enqueue(card: Card, column: Column, labels: Label[], mode?: WorkMode): void;
|
|
19
|
-
/**
|
|
20
|
-
* Remove and return the highest-priority item.
|
|
21
|
-
*/
|
|
22
|
-
dequeue(): QueueItem | null;
|
|
23
|
-
/**
|
|
24
|
-
* Remove a specific card from the queue.
|
|
25
|
-
*/
|
|
26
|
-
remove(cardId: string): QueueItem | null;
|
|
27
|
-
/**
|
|
28
|
-
* Check if a card is in the queue.
|
|
29
|
-
*/
|
|
30
|
-
has(cardId: string): boolean;
|
|
31
|
-
/**
|
|
32
|
-
* Get all queued card IDs.
|
|
33
|
-
*/
|
|
34
|
-
cardIds(): string[];
|
|
35
|
-
get length(): number;
|
|
36
|
-
peek(): QueueItem | null;
|
|
37
|
-
/** Copy of the queue in priority order (for introspection). */
|
|
38
|
-
snapshot(): QueueItem[];
|
|
39
|
-
}
|