@gethmy/agent 1.0.7 → 1.0.8

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,246 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ // Dynamic import so we can test the module without triggering side effects
4
+ // from other imports (log, etc.)
5
+ let ProgressTracker;
6
+ // Minimal mock of HarmonyApiClient
7
+ function makeMockClient() {
8
+ return {
9
+ updateAgentProgress: vi.fn().mockResolvedValue(undefined),
10
+ flushActivityLog: vi.fn().mockResolvedValue(undefined),
11
+ startAgentSession: vi.fn().mockResolvedValue(undefined),
12
+ endAgentSession: vi.fn().mockResolvedValue(undefined),
13
+ };
14
+ }
15
+ // Mock StreamParser — just an EventEmitter with the same typed interface
16
+ class MockParser extends EventEmitter {
17
+ emitToolStart(name, input) {
18
+ this.emit("tool_start", name, input);
19
+ }
20
+ emitToolEnd(name, toolUseId, content) {
21
+ this.emit("tool_end", name, toolUseId, content);
22
+ }
23
+ emitText(content) {
24
+ this.emit("text", content);
25
+ }
26
+ }
27
+ // Access private fields via type cast
28
+ function getPrivate(tracker) {
29
+ return tracker;
30
+ }
31
+ beforeEach(async () => {
32
+ // Reset module to get fresh class each test (avoids timer leakage between tests)
33
+ const mod = await import("../progress-tracker.js");
34
+ ProgressTracker = mod.ProgressTracker;
35
+ });
36
+ afterEach(() => {
37
+ vi.restoreAllMocks();
38
+ });
39
+ describe("ProgressTracker", () => {
40
+ describe("setSessionId", () => {
41
+ it("stores the session ID", () => {
42
+ const client = makeMockClient();
43
+ const tracker = new ProgressTracker(client, "card-1", 0, []);
44
+ tracker.stop(); // prevent heartbeat
45
+ expect(getPrivate(tracker).sessionId).toBeNull();
46
+ tracker.setSessionId("session-abc");
47
+ expect(getPrivate(tracker).sessionId).toBe("session-abc");
48
+ });
49
+ it("triggers flush with correct sessionId when sendUpdate is called", async () => {
50
+ const client = makeMockClient();
51
+ const tracker = new ProgressTracker(client, "card-1", 0, []);
52
+ tracker.stop();
53
+ tracker.setSessionId("session-xyz");
54
+ // Push an entry manually so flush has something to send
55
+ getPrivate(tracker).logBuffer.push({
56
+ phase: "exploring",
57
+ eventType: "tool_start",
58
+ toolName: "Read",
59
+ description: "Reading src/foo.ts",
60
+ metadata: {},
61
+ createdAt: new Date().toISOString(),
62
+ });
63
+ // Trigger sendUpdate by calling the private method indirectly
64
+ // (Force lastUpdateAt to 0 so throttle passes)
65
+ tracker.lastUpdateAt = 0;
66
+ tracker.sendUpdate("test task");
67
+ await vi.waitFor(() => {
68
+ expect(client.flushActivityLog).toHaveBeenCalledWith("card-1", expect.objectContaining({ sessionId: "session-xyz" }));
69
+ });
70
+ });
71
+ });
72
+ describe("tool_start events", () => {
73
+ it("creates a log entry in the buffer when tool has a description", () => {
74
+ const client = makeMockClient();
75
+ const tracker = new ProgressTracker(client, "card-2", 0, []);
76
+ tracker.stop();
77
+ const parser = new MockParser();
78
+ tracker.attach(parser);
79
+ parser.emitToolStart("Read", { file_path: "/src/foo.ts" });
80
+ const buf = getPrivate(tracker).logBuffer;
81
+ const entry = buf.find((e) => e.eventType === "tool_start");
82
+ expect(entry).toBeDefined();
83
+ expect(entry?.toolName).toBe("Read");
84
+ expect(entry?.description).toMatch(/Reading/);
85
+ expect(entry?.createdAt).toBeDefined();
86
+ });
87
+ it("does not create a log entry for tools with no description", () => {
88
+ const client = makeMockClient();
89
+ const tracker = new ProgressTracker(client, "card-2", 0, []);
90
+ tracker.stop();
91
+ const parser = new MockParser();
92
+ tracker.attach(parser);
93
+ // Unknown tool with no special description logic
94
+ parser.emitToolStart("SomeObscureUnknownTool", {});
95
+ const buf = getPrivate(tracker).logBuffer;
96
+ const entry = buf.find((e) => e.eventType === "tool_start" && e.toolName === "SomeObscureUnknownTool");
97
+ expect(entry).toBeUndefined();
98
+ });
99
+ });
100
+ describe("tool_end events", () => {
101
+ it("creates a log entry for every tool_end event", () => {
102
+ const client = makeMockClient();
103
+ const tracker = new ProgressTracker(client, "card-3", 0, []);
104
+ tracker.stop();
105
+ const parser = new MockParser();
106
+ tracker.attach(parser);
107
+ parser.emitToolEnd("Bash", "tool-use-1", "exit 0");
108
+ const buf = getPrivate(tracker).logBuffer;
109
+ const entry = buf.find((e) => e.eventType === "tool_end" && e.toolName === "Bash");
110
+ expect(entry).toBeDefined();
111
+ expect(entry?.description).toContain("Completed");
112
+ });
113
+ });
114
+ describe("buffer cap", () => {
115
+ it("caps buffer at 500 entries after pushing 510", () => {
116
+ const client = makeMockClient();
117
+ const tracker = new ProgressTracker(client, "card-4", 0, []);
118
+ tracker.stop();
119
+ const parser = new MockParser();
120
+ tracker.attach(parser);
121
+ // Emit 510 tool_end events (tool_end always creates an entry)
122
+ for (let i = 0; i < 510; i++) {
123
+ parser.emitToolEnd("Read", `id-${i}`, undefined);
124
+ }
125
+ const buf = getPrivate(tracker).logBuffer;
126
+ expect(buf.length).toBeLessThanOrEqual(500);
127
+ });
128
+ it("keeps the most recent entries when the buffer overflows", () => {
129
+ const client = makeMockClient();
130
+ const tracker = new ProgressTracker(client, "card-4b", 0, []);
131
+ tracker.stop();
132
+ const parser = new MockParser();
133
+ tracker.attach(parser);
134
+ for (let i = 0; i < 510; i++) {
135
+ parser.emitToolEnd(`Tool${i}`, `id-${i}`, undefined);
136
+ }
137
+ const buf = getPrivate(tracker).logBuffer;
138
+ // The oldest entries (0–9) should have been shifted out
139
+ expect(buf[0].toolName).not.toBe("Tool0");
140
+ });
141
+ });
142
+ describe("flushActivityLog", () => {
143
+ it("clears the buffer on successful flush", async () => {
144
+ const client = makeMockClient();
145
+ const tracker = new ProgressTracker(client, "card-5", 0, []);
146
+ tracker.stop();
147
+ tracker.setSessionId("session-flush");
148
+ getPrivate(tracker).logBuffer.push({
149
+ phase: "exploring",
150
+ eventType: "tool_end",
151
+ toolName: "Read",
152
+ description: "Completed: Read",
153
+ metadata: {},
154
+ createdAt: new Date().toISOString(),
155
+ });
156
+ // Invoke the private method
157
+ tracker.flushActivityLog();
158
+ // Wait for the async client call to resolve
159
+ await vi.waitFor(() => {
160
+ expect(client.flushActivityLog).toHaveBeenCalled();
161
+ });
162
+ // Buffer should be empty after successful flush
163
+ expect(getPrivate(tracker).logBuffer).toHaveLength(0);
164
+ });
165
+ it("retains buffer entries on flush error", async () => {
166
+ const client = makeMockClient();
167
+ client.flushActivityLog.mockRejectedValue(new Error("network error"));
168
+ const tracker = new ProgressTracker(client, "card-6", 0, []);
169
+ tracker.stop();
170
+ tracker.setSessionId("session-retry");
171
+ getPrivate(tracker).logBuffer.push({
172
+ phase: "exploring",
173
+ eventType: "tool_end",
174
+ toolName: "Read",
175
+ description: "Completed: Read",
176
+ metadata: {},
177
+ createdAt: new Date().toISOString(),
178
+ });
179
+ tracker.flushActivityLog();
180
+ await vi.waitFor(() => {
181
+ // After the rejected promise is caught, entries should be back in buffer
182
+ expect(getPrivate(tracker).logBuffer.length).toBeGreaterThan(0);
183
+ });
184
+ });
185
+ it("skips flush when no session ID is set", () => {
186
+ const client = makeMockClient();
187
+ const tracker = new ProgressTracker(client, "card-7", 0, []);
188
+ tracker.stop();
189
+ // No setSessionId called
190
+ getPrivate(tracker).logBuffer.push({
191
+ phase: "exploring",
192
+ eventType: "tool_end",
193
+ toolName: "Read",
194
+ description: "Completed: Read",
195
+ metadata: {},
196
+ createdAt: new Date().toISOString(),
197
+ });
198
+ tracker.flushActivityLog();
199
+ // Should not have called the client
200
+ expect(client.flushActivityLog).not.toHaveBeenCalled();
201
+ // Buffer should still contain the entry
202
+ expect(getPrivate(tracker).logBuffer).toHaveLength(1);
203
+ });
204
+ it("skips flush when buffer is empty", () => {
205
+ const client = makeMockClient();
206
+ const tracker = new ProgressTracker(client, "card-8", 0, []);
207
+ tracker.stop();
208
+ tracker.setSessionId("session-empty");
209
+ // logBuffer is empty by default
210
+ tracker.flushActivityLog();
211
+ expect(client.flushActivityLog).not.toHaveBeenCalled();
212
+ });
213
+ });
214
+ describe("sendUpdate payload", () => {
215
+ it("does not include recentActions in the updateAgentProgress payload", async () => {
216
+ const client = makeMockClient();
217
+ const tracker = new ProgressTracker(client, "card-9", 0, []);
218
+ tracker.stop();
219
+ tracker.lastUpdateAt = 0;
220
+ tracker.sendUpdate("doing stuff");
221
+ await vi.waitFor(() => {
222
+ expect(client.updateAgentProgress).toHaveBeenCalled();
223
+ });
224
+ const [, payload] = client.updateAgentProgress.mock.calls[0];
225
+ expect(payload).not.toHaveProperty("recentActions");
226
+ });
227
+ it("includes expected fields in updateAgentProgress payload", async () => {
228
+ const client = makeMockClient();
229
+ const tracker = new ProgressTracker(client, "card-10", 0, []);
230
+ tracker.stop();
231
+ tracker.lastUpdateAt = 0;
232
+ tracker.sendUpdate("building feature");
233
+ await vi.waitFor(() => {
234
+ expect(client.updateAgentProgress).toHaveBeenCalled();
235
+ });
236
+ const [cardId, payload] = client.updateAgentProgress.mock.calls[0];
237
+ expect(cardId).toBe("card-10");
238
+ expect(payload).toMatchObject({
239
+ status: "working",
240
+ currentTask: expect.any(String),
241
+ progressPercent: expect.any(Number),
242
+ phase: expect.any(String),
243
+ });
244
+ });
245
+ });
246
+ });
@@ -1,5 +1,13 @@
1
1
  import type { HarmonyApiClient } from "@gethmy/mcp/src/api-client.js";
