@gethmy/agent 1.0.0 → 1.0.1

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 (52) hide show
  1. package/README.md +5 -5
  2. package/dist/board-helpers.d.ts +23 -0
  3. package/dist/board-helpers.js +131 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +2 -11761
  6. package/dist/completion.d.ts +7 -0
  7. package/dist/completion.js +132 -0
  8. package/dist/config.d.ts +23 -0
  9. package/dist/config.js +91 -0
  10. package/dist/git-pr.d.ts +25 -0
  11. package/dist/git-pr.js +305 -0
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.js +165 -11730
  14. package/dist/log.d.ts +10 -0
  15. package/dist/log.js +35 -0
  16. package/dist/merge-monitor.d.ts +23 -0
  17. package/dist/merge-monitor.js +155 -0
  18. package/dist/pm.d.ts +14 -0
  19. package/dist/pm.js +63 -0
  20. package/dist/pool.d.ts +36 -0
  21. package/dist/pool.js +134 -0
  22. package/dist/progress-tracker.d.ts +39 -0
  23. package/dist/progress-tracker.js +189 -0
  24. package/dist/prompt.d.ts +5 -0
  25. package/dist/prompt.js +40 -0
  26. package/dist/queue.d.ts +37 -0
  27. package/dist/queue.js +96 -0
  28. package/dist/reconcile.d.ts +21 -0
  29. package/dist/reconcile.js +107 -0
  30. package/dist/review-completion.d.ts +31 -0
  31. package/dist/review-completion.js +247 -0
  32. package/dist/review-knowledge.d.ts +14 -0
  33. package/dist/review-knowledge.js +89 -0
  34. package/dist/review-prompt.d.ts +12 -0
  35. package/dist/review-prompt.js +100 -0
  36. package/dist/review-worker.d.ts +35 -0
  37. package/dist/review-worker.js +302 -0
  38. package/dist/review-worktree.d.ts +12 -0
  39. package/dist/review-worktree.js +83 -0
  40. package/dist/stream-parser.d.ts +22 -0
  41. package/dist/stream-parser.js +81 -0
  42. package/dist/types.d.ts +74 -0
  43. package/dist/types.js +53 -0
  44. package/dist/verification.d.ts +16 -0
  45. package/dist/verification.js +251 -0
  46. package/dist/watcher.d.ts +21 -0
  47. package/dist/watcher.js +62 -0
  48. package/dist/worker.d.ts +34 -0
  49. package/dist/worker.js +268 -0
  50. package/dist/worktree.d.ts +13 -0
  51. package/dist/worktree.js +115 -0
  52. package/package.json +6 -5
