@gethmy/agent 1.7.0 → 1.7.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 +8 -1
- package/dist/cli.js +6376 -205
- package/dist/index.js +6206 -341
- package/package.json +2 -2
- package/dist/board-helpers.d.ts +0 -31
- package/dist/board-helpers.js +0 -150
- package/dist/budget.d.ts +0 -47
- package/dist/budget.js +0 -161
- package/dist/cli.d.ts +0 -16
- package/dist/completion.d.ts +0 -32
- package/dist/completion.js +0 -304
- package/dist/config-validation.d.ts +0 -23
- package/dist/config-validation.js +0 -77
- package/dist/config.d.ts +0 -23
- package/dist/config.js +0 -103
- package/dist/episode-writer.d.ts +0 -84
- package/dist/episode-writer.js +0 -232
- package/dist/git-pr.d.ts +0 -38
- package/dist/git-pr.js +0 -399
- package/dist/http-server.d.ts +0 -79
- package/dist/http-server.js +0 -114
- package/dist/index.d.ts +0 -5
- package/dist/log.d.ts +0 -34
- package/dist/log.js +0 -100
- package/dist/merge-monitor.d.ts +0 -23
- package/dist/merge-monitor.js +0 -169
- package/dist/pm.d.ts +0 -14
- package/dist/pm.js +0 -63
- package/dist/pool.d.ts +0 -70
- package/dist/pool.js +0 -258
- package/dist/process-group.d.ts +0 -26
- package/dist/process-group.js +0 -72
- package/dist/progress-tracker.d.ts +0 -79
- package/dist/progress-tracker.js +0 -442
- package/dist/prompt.d.ts +0 -18
- package/dist/prompt.js +0 -117
- package/dist/queue.d.ts +0 -39
- package/dist/queue.js +0 -100
- package/dist/reconcile.d.ts +0 -35
- package/dist/reconcile.js +0 -174
- package/dist/recovery.d.ts +0 -30
- package/dist/recovery.js +0 -141
- package/dist/review-completion.d.ts +0 -40
- package/dist/review-completion.js +0 -474
- package/dist/review-knowledge.d.ts +0 -14
- package/dist/review-knowledge.js +0 -89
- package/dist/review-prompt.d.ts +0 -12
- package/dist/review-prompt.js +0 -103
- package/dist/review-worker.d.ts +0 -56
- package/dist/review-worker.js +0 -638
- package/dist/review-worktree.d.ts +0 -12
- package/dist/review-worktree.js +0 -95
- package/dist/run-log.d.ts +0 -6
- package/dist/run-log.js +0 -19
- package/dist/startup-banner.d.ts +0 -29
- package/dist/startup-banner.js +0 -143
- package/dist/state-store.d.ts +0 -88
- package/dist/state-store.js +0 -239
- package/dist/stream-parser-selftest.d.ts +0 -9
- package/dist/stream-parser-selftest.js +0 -97
- package/dist/stream-parser.d.ts +0 -43
- package/dist/stream-parser.js +0 -174
- package/dist/transitions.d.ts +0 -57
- package/dist/transitions.js +0 -131
- package/dist/types.d.ts +0 -140
- package/dist/types.js +0 -79
- package/dist/verification.d.ts +0 -39
- package/dist/verification.js +0 -317
- package/dist/watcher.d.ts +0 -53
- package/dist/watcher.js +0 -153
- package/dist/worker.d.ts +0 -53
- package/dist/worker.js +0 -464
- package/dist/worktree-gc.d.ts +0 -67
- package/dist/worktree-gc.js +0 -245
- package/dist/worktree.d.ts +0 -18
- package/dist/worktree.js +0 -177
package/dist/worker.js
DELETED
|
@@ -1,464 +0,0 @@
|
|
|
1
|
-
import { moveCardAndAddLabel } from "./board-helpers.js";
|
|
2
|
-
import { buildTokenPayload, runCompletion, } from "./completion.js";
|
|
3
|
-
import { log } from "./log.js";
|
|
4
|
-
import { signalGroup, spawnInGroup, terminateGroup } from "./process-group.js";
|
|
5
|
-
import { ProgressTracker } from "./progress-tracker.js";
|
|
6
|
-
import { buildPrompt } from "./prompt.js";
|
|
7
|
-
import { openRunLog } from "./run-log.js";
|
|
8
|
-
import { newRunId } from "./state-store.js";
|
|
9
|
-
import { StreamParser } from "./stream-parser.js";
|
|
10
|
-
import { runTransition, TransitionError } from "./transitions.js";
|
|
11
|
-
import { AGENT_NAME, agentIdentifier, } from "./types.js";
|
|
12
|
-
import { cleanupWorktree, createWorktree, makeBranchName } from "./worktree.js";
|
|
13
|
-
const TAG = "worker";
|
|
14
|
-
const CANCEL_SIGINT_TIMEOUT = 30_000;
|
|
15
|
-
const CANCEL_SIGTERM_TIMEOUT = 10_000;
|
|
16
|
-
export class Worker {
|
|
17
|
-
config;
|
|
18
|
-
client;
|
|
19
|
-
onDone;
|
|
20
|
-
workspaceId;
|
|
21
|
-
projectId;
|
|
22
|
-
stateStore;
|
|
23
|
-
id;
|
|
24
|
-
state = "idle";
|
|
25
|
-
cardId = null;
|
|
26
|
-
branchName = null;
|
|
27
|
-
worktreePath = null;
|
|
28
|
-
startedAt = null;
|
|
29
|
-
process = null;
|
|
30
|
-
timeoutTimer = null;
|
|
31
|
-
heartbeatTimer = null;
|
|
32
|
-
progressTracker = null;
|
|
33
|
-
lastSessionStats;
|
|
34
|
-
aborted = false;
|
|
35
|
-
sessionId = null;
|
|
36
|
-
runId = null;
|
|
37
|
-
constructor(id, config, client, _userEmail, onDone, workspaceId, projectId, stateStore) {
|
|
38
|
-
this.config = config;
|
|
39
|
-
this.client = client;
|
|
40
|
-
this.onDone = onDone;
|
|
41
|
-
this.workspaceId = workspaceId;
|
|
42
|
-
this.projectId = projectId;
|
|
43
|
-
this.stateStore = stateStore;
|
|
44
|
-
this.id = id;
|
|
45
|
-
}
|
|
46
|
-
startHeartbeat() {
|
|
47
|
-
this.stopHeartbeat();
|
|
48
|
-
const interval = this.config.timing.heartbeatMs;
|
|
49
|
-
this.heartbeatTimer = setInterval(() => {
|
|
50
|
-
if (this.runId) {
|
|
51
|
-
this.stateStore.heartbeat(this.runId).catch(() => {
|
|
52
|
-
/* next tick will retry */
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
}, interval);
|
|
56
|
-
// Don't block event loop shutdown for a pending heartbeat.
|
|
57
|
-
this.heartbeatTimer.unref();
|
|
58
|
-
}
|
|
59
|
-
stopHeartbeat() {
|
|
60
|
-
if (this.heartbeatTimer) {
|
|
61
|
-
clearInterval(this.heartbeatTimer);
|
|
62
|
-
this.heartbeatTimer = null;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
async recordPhase(phase) {
|
|
66
|
-
if (!this.runId)
|
|
67
|
-
return;
|
|
68
|
-
try {
|
|
69
|
-
await this.stateStore.updateRun(this.runId, {
|
|
70
|
-
phase,
|
|
71
|
-
lastHeartbeatAt: Date.now(),
|
|
72
|
-
worktreePath: this.worktreePath,
|
|
73
|
-
branchName: this.branchName,
|
|
74
|
-
sessionId: this.sessionId,
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
catch (err) {
|
|
78
|
-
log.warn(this.tag, `state store updateRun failed: ${err instanceof Error ? err.message : err}`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
get tag() {
|
|
82
|
-
return `${TAG}:${this.id}`;
|
|
83
|
-
}
|
|
84
|
-
get isIdle() {
|
|
85
|
-
return this.state === "idle";
|
|
86
|
-
}
|
|
87
|
-
get isActive() {
|
|
88
|
-
return (this.state === "preparing" ||
|
|
89
|
-
this.state === "running" ||
|
|
90
|
-
this.state === "verifying" ||
|
|
91
|
-
this.state === "completing");
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Start working on a card. Runs the full lifecycle:
|
|
95
|
-
* PREPARING → RUNNING → COMPLETING → IDLE
|
|
96
|
-
*/
|
|
97
|
-
async run(card, column, labels, subtasks) {
|
|
98
|
-
this.aborted = false;
|
|
99
|
-
this.cardId = card.id;
|
|
100
|
-
this.startedAt = Date.now();
|
|
101
|
-
this.runId = newRunId();
|
|
102
|
-
try {
|
|
103
|
-
// --- PREPARING ---
|
|
104
|
-
this.state = "preparing";
|
|
105
|
-
this.branchName = makeBranchName(card.short_id, card.title, this.config.worktree.failedBranchPrefix);
|
|
106
|
-
log.info(this.tag, `Preparing #${card.short_id} "${card.title}"`);
|
|
107
|
-
// Per-card attempt counter resets on success; DLQ triggers off it.
|
|
108
|
-
await this.stateStore.incrementAttempt(card.id);
|
|
109
|
-
// Start the heartbeat loop so the reconciler knows this run is
|
|
110
|
-
// still alive even if no phase transitions happen for a while
|
|
111
|
-
// (Claude can spend 5+ minutes in one tool call).
|
|
112
|
-
this.startHeartbeat();
|
|
113
|
-
// Record this run durably so we can recover on crash.
|
|
114
|
-
await this.stateStore.insertRun({
|
|
115
|
-
runId: this.runId,
|
|
116
|
-
cardId: card.id,
|
|
117
|
-
cardShortId: card.short_id,
|
|
118
|
-
pipeline: "implement",
|
|
119
|
-
workerId: this.id,
|
|
120
|
-
sessionId: null,
|
|
121
|
-
worktreePath: null,
|
|
122
|
-
branchName: this.branchName,
|
|
123
|
-
daemonPid: process.pid,
|
|
124
|
-
phase: "preparing",
|
|
125
|
-
startedAt: this.startedAt,
|
|
126
|
-
lastHeartbeatAt: this.startedAt,
|
|
127
|
-
endedAt: null,
|
|
128
|
-
status: "active",
|
|
129
|
-
costCents: 0,
|
|
130
|
-
});
|
|
131
|
-
// Start agent session and make it visible on the board
|
|
132
|
-
const { session } = await this.client.startAgentSession(card.id, {
|
|
133
|
-
agentIdentifier: agentIdentifier(this.id),
|
|
134
|
-
agentName: AGENT_NAME,
|
|
135
|
-
status: "working",
|
|
136
|
-
currentTask: "Setting up worktree",
|
|
137
|
-
progressPercent: 5,
|
|
138
|
-
});
|
|
139
|
-
const sid = session && typeof session === "object" && "id" in session
|
|
140
|
-
? session.id
|
|
141
|
-
: null;
|
|
142
|
-
if (!sid) {
|
|
143
|
-
log.warn(TAG, "startAgentSession returned no session id");
|
|
144
|
-
}
|
|
145
|
-
this.sessionId = sid;
|
|
146
|
-
await this.recordPhase("preparing");
|
|
147
|
-
// Move card to "In Progress" and add "agent" label so the board shows the progress ring
|
|
148
|
-
const moved = await moveCardAndAddLabel(this.client, card, "In Progress", "agent");
|
|
149
|
-
if (!moved) {
|
|
150
|
-
log.warn(this.tag, `Card #${card.short_id} was NOT moved to "In Progress" — check API logs`);
|
|
151
|
-
}
|
|
152
|
-
if (this.aborted)
|
|
153
|
-
return;
|
|
154
|
-
// Create worktree
|
|
155
|
-
this.worktreePath = createWorktree(this.config.worktree.basePath, this.config.worktree.baseBranch, this.branchName);
|
|
156
|
-
if (this.aborted)
|
|
157
|
-
return;
|
|
158
|
-
// --- RUNNING ---
|
|
159
|
-
this.state = "running";
|
|
160
|
-
await this.recordPhase("running");
|
|
161
|
-
const enriched = {
|
|
162
|
-
card,
|
|
163
|
-
column,
|
|
164
|
-
labels,
|
|
165
|
-
subtasks,
|
|
166
|
-
mode: "implement",
|
|
167
|
-
};
|
|
168
|
-
const prompt = await buildPrompt(enriched, this.branchName, this.worktreePath, this.client, this.workspaceId, this.projectId);
|
|
169
|
-
await this.client.updateAgentProgress(card.id, {
|
|
170
|
-
agentIdentifier: agentIdentifier(this.id),
|
|
171
|
-
agentName: AGENT_NAME,
|
|
172
|
-
status: "working",
|
|
173
|
-
currentTask: "Running Claude CLI",
|
|
174
|
-
progressPercent: 10,
|
|
175
|
-
});
|
|
176
|
-
// Start timeout watchdog
|
|
177
|
-
this.timeoutTimer = setTimeout(() => {
|
|
178
|
-
log.warn(this.tag, `Timeout reached (${this.config.maxTimeout}ms), cancelling`);
|
|
179
|
-
this.cancel();
|
|
180
|
-
}, this.config.maxTimeout);
|
|
181
|
-
// Spawn Claude CLI
|
|
182
|
-
await this.spawnClaude(prompt, card, subtasks);
|
|
183
|
-
if (this.aborted)
|
|
184
|
-
return;
|
|
185
|
-
// --- VERIFYING + COMPLETING ---
|
|
186
|
-
this.state = "verifying";
|
|
187
|
-
await this.recordPhase("verifying");
|
|
188
|
-
log.info(this.tag, `Claude finished for #${card.short_id}, running verification & completion`);
|
|
189
|
-
await this.client.updateAgentProgress(card.id, {
|
|
190
|
-
agentIdentifier: agentIdentifier(this.id),
|
|
191
|
-
agentName: AGENT_NAME,
|
|
192
|
-
status: "working",
|
|
193
|
-
currentTask: "Verifying implementation",
|
|
194
|
-
progressPercent: 75,
|
|
195
|
-
});
|
|
196
|
-
this.state = "completing";
|
|
197
|
-
await this.recordPhase("completing");
|
|
198
|
-
await runCompletion(this.client, card, this.branchName, this.worktreePath, this.config, this.id, this.lastSessionStats, this.workspaceId, this.sessionId, this.stateStore);
|
|
199
|
-
}
|
|
200
|
-
catch (err) {
|
|
201
|
-
this.state = "error";
|
|
202
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
203
|
-
log.error(this.tag, `Error on #${card.short_id}: ${msg}`);
|
|
204
|
-
// End session as paused. Retried — a transient API blip here used
|
|
205
|
-
// to orphan the session and leave the card stuck with a progress ring.
|
|
206
|
-
try {
|
|
207
|
-
await runTransition(this.client, card, {
|
|
208
|
-
endSession: {
|
|
209
|
-
status: "paused",
|
|
210
|
-
...buildTokenPayload(this.lastSessionStats),
|
|
211
|
-
},
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
catch (tErr) {
|
|
215
|
-
log.error(this.tag, `endAgentSession unrecoverable on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
|
|
216
|
-
}
|
|
217
|
-
if (this.runId) {
|
|
218
|
-
try {
|
|
219
|
-
await this.stateStore.endRun(this.runId, "paused", msg);
|
|
220
|
-
}
|
|
221
|
-
catch {
|
|
222
|
-
// state-store best-effort; already persisted on last heartbeat
|
|
223
|
-
}
|
|
224
|
-
await this.recordOutcome(card.id, "failure");
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
finally {
|
|
228
|
-
// Only bookkeep success when we actually succeeded. "cancelling"
|
|
229
|
-
// and aborted runs are failures/user-initiated stops, not wins —
|
|
230
|
-
// counting them as success would reset attempts and mask real
|
|
231
|
-
// failure loops.
|
|
232
|
-
const succeeded = this.runId && this.state !== "error" && !this.aborted;
|
|
233
|
-
if (succeeded) {
|
|
234
|
-
try {
|
|
235
|
-
await this.stateStore.endRun(this.runId, "completed");
|
|
236
|
-
}
|
|
237
|
-
catch {
|
|
238
|
-
// best-effort — state store failures don't block worker exit
|
|
239
|
-
}
|
|
240
|
-
await this.recordOutcome(card.id, "success");
|
|
241
|
-
}
|
|
242
|
-
else if (this.runId && this.aborted) {
|
|
243
|
-
// Cancelled run: don't touch attempts counter, but close the
|
|
244
|
-
// state store row so it isn't mistaken for a live run.
|
|
245
|
-
try {
|
|
246
|
-
await this.stateStore.endRun(this.runId, "paused", "cancelled");
|
|
247
|
-
}
|
|
248
|
-
catch {
|
|
249
|
-
// best-effort
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
this.cleanup();
|
|
253
|
-
this.state = "idle";
|
|
254
|
-
this.onDone(this);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
async recordOutcome(cardId, outcome) {
|
|
258
|
-
try {
|
|
259
|
-
const cost = this.lastSessionStats?.cost;
|
|
260
|
-
if (cost) {
|
|
261
|
-
const cents = Math.round(cost.totalCostUsd * 100);
|
|
262
|
-
await this.stateStore.addCost(cardId, cents);
|
|
263
|
-
}
|
|
264
|
-
await this.stateStore.recordOutcome(cardId, outcome);
|
|
265
|
-
}
|
|
266
|
-
catch (err) {
|
|
267
|
-
log.warn(this.tag, `recordOutcome(${outcome}) failed: ${err instanceof Error ? err.message : err}`);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
/**
|
|
271
|
-
* Pause the current work by suspending the Claude process (SIGSTOP).
|
|
272
|
-
*/
|
|
273
|
-
async pause() {
|
|
274
|
-
if (!this.isActive || !this.process || this.process.killed)
|
|
275
|
-
return;
|
|
276
|
-
log.info(this.tag, `Pausing work on ${this.cardId}`);
|
|
277
|
-
signalGroup(this.process, "SIGSTOP");
|
|
278
|
-
// Pause the timeout timer
|
|
279
|
-
if (this.timeoutTimer) {
|
|
280
|
-
clearTimeout(this.timeoutTimer);
|
|
281
|
-
this.timeoutTimer = null;
|
|
282
|
-
}
|
|
283
|
-
// Update agent session so the UI reflects the paused state
|
|
284
|
-
if (this.cardId) {
|
|
285
|
-
try {
|
|
286
|
-
await this.client.updateAgentProgress(this.cardId, {
|
|
287
|
-
agentIdentifier: agentIdentifier(this.id),
|
|
288
|
-
agentName: AGENT_NAME,
|
|
289
|
-
status: "paused",
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
catch {
|
|
293
|
-
log.warn(this.tag, "Failed to update agent session to paused");
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
/**
|
|
298
|
-
* Resume the Claude process after a pause (SIGCONT).
|
|
299
|
-
*/
|
|
300
|
-
async resume() {
|
|
301
|
-
if (!this.isActive || !this.process || this.process.killed)
|
|
302
|
-
return;
|
|
303
|
-
log.info(this.tag, `Resuming work on ${this.cardId}`);
|
|
304
|
-
signalGroup(this.process, "SIGCONT");
|
|
305
|
-
// Restart timeout timer with remaining time (use full timeout for simplicity)
|
|
306
|
-
this.timeoutTimer = setTimeout(() => {
|
|
307
|
-
log.warn(this.tag, `Timeout reached (${this.config.maxTimeout}ms), cancelling`);
|
|
308
|
-
this.cancel();
|
|
309
|
-
}, this.config.maxTimeout);
|
|
310
|
-
// Update agent session so the UI reflects the resumed state
|
|
311
|
-
if (this.cardId) {
|
|
312
|
-
try {
|
|
313
|
-
await this.client.updateAgentProgress(this.cardId, {
|
|
314
|
-
agentIdentifier: agentIdentifier(this.id),
|
|
315
|
-
agentName: AGENT_NAME,
|
|
316
|
-
status: "working",
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
catch {
|
|
320
|
-
log.warn(this.tag, "Failed to update agent session to working");
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
/**
|
|
325
|
-
* Cancel the current work. Sends escalating signals to the Claude process.
|
|
326
|
-
*/
|
|
327
|
-
async cancel() {
|
|
328
|
-
if (!this.isActive)
|
|
329
|
-
return;
|
|
330
|
-
this.aborted = true;
|
|
331
|
-
this.state = "cancelling";
|
|
332
|
-
log.info(this.tag, `Cancelling work on ${this.cardId}`);
|
|
333
|
-
if (this.process && !this.process.killed) {
|
|
334
|
-
await terminateGroup(this.process, {
|
|
335
|
-
sigintTimeoutMs: CANCEL_SIGINT_TIMEOUT,
|
|
336
|
-
sigtermTimeoutMs: CANCEL_SIGTERM_TIMEOUT,
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
// End agent session as paused (retry on API jitter).
|
|
340
|
-
if (this.cardId) {
|
|
341
|
-
try {
|
|
342
|
-
const stats = this.lastSessionStats ?? this.progressTracker?.stats;
|
|
343
|
-
await this.client.endAgentSession(this.cardId, {
|
|
344
|
-
status: "paused",
|
|
345
|
-
...buildTokenPayload(stats),
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
catch (err) {
|
|
349
|
-
log.warn(this.tag, `endAgentSession after cancel failed: ${err instanceof Error ? err.message : err}`);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
async spawnClaude(prompt, card, subtasks) {
|
|
354
|
-
return new Promise((resolve, reject) => {
|
|
355
|
-
const args = [
|
|
356
|
-
"-p",
|
|
357
|
-
"--verbose", // required for stream-json to emit all event types
|
|
358
|
-
"--output-format",
|
|
359
|
-
"stream-json",
|
|
360
|
-
"--model",
|
|
361
|
-
this.config.claude.model,
|
|
362
|
-
"--max-turns",
|
|
363
|
-
String(this.config.claude.maxTurns),
|
|
364
|
-
"--allowedTools",
|
|
365
|
-
"Bash,Read,Write,Edit,Glob,Grep,Agent,mcp__harmony__*",
|
|
366
|
-
...this.config.claude.additionalArgs,
|
|
367
|
-
"--",
|
|
368
|
-
prompt,
|
|
369
|
-
];
|
|
370
|
-
log.info(this.tag, `Spawning: claude ${args.slice(0, 4).join(" ")} ...`);
|
|
371
|
-
const runLog = openRunLog(this.tag, this.runId, card.short_id);
|
|
372
|
-
if (runLog) {
|
|
373
|
-
log.info(this.tag, `Run log: ${runLog.path}`);
|
|
374
|
-
runLog.stream.write(`# run=${this.runId} card=#${card.short_id} started=${new Date().toISOString()}\n` +
|
|
375
|
-
`# args: ${args.slice(0, -2).join(" ")} -- <prompt:${prompt.length} chars>\n\n`);
|
|
376
|
-
}
|
|
377
|
-
this.process = spawnInGroup("claude", args, {
|
|
378
|
-
cwd: this.worktreePath,
|
|
379
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
380
|
-
});
|
|
381
|
-
// Stream parser for structured NDJSON events
|
|
382
|
-
const parser = new StreamParser();
|
|
383
|
-
// Progress tracker for phase-based updates
|
|
384
|
-
this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks);
|
|
385
|
-
if (this.sessionId) {
|
|
386
|
-
this.progressTracker.setSessionId(this.sessionId);
|
|
387
|
-
}
|
|
388
|
-
this.progressTracker.attach(parser);
|
|
389
|
-
// Attach stdout to parser
|
|
390
|
-
if (this.process.stdout) {
|
|
391
|
-
parser.attach(this.process.stdout);
|
|
392
|
-
if (runLog) {
|
|
393
|
-
this.process.stdout.on("data", (chunk) => {
|
|
394
|
-
runLog.stream.write(chunk);
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
parser.on("parse_error", (msg) => {
|
|
399
|
-
log.debug(this.tag, `Stream parse error (non-fatal): ${msg}`);
|
|
400
|
-
runLog?.stream.write(`\n[parse_error] ${msg}\n`);
|
|
401
|
-
});
|
|
402
|
-
let stderr = "";
|
|
403
|
-
this.process.stderr?.on("data", (data) => {
|
|
404
|
-
stderr += data.toString();
|
|
405
|
-
runLog?.stream.write(`[stderr] ${data.toString()}`);
|
|
406
|
-
});
|
|
407
|
-
this.process.on("error", (err) => {
|
|
408
|
-
reject(new Error(`Failed to spawn claude: ${err.message}`));
|
|
409
|
-
});
|
|
410
|
-
this.process.on("close", (code) => {
|
|
411
|
-
this.process = null;
|
|
412
|
-
this.lastSessionStats = this.progressTracker?.stats;
|
|
413
|
-
this.progressTracker?.flushFinal();
|
|
414
|
-
this.progressTracker?.stop();
|
|
415
|
-
this.progressTracker = null;
|
|
416
|
-
if (runLog) {
|
|
417
|
-
const stats = this.lastSessionStats;
|
|
418
|
-
runLog.stream.write(`\n# exit code=${code} aborted=${this.aborted} ` +
|
|
419
|
-
`toolCalls=${stats?.toolCalls ?? 0} filesEdited=${stats?.filesEdited ?? 0} ` +
|
|
420
|
-
`cost=$${stats?.cost?.totalCostUsd.toFixed(4) ?? "0"} ` +
|
|
421
|
-
`ended=${new Date().toISOString()}\n`);
|
|
422
|
-
runLog.stream.end();
|
|
423
|
-
}
|
|
424
|
-
if (this.aborted) {
|
|
425
|
-
resolve(); // Cancellation is not an error
|
|
426
|
-
}
|
|
427
|
-
else if (code === 0) {
|
|
428
|
-
resolve();
|
|
429
|
-
}
|
|
430
|
-
else {
|
|
431
|
-
reject(new Error(`claude exited with code ${code}${stderr ? `: ${stderr.slice(0, 500)}` : ""}`));
|
|
432
|
-
}
|
|
433
|
-
});
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
cleanup() {
|
|
437
|
-
if (this.progressTracker) {
|
|
438
|
-
this.progressTracker.stop();
|
|
439
|
-
this.progressTracker = null;
|
|
440
|
-
}
|
|
441
|
-
this.stopHeartbeat();
|
|
442
|
-
this.lastSessionStats = undefined;
|
|
443
|
-
if (this.timeoutTimer) {
|
|
444
|
-
clearTimeout(this.timeoutTimer);
|
|
445
|
-
this.timeoutTimer = null;
|
|
446
|
-
}
|
|
447
|
-
// Clean up worktree + branch on error
|
|
448
|
-
if (this.worktreePath && this.state === "error") {
|
|
449
|
-
try {
|
|
450
|
-
cleanupWorktree(this.worktreePath, this.branchName ?? undefined);
|
|
451
|
-
}
|
|
452
|
-
catch {
|
|
453
|
-
log.warn(this.tag, "Failed to cleanup worktree");
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
this.process = null;
|
|
457
|
-
this.cardId = null;
|
|
458
|
-
this.branchName = null;
|
|
459
|
-
this.worktreePath = null;
|
|
460
|
-
this.startedAt = null;
|
|
461
|
-
this.runId = null;
|
|
462
|
-
this.sessionId = null;
|
|
463
|
-
}
|
|
464
|
-
}
|
package/dist/worktree-gc.d.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import type { StateStore } from "./state-store.js";
|
|
2
|
-
export interface RemoteBranchGcOptions {
|
|
3
|
-
/** Prefix to scan (e.g. `agent-attempts/`). */
|
|
4
|
-
prefix: string;
|
|
5
|
-
/** Retention in days. Branches older than this are removed. */
|
|
6
|
-
retentionDays: number;
|
|
7
|
-
/** Optional sync clock, for deterministic tests. */
|
|
8
|
-
now?: () => number;
|
|
9
|
-
}
|
|
10
|
-
export interface RemoteBranchGcResult {
|
|
11
|
-
scanned: number;
|
|
12
|
-
removed: string[];
|
|
13
|
-
skipped: string[];
|
|
14
|
-
errors: Array<{
|
|
15
|
-
ref: string;
|
|
16
|
-
error: string;
|
|
17
|
-
}>;
|
|
18
|
-
}
|
|
19
|
-
export interface GcResult {
|
|
20
|
-
checked: number;
|
|
21
|
-
removed: string[];
|
|
22
|
-
skipped: string[];
|
|
23
|
-
errors: Array<{
|
|
24
|
-
path: string;
|
|
25
|
-
error: string;
|
|
26
|
-
}>;
|
|
27
|
-
}
|
|
28
|
-
export interface GcOptions {
|
|
29
|
-
/** Directories younger than this are kept (a worker may be about to use them). */
|
|
30
|
-
minAgeMs?: number;
|
|
31
|
-
/** Optional sync clock, for deterministic tests. */
|
|
32
|
-
now?: () => number;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* One-shot garbage collection for `.harmony-worktrees/*`.
|
|
36
|
-
*
|
|
37
|
-
* A directory is removed when BOTH:
|
|
38
|
-
* - no active run in the state store has it as its `worktreePath`, AND
|
|
39
|
-
* - it was last modified more than `minAgeMs` ago (default 1h).
|
|
40
|
-
*
|
|
41
|
-
* The age check protects brand-new worktrees that a worker just created
|
|
42
|
-
* but hasn't yet recorded the path for in the state store.
|
|
43
|
-
*
|
|
44
|
-
* Returns a summary; callers decide whether to log at info or warn.
|
|
45
|
-
*/
|
|
46
|
-
export declare function runWorktreeGc(basePath: string, store: StateStore, opts?: GcOptions): GcResult;
|
|
47
|
-
/**
|
|
48
|
-
* Sweep stale failed-attempt branches off the remote.
|
|
49
|
-
*
|
|
50
|
-
* Lists `origin/<prefix>*`, asks git for each ref's committer timestamp via
|
|
51
|
-
* `for-each-ref`, and deletes anything older than `retentionDays`. Runs on
|
|
52
|
-
* the same GC tick that handles worktree directories — protecting unpushed
|
|
53
|
-
* commits is the job of the daemon (always push before verify); this sweep
|
|
54
|
-
* just keeps the namespace tidy.
|
|
55
|
-
*/
|
|
56
|
-
export declare function pruneFailedRemoteBranches(opts: RemoteBranchGcOptions): RemoteBranchGcResult;
|
|
57
|
-
export declare class WorktreeGc {
|
|
58
|
-
private basePath;
|
|
59
|
-
private store;
|
|
60
|
-
private intervalMs;
|
|
61
|
-
private remoteOpts?;
|
|
62
|
-
private timer;
|
|
63
|
-
constructor(basePath: string, store: StateStore, intervalMs: number, remoteOpts?: RemoteBranchGcOptions | undefined);
|
|
64
|
-
start(): void;
|
|
65
|
-
stop(): void;
|
|
66
|
-
private tick;
|
|
67
|
-
}
|