@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.
Files changed (76) hide show
  1. package/README.md +8 -1
  2. package/dist/cli.js +6376 -205
  3. package/dist/index.js +6206 -341
  4. package/package.json +2 -2
  5. package/dist/board-helpers.d.ts +0 -31
  6. package/dist/board-helpers.js +0 -150
  7. package/dist/budget.d.ts +0 -47
  8. package/dist/budget.js +0 -161
  9. package/dist/cli.d.ts +0 -16
  10. package/dist/completion.d.ts +0 -32
  11. package/dist/completion.js +0 -304
  12. package/dist/config-validation.d.ts +0 -23
  13. package/dist/config-validation.js +0 -77
  14. package/dist/config.d.ts +0 -23
  15. package/dist/config.js +0 -103
  16. package/dist/episode-writer.d.ts +0 -84
  17. package/dist/episode-writer.js +0 -232
  18. package/dist/git-pr.d.ts +0 -38
  19. package/dist/git-pr.js +0 -399
  20. package/dist/http-server.d.ts +0 -79
  21. package/dist/http-server.js +0 -114
  22. package/dist/index.d.ts +0 -5
  23. package/dist/log.d.ts +0 -34
  24. package/dist/log.js +0 -100
  25. package/dist/merge-monitor.d.ts +0 -23
  26. package/dist/merge-monitor.js +0 -169
  27. package/dist/pm.d.ts +0 -14
  28. package/dist/pm.js +0 -63
  29. package/dist/pool.d.ts +0 -70
  30. package/dist/pool.js +0 -258
  31. package/dist/process-group.d.ts +0 -26
  32. package/dist/process-group.js +0 -72
  33. package/dist/progress-tracker.d.ts +0 -79
  34. package/dist/progress-tracker.js +0 -442
  35. package/dist/prompt.d.ts +0 -18
  36. package/dist/prompt.js +0 -117
  37. package/dist/queue.d.ts +0 -39
  38. package/dist/queue.js +0 -100
  39. package/dist/reconcile.d.ts +0 -35
  40. package/dist/reconcile.js +0 -174
  41. package/dist/recovery.d.ts +0 -30
  42. package/dist/recovery.js +0 -141
  43. package/dist/review-completion.d.ts +0 -40
  44. package/dist/review-completion.js +0 -474
  45. package/dist/review-knowledge.d.ts +0 -14
  46. package/dist/review-knowledge.js +0 -89
  47. package/dist/review-prompt.d.ts +0 -12
  48. package/dist/review-prompt.js +0 -103
  49. package/dist/review-worker.d.ts +0 -56
  50. package/dist/review-worker.js +0 -638
  51. package/dist/review-worktree.d.ts +0 -12
  52. package/dist/review-worktree.js +0 -95
  53. package/dist/run-log.d.ts +0 -6
  54. package/dist/run-log.js +0 -19
  55. package/dist/startup-banner.d.ts +0 -29
  56. package/dist/startup-banner.js +0 -143
  57. package/dist/state-store.d.ts +0 -88
  58. package/dist/state-store.js +0 -239
  59. package/dist/stream-parser-selftest.d.ts +0 -9
  60. package/dist/stream-parser-selftest.js +0 -97
  61. package/dist/stream-parser.d.ts +0 -43
  62. package/dist/stream-parser.js +0 -174
  63. package/dist/transitions.d.ts +0 -57
  64. package/dist/transitions.js +0 -131
  65. package/dist/types.d.ts +0 -140
  66. package/dist/types.js +0 -79
  67. package/dist/verification.d.ts +0 -39
  68. package/dist/verification.js +0 -317
  69. package/dist/watcher.d.ts +0 -53
  70. package/dist/watcher.js +0 -153
  71. package/dist/worker.d.ts +0 -53
  72. package/dist/worker.js +0 -464
  73. package/dist/worktree-gc.d.ts +0 -67
  74. package/dist/worktree-gc.js +0 -245
  75. package/dist/worktree.d.ts +0 -18
  76. package/dist/worktree.js +0 -177
