@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.
- package/dist/cli.js +6386 -141
- package/dist/index.js +6216 -333
- 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 -39
- package/dist/budget.js +0 -73
- package/dist/cli.d.ts +0 -14
- package/dist/completion.d.ts +0 -36
- package/dist/completion.js +0 -322
- 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 -116
- package/dist/episode-writer.js +0 -349
- package/dist/git-diff-stat.d.ts +0 -24
- package/dist/git-diff-stat.js +0 -56
- package/dist/git-pr.d.ts +0 -38
- package/dist/git-pr.js +0 -399
- package/dist/http-server.d.ts +0 -66
- package/dist/http-server.js +0 -96
- 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 -71
- package/dist/pool.js +0 -259
- package/dist/process-group.d.ts +0 -26
- package/dist/process-group.js +0 -72
- package/dist/progress-tracker.d.ts +0 -82
- package/dist/progress-tracker.js +0 -457
- package/dist/prompt.d.ts +0 -23
- package/dist/prompt.js +0 -160
- 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 -35
- package/dist/review-completion.js +0 -475
- 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 -89
- package/dist/state-store.js +0 -230
- 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 -167
- package/dist/types.js +0 -76
- 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 -54
- package/dist/worker.js +0 -507
- 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/worktree-gc.js
DELETED
|
@@ -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
|
-
}
|
package/dist/worktree.d.ts
DELETED
|
@@ -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
|
-
}
|