@gethmy/agent 1.7.1 → 1.7.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.
Files changed (77) hide show
  1. package/dist/cli.js +6386 -141
  2. package/dist/index.js +6216 -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
package/dist/types.js DELETED
@@ -1,76 +0,0 @@
1
- export const DEFAULT_AGENT_CONFIG = {
2
- poolSize: 3,
3
- maxTimeout: 1_800_000, // 30 minutes
4
- pickupColumns: ["To Do"],
5
- priorityLabels: { urgent: 100, critical: 90, bug: 50 },
6
- columnBoost: true,
7
- completion: {
8
- createPR: false,
9
- moveToColumn: "Review",
10
- postSummary: true,
11
- },
12
- claude: {
13
- model: "opus",
14
- reviewModel: "sonnet",
15
- maxTurns: 200,
16
- additionalArgs: [],
17
- },
18
- worktree: {
19
- basePath: ".harmony-worktrees",
20
- baseBranch: "main",
21
- failedBranchPrefix: "agent-attempts/",
22
- approvedBranchPrefix: "agent/",
23
- failedAttemptRetentionDays: 7,
24
- },
25
- verification: {
26
- enabled: true,
27
- build: true,
28
- lint: true,
29
- autoFix: true,
30
- maxFixAttempts: 1,
31
- deepReview: false,
32
- devServerBasePort: 4200,
33
- timeout: 120_000,
34
- failColumn: "To Do",
35
- },
36
- review: {
37
- enabled: true,
38
- poolSize: 2,
39
- pickupColumns: ["Review"],
40
- moveToColumn: "Done",
41
- failColumn: "To Do",
42
- devServerPort: 4300,
43
- maxTimeout: 600_000,
44
- postFindings: true,
45
- maxReviewCycles: 3,
46
- createPR: true,
47
- approvedLabel: "Ready to Merge",
48
- approvedLabelColor: "#22c55e",
49
- mergeMonitor: true,
50
- mergedLabel: "Merged",
51
- mergedLabelColor: "#6366f1",
52
- },
53
- budget: {
54
- maxAttemptsPerCard: 3,
55
- dailyBudgetCents: 5000, // $50.00
56
- },
57
- http: {
58
- enabled: true,
59
- port: 47821,
60
- bindAddr: "127.0.0.1",
61
- },
62
- timing: {
63
- heartbeatMs: 30_000,
64
- staleHeartbeatMs: 120_000,
65
- reconcileIntervalMs: 60_000,
66
- worktreeGcIntervalMs: 5 * 60_000,
67
- },
68
- };
69
- // ============ LABELS ============
70
- export const NEED_REVIEW_LABEL = "Need Review";
71
- export const NEED_REVIEW_LABEL_COLOR = "#f59e0b";
72
- // ============ AGENT IDENTITY ============
73
- export const AGENT_NAME = "Harmony Agent";
74
- export function agentIdentifier(workerId) {
75
- return `harmony-daemon-${workerId}`;
76
- }
@@ -1,39 +0,0 @@
1
- import { type ChildProcess } from "node:child_process";
2
- import type { HarmonyApiClient } from "@gethmy/mcp/src/api-client.js";
3
- import type { AgentConfig } from "./types.js";
4
- export interface VerificationResult {
5
- passed: boolean;
6
- buildErrors: string[];
7
- lintWarnings: string[];
8
- reviewFindings: string[];
9
- }
10
- export declare function runVerification(worktreePath: string, config: AgentConfig, workerId: number): Promise<VerificationResult>;
11
- export declare function runBuild(worktreePath: string, timeout: number): string[];
12
- export declare function runLint(worktreePath: string, timeout: number): string[];
13
- export declare function runDeepReview(worktreePath: string, config: AgentConfig, workerId: number): Promise<string[]>;
14
- export declare function attemptAutoFix(worktreePath: string, config: AgentConfig, errors: string[]): void;
15
- export interface RecoveryInfo {
16
- /** Remote ref where the failed attempt was pushed. */
17
- branchName: string;
18
- /** Public URL of the branch (GitHub/GitLab/Bitbucket tree view), if known. */
19
- branchUrl: string | null;
20
- }
21
- export declare function reportFindings(client: HarmonyApiClient, cardId: string, result: VerificationResult, recovery?: RecoveryInfo | null): Promise<void>;
22
- export declare class DevServerReadinessError extends Error {
23
- constructor(message: string);
24
- }
25
- /**
26
- * Wait for a dev server to signal readiness on stdout/stderr.
27
- *
28
- * Rejects (does NOT resolve) on timeout or process error — callers that
29
- * need the server to be live for correctness (e.g. the review worker)
30
- * must not proceed without a confirmed signal. If the server dies before
31
- * becoming ready, we reject with the exit details.
32
- */
33
- export declare function waitForDevServer(proc: ChildProcess, timeout: number): Promise<void>;
34
- /**
35
- * Verify the dev server actually responds to HTTP GET before treating
36
- * it as ready. Listening alone isn't enough — a framework can bind but
37
- * still crash on the first request.
38
- */
39
- export declare function probeDevServer(port: number, timeoutMs?: number): Promise<void>;
@@ -1,317 +0,0 @@
1
- import { execFileSync, spawn } from "node:child_process";
2
- import { log } from "./log.js";
3
- import { spawnRunArgs } from "./pm.js";
4
- const TAG = "verification";
5
- // ============ PUBLIC API ============
6
- export async function runVerification(worktreePath, config, workerId) {
7
- const result = {
8
- passed: true,
9
- buildErrors: [],
10
- lintWarnings: [],
11
- reviewFindings: [],
12
- };
13
- if (config.verification.build) {
14
- log.info(TAG, `[worker:${workerId}] Running build...`);
15
- result.buildErrors = runBuild(worktreePath, config.verification.timeout);
16
- if (result.buildErrors.length > 0) {
17
- log.warn(TAG, `[worker:${workerId}] Build failed with ${result.buildErrors.length} error(s)`);
18
- result.passed = false;
19
- }
20
- else {
21
- log.info(TAG, `[worker:${workerId}] Build passed`);
22
- }
23
- }
24
- if (config.verification.lint) {
25
- log.info(TAG, `[worker:${workerId}] Running lint...`);
26
- result.lintWarnings = runLint(worktreePath, config.verification.timeout);
27
- if (result.lintWarnings.length > 0) {
28
- log.warn(TAG, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
29
- // Lint warnings alone don't block — only build errors block
30
- }
31
- else {
32
- log.info(TAG, `[worker:${workerId}] Lint passed`);
33
- }
34
- }
35
- if (config.verification.deepReview) {
36
- log.info(TAG, `[worker:${workerId}] Running deep review...`);
37
- result.reviewFindings = await runDeepReview(worktreePath, config, workerId);
38
- if (result.reviewFindings.length > 0) {
39
- log.warn(TAG, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
40
- }
41
- else {
42
- log.info(TAG, `[worker:${workerId}] Deep review passed`);
43
- }
44
- }
45
- return result;
46
- }
47
- export function runBuild(worktreePath, timeout) {
48
- try {
49
- const [cmd, args] = spawnRunArgs("build");
50
- execFileSync(cmd, args, {
51
- cwd: worktreePath,
52
- timeout,
53
- stdio: "pipe",
54
- });
55
- return [];
56
- }
57
- catch (err) {
58
- return parseErrorOutput(err);
59
- }
60
- }
61
- export function runLint(worktreePath, timeout) {
62
- try {
63
- const [cmd, args] = spawnRunArgs("lint");
64
- execFileSync(cmd, args, {
65
- cwd: worktreePath,
66
- timeout,
67
- stdio: "pipe",
68
- });
69
- return [];
70
- }
71
- catch (err) {
72
- return parseErrorOutput(err);
73
- }
74
- }
75
- export async function runDeepReview(worktreePath, config, workerId) {
76
- const port = config.verification.devServerBasePort + workerId;
77
- let devServer = null;
78
- try {
79
- // Start dev server in background
80
- const [cmd, args] = spawnRunArgs("dev", "--port", String(port));
81
- devServer = spawn(cmd, args, {
82
- cwd: worktreePath,
83
- stdio: ["ignore", "pipe", "pipe"],
84
- });
85
- // Wait for dev server to be ready, then confirm it answers HTTP.
86
- try {
87
- await waitForDevServer(devServer, 30_000);
88
- await probeDevServer(port);
89
- }
90
- catch (err) {
91
- log.error(TAG, `Dev server did not become ready: ${err instanceof Error ? err.message : err}`);
92
- return [];
93
- }
94
- // Get diff for review context
95
- let diff = "";
96
- try {
97
- diff = execFileSync("git", ["diff", `origin/${config.worktree.baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8", timeout: 30_000 });
98
- }
99
- catch {
100
- diff = "(unable to retrieve diff)";
101
- }
102
- // Spawn Claude for review
103
- const reviewPrompt = [
104
- "You are reviewing code changes for quality and correctness.",
105
- `A dev server is running at http://localhost:${port}.`,
106
- "Review the following diff and report any issues found.",
107
- "Output ONLY a numbered list of findings, one per line.",
108
- "If no issues, output: No issues found.",
109
- "",
110
- "```diff",
111
- diff.slice(0, 50_000),
112
- "```",
113
- ].join("\n");
114
- const output = execFileSync("claude", ["--print", "--model", "sonnet", "--max-turns", "10", "--", reviewPrompt], {
115
- cwd: worktreePath,
116
- encoding: "utf-8",
117
- timeout: config.verification.timeout,
118
- stdio: "pipe",
119
- });
120
- return parseReviewFindings(output);
121
- }
122
- catch (err) {
123
- log.error(TAG, `Deep review failed: ${err instanceof Error ? err.message : err}`);
124
- return [];
125
- }
126
- finally {
127
- if (devServer && !devServer.killed) {
128
- devServer.kill("SIGTERM");
129
- }
130
- }
131
- }
132
- export function attemptAutoFix(worktreePath, config, errors) {
133
- const errorSummary = errors.slice(0, 20).join("\n");
134
- const fixPrompt = [
135
- "The following build/lint errors were found after implementing a feature.",
136
- "Fix the source files to resolve these errors.",
137
- "Do NOT commit build artifacts or modify files in dist/.",
138
- "Fix source files only.",
139
- "",
140
- "Errors:",
141
- "```",
142
- errorSummary,
143
- "```",
144
- ].join("\n");
145
- const args = [
146
- "--print",
147
- "--model",
148
- config.claude.model,
149
- "--max-turns",
150
- "50",
151
- "--allowedTools",
152
- "Bash,Read,Write,Edit,Glob,Grep",
153
- "--",
154
- fixPrompt,
155
- ];
156
- log.info(TAG, "Spawning Claude for auto-fix...");
157
- execFileSync("claude", args, {
158
- cwd: worktreePath,
159
- timeout: config.verification.timeout,
160
- stdio: "pipe",
161
- });
162
- }
163
- export async function reportFindings(client, cardId, result, recovery) {
164
- const items = [];
165
- if (recovery) {
166
- const cmd = `git fetch && git checkout ${recovery.branchName}`;
167
- const url = recovery.branchUrl ? ` (${recovery.branchUrl})` : "";
168
- items.push(`Recovery: \`${cmd}\`${url}`);
169
- }
170
- for (const err of result.buildErrors) {
171
- items.push(`Build: ${err}`);
172
- }
173
- for (const err of result.lintWarnings) {
174
- items.push(`Lint: ${err}`);
175
- }
176
- for (const finding of result.reviewFindings) {
177
- items.push(`Review: ${finding}`);
178
- }
179
- const maxSubtasks = 10;
180
- const overflow = items.length - maxSubtasks;
181
- const toCreate = items.slice(0, maxSubtasks);
182
- await Promise.all(toCreate.map(async (item) => {
183
- const title = item.length > 120 ? `${item.slice(0, 117)}...` : item;
184
- try {
185
- await client.createSubtask(cardId, title);
186
- }
187
- catch (err) {
188
- log.error(TAG, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
189
- }
190
- }));
191
- if (overflow > 0) {
192
- try {
193
- await client.createSubtask(cardId, `...and ${overflow} more issues`);
194
- }
195
- catch {
196
- // best-effort
197
- }
198
- }
199
- log.info(TAG, `Reported ${Math.min(items.length, maxSubtasks)} finding(s) as subtasks on card ${cardId}`);
200
- }
201
- // ============ HELPERS ============
202
- function parseErrorOutput(err) {
203
- const stderr = err?.stderr?.toString() ?? "";
204
- const stdout = err?.stdout?.toString() ?? "";
205
- const combined = `${stderr}\n${stdout}`;
206
- const lines = combined
207
- .split("\n")
208
- .map((l) => l.trim())
209
- .filter((l) => l.length > 0 &&
210
- (l.includes("error") ||
211
- l.includes("Error") ||
212
- l.includes("✖") ||
213
- l.includes("×")))
214
- .map((l) => (l.length > 200 ? `${l.slice(0, 197)}...` : l));
215
- // If we couldn't parse specific error lines, return the whole output truncated
216
- if (lines.length === 0 && combined.trim().length > 0) {
217
- return [combined.trim().slice(0, 200)];
218
- }
219
- return lines;
220
- }
221
- function parseReviewFindings(output) {
222
- if (output.toLowerCase().includes("no issues found")) {
223
- return [];
224
- }
225
- return output
226
- .split("\n")
227
- .map((l) => l.trim())
228
- .filter((l) => /^\d+[.)]/.test(l))
229
- .map((l) => l.replace(/^\d+[.)]\s*/, ""))
230
- .filter((l) => l.length > 0);
231
- }
232
- export class DevServerReadinessError extends Error {
233
- constructor(message) {
234
- super(message);
235
- this.name = "DevServerReadinessError";
236
- }
237
- }
238
- /**
239
- * Wait for a dev server to signal readiness on stdout/stderr.
240
- *
241
- * Rejects (does NOT resolve) on timeout or process error — callers that
242
- * need the server to be live for correctness (e.g. the review worker)
243
- * must not proceed without a confirmed signal. If the server dies before
244
- * becoming ready, we reject with the exit details.
245
- */
246
- export function waitForDevServer(proc, timeout) {
247
- return new Promise((resolve, reject) => {
248
- let settled = false;
249
- const cleanup = () => {
250
- proc.stdout?.off("data", onData);
251
- proc.stderr?.off("data", onData);
252
- proc.off("error", onError);
253
- proc.off("exit", onExit);
254
- clearTimeout(timer);
255
- };
256
- const settleResolve = () => {
257
- if (settled)
258
- return;
259
- settled = true;
260
- cleanup();
261
- resolve();
262
- };
263
- const settleReject = (err) => {
264
- if (settled)
265
- return;
266
- settled = true;
267
- cleanup();
268
- reject(err);
269
- };
270
- const timer = setTimeout(() => {
271
- settleReject(new DevServerReadinessError(`dev server did not signal readiness within ${timeout}ms`));
272
- }, timeout);
273
- const onData = (data) => {
274
- const text = data.toString();
275
- if (text.includes("ready") ||
276
- text.includes("localhost") ||
277
- text.includes("Local:")) {
278
- settleResolve();
279
- }
280
- };
281
- const onError = (err) => {
282
- settleReject(err);
283
- };
284
- const onExit = (code, signal) => {
285
- settleReject(new DevServerReadinessError(`dev server exited before becoming ready (code=${code ?? "?"}, signal=${signal ?? "?"})`));
286
- };
287
- proc.stdout?.on("data", onData);
288
- proc.stderr?.on("data", onData);
289
- proc.on("error", onError);
290
- proc.on("exit", onExit);
291
- });
292
- }
293
- /**
294
- * Verify the dev server actually responds to HTTP GET before treating
295
- * it as ready. Listening alone isn't enough — a framework can bind but
296
- * still crash on the first request.
297
- */
298
- export async function probeDevServer(port, timeoutMs = 5000) {
299
- const controller = new AbortController();
300
- const timer = setTimeout(() => controller.abort(), timeoutMs);
301
- try {
302
- const res = await fetch(`http://localhost:${port}/`, {
303
- signal: controller.signal,
304
- });
305
- if (!res.ok && res.status >= 500) {
306
- throw new DevServerReadinessError(`dev server returned ${res.status} on probe`);
307
- }
308
- }
309
- catch (err) {
310
- if (err instanceof DevServerReadinessError)
311
- throw err;
312
- throw new DevServerReadinessError(`dev server probe failed: ${err instanceof Error ? err.message : String(err)}`);
313
- }
314
- finally {
315
- clearTimeout(timer);
316
- }
317
- }
package/dist/watcher.d.ts DELETED
@@ -1,53 +0,0 @@
1
- import type { RealtimeCredentials } from "./types.js";
2
- export interface CardBroadcastEvent {
3
- event: "card_update" | "card_created";
4
- payload: Record<string, unknown>;
5
- }
6
- export interface AgentCommandEvent {
7
- cardId: string;
8
- command: "pause" | "resume" | "stop";
9
- }
10
- export type CardBroadcastHandler = (event: CardBroadcastEvent) => void;
11
- export type AgentCommandHandler = (event: AgentCommandEvent) => void;
12
- export interface PresenceIdentity {
13
- userId: string;
14
- userEmail: string;
15
- agentIdentifier: string;
16
- agentName: string;
17
- }
18
- /**
19
- * Subscribes to Supabase broadcast events on the board channel.
20
- * The harmony-api broadcasts card_update and card_created events
21
- * after every mutation — this works with the anon key (no RLS needed).
22
- */
23
- export declare class Watcher {
24
- private credentials;
25
- private projectId;
26
- private identity;
27
- private onCardBroadcast;
28
- private onAgentCommand?;
29
- private channel;
30
- private presenceChannel;
31
- private supabase;
32
- private daemonId;
33
- private connected;
34
- private presenceTracked;
35
- private suppressStartupLogs;
36
- private readyResolve;
37
- private readyPromise;
38
- get isConnected(): boolean;
39
- /** Resolves once both broadcast subscription is active AND presence is tracked. */
40
- ready(): Promise<void>;
41
- /**
42
- * Re-enable startup-state log lines. The startup banner suppresses them
43
- * by default in pretty mode (it absorbs subscribe/presence into one
44
- * "✓ Realtime" line). After the banner has rendered, main() calls this
45
- * so any realtime events that arrive *after* the banner — e.g. a slow
46
- * subscribe that missed the banner's 3 s deadline — log normally.
47
- */
48
- allowStartupLogs(): void;
49
- private maybeResolveReady;
50
- constructor(credentials: RealtimeCredentials, projectId: string, identity: PresenceIdentity, onCardBroadcast: CardBroadcastHandler, onAgentCommand?: AgentCommandHandler | undefined);
51
- start(): Promise<void>;
52
- stop(): Promise<void>;
53
- }
package/dist/watcher.js DELETED
@@ -1,153 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { createClient } from "@supabase/supabase-js";
3
- import { isPretty, log } from "./log.js";
4
- const TAG = "watcher";
5
- /**
6
- * Subscribes to Supabase broadcast events on the board channel.
7
- * The harmony-api broadcasts card_update and card_created events
8
- * after every mutation — this works with the anon key (no RLS needed).
9
- */
10
- export class Watcher {
11
- credentials;
12
- projectId;
13
- identity;
14
- onCardBroadcast;
15
- onAgentCommand;
16
- channel = null;
17
- presenceChannel = null;
18
- supabase = null;
19
- daemonId = randomUUID();
20
- connected = false;
21
- presenceTracked = false;
22
- suppressStartupLogs = true;
23
- readyResolve = null;
24
- readyPromise = new Promise((resolve) => {
25
- this.readyResolve = resolve;
26
- });
27
- get isConnected() {
28
- return this.connected;
29
- }
30
- /** Resolves once both broadcast subscription is active AND presence is tracked. */
31
- ready() {
32
- return this.readyPromise;
33
- }
34
- /**
35
- * Re-enable startup-state log lines. The startup banner suppresses them
36
- * by default in pretty mode (it absorbs subscribe/presence into one
37
- * "✓ Realtime" line). After the banner has rendered, main() calls this
38
- * so any realtime events that arrive *after* the banner — e.g. a slow
39
- * subscribe that missed the banner's 3 s deadline — log normally.
40
- */
41
- allowStartupLogs() {
42
- this.suppressStartupLogs = false;
43
- }
44
- maybeResolveReady() {
45
- if (this.connected && this.presenceTracked && this.readyResolve) {
46
- this.readyResolve();
47
- this.readyResolve = null;
48
- }
49
- }
50
- constructor(credentials, projectId, identity, onCardBroadcast, onAgentCommand) {
51
- this.credentials = credentials;
52
- this.projectId = projectId;
53
- this.identity = identity;
54
- this.onCardBroadcast = onCardBroadcast;
55
- this.onAgentCommand = onAgentCommand;
56
- }
57
- async start() {
58
- // In pretty mode the startup banner absorbs realtime readiness into
59
- // its "✓ Realtime" line, so this connect message is redundant noise.
60
- if (!isPretty()) {
61
- log.info(TAG, "Connecting to Supabase realtime (broadcast)...");
62
- }
63
- this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
64
- // Presence channel — separate from the broadcast channel to avoid
65
- // conflicting with frontend BoardContext's board-{projectId} subscription
66
- const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
67
- const channel = this.supabase
68
- .channel(`board-${this.projectId}`)
69
- .on("broadcast", { event: "card_update" }, (msg) => {
70
- log.debug(TAG, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
71
- this.onCardBroadcast({
72
- event: "card_update",
73
- payload: msg.payload ?? {},
74
- });
75
- })
76
- .on("broadcast", { event: "card_created" }, (msg) => {
77
- log.debug(TAG, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
78
- this.onCardBroadcast({
79
- event: "card_created",
80
- payload: msg.payload ?? {},
81
- });
82
- })
83
- .on("broadcast", { event: "agent_command" }, (msg) => {
84
- const payload = msg.payload ?? {};
85
- const cardId = payload.card_id;
86
- const command = payload.command;
87
- if (cardId && command) {
88
- log.info(TAG, `Broadcast: agent_command ${command} for ${cardId}`);
89
- this.onAgentCommand?.({ cardId, command });
90
- }
91
- })
92
- .subscribe((status) => {
93
- if (status === "SUBSCRIBED") {
94
- this.connected = true;
95
- if (!isPretty() || !this.suppressStartupLogs) {
96
- log.info(TAG, "Broadcast subscription active");
97
- }
98
- this.maybeResolveReady();
99
- }
100
- else if (status === "CHANNEL_ERROR") {
101
- this.connected = false;
102
- log.error(TAG, "Broadcast channel error — will rely on reconciliation");
103
- }
104
- else if (status === "TIMED_OUT") {
105
- this.connected = false;
106
- log.warn(TAG, "Broadcast subscription timed out — retrying...");
107
- }
108
- else if (status === "CLOSED") {
109
- this.connected = false;
110
- }
111
- });
112
- this.channel = channel;
113
- // Subscribe presence channel for daemon online indicator
114
- presenceChannel
115
- .on("presence", { event: "sync" }, () => {
116
- log.debug(TAG, "Presence sync");
117
- })
118
- .subscribe(async (status) => {
119
- if (status === "SUBSCRIBED") {
120
- await presenceChannel.track({
121
- daemonId: this.daemonId,
122
- startedAt: new Date().toISOString(),
123
- userId: this.identity.userId,
124
- userEmail: this.identity.userEmail,
125
- agentIdentifier: this.identity.agentIdentifier,
126
- agentName: this.identity.agentName,
127
- });
128
- if (!isPretty() || !this.suppressStartupLogs) {
129
- log.info(TAG, "Presence tracked on board-presence channel");
130
- }
131
- this.presenceTracked = true;
132
- this.maybeResolveReady();
133
- }
134
- });
135
- this.presenceChannel = presenceChannel;
136
- }
137
- async stop() {
138
- if (this.presenceChannel) {
139
- await this.supabase?.removeChannel(this.presenceChannel);
140
- this.presenceChannel = null;
141
- }
142
- if (this.channel) {
143
- await this.supabase?.removeChannel(this.channel);
144
- this.channel = null;
145
- }
146
- if (this.supabase) {
147
- await this.supabase.realtime.disconnect();
148
- this.supabase = null;
149
- }
150
- this.connected = false;
151
- log.info(TAG, "Broadcast subscription stopped");
152
- }
153
- }
package/dist/worker.d.ts DELETED
@@ -1,54 +0,0 @@
1
- import type { HarmonyApiClient } from "@gethmy/mcp/src/api-client.js";
2
- import type { Card, Column, Label, Subtask } from "@harmony/shared";
3
- import { type StateStore } from "./state-store.js";
4
- import { type AgentConfig, type WorkerState } from "./types.js";
5
- export declare class Worker {
6
- private config;
7
- private client;
8
- private onDone;
9
- private workspaceId;
10
- private projectId;
11
- private stateStore;
12
- id: number;
13
- state: WorkerState;
14
- cardId: string | null;
15
- branchName: string | null;
16
- worktreePath: string | null;
17
- startedAt: number | null;
18
- private process;
19
- private timeoutTimer;
20
- private heartbeatTimer;
21
- private progressTracker;
22
- private lastSessionStats;
23
- private aborted;
24
- private verificationFailed;
25
- private sessionId;
26
- private runId;
27
- constructor(id: number, config: AgentConfig, client: HarmonyApiClient, _userEmail: string, onDone: (worker: Worker) => void, workspaceId: string, projectId: string, stateStore: StateStore);
28
- private startHeartbeat;
29
- private stopHeartbeat;
30
- private recordPhase;
31
- get tag(): string;
32
- get isIdle(): boolean;
33
- get isActive(): boolean;
34
- /**
35
- * Start working on a card. Runs the full lifecycle:
36
- * PREPARING → RUNNING → COMPLETING → IDLE
37
- */
38
- run(card: Card, column: Column, labels: Label[], subtasks: Subtask[]): Promise<void>;
39
- private recordOutcome;
40
- /**
41
- * Pause the current work by suspending the Claude process (SIGSTOP).
42
- */
43
- pause(): Promise<void>;
44
- /**
45
- * Resume the Claude process after a pause (SIGCONT).
46
- */
47
- resume(): Promise<void>;
48
- /**
49
- * Cancel the current work. Sends escalating signals to the Claude process.
50
- */
51
- cancel(): Promise<void>;
52
- private spawnClaude;
53
- private cleanup;
54
- }