@@ -1,245 +0,0 @@
1
- import { execFileSync } from "node:child_process";
2
- import { readdirSync, statSync } from "node:fs";
3
- import { resolve } from "node:path";
4
- import { log } from "./log.js";
5
- import { cleanupWorktree } from "./worktree.js";
6
- const TAG = "worktree-gc";
7
- /**
8
- * One-shot garbage collection for `.harmony-worktrees/*`.
9
- *
10
- * A directory is removed when BOTH:
11
- * - no active run in the state store has it as its `worktreePath`, AND
12
- * - it was last modified more than `minAgeMs` ago (default 1h).
13
- *
14
- * The age check protects brand-new worktrees that a worker just created
15
- * but hasn't yet recorded the path for in the state store.
16
- *
17
- * Returns a summary; callers decide whether to log at info or warn.
18
- */
19
- export function runWorktreeGc(basePath, store, opts = {}) {
20
- const minAgeMs = opts.minAgeMs ?? 60 * 60 * 1000;
21
- const now = (opts.now ?? Date.now)();
22
- const result = { checked: 0, removed: [], skipped: [], errors: [] };
23
- const repoRoot = getRepoRoot();
24
- if (!repoRoot) {
25
- result.errors.push({ path: "<repo-root>", error: "not a git repo" });
26
- return result;
27
- }
28
- const baseAbs = resolve(repoRoot, basePath);
29
- let entries;
30
- try {
31
- entries = readdirSync(baseAbs);
32
- }
33
- catch {
34
- // Directory doesn't exist yet — nothing to GC, not an error.
35
- return result;
36
- }
37
- const activePaths = new Set(store
38
- .getActiveRuns()
39
- .map((r) => r.worktreePath)
40
- .filter((p) => !!p));
41
- for (const entry of entries) {
42
- const full = resolve(baseAbs, entry);
43
- result.checked++;
44
- let mtimeMs;
45
- try {
46
- const stat = statSync(full);
47
- if (!stat.isDirectory()) {
48
- result.skipped.push(full);
49
- continue;
50
- }
51
- mtimeMs = stat.mtimeMs;
52
- }
53
- catch (err) {
54
- result.errors.push({
55
- path: full,
56
- error: err instanceof Error ? err.message : String(err),
57
- });
58
- continue;
59
- }
60
- if (activePaths.has(full)) {
61
- result.skipped.push(full);
62
- continue;
63
- }
64
- if (now - mtimeMs < minAgeMs) {
65
- result.skipped.push(full);
66
- continue;
67
- }
68
- try {
69
- cleanupWorktree(full);
70
- result.removed.push(full);
71
- }
72
- catch (err) {
73
- result.errors.push({
74
- path: full,
75
- error: err instanceof Error ? err.message : String(err),
76
- });
77
- }
78
- }
79
- // Prune any stale metadata leftover from deleted worktrees.
80
- try {
81
- execFileSync("git", ["worktree", "prune", "--expire=now"], {
82
- cwd: repoRoot,
83
- stdio: "pipe",
84
- });
85
- }
86
- catch {
87
- // non-fatal
88
- }
89
- if (result.removed.length > 0) {
90
- log.info(TAG, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
91
- }
92
- if (result.errors.length > 0) {
93
- log.warn(TAG, `GC had ${result.errors.length} error(s): ${result.errors
94
- .map((e) => `${e.path}: ${e.error}`)
95
- .join("; ")}`);
96
- }
97
- return result;
98
- }
99
- /**
100
- * Sweep stale failed-attempt branches off the remote.
101
- *
102
- * Lists `origin/<prefix>*`, asks git for each ref's committer timestamp via
103
- * `for-each-ref`, and deletes anything older than `retentionDays`. Runs on
104
- * the same GC tick that handles worktree directories — protecting unpushed
105
- * commits is the job of the daemon (always push before verify); this sweep
106
- * just keeps the namespace tidy.
107
- */
108
- export function pruneFailedRemoteBranches(opts) {
109
- const result = {
110
- scanned: 0,
111
- removed: [],
112
- skipped: [],
113
- errors: [],
114
- };
115
- if (!opts.prefix)
116
- return result;
117
- // 0 or negative retention is opt-out — caller wants nothing pruned.
118
- if (!Number.isFinite(opts.retentionDays) || opts.retentionDays <= 0) {
119
- return result;
120
- }
121
- const repoRoot = getRepoRoot();
122
- if (!repoRoot) {
123
- result.errors.push({ ref: "<repo-root>", error: "not a git repo" });
124
- return result;
125
- }
126
- // Refresh the remote refs we know about. Pruned remote branches drop from
127
- // local tracking, so we never try to delete refs the server already lost.
128
- try {
129
- execFileSync("git", ["fetch", "--prune", "origin"], {
130
- cwd: repoRoot,
131
- stdio: "pipe",
132
- });
133
- }
134
- catch (err) {
135
- result.errors.push({
136
- ref: "fetch",
137
- error: err instanceof Error ? err.message : String(err),
138
- });
139
- // Continue — for-each-ref may still find usable cached refs.
140
- }
141
- const refPattern = `refs/remotes/origin/${opts.prefix}*`;
142
- let listing = "";
143
- try {
144
- listing = execFileSync("git", [
145
- "for-each-ref",
146
- "--format=%(refname:strip=3) %(committerdate:unix)",
147
- refPattern,
148
- ], { cwd: repoRoot, encoding: "utf-8" });
149
- }
150
- catch (err) {
151
- result.errors.push({
152
- ref: refPattern,
153
- error: err instanceof Error ? err.message : String(err),
154
- });
155
- return result;
156
- }
157
- const cutoffSecs = (opts.now ?? Date.now)() / 1000 - opts.retentionDays * 24 * 60 * 60;
158
- for (const line of listing.split("\n")) {
159
- const trimmed = line.trim();
160
- if (!trimmed)
161
- continue;
162
- const sp = trimmed.lastIndexOf(" ");
163
- if (sp < 0)
164
- continue;
165
- const ref = trimmed.slice(0, sp);
166
- const ts = Number(trimmed.slice(sp + 1));
167
- result.scanned++;
168
- if (!Number.isFinite(ts) || ts >= cutoffSecs) {
169
- result.skipped.push(ref);
170
- continue;
171
- }
172
- try {
173
- execFileSync("git", ["push", "origin", `:refs/heads/${ref}`], {
174
- cwd: repoRoot,
175
- stdio: "pipe",
176
- });
177
- result.removed.push(ref);
178
- }
179
- catch (err) {
180
- result.errors.push({
181
- ref,
182
- error: err instanceof Error ? err.message : String(err),
183
- });
184
- }
185
- }
186
- if (result.removed.length > 0) {
187
- log.info(TAG, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
188
- }
189
- if (result.errors.length > 0) {
190
- log.warn(TAG, `Remote branch GC had ${result.errors.length} error(s): ${result.errors
191
- .map((e) => `${e.ref}: ${e.error}`)
192
- .join("; ")}`);
193
- }
194
- return result;
195
- }
196
- export class WorktreeGc {
197
- basePath;
198
- store;
199
- intervalMs;
200
- remoteOpts;
201
- timer = null;
202
- constructor(basePath, store, intervalMs, remoteOpts) {
203
- this.basePath = basePath;
204
- this.store = store;
205
- this.intervalMs = intervalMs;
206
- this.remoteOpts = remoteOpts;
207
- }
208
- start() {
209
- // Run once at startup, then on interval.
210
- this.tick();
211
- this.timer = setInterval(() => this.tick(), this.intervalMs);
212
- }
213
- stop() {
214
- if (this.timer) {
215
- clearInterval(this.timer);
216
- this.timer = null;
217
- }
218
- }
219
- tick() {
220
- try {
221
- runWorktreeGc(this.basePath, this.store);
222
- }
223
- catch (err) {
224
- log.warn(TAG, `GC tick failed: ${err instanceof Error ? err.message : err}`);
225
- }
226
- if (this.remoteOpts) {
227
- try {
228
- pruneFailedRemoteBranches(this.remoteOpts);
229
- }
230
- catch (err) {
231
- log.warn(TAG, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
232
- }
233
- }
234
- }
235
- }
236
- function getRepoRoot() {
237
- try {
238
- return execFileSync("git", ["rev-parse", "--show-toplevel"], {
239
- encoding: "utf-8",
240
- }).trim();
241
- }
242
- catch {
243
- return null;
244
- }
245
- }
@@ -1,18 +0,0 @@
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
- * Agent branches start under the failedBranchPrefix (default `agent-attempts/`)
14
- * and are renamed to the approvedBranchPrefix (default `agent/`) only after the
15
- * Review pipeline approves them. Branches under `agent-attempts/` are pruned
16
- * by the GC after `failedAttemptRetentionDays`.
17
- */
18
- export declare function makeBranchName(shortId: number, title: string, prefix?: string): string;
package/dist/worktree.js DELETED
@@ -1,177 +0,0 @@
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, branchName);
19
- }
20
- // Prune stale worktree metadata. If a previous daemon crashed or its
21
- // worktree dir was deleted externally, git may still think the branch is
22
- // checked out, which blocks `git branch -D` and `git worktree add`.
23
- // `--expire=now` overrides `gc.worktreePruneExpire` (default 3 months) so
24
- // freshly-orphaned entries are removed immediately.
25
- try {
26
- execFileSync("git", ["worktree", "prune", "--expire=now"], {
27
- cwd: repoRoot,
28
- stdio: "pipe",
29
- });
30
- }
31
- catch {
32
- // non-fatal
33
- }
34
- // Fetch latest from remote to ensure base branch is up to date
35
- try {
36
- execFileSync("git", ["fetch", "origin", baseBranch], {
37
- cwd: repoRoot,
38
- stdio: "pipe",
39
- });
40
- }
41
- catch {
42
- log.warn(TAG, "Failed to fetch latest — continuing with local state");
43
- }
44
- // Create worktree with a fresh branch based on origin/<baseBranch>.
45
- // `-B` resets the branch if it already exists — agent branches are owned
46
- // per-attempt, so starting fresh from origin is the desired behavior.
47
- log.info(TAG, `Creating worktree: ${worktreeDir} (branch: ${branchName})`);
48
- try {
49
- execFileSync("git", [
50
- "worktree",
51
- "add",
52
- "-B",
53
- branchName,
54
- worktreeDir,
55
- `origin/${baseBranch}`,
56
- ], { cwd: repoRoot, stdio: "pipe" });
57
- }
58
- catch (err) {
59
- // Last-resort recovery: if `-B` still fails (e.g. branch checked out in
60
- // another registered worktree), force-delete the branch and retry.
61
- const msg = err instanceof Error ? err.message : String(err);
62
- log.warn(TAG, `worktree add failed, attempting forced recovery: ${msg}`);
63
- // Remove any registered worktree at this path (phantom or otherwise).
64
- try {
65
- execFileSync("git", ["worktree", "remove", worktreeDir, "--force"], {
66
- cwd: repoRoot,
67
- stdio: "pipe",
68
- });
69
- }
70
- catch {
71
- // best-effort
72
- }
73
- // Force-prune any stale worktree admin entries referencing this branch.
74
- try {
75
- execFileSync("git", ["worktree", "prune", "--expire=now"], {
76
- cwd: repoRoot,
77
- stdio: "pipe",
78
- });
79
- }
80
- catch {
81
- // best-effort
82
- }
83
- try {
84
- execFileSync("git", ["branch", "-D", branchName], {
85
- cwd: repoRoot,
86
- stdio: "pipe",
87
- });
88
- }
89
- catch {
90
- // ignore; retry will surface the real error
91
- }
92
- execFileSync("git", [
93
- "worktree",
94
- "add",
95
- "-B",
96
- branchName,
97
- worktreeDir,
98
- `origin/${baseBranch}`,
99
- ], { cwd: repoRoot, stdio: "pipe" });
100
- }
101
- // Install dependencies in the worktree
102
- log.info(TAG, "Installing dependencies in worktree...");
103
- try {
104
- execSync(installCommand(), {
105
- cwd: worktreeDir,
106
- stdio: "pipe",
107
- timeout: 60_000,
108
- });
109
- }
110
- catch {
111
- log.warn(TAG, "Install failed (may be fine if deps are hoisted)");
112
- }
113
- return worktreeDir;
114
- }
115
- /**
116
- * Remove a git worktree and its branch.
117
- */
118
- export function cleanupWorktree(worktreePath, branchName) {
119
- const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
120
- encoding: "utf-8",
121
- }).trim();
122
- try {
123
- execFileSync("git", ["worktree", "remove", worktreePath, "--force"], {
124
- cwd: repoRoot,
125
- stdio: "pipe",
126
- });
127
- log.info(TAG, `Removed worktree: ${worktreePath}`);
128
- }
129
- catch (err) {
130
- log.warn(TAG, `Failed to remove worktree cleanly: ${err instanceof Error ? err.message : err}`);
131
- // Force-remove the directory if git worktree remove failed
132
- if (existsSync(worktreePath)) {
133
- rmSync(worktreePath, { recursive: true, force: true });
134
- }
135
- // Prune stale worktree entries
136
- try {
137
- execFileSync("git", ["worktree", "prune", "--expire=now"], {
138
- cwd: repoRoot,
139
- stdio: "pipe",
140
- });
141
- }
142
- catch {
143
- // best-effort
144
- }
145
- }
146
- // Delete the branch so it doesn't block future runs
147
- if (branchName) {
148
- try {
149
- execFileSync("git", ["branch", "-D", branchName], {
150
- cwd: repoRoot,
151
- stdio: "pipe",
152
- });
153
- }
154
- catch {
155
- // best-effort
156
- }
157
- }
158
- }
159
- /**
160
- * Generate a branch name from a card's short ID and title.
161
- *
162
- * Agent branches start under the failedBranchPrefix (default `agent-attempts/`)
163
- * and are renamed to the approvedBranchPrefix (default `agent/`) only after the
164
- * Review pipeline approves them. Branches under `agent-attempts/` are pruned
165
- * by the GC after `failedAttemptRetentionDays`.
166
- */
167
- export function makeBranchName(shortId, title, prefix = "agent-attempts/") {
168
- const slug = title
169
- .toLowerCase()
170
- .trim()
171
- .replace(/[^\w\s-]/g, "")
172
- .replace(/\s+/g, "-")
173
- .replace(/-+/g, "-")
174
- .replace(/^-+|-+$/g, "")
175
- .slice(0, 40);
176
- return `${prefix}${shortId}-${slug || "task"}`;
177
- }