2
2
  import type { CostUpdate, StreamParser } from "./stream-parser.js";
3
+ export interface ActivityLogEntry {
4
+ phase: string | null;
5
+ eventType: "tool_start" | "tool_end" | "phase_change" | "error" | "summary";
6
+ toolName: string | null;
7
+ description: string;
8
+ metadata: Record<string, unknown>;
9
+ createdAt: string;
10
+ }
3
11
  export declare class ProgressTracker {
4
12
  private client;
5
13
  private cardId;
@@ -20,10 +28,12 @@ export declare class ProgressTracker {
20
28
  private filesEdited;
21
29
  private filesRead;
22
30
  private lastCost;
23
- private recentActions;
31
+ private logBuffer;
32
+ private sessionId;
24
33
  constructor(client: HarmonyApiClient, cardId: string, workerId: number, subtasks: {
25
34
  completed: boolean;
26
35
  }[]);
36
+ setSessionId(id: string): void;
27
37
  /**
28
38
  * Wire up the parser events and start the heartbeat.
29
39
  */
@@ -54,9 +64,12 @@ export declare class ProgressTracker {
54
64
  */
55
65
  private shortPath;
56
66
  private scheduleUpdate;
57
- private pushRecentAction;
58
67
  private sendUpdate;
59
68
  private startHeartbeat;
69
+ flushFinal(): void;
70
+ private pushLogEntry;
71
+ private flushActivityLog;
72
+ private extractToolMetadata;
60
73
  /**
61
74
  * Safely extract a string property from an unknown tool input.
62
75
  */
@@ -4,7 +4,7 @@ const TAG = "progress-tracker";
4
4
  const THROTTLE_MS = 5_000;
5
5
  const HEARTBEAT_MS = 60_000;
6
6
  const MAX_TASK_LENGTH = 120;
7
- const MAX_RECENT_ACTIONS = 5;
7
+ const MAX_LOG_BUFFER = 500;
8
8
  // Hoisted regexes — avoids recompilation on every call
9
9
  const SENTENCE_SPLIT = /\.\s|\n/;
10
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;
@@ -60,7 +60,8 @@ export class ProgressTracker {
60
60
  filesEdited = new Set();
61
61
  filesRead = new Set();
62
62
  lastCost = null;
63
- recentActions = [];
63
+ logBuffer = [];
64
+ sessionId = null;
64
65
  constructor(client, cardId, workerId, subtasks) {
65
66
  this.client = client;
66
67
  this.cardId = cardId;
@@ -69,15 +70,35 @@ export class ProgressTracker {
69
70
  this.subtaskCompleted = subtasks.filter((s) => s.completed).length;
70
71
  this.subtaskMode = subtasks.length > 0;
71
72
  }
73
+ setSessionId(id) {
74
+ this.sessionId = id;
75
+ }
72
76
  /**
73
77
  * Wire up the parser events and start the heartbeat.
74
78
  */
75
79
  attach(parser) {
76
80
  parser.on("tool_start", (name, input) => {
77
81
  this.onToolStart(name, input);
82
+ const desc = this.describeToolAction(name, input);
83
+ if (desc) {
84
+ this.pushLogEntry({
85
+ phase: this.phase,
86
+ eventType: "tool_start",
87
+ toolName: name,
88
+ description: desc,
89
+ metadata: this.extractToolMetadata(name, input),
90
+ });
91
+ }
78
92
  });
79
93
  parser.on("tool_end", (name, _id, content) => {
80
94
  this.onToolEnd(name, content);
95
+ this.pushLogEntry({
96
+ phase: this.phase,
97
+ eventType: "tool_end",
98
+ toolName: name,
99
+ description: `Completed: ${name}`,
100
+ metadata: {},
101
+ });
81
102
  });
82
103
  parser.on("text", (content) => {
83
104
  this.onText(content);
@@ -159,7 +180,6 @@ export class ProgressTracker {
159
180
  const action = this.describeToolAction(name, input);
160
181
  if (action) {
161
182
  this.lastAction = action;
162
- this.pushRecentAction(action);
163
183
  }
164
184
  this.incrementProgress();
165
185
  }
@@ -202,6 +222,13 @@ export class ProgressTracker {
202
222
  this.progress = Math.max(this.progress, PHASES[newPhase].min);
203
223
  // Reset stale action from prior phase; new phase starts with its own label
204
224
  this.lastAction = "";
225
+ this.pushLogEntry({
226
+ phase: newPhase,
227
+ eventType: "phase_change",
228
+ toolName: null,
229
+ description: `Entering ${newPhase} phase`,
230
+ metadata: {},
231
+ });
205
232
  this.scheduleUpdate(PHASES[newPhase].label);
206
233
  }
207
234
  incrementProgress() {
@@ -307,15 +334,6 @@ export class ProgressTracker {
307
334
  }
308
335
  // If there's already a pending update, pendingTask is now updated — it will use the fresh value
309
336
  }
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
- }
318
- }
319
337
  sendUpdate(currentTask) {
320
338
  this.lastUpdateAt = Date.now();
321
339
  log.debug(TAG, `Progress: ${this.progress}% — ${currentTask}`);
@@ -329,11 +347,11 @@ export class ProgressTracker {
329
347
  phase: this.phase,
330
348
  filesChanged: this.filesEdited.size,
331
349
  costCents: Math.round((this.lastCost?.totalCostUsd ?? 0) * 100),
332
- recentActions: this.recentActions,
333
350
  })
334
351
  .catch((err) => {
335
352
  log.warn(TAG, `Failed to send progress update: ${err}`);
336
353
  });
354
+ this.flushActivityLog();
337
355
  }
338
356
  startHeartbeat() {
339
357
  if (this.heartbeatTimer) {
@@ -349,6 +367,57 @@ export class ProgressTracker {
349
367
  }
350
368
  }, HEARTBEAT_MS);
351
369
  }
370
+ flushFinal() {
371
+ this.flushActivityLog();
372
+ }
373
+ pushLogEntry(entry) {
374
+ this.logBuffer.push({
375
+ ...entry,
376
+ createdAt: new Date().toISOString(),
377
+ });
378
+ if (this.logBuffer.length > MAX_LOG_BUFFER) {
379
+ this.logBuffer.shift();
380
+ }
381
+ }
382
+ flushActivityLog() {
383
+ if (!this.sessionId || this.logBuffer.length === 0)
384
+ return;
385
+ const raw = [...this.logBuffer];
386
+ this.logBuffer = [];
387
+ this.client
388
+ .flushActivityLog(this.cardId, {
389
+ sessionId: this.sessionId,
390
+ entries: raw.map((e) => ({
391
+ ...e,
392
+ phase: e.phase ?? undefined,
393
+ toolName: e.toolName ?? undefined,
394
+ })),
395
+ })
396
+ .catch((err) => {
397
+ log.warn(TAG, `Failed to flush activity log: ${err}`);
398
+ // Put entries back at the front of the buffer for retry
399
+ this.logBuffer.unshift(...raw);
400
+ if (this.logBuffer.length > MAX_LOG_BUFFER) {
401
+ this.logBuffer.length = MAX_LOG_BUFFER;
402
+ }
403
+ });
404
+ }
405
+ extractToolMetadata(name, input) {
406
+ const meta = {};
407
+ const fp = this.extractString(input, "file_path");
408
+ if (fp)
409
+ meta.file_path = fp;
410
+ const cmd = this.extractString(input, "command");
411
+ if (cmd)
412
+ meta.command = cmd.split("\n")[0].slice(0, 200);
413
+ const pattern = this.extractString(input, "pattern");
414
+ if (pattern)
415
+ meta.pattern = pattern;
416
+ const desc = this.extractString(input, "description");
417
+ if (desc)
418
+ meta.description = desc;
419
+ return meta;
420
+ }
352
421
  /**
353
422
  * Safely extract a string property from an unknown tool input.
354
423
  */
package/dist/watcher.d.ts CHANGED
@@ -20,7 +20,9 @@ export declare class Watcher {
20
20
  private onCardBroadcast;
21
21
  private onAgentCommand?;
22
22
  private channel;
23
+ private presenceChannel;
23
24
  private supabase;
25
+ private daemonId;
24
26
  constructor(credentials: RealtimeCredentials, projectId: string, onCardBroadcast: CardBroadcastHandler, onAgentCommand?: AgentCommandHandler | undefined);
25
27
  start(): Promise<void>;
26
28
  stop(): Promise<void>;
package/dist/watcher.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { createClient } from "@supabase/supabase-js";
2
3
  import { log } from "./log.js";
3
4
  const TAG = "watcher";
@@ -12,7 +13,9 @@ export class Watcher {
12
13
  onCardBroadcast;
13
14
  onAgentCommand;
14
15
  channel = null;
16
+ presenceChannel = null;
15
17
  supabase = null;
18
+ daemonId = randomUUID();
16
19
  constructor(credentials, projectId, onCardBroadcast, onAgentCommand) {
17
20
  this.credentials = credentials;
18
21
  this.projectId = projectId;
@@ -22,6 +25,9 @@ export class Watcher {
22
25
  async start() {
23
26
  log.info(TAG, "Connecting to Supabase realtime (broadcast)...");
24
27
  this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
28
+ // Presence channel — separate from the broadcast channel to avoid
29
+ // conflicting with frontend BoardContext's board-{projectId} subscription
30
+ const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
25
31
  const channel = this.supabase
26
32
  .channel(`board-${this.projectId}`)
27
33
  .on("broadcast", { event: "card_update" }, (msg) => {
@@ -59,8 +65,27 @@ export class Watcher {
59
65
  }
60
66
  });
61
67
  this.channel = channel;
68
+ // Subscribe presence channel for daemon online indicator
69
+ presenceChannel
70
+ .on("presence", { event: "sync" }, () => {
71
+ log.debug(TAG, "Presence sync");
72
+ })
73
+ .subscribe(async (status) => {
74
+ if (status === "SUBSCRIBED") {
75
+ await presenceChannel.track({
76
+ daemonId: this.daemonId,
77
+ startedAt: new Date().toISOString(),
78
+ });
79
+ log.info(TAG, "Presence tracked on board-presence channel");
80
+ }
81
+ });
82
+ this.presenceChannel = presenceChannel;
62
83
  }
63
84
  async stop() {
85
+ if (this.presenceChannel) {
86
+ await this.supabase?.removeChannel(this.presenceChannel);
87
+ this.presenceChannel = null;
88
+ }
64
89
  if (this.channel) {
65
90
  await this.supabase?.removeChannel(this.channel);
66
91
  this.channel = null;
package/dist/worker.d.ts CHANGED
@@ -18,6 +18,7 @@ export declare class Worker {
18
18
  private progressTracker;
19
19
  private lastSessionStats;
20
20
  private aborted;
21
+ private sessionId;
21
22
  constructor(id: number, config: AgentConfig, client: HarmonyApiClient, _userEmail: string, onDone: (worker: Worker) => void, workspaceId: string, projectId: string);
22
23
  get tag(): string;
23
24
  get isIdle(): boolean;
package/dist/worker.js CHANGED
@@ -27,6 +27,7 @@ export class Worker {
27
27
  progressTracker = null;
28
28
  lastSessionStats;
29
29
  aborted = false;
30
+ sessionId = null;
30
31
  constructor(id, config, client, _userEmail, onDone, workspaceId, projectId) {
31
32
  this.config = config;
32
33
  this.client = client;
@@ -61,13 +62,20 @@ export class Worker {
61
62
  this.branchName = makeBranchName(card.short_id, card.title);
62
63
  log.info(this.tag, `Preparing #${card.short_id} "${card.title}"`);
63
64
  // Start agent session and make it visible on the board
64
- await this.client.startAgentSession(card.id, {
65
+ const { session } = await this.client.startAgentSession(card.id, {
65
66
  agentIdentifier: agentIdentifier(this.id),
66
67
  agentName: AGENT_NAME,
67
68
  status: "working",
68
69
  currentTask: "Setting up worktree",
69
70
  progressPercent: 5,
70
71
  });
72
+ const sid = session && typeof session === "object" && "id" in session
73
+ ? session.id
74
+ : null;
75
+ if (!sid) {
76
+ log.warn(TAG, "startAgentSession returned no session id");
77
+ }
78
+ this.sessionId = sid;
71
79
  // Move card to "In Progress" and add "agent" label so the board shows the progress ring
72
80
  const moved = await moveCardAndAddLabel(this.client, card, "In Progress", "agent");
73
81
  if (!moved) {
@@ -256,6 +264,9 @@ export class Worker {
256
264
  const parser = new StreamParser();
257
265
  // Progress tracker for phase-based updates
258
266
  this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks);
267
+ if (this.sessionId) {
268
+ this.progressTracker.setSessionId(this.sessionId);
269
+ }
259
270
  this.progressTracker.attach(parser);
260
271
  // Attach stdout to parser
261
272
  if (this.process.stdout) {
@@ -274,6 +285,7 @@ export class Worker {
274
285
  this.process.on("close", (code) => {
275
286
  this.process = null;
276
287
  this.lastSessionStats = this.progressTracker?.stats;
288
+ this.progressTracker?.flushFinal();
277
289
  this.progressTracker?.stop();
278
290
  this.progressTracker = null;
279
291
  if (this.aborted) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/agent",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "Push-based agent daemon for Harmony — watches board assignments and spawns Claude CLI workers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -40,7 +40,8 @@
40
40
  "prebuild": "cd ../harmony-shared && bun run build && cd ../memory && bun run build",
41
41
  "build": "tsc",
42
42
  "typecheck": "tsc --noEmit",
43
- "prepublishOnly": "npm run build"
43
+ "prepublishOnly": "npm run build",
44
+ "test": "vitest run"
44
45
  },
45
46
  "dependencies": {
46
47
  "@supabase/supabase-js": "2.95.3",
@@ -49,6 +50,7 @@
49
50
  "devDependencies": {
50
51
  "@harmony/shared": "workspace:*",
51
52
  "@types/node": "^25.5.0",
52
- "typescript": "^6.0.1"
53
+ "typescript": "^6.0.1",
54
+ "vitest": "^3.2.1"
53
55
  }
54
56
  }