@gethmy/agent 1.7.1 → 1.7.3

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