@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/worker.js
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { moveCardAndAddLabel } from "./board-helpers.js";
|
|
3
|
+
import { runCompletion } from "./completion.js";
|
|
4
|
+
import { log } from "./log.js";
|
|
5
|
+
import { ProgressTracker } from "./progress-tracker.js";
|
|
6
|
+
import { buildPrompt } from "./prompt.js";
|
|
7
|
+
import { StreamParser } from "./stream-parser.js";
|
|
8
|
+
import { AGENT_NAME, agentIdentifier, } from "./types.js";
|
|
9
|
+
import { cleanupWorktree, createWorktree, makeBranchName } from "./worktree.js";
|
|
10
|
+
const TAG = "worker";
|
|
11
|
+
const CANCEL_SIGINT_TIMEOUT = 30_000;
|
|
12
|
+
const CANCEL_SIGTERM_TIMEOUT = 10_000;
|
|
13
|
+
export class Worker {
|
|
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
|
+
timeoutTimer = null;
|
|
25
|
+
progressTracker = null;
|
|
26
|
+
lastSessionStats;
|
|
27
|
+
aborted = false;
|
|
28
|
+
constructor(id, config, client, _userEmail, onDone) {
|
|
29
|
+
this.config = config;
|
|
30
|
+
this.client = client;
|
|
31
|
+
this.onDone = onDone;
|
|
32
|
+
this.id = id;
|
|
33
|
+
}
|
|
34
|
+
get tag() {
|
|
35
|
+
return `${TAG}:${this.id}`;
|
|
36
|
+
}
|
|
37
|
+
get isIdle() {
|
|
38
|
+
return this.state === "idle";
|
|
39
|
+
}
|
|
40
|
+
get isActive() {
|
|
41
|
+
return (this.state === "preparing" ||
|
|
42
|
+
this.state === "running" ||
|
|
43
|
+
this.state === "verifying" ||
|
|
44
|
+
this.state === "completing");
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Start working on a card. Runs the full lifecycle:
|
|
48
|
+
* PREPARING → RUNNING → COMPLETING → IDLE
|
|
49
|
+
*/
|
|
50
|
+
async run(card, column, labels, subtasks) {
|
|
51
|
+
this.aborted = false;
|
|
52
|
+
this.cardId = card.id;
|
|
53
|
+
this.startedAt = Date.now();
|
|
54
|
+
try {
|
|
55
|
+
// --- PREPARING ---
|
|
56
|
+
this.state = "preparing";
|
|
57
|
+
this.branchName = makeBranchName(card.short_id, card.title);
|
|
58
|
+
log.info(this.tag, `Preparing #${card.short_id} "${card.title}"`);
|
|
59
|
+
// Start agent session and make it visible on the board
|
|
60
|
+
await this.client.startAgentSession(card.id, {
|
|
61
|
+
agentIdentifier: agentIdentifier(this.id),
|
|
62
|
+
agentName: AGENT_NAME,
|
|
63
|
+
status: "working",
|
|
64
|
+
currentTask: "Setting up worktree",
|
|
65
|
+
progressPercent: 5,
|
|
66
|
+
});
|
|
67
|
+
// Move card to "In Progress" and add "agent" label so the board shows the progress ring
|
|
68
|
+
const moved = await moveCardAndAddLabel(this.client, card, "In Progress", "agent");
|
|
69
|
+
if (!moved) {
|
|
70
|
+
log.warn(this.tag, `Card #${card.short_id} was NOT moved to "In Progress" — check API logs`);
|
|
71
|
+
}
|
|
72
|
+
if (this.aborted)
|
|
73
|
+
return;
|
|
74
|
+
// Create worktree
|
|
75
|
+
this.worktreePath = createWorktree(this.config.worktree.basePath, this.config.worktree.baseBranch, this.branchName);
|
|
76
|
+
if (this.aborted)
|
|
77
|
+
return;
|
|
78
|
+
// --- RUNNING ---
|
|
79
|
+
this.state = "running";
|
|
80
|
+
const enriched = {
|
|
81
|
+
card,
|
|
82
|
+
column,
|
|
83
|
+
labels,
|
|
84
|
+
subtasks,
|
|
85
|
+
mode: "implement",
|
|
86
|
+
};
|
|
87
|
+
const prompt = buildPrompt(enriched, this.branchName, this.worktreePath);
|
|
88
|
+
await this.client.updateAgentProgress(card.id, {
|
|
89
|
+
agentIdentifier: agentIdentifier(this.id),
|
|
90
|
+
agentName: AGENT_NAME,
|
|
91
|
+
status: "working",
|
|
92
|
+
currentTask: "Running Claude CLI",
|
|
93
|
+
progressPercent: 10,
|
|
94
|
+
});
|
|
95
|
+
// Start timeout watchdog
|
|
96
|
+
this.timeoutTimer = setTimeout(() => {
|
|
97
|
+
log.warn(this.tag, `Timeout reached (${this.config.maxTimeout}ms), cancelling`);
|
|
98
|
+
this.cancel();
|
|
99
|
+
}, this.config.maxTimeout);
|
|
100
|
+
// Spawn Claude CLI
|
|
101
|
+
await this.spawnClaude(prompt, card, subtasks);
|
|
102
|
+
if (this.aborted)
|
|
103
|
+
return;
|
|
104
|
+
// --- VERIFYING + COMPLETING ---
|
|
105
|
+
this.state = "verifying";
|
|
106
|
+
log.info(this.tag, `Claude finished for #${card.short_id}, running verification & completion`);
|
|
107
|
+
await this.client.updateAgentProgress(card.id, {
|
|
108
|
+
agentIdentifier: agentIdentifier(this.id),
|
|
109
|
+
agentName: AGENT_NAME,
|
|
110
|
+
status: "working",
|
|
111
|
+
currentTask: "Verifying implementation",
|
|
112
|
+
progressPercent: 75,
|
|
113
|
+
});
|
|
114
|
+
this.state = "completing";
|
|
115
|
+
await runCompletion(this.client, card, this.branchName, this.worktreePath, this.config, this.id, this.lastSessionStats);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
this.state = "error";
|
|
119
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
120
|
+
log.error(this.tag, `Error on #${card.short_id}: ${msg}`);
|
|
121
|
+
// End session as paused on error
|
|
122
|
+
try {
|
|
123
|
+
await this.client.endAgentSession(card.id, {
|
|
124
|
+
status: "paused",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// best-effort
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
this.cleanup();
|
|
133
|
+
this.state = "idle";
|
|
134
|
+
this.onDone(this);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Pause the current work by suspending the Claude process (SIGSTOP).
|
|
139
|
+
*/
|
|
140
|
+
async pause() {
|
|
141
|
+
if (!this.isActive || !this.process || this.process.killed)
|
|
142
|
+
return;
|
|
143
|
+
log.info(this.tag, `Pausing work on ${this.cardId}`);
|
|
144
|
+
this.process.kill("SIGSTOP");
|
|
145
|
+
// Pause the timeout timer
|
|
146
|
+
if (this.timeoutTimer) {
|
|
147
|
+
clearTimeout(this.timeoutTimer);
|
|
148
|
+
this.timeoutTimer = null;
|
|
149
|
+
}
|
|
150
|
+
// Update agent session so the UI reflects the paused state
|
|
151
|
+
if (this.cardId) {
|
|
152
|
+
try {
|
|
153
|
+
await this.client.updateAgentProgress(this.cardId, {
|
|
154
|
+
agentIdentifier: agentIdentifier(this.id),
|
|
155
|
+
agentName: AGENT_NAME,
|
|
156
|
+
status: "paused",
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
log.warn(this.tag, "Failed to update agent session to paused");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Resume the Claude process after a pause (SIGCONT).
|
|
166
|
+
*/
|
|
167
|
+
async resume() {
|
|
168
|
+
if (!this.isActive || !this.process || this.process.killed)
|
|
169
|
+
return;
|
|
170
|
+
log.info(this.tag, `Resuming work on ${this.cardId}`);
|
|
171
|
+
this.process.kill("SIGCONT");
|
|
172
|
+
// Restart timeout timer with remaining time (use full timeout for simplicity)
|
|
173
|
+
this.timeoutTimer = setTimeout(() => {
|
|
174
|
+
log.warn(this.tag, `Timeout reached (${this.config.maxTimeout}ms), cancelling`);
|
|
175
|
+
this.cancel();
|
|
176
|
+
}, this.config.maxTimeout);
|
|
177
|
+
// Update agent session so the UI reflects the resumed state
|
|
178
|
+
if (this.cardId) {
|
|
179
|
+
try {
|
|
180
|
+
await this.client.updateAgentProgress(this.cardId, {
|
|
181
|
+
agentIdentifier: agentIdentifier(this.id),
|
|
182
|
+
agentName: AGENT_NAME,
|
|
183
|
+
status: "working",
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
log.warn(this.tag, "Failed to update agent session to working");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Cancel the current work. Sends escalating signals to the Claude process.
|
|
193
|
+
*/
|
|
194
|
+
async cancel() {
|
|
195
|
+
if (!this.isActive)
|
|
196
|
+
return;
|
|
197
|
+
this.aborted = true;
|
|
198
|
+
this.state = "cancelling";
|
|
199
|
+
log.info(this.tag, `Cancelling work on ${this.cardId}`);
|
|
200
|
+
if (this.process && !this.process.killed) {
|
|
201
|
+
// Resume first in case the process is suspended
|
|
202
|
+
this.process.kill("SIGCONT");
|
|
203
|
+
// Step 1: SIGINT (let Claude save state gracefully)
|
|
204
|
+
this.process.kill("SIGINT");
|
|
205
|
+
log.debug(this.tag, "Sent SIGINT");
|
|
206
|
+
const sigintDead = await this.waitForExit(CANCEL_SIGINT_TIMEOUT);
|
|
207
|
+
if (sigintDead)
|
|
208
|
+
return;
|
|
209
|
+
// Step 2: SIGTERM
|
|
210
|
+
this.process.kill("SIGTERM");
|
|
211
|
+
log.debug(this.tag, "Sent SIGTERM");
|
|
212
|
+
const sigtermDead = await this.waitForExit(CANCEL_SIGTERM_TIMEOUT);
|
|
213
|
+
if (sigtermDead)
|
|
214
|
+
return;
|
|
215
|
+
// Step 3: SIGKILL
|
|
216
|
+
this.process.kill("SIGKILL");
|
|
217
|
+
log.warn(this.tag, "Sent SIGKILL");
|
|
218
|
+
}
|
|
219
|
+
// End agent session as paused
|
|
220
|
+
if (this.cardId) {
|
|
221
|
+
try {
|
|
222
|
+
await this.client.endAgentSession(this.cardId, { status: "paused" });
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// best-effort
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async spawnClaude(prompt, card, subtasks) {
|
|
230
|
+
return new Promise((resolve, reject) => {
|
|
231
|
+
const args = [
|
|
232
|
+
"-p",
|
|
233
|
+
"--verbose", // required for stream-json to emit all event types
|
|
234
|
+
"--output-format",
|
|
235
|
+
"stream-json",
|
|
236
|
+
"--model",
|
|
237
|
+
this.config.claude.model,
|
|
238
|
+
"--max-turns",
|
|
239
|
+
String(this.config.claude.maxTurns),
|
|
240
|
+
"--allowedTools",
|
|
241
|
+
"Bash,Read,Write,Edit,Glob,Grep,Agent,mcp__harmony__*",
|
|
242
|
+
...this.config.claude.additionalArgs,
|
|
243
|
+
"--",
|
|
244
|
+
prompt,
|
|
245
|
+
];
|
|
246
|
+
log.info(this.tag, `Spawning: claude ${args.slice(0, 4).join(" ")} ...`);
|
|
247
|
+
this.process = spawn("claude", args, {
|
|
248
|
+
cwd: this.worktreePath,
|
|
249
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
250
|
+
});
|
|
251
|
+
// Stream parser for structured NDJSON events
|
|
252
|
+
const parser = new StreamParser();
|
|
253
|
+
// Progress tracker for phase-based updates
|
|
254
|
+
this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks);
|
|
255
|
+
this.progressTracker.attach(parser);
|
|
256
|
+
// Attach stdout to parser
|
|
257
|
+
if (this.process.stdout) {
|
|
258
|
+
parser.attach(this.process.stdout);
|
|
259
|
+
}
|
|
260
|
+
parser.on("parse_error", (msg) => {
|
|
261
|
+
log.debug(this.tag, `Stream parse error (non-fatal): ${msg}`);
|
|
262
|
+
});
|
|
263
|
+
let stderr = "";
|
|
264
|
+
this.process.stderr?.on("data", (data) => {
|
|
265
|
+
stderr += data.toString();
|
|
266
|
+
});
|
|
267
|
+
this.process.on("error", (err) => {
|
|
268
|
+
reject(new Error(`Failed to spawn claude: ${err.message}`));
|
|
269
|
+
});
|
|
270
|
+
this.process.on("close", (code) => {
|
|
271
|
+
this.process = null;
|
|
272
|
+
this.lastSessionStats = this.progressTracker?.stats;
|
|
273
|
+
this.progressTracker?.stop();
|
|
274
|
+
this.progressTracker = null;
|
|
275
|
+
if (this.aborted) {
|
|
276
|
+
resolve(); // Cancellation is not an error
|
|
277
|
+
}
|
|
278
|
+
else if (code === 0) {
|
|
279
|
+
resolve();
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
reject(new Error(`claude exited with code ${code}${stderr ? `: ${stderr.slice(0, 500)}` : ""}`));
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
waitForExit(timeout) {
|
|
288
|
+
return new Promise((resolve) => {
|
|
289
|
+
if (!this.process || this.process.killed) {
|
|
290
|
+
resolve(true);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const timer = setTimeout(() => {
|
|
294
|
+
resolve(false);
|
|
295
|
+
}, timeout);
|
|
296
|
+
this.process.once("close", () => {
|
|
297
|
+
clearTimeout(timer);
|
|
298
|
+
resolve(true);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
cleanup() {
|
|
303
|
+
if (this.progressTracker) {
|
|
304
|
+
this.progressTracker.stop();
|
|
305
|
+
this.progressTracker = null;
|
|
306
|
+
}
|
|
307
|
+
this.lastSessionStats = undefined;
|
|
308
|
+
if (this.timeoutTimer) {
|
|
309
|
+
clearTimeout(this.timeoutTimer);
|
|
310
|
+
this.timeoutTimer = null;
|
|
311
|
+
}
|
|
312
|
+
// Clean up worktree + branch on error
|
|
313
|
+
if (this.worktreePath && this.state === "error") {
|
|
314
|
+
try {
|
|
315
|
+
cleanupWorktree(this.worktreePath, this.branchName ?? undefined);
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
log.warn(this.tag, "Failed to cleanup worktree");
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
this.process = null;
|
|
322
|
+
this.cardId = null;
|
|
323
|
+
this.branchName = null;
|
|
324
|
+
this.worktreePath = null;
|
|
325
|
+
this.startedAt = null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a git worktree for the agent to work in.
|
|
3
|
+
* Returns the absolute path to the new worktree.
|
|
4
|
+
*/
|
|
5
|
+
export declare function createWorktree(basePath: string, baseBranch: string, branchName: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Remove a git worktree and its branch.
|
|
8
|
+
*/
|
|
9
|
+
export declare function cleanupWorktree(worktreePath: string, branchName?: string): void;
|
|
10
|
+
/**
|
|
11
|
+
* Generate a branch name from a card's short ID and title.
|
|
12
|
+
*/
|
|
13
|
+
export declare function makeBranchName(shortId: number, title: string): string;
|
package/dist/worktree.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
2
|
+
import { existsSync, rmSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { log } from "./log.js";
|
|
5
|
+
import { installCommand } from "./pm.js";
|
|
6
|
+
const TAG = "worktree";
|
|
7
|
+
/**
|
|
8
|
+
* Create a git worktree for the agent to work in.
|
|
9
|
+
* Returns the absolute path to the new worktree.
|
|
10
|
+
*/
|
|
11
|
+
export function createWorktree(basePath, baseBranch, branchName) {
|
|
12
|
+
const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
13
|
+
encoding: "utf-8",
|
|
14
|
+
}).trim();
|
|
15
|
+
const worktreeDir = resolve(repoRoot, basePath, branchName);
|
|
16
|
+
if (existsSync(worktreeDir)) {
|
|
17
|
+
log.warn(TAG, `Worktree already exists at ${worktreeDir}, cleaning up`);
|
|
18
|
+
cleanupWorktree(worktreeDir);
|
|
19
|
+
}
|
|
20
|
+
// Fetch latest from remote to ensure base branch is up to date
|
|
21
|
+
try {
|
|
22
|
+
execFileSync("git", ["fetch", "origin", baseBranch], {
|
|
23
|
+
cwd: repoRoot,
|
|
24
|
+
stdio: "pipe",
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
log.warn(TAG, "Failed to fetch latest — continuing with local state");
|
|
29
|
+
}
|
|
30
|
+
// Delete stale branch if it exists from a previous run
|
|
31
|
+
try {
|
|
32
|
+
execFileSync("git", ["branch", "-D", branchName], {
|
|
33
|
+
cwd: repoRoot,
|
|
34
|
+
stdio: "pipe",
|
|
35
|
+
});
|
|
36
|
+
log.info(TAG, `Deleted stale branch: ${branchName}`);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Branch doesn't exist — that's fine
|
|
40
|
+
}
|
|
41
|
+
// Create worktree with a new branch based on origin/<baseBranch>
|
|
42
|
+
log.info(TAG, `Creating worktree: ${worktreeDir} (branch: ${branchName})`);
|
|
43
|
+
execFileSync("git", ["worktree", "add", "-b", branchName, worktreeDir, `origin/${baseBranch}`], { cwd: repoRoot, stdio: "pipe" });
|
|
44
|
+
// Install dependencies in the worktree
|
|
45
|
+
log.info(TAG, "Installing dependencies in worktree...");
|
|
46
|
+
try {
|
|
47
|
+
execSync(installCommand(), {
|
|
48
|
+
cwd: worktreeDir,
|
|
49
|
+
stdio: "pipe",
|
|
50
|
+
timeout: 60_000,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
log.warn(TAG, "Install failed (may be fine if deps are hoisted)");
|
|
55
|
+
}
|
|
56
|
+
return worktreeDir;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Remove a git worktree and its branch.
|
|
60
|
+
*/
|
|
61
|
+
export function cleanupWorktree(worktreePath, branchName) {
|
|
62
|
+
const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
63
|
+
encoding: "utf-8",
|
|
64
|
+
}).trim();
|
|
65
|
+
try {
|
|
66
|
+
execFileSync("git", ["worktree", "remove", worktreePath, "--force"], {
|
|
67
|
+
cwd: repoRoot,
|
|
68
|
+
stdio: "pipe",
|
|
69
|
+
});
|
|
70
|
+
log.info(TAG, `Removed worktree: ${worktreePath}`);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
log.warn(TAG, `Failed to remove worktree cleanly: ${err instanceof Error ? err.message : err}`);
|
|
74
|
+
// Force-remove the directory if git worktree remove failed
|
|
75
|
+
if (existsSync(worktreePath)) {
|
|
76
|
+
rmSync(worktreePath, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
// Prune stale worktree entries
|
|
79
|
+
try {
|
|
80
|
+
execFileSync("git", ["worktree", "prune"], {
|
|
81
|
+
cwd: repoRoot,
|
|
82
|
+
stdio: "pipe",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// best-effort
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Delete the branch so it doesn't block future runs
|
|
90
|
+
if (branchName) {
|
|
91
|
+
try {
|
|
92
|
+
execFileSync("git", ["branch", "-D", branchName], {
|
|
93
|
+
cwd: repoRoot,
|
|
94
|
+
stdio: "pipe",
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// best-effort
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Generate a branch name from a card's short ID and title.
|
|
104
|
+
*/
|
|
105
|
+
export function makeBranchName(shortId, title) {
|
|
106
|
+
const slug = title
|
|
107
|
+
.toLowerCase()
|
|
108
|
+
.trim()
|
|
109
|
+
.replace(/[^\w\s-]/g, "")
|
|
110
|
+
.replace(/\s+/g, "-")
|
|
111
|
+
.replace(/-+/g, "-")
|
|
112
|
+
.replace(/^-+|-+$/g, "")
|
|
113
|
+
.slice(0, 40);
|
|
114
|
+
return `agent/${shortId}-${slug || "task"}`;
|
|
115
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gethmy/agent",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
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",
|
|
@@ -32,21 +32,22 @@
|
|
|
32
32
|
"automation"
|
|
33
33
|
],
|
|
34
34
|
"engines": {
|
|
35
|
+
"node": ">=20.0.0",
|
|
35
36
|
"bun": ">=1.0.0"
|
|
36
37
|
},
|
|
37
38
|
"scripts": {
|
|
38
|
-
"start": "
|
|
39
|
-
"build": "
|
|
39
|
+
"start": "node dist/index.js",
|
|
40
|
+
"build": "tsc",
|
|
40
41
|
"typecheck": "tsc --noEmit",
|
|
41
|
-
"prepublishOnly": "
|
|
42
|
+
"prepublishOnly": "npm run build"
|
|
42
43
|
},
|
|
43
44
|
"dependencies": {
|
|
44
45
|
"@supabase/supabase-js": "2.95.3",
|
|
45
|
-
"@gethmy/mcp": "
|
|
46
|
+
"@gethmy/mcp": "^2.0.0"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"@harmony/shared": "workspace:*",
|
|
49
|
-
"@types/node": "^25.
|
|
50
|
-
"typescript": "^
|
|
50
|
+
"@types/node": "^25.5.0",
|
|
51
|
+
"typescript": "^6.0.1"
|
|
51
52
|
}
|
|
52
53
|
}
|