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