@@ -0,0 +1,302 @@
1
+ import { execFileSync, spawn } from "node:child_process";
2
+ import { addLabelByName, moveCardToColumn } from "./board-helpers.js";
3
+ import { log } from "./log.js";
4
+ import { parseReviewOutput, runReviewCompletion } from "./review-completion.js";
5
+ import { buildReviewSystemPrompt, buildReviewUserPrompt, } from "./review-prompt.js";
6
+ import { checkoutExistingBranch, extractBranchFromDescription, } from "./review-worktree.js";
7
+ import { AGENT_NAME, agentIdentifier, } from "./types.js";
8
+ import { waitForDevServer } from "./verification.js";
9
+ import { cleanupWorktree } from "./worktree.js";
10
+ const TAG = "review-worker";
11
+ const CANCEL_SIGINT_TIMEOUT = 30_000;
12
+ const CANCEL_SIGTERM_TIMEOUT = 10_000;
13
+ export class ReviewWorker {
14
+ config;
15
+ client;
16
+ onDone;
17
+ id;
18
+ state = "idle";
19
+ cardId = null;
20
+ branchName = null;
21
+ worktreePath = null;
22
+ startedAt = null;
23
+ process = null;
24
+ devServerProcess = null;
25
+ timeoutTimer = null;
26
+ aborted = false;
27
+ constructor(id, config, client, _userEmail, onDone) {
28
+ this.config = config;
29
+ this.client = client;
30
+ this.onDone = onDone;
31
+ this.id = id;
32
+ }
33
+ get tag() {
34
+ return `${TAG}:${this.id}`;
35
+ }
36
+ get isIdle() {
37
+ return this.state === "idle";
38
+ }
39
+ get isActive() {
40
+ return (this.state === "preparing" ||
41
+ this.state === "running" ||
42
+ this.state === "verifying" ||
43
+ this.state === "completing");
44
+ }
45
+ /**
46
+ * Start reviewing a card. Runs the full lifecycle:
47
+ * PREPARING → REVIEWING → COMPLETING → IDLE
48
+ */
49
+ async run(card, column, labels, subtasks) {
50
+ this.aborted = false;
51
+ this.cardId = card.id;
52
+ this.startedAt = Date.now();
53
+ try {
54
+ // --- PREPARING ---
55
+ this.state = "preparing";
56
+ log.info(this.tag, `Preparing review for #${card.short_id} "${card.title}"`);
57
+ // Extract branch name from card description
58
+ this.branchName = extractBranchFromDescription(card.description);
59
+ if (!this.branchName) {
60
+ throw new Error(`No branch name found in card #${card.short_id} description. ` +
61
+ "Expected: Branch: `agent/...`");
62
+ }
63
+ log.info(this.tag, `Review branch: ${this.branchName}`);
64
+ // Start agent session and make it visible on the board
65
+ await this.client.startAgentSession(card.id, {
66
+ agentIdentifier: agentIdentifier(this.id),
67
+ agentName: `${AGENT_NAME} (Review)`,
68
+ status: "working",
69
+ currentTask: "Setting up review worktree",
70
+ progressPercent: 5,
71
+ });
72
+ // Fire label addition concurrently with sync worktree checkout
73
+ const labelPromise = addLabelByName(this.client, card, "agent", "#8b5cf6");
74
+ // Checkout existing branch into worktree (sync)
75
+ this.worktreePath = checkoutExistingBranch(this.config.worktree.basePath, this.branchName);
76
+ await labelPromise;
77
+ if (this.aborted)
78
+ return;
79
+ // --- REVIEWING ---
80
+ this.state = "running";
81
+ // Start dev server
82
+ // Use devServerPort directly — review worker IDs are offset by poolSize
83
+ // so we subtract to get a 0-based index for port allocation
84
+ const port = this.config.review.devServerPort + (this.id - this.config.poolSize);
85
+ log.info(this.tag, `Starting dev server on port ${port}...`);
86
+ this.devServerProcess = spawn("bun", ["run", "dev", "--", "--port", String(port)], {
87
+ cwd: this.worktreePath,
88
+ stdio: ["ignore", "pipe", "pipe"],
89
+ });
90
+ await waitForDevServer(this.devServerProcess, 30_000);
91
+ log.info(this.tag, `Dev server ready on port ${port}`);
92
+ if (this.aborted)
93
+ return;
94
+ // Get diff
95
+ let diff = "";
96
+ try {
97
+ diff = execFileSync("git", ["diff", `origin/${this.config.worktree.baseBranch}..HEAD`], { cwd: this.worktreePath, encoding: "utf-8", timeout: 30_000 });
98
+ }
99
+ catch {
100
+ diff = "(unable to retrieve diff)";
101
+ }
102
+ const previewUrl = `http://localhost:${port}`;
103
+ const enriched = {
104
+ card,
105
+ column,
106
+ labels,
107
+ subtasks,
108
+ mode: "review",
109
+ };
110
+ const systemPrompt = buildReviewSystemPrompt();
111
+ const userPrompt = buildReviewUserPrompt(enriched, this.branchName, this.worktreePath, previewUrl, diff);
112
+ await this.client.updateAgentProgress(card.id, {
113
+ agentIdentifier: agentIdentifier(this.id),
114
+ agentName: `${AGENT_NAME} (Review)`,
115
+ status: "working",
116
+ currentTask: "Running Claude review",
117
+ progressPercent: 20,
118
+ });
119
+ // Start timeout watchdog
120
+ this.timeoutTimer = setTimeout(() => {
121
+ log.warn(this.tag, `Review timeout reached (${this.config.review.maxTimeout}ms), cancelling`);
122
+ this.cancel();
123
+ }, this.config.review.maxTimeout);
124
+ // Spawn Claude CLI for review
125
+ const stdout = await this.spawnClaude(userPrompt, systemPrompt);
126
+ if (this.aborted)
127
+ return;
128
+ // --- COMPLETING ---
129
+ this.state = "completing";
130
+ log.info(this.tag, `Claude review finished for #${card.short_id}`);
131
+ // Kill dev server
132
+ this.killDevServer();
133
+ // Parse findings
134
+ const result = parseReviewOutput(stdout);
135
+ log.info(this.tag, `Review verdict: ${result.verdict} (${result.findings.length} finding(s))`);
136
+ await this.client.updateAgentProgress(card.id, {
137
+ agentIdentifier: agentIdentifier(this.id),
138
+ agentName: `${AGENT_NAME} (Review)`,
139
+ status: "working",
140
+ currentTask: `Processing ${result.verdict} verdict`,
141
+ progressPercent: 80,
142
+ });
143
+ // Run review completion pipeline
144
+ await runReviewCompletion(this.client, card, result, this.config, this.worktreePath, this.branchName);
145
+ }
146
+ catch (err) {
147
+ this.state = "error";
148
+ const msg = err instanceof Error ? err.message : String(err);
149
+ log.error(this.tag, `Error reviewing #${card.short_id}: ${msg}`);
150
+ // End session as paused on error
151
+ try {
152
+ await this.client.endAgentSession(card.id, { status: "paused" });
153
+ }
154
+ catch {
155
+ // best-effort
156
+ }
157
+ // Move card out of Review to break the re-enqueue loop
158
+ try {
159
+ await moveCardToColumn(this.client, card, this.config.review.failColumn);
160
+ log.info(this.tag, `Moved #${card.short_id} to "${this.config.review.failColumn}" after error`);
161
+ }
162
+ catch {
163
+ log.warn(this.tag, "Failed to move card to fail column after error");
164
+ }
165
+ }
166
+ finally {
167
+ this.cleanup();
168
+ this.state = "idle";
169
+ this.onDone(this);
170
+ }
171
+ }
172
+ /**
173
+ * Cancel the current review. Sends escalating signals to both processes.
174
+ */
175
+ async cancel() {
176
+ if (!this.isActive)
177
+ return;
178
+ this.aborted = true;
179
+ this.state = "cancelling";
180
+ log.info(this.tag, `Cancelling review on ${this.cardId}`);
181
+ // Kill dev server first
182
+ this.killDevServer();
183
+ // Then kill Claude process with escalating signals
184
+ if (this.process && !this.process.killed) {
185
+ this.process.kill("SIGINT");
186
+ log.debug(this.tag, "Sent SIGINT to Claude");
187
+ const sigintDead = await this.waitForExit(CANCEL_SIGINT_TIMEOUT);
188
+ if (!sigintDead) {
189
+ this.process.kill("SIGTERM");
190
+ log.debug(this.tag, "Sent SIGTERM to Claude");
191
+ const sigtermDead = await this.waitForExit(CANCEL_SIGTERM_TIMEOUT);
192
+ if (!sigtermDead) {
193
+ this.process.kill("SIGKILL");
194
+ log.warn(this.tag, "Sent SIGKILL to Claude");
195
+ }
196
+ }
197
+ }
198
+ // End agent session as paused
199
+ if (this.cardId) {
200
+ try {
201
+ await this.client.endAgentSession(this.cardId, { status: "paused" });
202
+ }
203
+ catch {
204
+ // best-effort
205
+ }
206
+ }
207
+ }
208
+ spawnClaude(prompt, systemPrompt) {
209
+ return new Promise((resolve, reject) => {
210
+ const args = [
211
+ "--print",
212
+ "--model",
213
+ this.config.claude.model,
214
+ "--max-turns",
215
+ String(this.config.claude.maxTurns),
216
+ "--allowedTools",
217
+ "Bash(readonly),Read,Glob,Grep,Agent,mcp__harmony__*",
218
+ ...(systemPrompt ? ["--append-system-prompt", systemPrompt] : []),
219
+ ...this.config.claude.additionalArgs,
220
+ "--",
221
+ prompt,
222
+ ];
223
+ log.info(this.tag, `Spawning review: claude ${args.slice(0, 3).join(" ")} ...`);
224
+ this.process = spawn("claude", args, {
225
+ cwd: this.worktreePath,
226
+ stdio: ["ignore", "pipe", "pipe"],
227
+ });
228
+ let stdout = "";
229
+ let stderr = "";
230
+ this.process.stdout?.on("data", (data) => {
231
+ const text = data.toString();
232
+ stdout += text;
233
+ for (const line of text.split("\n")) {
234
+ if (line.trim()) {
235
+ log.debug(this.tag, `claude> ${line.slice(0, 200)}`);
236
+ }
237
+ }
238
+ });
239
+ this.process.stderr?.on("data", (data) => {
240
+ stderr += data.toString();
241
+ });
242
+ this.process.on("error", (err) => {
243
+ reject(new Error(`Failed to spawn claude: ${err.message}`));
244
+ });
245
+ this.process.on("close", (code) => {
246
+ this.process = null;
247
+ if (this.aborted) {
248
+ resolve(stdout);
249
+ }
250
+ else if (code === 0) {
251
+ resolve(stdout);
252
+ }
253
+ else {
254
+ reject(new Error(`claude exited with code ${code}${stderr ? `: ${stderr.slice(0, 500)}` : ""}`));
255
+ }
256
+ });
257
+ });
258
+ }
259
+ waitForExit(timeout) {
260
+ return new Promise((resolve) => {
261
+ if (!this.process || this.process.killed) {
262
+ resolve(true);
263
+ return;
264
+ }
265
+ const timer = setTimeout(() => {
266
+ resolve(false);
267
+ }, timeout);
268
+ this.process.once("close", () => {
269
+ clearTimeout(timer);
270
+ resolve(true);
271
+ });
272
+ });
273
+ }
274
+ killDevServer() {
275
+ if (this.devServerProcess && !this.devServerProcess.killed) {
276
+ this.devServerProcess.kill("SIGTERM");
277
+ this.devServerProcess = null;
278
+ log.debug(this.tag, "Killed dev server");
279
+ }
280
+ }
281
+ cleanup() {
282
+ if (this.timeoutTimer) {
283
+ clearTimeout(this.timeoutTimer);
284
+ this.timeoutTimer = null;
285
+ }
286
+ this.killDevServer();
287
+ // Clean up worktree on error
288
+ if (this.worktreePath && this.state === "error") {
289
+ try {
290
+ cleanupWorktree(this.worktreePath, this.branchName ?? undefined);
291
+ }
292
+ catch {
293
+ log.warn(this.tag, "Failed to cleanup review worktree");
294
+ }
295
+ }
296
+ this.process = null;
297
+ this.cardId = null;
298
+ this.branchName = null;
299
+ this.worktreePath = null;
300
+ this.startedAt = null;
301
+ }
302
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Checkout an existing remote branch into a worktree for review.
3
+ * Unlike createWorktree() which creates a new branch, this checks out
4
+ * an existing branch that was pushed by the implementation worker.
5
+ */
6
+ export declare function checkoutExistingBranch(basePath: string, branchName: string): string;
7
+ /**
8
+ * Extract branch name from card description's completion summary.
9
+ * Looks for: Branch: `agent/...`
10
+ * Validates that the extracted name contains only safe characters.
11
+ */
12
+ export declare function extractBranchFromDescription(description: string | null | undefined): string | null;
@@ -0,0 +1,83 @@
1
+ import { execFileSync, execSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { log } from "./log.js";
5
+ import { installCommand } from "./pm.js";
6
+ import { cleanupWorktree } from "./worktree.js";
7
+ const TAG = "review-worktree";
8
+ /**
9
+ * Checkout an existing remote branch into a worktree for review.
10
+ * Unlike createWorktree() which creates a new branch, this checks out
11
+ * an existing branch that was pushed by the implementation worker.
12
+ */
13
+ export function checkoutExistingBranch(basePath, branchName) {
14
+ const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
15
+ encoding: "utf-8",
16
+ }).trim();
17
+ const worktreeDir = resolve(repoRoot, basePath, `review-${branchName}`);
18
+ if (existsSync(worktreeDir)) {
19
+ log.warn(TAG, `Review worktree already exists at ${worktreeDir}, cleaning up`);
20
+ cleanupWorktree(worktreeDir);
21
+ }
22
+ // Fetch latest to ensure we have the remote branch
23
+ try {
24
+ execFileSync("git", ["fetch", "origin", branchName], {
25
+ cwd: repoRoot,
26
+ stdio: "pipe",
27
+ });
28
+ }
29
+ catch {
30
+ throw new Error(`Failed to fetch remote branch: ${branchName}`);
31
+ }
32
+ // Delete stale local branch if it exists
33
+ try {
34
+ execFileSync("git", ["branch", "-D", branchName], {
35
+ cwd: repoRoot,
36
+ stdio: "pipe",
37
+ });
38
+ }
39
+ catch {
40
+ // Branch doesn't exist locally — that's fine
41
+ }
42
+ // Create worktree from the remote branch
43
+ log.info(TAG, `Creating review worktree: ${worktreeDir} (branch: ${branchName})`);
44
+ execFileSync("git", [
45
+ "worktree",
46
+ "add",
47
+ "--track",
48
+ "-b",
49
+ branchName,
50
+ worktreeDir,
51
+ `origin/${branchName}`,
52
+ ], { cwd: repoRoot, stdio: "pipe" });
53
+ // Install dependencies
54
+ log.info(TAG, "Installing dependencies in review worktree...");
55
+ try {
56
+ execSync(installCommand(), {
57
+ cwd: worktreeDir,
58
+ stdio: "pipe",
59
+ timeout: 60_000,
60
+ });
61
+ }
62
+ catch {
63
+ log.warn(TAG, "Install failed (may be fine if deps are hoisted)");
64
+ }
65
+ return worktreeDir;
66
+ }
67
+ /**
68
+ * Extract branch name from card description's completion summary.
69
+ * Looks for: Branch: `agent/...`
70
+ * Validates that the extracted name contains only safe characters.
71
+ */
72
+ export function extractBranchFromDescription(description) {
73
+ if (!description)
74
+ return null;
75
+ const match = description.match(/Branch:\s*`([^`]+)`/);
76
+ const branch = match?.[1] ?? null;
77
+ // Validate branch name contains only safe git ref characters
78
+ if (branch && !/^[a-zA-Z0-9/_.-]+$/.test(branch)) {
79
+ log.warn(TAG, `Extracted branch name contains unsafe characters: ${branch}`);
80
+ return null;
81
+ }
82
+ return branch;
83
+ }
@@ -0,0 +1,22 @@
1
+ import { EventEmitter } from "node:events";
2
+ import type { Readable } from "node:stream";
3
+ export interface StreamParserEvents {
4
+ tool_start: [name: string, input: unknown];
5
+ tool_end: [name: string, toolUseId: string];
6
+ text: [content: string];
7
+ result: [stopReason: string];
8
+ error: [msg: string];
9
+ }
10
+ export declare class StreamParser extends EventEmitter<StreamParserEvents> {
11
+ private buffer;
12
+ private finalText;
13
+ get outputText(): string;
14
+ /**
15
+ * Pipe a readable stream (Claude CLI stdout) through the parser.
16
+ * Returns when the stream ends.
17
+ */
18
+ pipe(stream: Readable): void;
19
+ private flush;
20
+ private parseLine;
21
+ private handleMessage;
22
+ }
@@ -0,0 +1,81 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { log } from "./log.js";
3
+ const TAG = "stream-parser";
4
+ export class StreamParser extends EventEmitter {
5
+ buffer = "";
6
+ finalText = "";
7
+ get outputText() {
8
+ return this.finalText;
9
+ }
10
+ /**
11
+ * Pipe a readable stream (Claude CLI stdout) through the parser.
12
+ * Returns when the stream ends.
13
+ */
14
+ pipe(stream) {
15
+ stream.on("data", (chunk) => {
16
+ this.buffer += chunk.toString();
17
+ this.flush();
18
+ });
19
+ stream.on("end", () => {
20
+ // Process any remaining buffer
21
+ if (this.buffer.trim()) {
22
+ this.flush();
23
+ }
24
+ });
25
+ }
26
+ flush() {
27
+ const lines = this.buffer.split("\n");
28
+ // Keep incomplete last line in buffer
29
+ this.buffer = lines.pop() ?? "";
30
+ for (const line of lines) {
31
+ const trimmed = line.trim();
32
+ if (!trimmed)
33
+ continue;
34
+ this.parseLine(trimmed);
35
+ }
36
+ }
37
+ parseLine(line) {
38
+ let msg;
39
+ try {
40
+ msg = JSON.parse(line);
41
+ }
42
+ catch {
43
+ // Not valid JSON — skip silently (could be stray output)
44
+ log.debug(TAG, `Non-JSON line: ${line.slice(0, 100)}`);
45
+ return;
46
+ }
47
+ try {
48
+ this.handleMessage(msg);
49
+ }
50
+ catch (err) {
51
+ const errMsg = err instanceof Error ? err.message : String(err);
52
+ log.warn(TAG, `Error handling stream event: ${errMsg}`);
53
+ this.emit("error", errMsg);
54
+ }
55
+ }
56
+ handleMessage(msg) {
57
+ switch (msg.type) {
58
+ case "assistant": {
59
+ if (msg.subtype === "tool_use" && msg.tool_name) {
60
+ this.emit("tool_start", msg.tool_name, msg.input);
61
+ }
62
+ else if (msg.subtype === "text" && msg.content) {
63
+ this.finalText += msg.content;
64
+ this.emit("text", msg.content);
65
+ }
66
+ break;
67
+ }
68
+ case "tool_result": {
69
+ if (msg.tool_use_id && msg.tool_name) {
70
+ this.emit("tool_end", msg.tool_name, msg.tool_use_id);
71
+ }
72
+ break;
73
+ }
74
+ case "result": {
75
+ this.emit("result", msg.stop_reason ?? "unknown");
76
+ break;
77
+ }
78
+ // Ignore system, ping, cost_update, etc.
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,74 @@
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 AGENT_NAME = "Harmony Agent";
53
+ export declare function agentIdentifier(workerId: number): string;
54
+ export type ProgressPhase = "exploring" | "implementing" | "testing" | "committing" | "finishing";
55
+ export type WorkerState = "idle" | "preparing" | "running" | "completing" | "verifying" | "cancelling" | "error";
56
+ export interface QueueItem {
57
+ cardId: string;
58
+ shortId: number;
59
+ title: string;
60
+ priority: number;
61
+ enqueuedAt: number;
62
+ mode: WorkMode;
63
+ }
64
+ export interface EnrichedCard {
65
+ card: Card;
66
+ column: Column;
67
+ labels: Label[];
68
+ subtasks: Subtask[];
69
+ mode: WorkMode;
70
+ }
71
+ export interface RealtimeCredentials {
72
+ supabaseUrl: string;
73
+ supabaseAnonKey: string;
74
+ }
package/dist/types.js ADDED
@@ -0,0 +1,53 @@
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
+ // ============ AGENT IDENTITY ============
50
+ export const AGENT_NAME = "Harmony Agent";
51
+ export function agentIdentifier(workerId) {
52
+ return `harmony-daemon-${workerId}`;
53
+ }
@@ -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>;