@gethmy/agent 1.0.0 → 1.0.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 +7 -6
- package/dist/board-helpers.d.ts +31 -0
- package/dist/board-helpers.js +150 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +2 -11761
- package/dist/completion.d.ts +14 -0
- package/dist/completion.js +142 -0
- package/dist/config.d.ts +23 -0
- package/dist/config.js +91 -0
- package/dist/git-pr.d.ts +25 -0
- package/dist/git-pr.js +305 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +169 -11730
- package/dist/log.d.ts +10 -0
- package/dist/log.js +35 -0
- package/dist/merge-monitor.d.ts +23 -0
- package/dist/merge-monitor.js +167 -0
- package/dist/pm.d.ts +14 -0
- package/dist/pm.js +63 -0
- package/dist/pool.d.ts +40 -0
- package/dist/pool.js +157 -0
- package/dist/progress-tracker.d.ts +64 -0
- package/dist/progress-tracker.js +361 -0
- package/dist/prompt.d.ts +5 -0
- package/dist/prompt.js +40 -0
- package/dist/queue.d.ts +37 -0
- package/dist/queue.js +96 -0
- package/dist/reconcile.d.ts +21 -0
- package/dist/reconcile.js +114 -0
- package/dist/review-completion.d.ts +31 -0
- package/dist/review-completion.js +253 -0
- package/dist/review-knowledge.d.ts +14 -0
- package/dist/review-knowledge.js +89 -0
- package/dist/review-prompt.d.ts +12 -0
- package/dist/review-prompt.js +103 -0
- package/dist/review-worker.d.ts +46 -0
- package/dist/review-worker.js +437 -0
- package/dist/review-worktree.d.ts +12 -0
- package/dist/review-worktree.js +83 -0
- package/dist/stream-parser.d.ts +31 -0
- package/dist/stream-parser.js +95 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.js +56 -0
- package/dist/verification.d.ts +16 -0
- package/dist/verification.js +251 -0
- package/dist/watcher.d.ts +27 -0
- package/dist/watcher.js +74 -0
- package/dist/worker.d.ts +43 -0
- package/dist/worker.js +327 -0
- package/dist/worktree.d.ts +13 -0
- package/dist/worktree.js +115 -0
- package/package.json +8 -7
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Card, Column, Label, Subtask } from "@harmony/shared";
|
|
2
|
+
export type WorkMode = "implement" | "review";
|
|
3
|
+
export interface AgentConfig {
|
|
4
|
+
poolSize: number;
|
|
5
|
+
maxTimeout: number;
|
|
6
|
+
pickupColumns: string[];
|
|
7
|
+
priorityLabels: Record<string, number>;
|
|
8
|
+
columnBoost: boolean;
|
|
9
|
+
completion: {
|
|
10
|
+
createPR: boolean;
|
|
11
|
+
moveToColumn: string;
|
|
12
|
+
postSummary: boolean;
|
|
13
|
+
};
|
|
14
|
+
claude: {
|
|
15
|
+
model: string;
|
|
16
|
+
maxTurns: number;
|
|
17
|
+
additionalArgs: string[];
|
|
18
|
+
};
|
|
19
|
+
worktree: {
|
|
20
|
+
basePath: string;
|
|
21
|
+
baseBranch: string;
|
|
22
|
+
};
|
|
23
|
+
verification: {
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
build: boolean;
|
|
26
|
+
lint: boolean;
|
|
27
|
+
autoFix: boolean;
|
|
28
|
+
maxFixAttempts: number;
|
|
29
|
+
deepReview: boolean;
|
|
30
|
+
devServerBasePort: number;
|
|
31
|
+
timeout: number;
|
|
32
|
+
failColumn: string;
|
|
33
|
+
};
|
|
34
|
+
review: {
|
|
35
|
+
enabled: boolean;
|
|
36
|
+
pickupColumns: string[];
|
|
37
|
+
moveToColumn: string;
|
|
38
|
+
failColumn: string;
|
|
39
|
+
devServerPort: number;
|
|
40
|
+
maxTimeout: number;
|
|
41
|
+
postFindings: boolean;
|
|
42
|
+
maxReviewCycles: number;
|
|
43
|
+
createPR: boolean;
|
|
44
|
+
approvedLabel: string;
|
|
45
|
+
approvedLabelColor: string;
|
|
46
|
+
mergeMonitor: boolean;
|
|
47
|
+
mergedLabel: string;
|
|
48
|
+
mergedLabelColor: string;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export declare const DEFAULT_AGENT_CONFIG: AgentConfig;
|
|
52
|
+
export declare const NEED_REVIEW_LABEL = "Need Review";
|
|
53
|
+
export declare const NEED_REVIEW_LABEL_COLOR = "#f59e0b";
|
|
54
|
+
export declare const AGENT_NAME = "Harmony Agent";
|
|
55
|
+
export declare function agentIdentifier(workerId: number): string;
|
|
56
|
+
export type ProgressPhase = "exploring" | "implementing" | "testing" | "committing" | "finishing";
|
|
57
|
+
export type WorkerState = "idle" | "preparing" | "running" | "completing" | "verifying" | "cancelling" | "error";
|
|
58
|
+
export interface QueueItem {
|
|
59
|
+
cardId: string;
|
|
60
|
+
shortId: number;
|
|
61
|
+
title: string;
|
|
62
|
+
priority: number;
|
|
63
|
+
enqueuedAt: number;
|
|
64
|
+
mode: WorkMode;
|
|
65
|
+
}
|
|
66
|
+
export interface EnrichedCard {
|
|
67
|
+
card: Card;
|
|
68
|
+
column: Column;
|
|
69
|
+
labels: Label[];
|
|
70
|
+
subtasks: Subtask[];
|
|
71
|
+
mode: WorkMode;
|
|
72
|
+
}
|
|
73
|
+
export interface RealtimeCredentials {
|
|
74
|
+
supabaseUrl: string;
|
|
75
|
+
supabaseAnonKey: string;
|
|
76
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export const DEFAULT_AGENT_CONFIG = {
|
|
2
|
+
poolSize: 1,
|
|
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
|
+
maxTurns: 200,
|
|
15
|
+
additionalArgs: [],
|
|
16
|
+
},
|
|
17
|
+
worktree: {
|
|
18
|
+
basePath: ".harmony-worktrees",
|
|
19
|
+
baseBranch: "main",
|
|
20
|
+
},
|
|
21
|
+
verification: {
|
|
22
|
+
enabled: true,
|
|
23
|
+
build: true,
|
|
24
|
+
lint: true,
|
|
25
|
+
autoFix: true,
|
|
26
|
+
maxFixAttempts: 1,
|
|
27
|
+
deepReview: false,
|
|
28
|
+
devServerBasePort: 4200,
|
|
29
|
+
timeout: 120_000,
|
|
30
|
+
failColumn: "Needs Fix",
|
|
31
|
+
},
|
|
32
|
+
review: {
|
|
33
|
+
enabled: true,
|
|
34
|
+
pickupColumns: ["Review"],
|
|
35
|
+
moveToColumn: "Done",
|
|
36
|
+
failColumn: "To Do",
|
|
37
|
+
devServerPort: 4300,
|
|
38
|
+
maxTimeout: 600_000,
|
|
39
|
+
postFindings: true,
|
|
40
|
+
maxReviewCycles: 3,
|
|
41
|
+
createPR: true,
|
|
42
|
+
approvedLabel: "Ready to Merge",
|
|
43
|
+
approvedLabelColor: "#22c55e",
|
|
44
|
+
mergeMonitor: true,
|
|
45
|
+
mergedLabel: "Merged",
|
|
46
|
+
mergedLabelColor: "#6366f1",
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
// ============ LABELS ============
|
|
50
|
+
export const NEED_REVIEW_LABEL = "Need Review";
|
|
51
|
+
export const NEED_REVIEW_LABEL_COLOR = "#f59e0b";
|
|
52
|
+
// ============ AGENT IDENTITY ============
|
|
53
|
+
export const AGENT_NAME = "Harmony Agent";
|
|
54
|
+
export function agentIdentifier(workerId) {
|
|
55
|
+
return `harmony-daemon-${workerId}`;
|
|
56
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
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 declare function reportFindings(client: HarmonyApiClient, cardId: string, result: VerificationResult): Promise<void>;
|
|
16
|
+
export declare function waitForDevServer(proc: ChildProcess, timeout: number): Promise<void>;
|
|
@@ -0,0 +1,251 @@
|
|
|
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
|
|
86
|
+
await waitForDevServer(devServer, 30_000);
|
|
87
|
+
// Get diff for review context
|
|
88
|
+
let diff = "";
|
|
89
|
+
try {
|
|
90
|
+
diff = execFileSync("git", ["diff", `origin/${config.worktree.baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8", timeout: 30_000 });
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
diff = "(unable to retrieve diff)";
|
|
94
|
+
}
|
|
95
|
+
// Spawn Claude for review
|
|
96
|
+
const reviewPrompt = [
|
|
97
|
+
"You are reviewing code changes for quality and correctness.",
|
|
98
|
+
`A dev server is running at http://localhost:${port}.`,
|
|
99
|
+
"Review the following diff and report any issues found.",
|
|
100
|
+
"Output ONLY a numbered list of findings, one per line.",
|
|
101
|
+
"If no issues, output: No issues found.",
|
|
102
|
+
"",
|
|
103
|
+
"```diff",
|
|
104
|
+
diff.slice(0, 50_000),
|
|
105
|
+
"```",
|
|
106
|
+
].join("\n");
|
|
107
|
+
const output = execFileSync("claude", ["--print", "--model", "sonnet", "--max-turns", "10", "--", reviewPrompt], {
|
|
108
|
+
cwd: worktreePath,
|
|
109
|
+
encoding: "utf-8",
|
|
110
|
+
timeout: config.verification.timeout,
|
|
111
|
+
stdio: "pipe",
|
|
112
|
+
});
|
|
113
|
+
return parseReviewFindings(output);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
log.error(TAG, `Deep review failed: ${err instanceof Error ? err.message : err}`);
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
if (devServer && !devServer.killed) {
|
|
121
|
+
devServer.kill("SIGTERM");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export function attemptAutoFix(worktreePath, config, errors) {
|
|
126
|
+
const errorSummary = errors.slice(0, 20).join("\n");
|
|
127
|
+
const fixPrompt = [
|
|
128
|
+
"The following build/lint errors were found after implementing a feature.",
|
|
129
|
+
"Fix the source files to resolve these errors.",
|
|
130
|
+
"Do NOT commit build artifacts or modify files in dist/.",
|
|
131
|
+
"Fix source files only.",
|
|
132
|
+
"",
|
|
133
|
+
"Errors:",
|
|
134
|
+
"```",
|
|
135
|
+
errorSummary,
|
|
136
|
+
"```",
|
|
137
|
+
].join("\n");
|
|
138
|
+
const args = [
|
|
139
|
+
"--print",
|
|
140
|
+
"--model",
|
|
141
|
+
config.claude.model,
|
|
142
|
+
"--max-turns",
|
|
143
|
+
"50",
|
|
144
|
+
"--allowedTools",
|
|
145
|
+
"Bash,Read,Write,Edit,Glob,Grep",
|
|
146
|
+
"--",
|
|
147
|
+
fixPrompt,
|
|
148
|
+
];
|
|
149
|
+
log.info(TAG, "Spawning Claude for auto-fix...");
|
|
150
|
+
execFileSync("claude", args, {
|
|
151
|
+
cwd: worktreePath,
|
|
152
|
+
timeout: config.verification.timeout,
|
|
153
|
+
stdio: "pipe",
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
export async function reportFindings(client, cardId, result) {
|
|
157
|
+
const items = [];
|
|
158
|
+
for (const err of result.buildErrors) {
|
|
159
|
+
items.push(`Build: ${err}`);
|
|
160
|
+
}
|
|
161
|
+
for (const err of result.lintWarnings) {
|
|
162
|
+
items.push(`Lint: ${err}`);
|
|
163
|
+
}
|
|
164
|
+
for (const finding of result.reviewFindings) {
|
|
165
|
+
items.push(`Review: ${finding}`);
|
|
166
|
+
}
|
|
167
|
+
const maxSubtasks = 10;
|
|
168
|
+
const overflow = items.length - maxSubtasks;
|
|
169
|
+
const toCreate = items.slice(0, maxSubtasks);
|
|
170
|
+
await Promise.all(toCreate.map(async (item) => {
|
|
171
|
+
const title = item.length > 120 ? `${item.slice(0, 117)}...` : item;
|
|
172
|
+
try {
|
|
173
|
+
await client.createSubtask(cardId, title);
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
log.error(TAG, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
|
|
177
|
+
}
|
|
178
|
+
}));
|
|
179
|
+
if (overflow > 0) {
|
|
180
|
+
try {
|
|
181
|
+
await client.createSubtask(cardId, `...and ${overflow} more issues`);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// best-effort
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
log.info(TAG, `Reported ${Math.min(items.length, maxSubtasks)} finding(s) as subtasks on card ${cardId}`);
|
|
188
|
+
}
|
|
189
|
+
// ============ HELPERS ============
|
|
190
|
+
function parseErrorOutput(err) {
|
|
191
|
+
const stderr = err?.stderr?.toString() ?? "";
|
|
192
|
+
const stdout = err?.stdout?.toString() ?? "";
|
|
193
|
+
const combined = `${stderr}\n${stdout}`;
|
|
194
|
+
const lines = combined
|
|
195
|
+
.split("\n")
|
|
196
|
+
.map((l) => l.trim())
|
|
197
|
+
.filter((l) => l.length > 0 &&
|
|
198
|
+
(l.includes("error") ||
|
|
199
|
+
l.includes("Error") ||
|
|
200
|
+
l.includes("✖") ||
|
|
201
|
+
l.includes("×")))
|
|
202
|
+
.map((l) => (l.length > 200 ? `${l.slice(0, 197)}...` : l));
|
|
203
|
+
// If we couldn't parse specific error lines, return the whole output truncated
|
|
204
|
+
if (lines.length === 0 && combined.trim().length > 0) {
|
|
205
|
+
return [combined.trim().slice(0, 200)];
|
|
206
|
+
}
|
|
207
|
+
return lines;
|
|
208
|
+
}
|
|
209
|
+
function parseReviewFindings(output) {
|
|
210
|
+
if (output.toLowerCase().includes("no issues found")) {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
return output
|
|
214
|
+
.split("\n")
|
|
215
|
+
.map((l) => l.trim())
|
|
216
|
+
.filter((l) => /^\d+[.)]/.test(l))
|
|
217
|
+
.map((l) => l.replace(/^\d+[.)]\s*/, ""))
|
|
218
|
+
.filter((l) => l.length > 0);
|
|
219
|
+
}
|
|
220
|
+
export function waitForDevServer(proc, timeout) {
|
|
221
|
+
return new Promise((resolve, reject) => {
|
|
222
|
+
const cleanup = () => {
|
|
223
|
+
proc.stdout?.off("data", onData);
|
|
224
|
+
proc.stderr?.off("data", onData);
|
|
225
|
+
proc.off("error", onError);
|
|
226
|
+
};
|
|
227
|
+
const timer = setTimeout(() => {
|
|
228
|
+
log.warn(TAG, "Dev server readiness not detected, proceeding anyway");
|
|
229
|
+
cleanup();
|
|
230
|
+
resolve();
|
|
231
|
+
}, timeout);
|
|
232
|
+
const onData = (data) => {
|
|
233
|
+
const text = data.toString();
|
|
234
|
+
if (text.includes("ready") ||
|
|
235
|
+
text.includes("localhost") ||
|
|
236
|
+
text.includes("Local:")) {
|
|
237
|
+
clearTimeout(timer);
|
|
238
|
+
cleanup();
|
|
239
|
+
resolve();
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
const onError = (err) => {
|
|
243
|
+
clearTimeout(timer);
|
|
244
|
+
cleanup();
|
|
245
|
+
reject(err);
|
|
246
|
+
};
|
|
247
|
+
proc.stdout?.on("data", onData);
|
|
248
|
+
proc.stderr?.on("data", onData);
|
|
249
|
+
proc.on("error", onError);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
/**
|
|
13
|
+
* Subscribes to Supabase broadcast events on the board channel.
|
|
14
|
+
* The harmony-api broadcasts card_update and card_created events
|
|
15
|
+
* after every mutation — this works with the anon key (no RLS needed).
|
|
16
|
+
*/
|
|
17
|
+
export declare class Watcher {
|
|
18
|
+
private credentials;
|
|
19
|
+
private projectId;
|
|
20
|
+
private onCardBroadcast;
|
|
21
|
+
private onAgentCommand?;
|
|
22
|
+
private channel;
|
|
23
|
+
private supabase;
|
|
24
|
+
constructor(credentials: RealtimeCredentials, projectId: string, onCardBroadcast: CardBroadcastHandler, onAgentCommand?: AgentCommandHandler | undefined);
|
|
25
|
+
start(): Promise<void>;
|
|
26
|
+
stop(): Promise<void>;
|
|
27
|
+
}
|
package/dist/watcher.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createClient } from "@supabase/supabase-js";
|
|
2
|
+
import { log } from "./log.js";
|
|
3
|
+
const TAG = "watcher";
|
|
4
|
+
/**
|
|
5
|
+
* Subscribes to Supabase broadcast events on the board channel.
|
|
6
|
+
* The harmony-api broadcasts card_update and card_created events
|
|
7
|
+
* after every mutation — this works with the anon key (no RLS needed).
|
|
8
|
+
*/
|
|
9
|
+
export class Watcher {
|
|
10
|
+
credentials;
|
|
11
|
+
projectId;
|
|
12
|
+
onCardBroadcast;
|
|
13
|
+
onAgentCommand;
|
|
14
|
+
channel = null;
|
|
15
|
+
supabase = null;
|
|
16
|
+
constructor(credentials, projectId, onCardBroadcast, onAgentCommand) {
|
|
17
|
+
this.credentials = credentials;
|
|
18
|
+
this.projectId = projectId;
|
|
19
|
+
this.onCardBroadcast = onCardBroadcast;
|
|
20
|
+
this.onAgentCommand = onAgentCommand;
|
|
21
|
+
}
|
|
22
|
+
async start() {
|
|
23
|
+
log.info(TAG, "Connecting to Supabase realtime (broadcast)...");
|
|
24
|
+
this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
|
|
25
|
+
const channel = this.supabase
|
|
26
|
+
.channel(`board-${this.projectId}`)
|
|
27
|
+
.on("broadcast", { event: "card_update" }, (msg) => {
|
|
28
|
+
log.debug(TAG, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
|
|
29
|
+
this.onCardBroadcast({
|
|
30
|
+
event: "card_update",
|
|
31
|
+
payload: msg.payload ?? {},
|
|
32
|
+
});
|
|
33
|
+
})
|
|
34
|
+
.on("broadcast", { event: "card_created" }, (msg) => {
|
|
35
|
+
log.debug(TAG, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
|
|
36
|
+
this.onCardBroadcast({
|
|
37
|
+
event: "card_created",
|
|
38
|
+
payload: msg.payload ?? {},
|
|
39
|
+
});
|
|
40
|
+
})
|
|
41
|
+
.on("broadcast", { event: "agent_command" }, (msg) => {
|
|
42
|
+
const payload = msg.payload ?? {};
|
|
43
|
+
const cardId = payload.card_id;
|
|
44
|
+
const command = payload.command;
|
|
45
|
+
if (cardId && command) {
|
|
46
|
+
log.info(TAG, `Broadcast: agent_command ${command} for ${cardId}`);
|
|
47
|
+
this.onAgentCommand?.({ cardId, command });
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
.subscribe((status) => {
|
|
51
|
+
if (status === "SUBSCRIBED") {
|
|
52
|
+
log.info(TAG, "Broadcast subscription active");
|
|
53
|
+
}
|
|
54
|
+
else if (status === "CHANNEL_ERROR") {
|
|
55
|
+
log.error(TAG, "Broadcast channel error — will rely on reconciliation");
|
|
56
|
+
}
|
|
57
|
+
else if (status === "TIMED_OUT") {
|
|
58
|
+
log.warn(TAG, "Broadcast subscription timed out — retrying...");
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
this.channel = channel;
|
|
62
|
+
}
|
|
63
|
+
async stop() {
|
|
64
|
+
if (this.channel) {
|
|
65
|
+
await this.supabase?.removeChannel(this.channel);
|
|
66
|
+
this.channel = null;
|
|
67
|
+
}
|
|
68
|
+
if (this.supabase) {
|
|
69
|
+
await this.supabase.realtime.disconnect();
|
|
70
|
+
this.supabase = null;
|
|
71
|
+
}
|
|
72
|
+
log.info(TAG, "Broadcast subscription stopped");
|
|
73
|
+
}
|
|
74
|
+
}
|
package/dist/worker.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { HarmonyApiClient } from "@gethmy/mcp/src/api-client.js";
|
|
2
|
+
import type { Card, Column, Label, Subtask } from "@harmony/shared";
|
|
3
|
+
import { type AgentConfig, type WorkerState } from "./types.js";
|
|
4
|
+
export declare class Worker {
|
|
5
|
+
private config;
|
|
6
|
+
private client;
|
|
7
|
+
private onDone;
|
|
8
|
+
id: number;
|
|
9
|
+
state: WorkerState;
|
|
10
|
+
cardId: string | null;
|
|
11
|
+
branchName: string | null;
|
|
12
|
+
worktreePath: string | null;
|
|
13
|
+
startedAt: number | null;
|
|
14
|
+
private process;
|
|
15
|
+
private timeoutTimer;
|
|
16
|
+
private progressTracker;
|
|
17
|
+
private lastSessionStats;
|
|
18
|
+
private aborted;
|
|
19
|
+
constructor(id: number, config: AgentConfig, client: HarmonyApiClient, _userEmail: string, onDone: (worker: Worker) => void);
|
|
20
|
+
get tag(): string;
|
|
21
|
+
get isIdle(): boolean;
|
|
22
|
+
get isActive(): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Start working on a card. Runs the full lifecycle:
|
|
25
|
+
* PREPARING → RUNNING → COMPLETING → IDLE
|
|
26
|
+
*/
|
|
27
|
+
run(card: Card, column: Column, labels: Label[], subtasks: Subtask[]): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Pause the current work by suspending the Claude process (SIGSTOP).
|
|
30
|
+
*/
|
|
31
|
+
pause(): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Resume the Claude process after a pause (SIGCONT).
|
|
34
|
+
*/
|
|
35
|
+
resume(): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Cancel the current work. Sends escalating signals to the Claude process.
|
|
38
|
+
*/
|
|
39
|
+
cancel(): Promise<void>;
|
|
40
|
+
private spawnClaude;
|
|
41
|
+
private waitForExit;
|
|
42
|
+
private cleanup;
|
|
43
|
+
}
|