nightralph 0.0.10 → 0.0.12
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/index.js +1933 -26
- package/dist/index.js.map +4 -4
- package/dist/meta.json +251 -237
- package/package.json +2 -2
- package/dist/demo-display.js +0 -298
package/dist/index.js
CHANGED
|
@@ -1,27 +1,1934 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
3
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
|
|
7
|
+
import { dirname as dirname3, join as join5 } from "node:path";
|
|
6
8
|
import yargs from "yargs";
|
|
7
9
|
import { hideBin } from "yargs/helpers";
|
|
8
|
-
import { select } from "@inquirer/prompts";
|
|
9
|
-
|
|
10
|
+
import { select as select2 } from "@inquirer/prompts";
|
|
11
|
+
|
|
12
|
+
// src/setup.ts
|
|
10
13
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
cpSync,
|
|
15
|
+
existsSync,
|
|
16
|
+
mkdirSync,
|
|
17
|
+
readdirSync,
|
|
18
|
+
readFileSync,
|
|
19
|
+
symlinkSync,
|
|
20
|
+
writeFileSync
|
|
21
|
+
} from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { execSync } from "node:child_process";
|
|
24
|
+
var SKILL_DIRS = [
|
|
25
|
+
"grill",
|
|
26
|
+
"grilling",
|
|
27
|
+
"domain-modeling",
|
|
28
|
+
"to-spec",
|
|
29
|
+
"to-tickets",
|
|
30
|
+
"tdd"
|
|
31
|
+
];
|
|
32
|
+
var DOC_TEMPLATES = [
|
|
33
|
+
"triage-labels.md",
|
|
34
|
+
"domain.md"
|
|
35
|
+
];
|
|
36
|
+
var START_MARKER = "<!-- nightralph:start -->";
|
|
37
|
+
var END_MARKER = "<!-- nightralph:end -->";
|
|
38
|
+
function writeManagedSection(filePath, content) {
|
|
39
|
+
const block = [
|
|
40
|
+
START_MARKER,
|
|
41
|
+
content,
|
|
42
|
+
END_MARKER
|
|
43
|
+
].join("\n");
|
|
44
|
+
if (!existsSync(filePath)) {
|
|
45
|
+
writeFileSync(filePath, block + "\n");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const existing = readFileSync(filePath, "utf8");
|
|
49
|
+
const startIdx = existing.indexOf(START_MARKER);
|
|
50
|
+
const endIdx = existing.indexOf(END_MARKER);
|
|
51
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
52
|
+
const before = existing.slice(0, startIdx);
|
|
53
|
+
const after = existing.slice(
|
|
54
|
+
endIdx + END_MARKER.length
|
|
55
|
+
);
|
|
56
|
+
writeFileSync(filePath, before + block + after);
|
|
57
|
+
} else {
|
|
58
|
+
const sep = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
59
|
+
writeFileSync(filePath, existing + sep + block + "\n");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
__name(writeManagedSection, "writeManagedSection");
|
|
63
|
+
function checkGhAuth() {
|
|
64
|
+
try {
|
|
65
|
+
execSync("gh auth status", { stdio: "ignore" });
|
|
66
|
+
return true;
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
__name(checkGhAuth, "checkGhAuth");
|
|
72
|
+
var TRACKER_TEMPLATES = {
|
|
73
|
+
local: "issue-tracker.md",
|
|
74
|
+
github: "issue-tracker-github.md"
|
|
75
|
+
};
|
|
76
|
+
function setup(opts) {
|
|
77
|
+
const {
|
|
78
|
+
bundledSkillsDir,
|
|
79
|
+
docsTemplatesDir,
|
|
80
|
+
projectDir,
|
|
81
|
+
claude,
|
|
82
|
+
tracker
|
|
83
|
+
} = opts;
|
|
84
|
+
const agentsSkillsDir = join(
|
|
85
|
+
projectDir,
|
|
86
|
+
".agents",
|
|
87
|
+
"skills"
|
|
88
|
+
);
|
|
89
|
+
const upstream = JSON.parse(
|
|
90
|
+
readFileSync(
|
|
91
|
+
join(bundledSkillsDir, "upstream.json"),
|
|
92
|
+
"utf8"
|
|
93
|
+
)
|
|
94
|
+
);
|
|
95
|
+
console.log("Installing skills to .agents/skills/\n");
|
|
96
|
+
for (const skill of SKILL_DIRS) {
|
|
97
|
+
const src = join(bundledSkillsDir, skill);
|
|
98
|
+
const dest = join(agentsSkillsDir, skill);
|
|
99
|
+
mkdirSync(dest, { recursive: true });
|
|
100
|
+
cpSync(src, dest, { recursive: true });
|
|
101
|
+
const files = readdirSync(src);
|
|
102
|
+
console.log(
|
|
103
|
+
` ${skill}/ (${files.join(", ")})`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (claude) {
|
|
107
|
+
const claudeSkillsDir = join(
|
|
108
|
+
projectDir,
|
|
109
|
+
".claude",
|
|
110
|
+
"skills"
|
|
111
|
+
);
|
|
112
|
+
mkdirSync(claudeSkillsDir, { recursive: true });
|
|
113
|
+
for (const skill of SKILL_DIRS) {
|
|
114
|
+
const linkPath = join(claudeSkillsDir, skill);
|
|
115
|
+
const target = join(
|
|
116
|
+
"..",
|
|
117
|
+
"..",
|
|
118
|
+
".agents",
|
|
119
|
+
"skills",
|
|
120
|
+
skill
|
|
121
|
+
);
|
|
122
|
+
if (existsSync(linkPath)) continue;
|
|
123
|
+
symlinkSync(target, linkPath);
|
|
124
|
+
}
|
|
125
|
+
console.log(
|
|
126
|
+
"\nCreated symlinks in .claude/skills/"
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
const docsAgentsDir = join(
|
|
130
|
+
projectDir,
|
|
131
|
+
"docs",
|
|
132
|
+
"agents"
|
|
133
|
+
);
|
|
134
|
+
mkdirSync(docsAgentsDir, { recursive: true });
|
|
135
|
+
console.log("");
|
|
136
|
+
for (const tmpl of DOC_TEMPLATES) {
|
|
137
|
+
const dest = join(docsAgentsDir, tmpl);
|
|
138
|
+
if (existsSync(dest)) {
|
|
139
|
+
console.log(
|
|
140
|
+
` docs/agents/${tmpl} (already exists)`
|
|
141
|
+
);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const src = join(docsTemplatesDir, tmpl);
|
|
145
|
+
writeFileSync(dest, readFileSync(src, "utf8"));
|
|
146
|
+
console.log(` docs/agents/${tmpl} (created)`);
|
|
147
|
+
}
|
|
148
|
+
const trackerSrc = join(
|
|
149
|
+
docsTemplatesDir,
|
|
150
|
+
TRACKER_TEMPLATES[tracker]
|
|
151
|
+
);
|
|
152
|
+
const trackerDest = join(docsAgentsDir, "issue-tracker.md");
|
|
153
|
+
writeFileSync(trackerDest, readFileSync(trackerSrc, "utf8"));
|
|
154
|
+
console.log(
|
|
155
|
+
` docs/agents/issue-tracker.md (${tracker})`
|
|
156
|
+
);
|
|
157
|
+
const trackerLine = tracker === "github" ? "Issues are tracked via GitHub Issues using the `gh` CLI." : "Issues are tracked as local markdown files under `.scratch/`.";
|
|
158
|
+
const managedContent = [
|
|
159
|
+
"## Agent skills",
|
|
160
|
+
"",
|
|
161
|
+
"### Issue tracker",
|
|
162
|
+
"",
|
|
163
|
+
`${trackerLine} See \`docs/agents/issue-tracker.md\`.`,
|
|
164
|
+
"",
|
|
165
|
+
"### Triage labels",
|
|
166
|
+
"",
|
|
167
|
+
"Default label vocabulary. See `docs/agents/triage-labels.md`.",
|
|
168
|
+
"",
|
|
169
|
+
"### Domain docs",
|
|
170
|
+
"",
|
|
171
|
+
"Single-context layout. See `docs/agents/domain.md`."
|
|
172
|
+
].join("\n");
|
|
173
|
+
const agentsPath = join(projectDir, "AGENTS.md");
|
|
174
|
+
writeManagedSection(agentsPath, managedContent);
|
|
175
|
+
console.log("\n AGENTS.md (updated)");
|
|
176
|
+
console.log(
|
|
177
|
+
`
|
|
178
|
+
Source: ${upstream.repo} @ ${upstream.commit}`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
__name(setup, "setup");
|
|
182
|
+
|
|
183
|
+
// src/orchestrator.ts
|
|
184
|
+
import { spawn } from "node:child_process";
|
|
18
185
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
186
|
+
createWriteStream,
|
|
187
|
+
mkdirSync as mkdirSync2,
|
|
188
|
+
readFileSync as readFileSync3,
|
|
189
|
+
writeFileSync as writeFileSync3,
|
|
190
|
+
unlinkSync,
|
|
191
|
+
readdirSync as readdirSync2
|
|
192
|
+
} from "node:fs";
|
|
193
|
+
import { tmpdir } from "node:os";
|
|
194
|
+
import { join as join3, dirname, basename as basename2 } from "node:path";
|
|
195
|
+
import { createInterface } from "node:readline";
|
|
196
|
+
|
|
197
|
+
// src/worktree.ts
|
|
198
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
199
|
+
import { promisify } from "node:util";
|
|
200
|
+
import { basename, join as join2 } from "node:path";
|
|
201
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
202
|
+
var execFileAsync = promisify(execFile);
|
|
203
|
+
function getRepoRoot() {
|
|
204
|
+
const stdout = execFileSync(
|
|
205
|
+
"git",
|
|
206
|
+
["rev-parse", "--show-toplevel"],
|
|
207
|
+
{ encoding: "utf8" }
|
|
208
|
+
);
|
|
209
|
+
return stdout.trim();
|
|
210
|
+
}
|
|
211
|
+
__name(getRepoRoot, "getRepoRoot");
|
|
212
|
+
function getRepoName() {
|
|
213
|
+
return basename(getRepoRoot());
|
|
214
|
+
}
|
|
215
|
+
__name(getRepoName, "getRepoName");
|
|
216
|
+
function getCurrentBranch() {
|
|
217
|
+
const stdout = execFileSync(
|
|
218
|
+
"git",
|
|
219
|
+
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
220
|
+
{ encoding: "utf8" }
|
|
221
|
+
);
|
|
222
|
+
return stdout.trim();
|
|
223
|
+
}
|
|
224
|
+
__name(getCurrentBranch, "getCurrentBranch");
|
|
225
|
+
async function branchExists(repoRoot, branchName) {
|
|
226
|
+
try {
|
|
227
|
+
await execFileAsync(
|
|
228
|
+
"git",
|
|
229
|
+
["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`],
|
|
230
|
+
{ cwd: repoRoot }
|
|
231
|
+
);
|
|
232
|
+
return true;
|
|
233
|
+
} catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
__name(branchExists, "branchExists");
|
|
238
|
+
async function createWorktree(opts) {
|
|
239
|
+
const { repoRoot, repoName, baseBranch, ticket } = opts;
|
|
240
|
+
const branchName = `${repoName}/${String(
|
|
241
|
+
ticket.num
|
|
242
|
+
).padStart(2, "0")}-${ticket.slug}`;
|
|
243
|
+
const worktreePath = join2(
|
|
244
|
+
repoRoot,
|
|
245
|
+
"..",
|
|
246
|
+
`${repoName}.${String(
|
|
247
|
+
ticket.num
|
|
248
|
+
).padStart(2, "0")}-${ticket.slug}`
|
|
249
|
+
);
|
|
250
|
+
if (existsSync2(worktreePath)) {
|
|
251
|
+
try {
|
|
252
|
+
await execFileAsync(
|
|
253
|
+
"git",
|
|
254
|
+
["worktree", "remove", "--force", worktreePath],
|
|
255
|
+
{ cwd: repoRoot }
|
|
256
|
+
);
|
|
257
|
+
} catch {
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
await execFileAsync(
|
|
262
|
+
"git",
|
|
263
|
+
["worktree", "prune"],
|
|
264
|
+
{ cwd: repoRoot }
|
|
265
|
+
);
|
|
266
|
+
} catch {
|
|
267
|
+
}
|
|
268
|
+
if (await branchExists(repoRoot, branchName)) {
|
|
269
|
+
await execFileAsync(
|
|
270
|
+
"git",
|
|
271
|
+
["branch", "-D", branchName],
|
|
272
|
+
{ cwd: repoRoot }
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
await execFileAsync(
|
|
276
|
+
"git",
|
|
277
|
+
[
|
|
278
|
+
"worktree",
|
|
279
|
+
"add",
|
|
280
|
+
"-b",
|
|
281
|
+
branchName,
|
|
282
|
+
worktreePath,
|
|
283
|
+
baseBranch
|
|
284
|
+
],
|
|
285
|
+
{ cwd: repoRoot }
|
|
286
|
+
);
|
|
287
|
+
return { worktreePath, branchName };
|
|
288
|
+
}
|
|
289
|
+
__name(createWorktree, "createWorktree");
|
|
290
|
+
async function removeWorktree(worktreePath) {
|
|
291
|
+
await execFileAsync(
|
|
292
|
+
"git",
|
|
293
|
+
["-C", worktreePath, "worktree", "remove", worktreePath]
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
__name(removeWorktree, "removeWorktree");
|
|
297
|
+
function hasNewCommits(repoRoot, base, branch) {
|
|
298
|
+
const stdout = execFileSync(
|
|
299
|
+
"git",
|
|
300
|
+
["rev-list", "--count", `${base}..${branch}`],
|
|
301
|
+
{ cwd: repoRoot, encoding: "utf8" }
|
|
302
|
+
);
|
|
303
|
+
return Number(stdout.trim()) > 0;
|
|
304
|
+
}
|
|
305
|
+
__name(hasNewCommits, "hasNewCommits");
|
|
306
|
+
function isDirty(cwd) {
|
|
307
|
+
const stdout = execFileSync(
|
|
308
|
+
"git",
|
|
309
|
+
["status", "--porcelain"],
|
|
310
|
+
{ cwd, encoding: "utf8" }
|
|
311
|
+
);
|
|
312
|
+
return stdout.trim().length > 0;
|
|
313
|
+
}
|
|
314
|
+
__name(isDirty, "isDirty");
|
|
315
|
+
async function commitDirty(cwd, message) {
|
|
316
|
+
if (!isDirty(cwd)) return false;
|
|
317
|
+
await execFileAsync(
|
|
318
|
+
"git",
|
|
319
|
+
["add", "-A"],
|
|
320
|
+
{ cwd }
|
|
321
|
+
);
|
|
322
|
+
try {
|
|
323
|
+
await execFileAsync(
|
|
324
|
+
"git",
|
|
325
|
+
[
|
|
326
|
+
"commit",
|
|
327
|
+
"-m",
|
|
328
|
+
message ?? "nightralph: auto-commit remaining changes"
|
|
329
|
+
],
|
|
330
|
+
{ cwd }
|
|
331
|
+
);
|
|
332
|
+
return true;
|
|
333
|
+
} catch {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
__name(commitDirty, "commitDirty");
|
|
338
|
+
|
|
339
|
+
// src/progress.ts
|
|
340
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
341
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
342
|
+
import { promisify as promisify2 } from "node:util";
|
|
343
|
+
var TICKET_FILENAME_RE = /^(\d+)-(.+)\.md$/;
|
|
344
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
345
|
+
function generateProgress(feature, tickets) {
|
|
346
|
+
return {
|
|
347
|
+
feature,
|
|
348
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
349
|
+
tickets: tickets.map((ticket) => ({
|
|
350
|
+
num: ticket.num,
|
|
351
|
+
slug: ticket.slug,
|
|
352
|
+
filename: ticket.filename,
|
|
353
|
+
done: false,
|
|
354
|
+
branch: null,
|
|
355
|
+
exitCode: null,
|
|
356
|
+
completedAt: null
|
|
357
|
+
}))
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
__name(generateProgress, "generateProgress");
|
|
361
|
+
function readProgress(progressPath) {
|
|
362
|
+
let content;
|
|
363
|
+
try {
|
|
364
|
+
content = readFileSync2(progressPath, "utf8");
|
|
365
|
+
} catch {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
const featureMatch = content.match(/^# Progress: (.*)$/m);
|
|
369
|
+
const startedMatch = content.match(/^Started: (.*)$/m);
|
|
370
|
+
const tickets = [];
|
|
371
|
+
let current = null;
|
|
372
|
+
for (const line of content.split("\n")) {
|
|
373
|
+
const ticketMatch = line.match(/^- \[( |x)\] (.+)$/);
|
|
374
|
+
if (ticketMatch) {
|
|
375
|
+
if (current) tickets.push(current);
|
|
376
|
+
const filename = ticketMatch[2];
|
|
377
|
+
const parsed = filename.match(TICKET_FILENAME_RE);
|
|
378
|
+
current = {
|
|
379
|
+
num: parsed ? Number(parsed[1]) : NaN,
|
|
380
|
+
slug: parsed ? parsed[2] : "",
|
|
381
|
+
filename,
|
|
382
|
+
done: ticketMatch[1] === "x",
|
|
383
|
+
branch: null,
|
|
384
|
+
exitCode: null,
|
|
385
|
+
completedAt: null
|
|
386
|
+
};
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (!current) continue;
|
|
390
|
+
const branchMatch = line.match(/^ {2}branch: (.*)$/);
|
|
391
|
+
if (branchMatch) {
|
|
392
|
+
current.branch = branchMatch[1];
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
const exitMatch = line.match(/^ {2}exit: (-?\d+)$/);
|
|
396
|
+
if (exitMatch) {
|
|
397
|
+
current.exitCode = Number(exitMatch[1]);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
const completedMatch = line.match(/^ {2}completed: (.*)$/);
|
|
401
|
+
if (completedMatch) {
|
|
402
|
+
current.completedAt = completedMatch[1];
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (current) tickets.push(current);
|
|
407
|
+
return {
|
|
408
|
+
feature: featureMatch ? featureMatch[1] : "",
|
|
409
|
+
startedAt: startedMatch ? startedMatch[1] : "",
|
|
410
|
+
tickets
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
__name(readProgress, "readProgress");
|
|
414
|
+
function mergeProgress(fresh, existing) {
|
|
415
|
+
if (!existing) return fresh;
|
|
416
|
+
const existingByFilename = new Map(
|
|
417
|
+
existing.tickets.map((t) => [t.filename, t])
|
|
418
|
+
);
|
|
419
|
+
return {
|
|
420
|
+
...fresh,
|
|
421
|
+
tickets: fresh.tickets.map((ticket) => {
|
|
422
|
+
const prior = existingByFilename.get(ticket.filename);
|
|
423
|
+
return prior && prior.done ? { ...prior } : ticket;
|
|
424
|
+
})
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
__name(mergeProgress, "mergeProgress");
|
|
428
|
+
function renderProgress(data) {
|
|
429
|
+
const lines = [
|
|
430
|
+
`# Progress: ${data.feature}`,
|
|
431
|
+
"",
|
|
432
|
+
`Started: ${data.startedAt}`,
|
|
433
|
+
""
|
|
434
|
+
];
|
|
435
|
+
for (const ticket of data.tickets) {
|
|
436
|
+
const checkbox = ticket.done ? "[x]" : "[ ]";
|
|
437
|
+
lines.push(`- ${checkbox} ${ticket.filename}`);
|
|
438
|
+
if (ticket.done || ticket.exitCode !== null) {
|
|
439
|
+
if (ticket.branch) {
|
|
440
|
+
lines.push(
|
|
441
|
+
` branch: ${ticket.branch}`
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
if (ticket.exitCode !== null) {
|
|
445
|
+
lines.push(
|
|
446
|
+
` exit: ${ticket.exitCode}`
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
if (ticket.completedAt) {
|
|
450
|
+
lines.push(
|
|
451
|
+
` completed: ${ticket.completedAt}`
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return lines.join("\n") + "\n";
|
|
457
|
+
}
|
|
458
|
+
__name(renderProgress, "renderProgress");
|
|
459
|
+
function checkTicket(data, ticketNum, result) {
|
|
460
|
+
const ticket = data.tickets.find(
|
|
461
|
+
(t) => t.num === ticketNum
|
|
462
|
+
);
|
|
463
|
+
if (!ticket) return;
|
|
464
|
+
ticket.done = result.done ?? true;
|
|
465
|
+
ticket.branch = result.branch;
|
|
466
|
+
ticket.exitCode = result.exitCode;
|
|
467
|
+
if (ticket.done) {
|
|
468
|
+
ticket.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
__name(checkTicket, "checkTicket");
|
|
472
|
+
function writeProgress(progressPath, data) {
|
|
473
|
+
writeFileSync2(
|
|
474
|
+
progressPath,
|
|
475
|
+
renderProgress(data)
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
__name(writeProgress, "writeProgress");
|
|
479
|
+
async function commitProgress(repoRoot, progressPath, message) {
|
|
480
|
+
await execFileAsync2(
|
|
481
|
+
"git",
|
|
482
|
+
["add", "--force", progressPath],
|
|
483
|
+
{ cwd: repoRoot }
|
|
484
|
+
);
|
|
485
|
+
await execFileAsync2(
|
|
486
|
+
"git",
|
|
487
|
+
["commit", "-m", message, "--", progressPath],
|
|
488
|
+
{ cwd: repoRoot }
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
__name(commitProgress, "commitProgress");
|
|
492
|
+
|
|
493
|
+
// src/merge.ts
|
|
494
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
495
|
+
import { promisify as promisify3 } from "node:util";
|
|
496
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
497
|
+
async function mergeBranch(repoRoot, branch) {
|
|
498
|
+
if (!repoRoot || typeof repoRoot !== "string") {
|
|
499
|
+
throw new Error("repoRoot must be a non-empty string");
|
|
500
|
+
}
|
|
501
|
+
if (!branch || typeof branch !== "string") {
|
|
502
|
+
throw new Error("branch must be a non-empty string");
|
|
503
|
+
}
|
|
504
|
+
try {
|
|
505
|
+
await execFileAsync3(
|
|
506
|
+
"git",
|
|
507
|
+
["merge", branch, "--no-edit"],
|
|
508
|
+
{ cwd: repoRoot }
|
|
509
|
+
);
|
|
510
|
+
return true;
|
|
511
|
+
} catch {
|
|
512
|
+
try {
|
|
513
|
+
await execFileAsync3(
|
|
514
|
+
"git",
|
|
515
|
+
["merge", "--abort"],
|
|
516
|
+
{ cwd: repoRoot }
|
|
517
|
+
);
|
|
518
|
+
} catch {
|
|
519
|
+
}
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
__name(mergeBranch, "mergeBranch");
|
|
524
|
+
async function rebaseBranch(repoRoot, branch, onto, worktreePath) {
|
|
525
|
+
if (!repoRoot || typeof repoRoot !== "string") {
|
|
526
|
+
throw new Error("repoRoot must be a non-empty string");
|
|
527
|
+
}
|
|
528
|
+
if (!branch || typeof branch !== "string") {
|
|
529
|
+
throw new Error("branch must be a non-empty string");
|
|
530
|
+
}
|
|
531
|
+
if (!onto || typeof onto !== "string") {
|
|
532
|
+
throw new Error("onto must be a non-empty string");
|
|
533
|
+
}
|
|
534
|
+
if (worktreePath) {
|
|
535
|
+
try {
|
|
536
|
+
await execFileAsync3(
|
|
537
|
+
"git",
|
|
538
|
+
["rebase", onto],
|
|
539
|
+
{ cwd: worktreePath }
|
|
540
|
+
);
|
|
541
|
+
return true;
|
|
542
|
+
} catch {
|
|
543
|
+
try {
|
|
544
|
+
await execFileAsync3(
|
|
545
|
+
"git",
|
|
546
|
+
["rebase", "--abort"],
|
|
547
|
+
{ cwd: worktreePath }
|
|
548
|
+
);
|
|
549
|
+
} catch {
|
|
550
|
+
}
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
try {
|
|
555
|
+
await execFileAsync3(
|
|
556
|
+
"git",
|
|
557
|
+
["checkout", branch],
|
|
558
|
+
{ cwd: repoRoot }
|
|
559
|
+
);
|
|
560
|
+
await execFileAsync3(
|
|
561
|
+
"git",
|
|
562
|
+
["rebase", onto],
|
|
563
|
+
{ cwd: repoRoot }
|
|
564
|
+
);
|
|
565
|
+
await execFileAsync3(
|
|
566
|
+
"git",
|
|
567
|
+
["checkout", onto],
|
|
568
|
+
{ cwd: repoRoot }
|
|
569
|
+
);
|
|
570
|
+
return true;
|
|
571
|
+
} catch {
|
|
572
|
+
try {
|
|
573
|
+
await execFileAsync3(
|
|
574
|
+
"git",
|
|
575
|
+
["rebase", "--abort"],
|
|
576
|
+
{ cwd: repoRoot }
|
|
577
|
+
);
|
|
578
|
+
} catch {
|
|
579
|
+
}
|
|
580
|
+
try {
|
|
581
|
+
await execFileAsync3(
|
|
582
|
+
"git",
|
|
583
|
+
["checkout", onto],
|
|
584
|
+
{ cwd: repoRoot }
|
|
585
|
+
);
|
|
586
|
+
} catch {
|
|
587
|
+
}
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
__name(rebaseBranch, "rebaseBranch");
|
|
592
|
+
async function updateWorktreeFromBase(worktreePath, baseBranch) {
|
|
593
|
+
try {
|
|
594
|
+
await execFileAsync3(
|
|
595
|
+
"git",
|
|
596
|
+
["merge", baseBranch, "--no-edit"],
|
|
597
|
+
{ cwd: worktreePath }
|
|
598
|
+
);
|
|
599
|
+
} catch {
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
__name(updateWorktreeFromBase, "updateWorktreeFromBase");
|
|
603
|
+
async function mergeWave(opts) {
|
|
604
|
+
const { repoRoot, baseBranch, branches, strategy } = opts;
|
|
605
|
+
if (!repoRoot || typeof repoRoot !== "string") {
|
|
606
|
+
throw new Error("repoRoot must be a non-empty string");
|
|
607
|
+
}
|
|
608
|
+
if (!baseBranch || typeof baseBranch !== "string") {
|
|
609
|
+
throw new Error("baseBranch must be a non-empty string");
|
|
610
|
+
}
|
|
611
|
+
const merged = [];
|
|
612
|
+
let stoppedAt = null;
|
|
613
|
+
for (let i = 0; i < branches.length; i++) {
|
|
614
|
+
const { branch, ticketNum, worktreePath } = branches[i];
|
|
615
|
+
const mergingResult = {
|
|
616
|
+
branch,
|
|
617
|
+
ticketNum,
|
|
618
|
+
merged: false,
|
|
619
|
+
conflicted: false
|
|
620
|
+
};
|
|
621
|
+
if (i === 0) {
|
|
622
|
+
const success = await mergeBranch(
|
|
623
|
+
repoRoot,
|
|
624
|
+
branch
|
|
625
|
+
);
|
|
626
|
+
mergingResult.merged = success;
|
|
627
|
+
mergingResult.conflicted = !success;
|
|
628
|
+
if (!success) {
|
|
629
|
+
if (strategy === "stop") {
|
|
630
|
+
stoppedAt = mergingResult;
|
|
631
|
+
merged.push(mergingResult);
|
|
632
|
+
break;
|
|
633
|
+
}
|
|
634
|
+
merged.push(mergingResult);
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
} else {
|
|
638
|
+
const rebaseSuccess = await rebaseBranch(
|
|
639
|
+
repoRoot,
|
|
640
|
+
branch,
|
|
641
|
+
baseBranch,
|
|
642
|
+
worktreePath
|
|
643
|
+
);
|
|
644
|
+
if (!rebaseSuccess) {
|
|
645
|
+
mergingResult.conflicted = true;
|
|
646
|
+
if (strategy === "stop") {
|
|
647
|
+
stoppedAt = mergingResult;
|
|
648
|
+
merged.push(mergingResult);
|
|
649
|
+
break;
|
|
650
|
+
} else {
|
|
651
|
+
merged.push(mergingResult);
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
const mergeSuccess = await mergeBranch(
|
|
656
|
+
repoRoot,
|
|
657
|
+
branch
|
|
658
|
+
);
|
|
659
|
+
mergingResult.merged = mergeSuccess;
|
|
660
|
+
mergingResult.conflicted = !mergeSuccess;
|
|
661
|
+
if (!mergeSuccess) {
|
|
662
|
+
if (strategy === "stop") {
|
|
663
|
+
stoppedAt = mergingResult;
|
|
664
|
+
merged.push(mergingResult);
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
merged.push(mergingResult);
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
merged.push(mergingResult);
|
|
672
|
+
}
|
|
673
|
+
return {
|
|
674
|
+
merged,
|
|
675
|
+
stoppedAt
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
__name(mergeWave, "mergeWave");
|
|
679
|
+
|
|
680
|
+
// src/display.ts
|
|
681
|
+
var ESC = "\x1B[";
|
|
682
|
+
var ansi = {
|
|
683
|
+
cursorUp(n) {
|
|
684
|
+
return `${ESC}${n}A`;
|
|
685
|
+
},
|
|
686
|
+
cursorTo(col) {
|
|
687
|
+
return `${ESC}${col}G`;
|
|
688
|
+
},
|
|
689
|
+
eraseLine() {
|
|
690
|
+
return `${ESC}2K`;
|
|
691
|
+
},
|
|
692
|
+
eraseLines(n) {
|
|
693
|
+
let out = "";
|
|
694
|
+
for (let i = 0; i < n; i++) {
|
|
695
|
+
out += ansi.eraseLine();
|
|
696
|
+
if (i < n - 1) out += ansi.cursorUp(1);
|
|
697
|
+
}
|
|
698
|
+
return out;
|
|
699
|
+
},
|
|
700
|
+
cursorHide() {
|
|
701
|
+
return `${ESC}?25l`;
|
|
702
|
+
},
|
|
703
|
+
cursorShow() {
|
|
704
|
+
return `${ESC}?25h`;
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
var color = {
|
|
708
|
+
green(text) {
|
|
709
|
+
return `${ESC}32m${text}${ESC}0m`;
|
|
710
|
+
},
|
|
711
|
+
yellow(text) {
|
|
712
|
+
return `${ESC}33m${text}${ESC}0m`;
|
|
713
|
+
},
|
|
714
|
+
red(text) {
|
|
715
|
+
return `${ESC}31m${text}${ESC}0m`;
|
|
716
|
+
},
|
|
717
|
+
dim(text) {
|
|
718
|
+
return `${ESC}2m${text}${ESC}0m`;
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
var FILLED_DOT = "\u25CF";
|
|
722
|
+
var EMPTY_DOT = "\u25CB";
|
|
723
|
+
function renderDot(status) {
|
|
724
|
+
switch (status) {
|
|
725
|
+
case "pending":
|
|
726
|
+
return color.dim(EMPTY_DOT);
|
|
727
|
+
case "in-progress":
|
|
728
|
+
return color.yellow(FILLED_DOT);
|
|
729
|
+
case "done":
|
|
730
|
+
return color.green(FILLED_DOT);
|
|
731
|
+
case "failed":
|
|
732
|
+
return color.red(FILLED_DOT);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
__name(renderDot, "renderDot");
|
|
736
|
+
var StatusDisplay = class {
|
|
737
|
+
static {
|
|
738
|
+
__name(this, "StatusDisplay");
|
|
739
|
+
}
|
|
740
|
+
wave = null;
|
|
741
|
+
messages = [];
|
|
742
|
+
renderedLines = 0;
|
|
743
|
+
config;
|
|
744
|
+
viewportStart = 0;
|
|
745
|
+
resizeListener = null;
|
|
746
|
+
signalHandler = null;
|
|
747
|
+
constructor(config) {
|
|
748
|
+
this.config = {
|
|
749
|
+
messageLines: config?.messageLines ?? 10,
|
|
750
|
+
stream: config?.stream ?? process.stdout
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
getAvailableRows() {
|
|
754
|
+
const termHeight = this.config.stream.rows ?? 24;
|
|
755
|
+
const overhead = 2 + this.config.messageLines;
|
|
756
|
+
const available = termHeight - overhead;
|
|
757
|
+
return Math.max(1, available);
|
|
758
|
+
}
|
|
759
|
+
startWave(waveNum, totalWaves, tickets) {
|
|
760
|
+
this.wave = {
|
|
761
|
+
waveNum,
|
|
762
|
+
totalWaves,
|
|
763
|
+
tickets: tickets.map((t) => ({
|
|
764
|
+
...t,
|
|
765
|
+
status: "pending"
|
|
766
|
+
}))
|
|
767
|
+
};
|
|
768
|
+
this.messages = [];
|
|
769
|
+
this.viewportStart = 0;
|
|
770
|
+
this.config.stream.write(ansi.cursorHide());
|
|
771
|
+
if (this.config.stream.isTTY) {
|
|
772
|
+
if (this.resizeListener) {
|
|
773
|
+
this.config.stream.removeListener(
|
|
774
|
+
"resize",
|
|
775
|
+
this.resizeListener
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
this.resizeListener = () => this.render();
|
|
779
|
+
this.config.stream.on(
|
|
780
|
+
"resize",
|
|
781
|
+
this.resizeListener
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
if (this.signalHandler) {
|
|
785
|
+
process.removeListener("SIGINT", this.signalHandler);
|
|
786
|
+
process.removeListener("SIGTERM", this.signalHandler);
|
|
787
|
+
}
|
|
788
|
+
this.signalHandler = () => this.cleanup();
|
|
789
|
+
process.on("SIGINT", this.signalHandler);
|
|
790
|
+
process.on("SIGTERM", this.signalHandler);
|
|
791
|
+
this.render();
|
|
792
|
+
}
|
|
793
|
+
setTicketStatus(num, status) {
|
|
794
|
+
if (!this.wave) return;
|
|
795
|
+
const ticket = this.wave.tickets.find((t) => t.num === num);
|
|
796
|
+
if (ticket) {
|
|
797
|
+
ticket.status = status;
|
|
798
|
+
if (status === "in-progress") {
|
|
799
|
+
const ticketIndex = this.wave.tickets.indexOf(ticket);
|
|
800
|
+
const availableRows = this.getAvailableRows();
|
|
801
|
+
const totalTickets = this.wave.tickets.length;
|
|
802
|
+
if (ticketIndex < this.viewportStart) {
|
|
803
|
+
this.viewportStart = ticketIndex;
|
|
804
|
+
} else if (ticketIndex >= this.viewportStart + availableRows) {
|
|
805
|
+
this.viewportStart = ticketIndex - availableRows + 3;
|
|
806
|
+
}
|
|
807
|
+
this.viewportStart = Math.max(
|
|
808
|
+
0,
|
|
809
|
+
Math.min(
|
|
810
|
+
this.viewportStart,
|
|
811
|
+
totalTickets - 1
|
|
812
|
+
)
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
this.render();
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
log(message) {
|
|
819
|
+
this.messages.push(message);
|
|
820
|
+
if (this.messages.length > this.config.messageLines) {
|
|
821
|
+
this.messages.shift();
|
|
822
|
+
}
|
|
823
|
+
this.render();
|
|
824
|
+
}
|
|
825
|
+
cleanup() {
|
|
826
|
+
if (this.resizeListener && this.config.stream.isTTY) {
|
|
827
|
+
this.config.stream.removeListener("resize", this.resizeListener);
|
|
828
|
+
}
|
|
829
|
+
if (this.signalHandler) {
|
|
830
|
+
process.removeListener("SIGINT", this.signalHandler);
|
|
831
|
+
process.removeListener("SIGTERM", this.signalHandler);
|
|
832
|
+
}
|
|
833
|
+
this.config.stream.write(ansi.cursorShow());
|
|
834
|
+
}
|
|
835
|
+
render() {
|
|
836
|
+
if (!this.wave) return;
|
|
837
|
+
let output = "";
|
|
838
|
+
if (this.renderedLines > 0) {
|
|
839
|
+
output += ansi.eraseLines(this.renderedLines + 1);
|
|
840
|
+
}
|
|
841
|
+
const completed = this.wave.tickets.filter(
|
|
842
|
+
(t) => t.status === "done"
|
|
843
|
+
).length;
|
|
844
|
+
const total = this.wave.tickets.length;
|
|
845
|
+
const pct = total === 0 ? 0 : Math.round(completed / total * 100);
|
|
846
|
+
output += `Wave ${this.wave.waveNum}/${this.wave.totalWaves} -- ${pct}% complete
|
|
847
|
+
`;
|
|
848
|
+
const availableRows = this.getAvailableRows();
|
|
849
|
+
const tickets = this.wave.tickets;
|
|
850
|
+
let renderedTicketCount = 0;
|
|
851
|
+
if (tickets.length <= availableRows) {
|
|
852
|
+
for (const ticket of tickets) {
|
|
853
|
+
const dot = renderDot(ticket.status);
|
|
854
|
+
output += `${dot} ${ticket.filename}
|
|
855
|
+
`;
|
|
856
|
+
}
|
|
857
|
+
renderedTicketCount = tickets.length;
|
|
858
|
+
} else {
|
|
859
|
+
this.viewportStart = Math.max(
|
|
860
|
+
0,
|
|
861
|
+
Math.min(
|
|
862
|
+
this.viewportStart,
|
|
863
|
+
tickets.length - 1
|
|
864
|
+
)
|
|
865
|
+
);
|
|
866
|
+
let hasMoreAbove = this.viewportStart > 0;
|
|
867
|
+
let tentativeCount = availableRows - (hasMoreAbove ? 1 : 0);
|
|
868
|
+
let hasMoreBelow = this.viewportStart + tentativeCount < tickets.length;
|
|
869
|
+
let ticketsToShow = Math.max(
|
|
870
|
+
0,
|
|
871
|
+
tentativeCount - (hasMoreBelow ? 1 : 0)
|
|
872
|
+
);
|
|
873
|
+
const maxStart = Math.max(
|
|
874
|
+
0,
|
|
875
|
+
tickets.length - Math.max(1, ticketsToShow)
|
|
876
|
+
);
|
|
877
|
+
if (this.viewportStart > maxStart) {
|
|
878
|
+
this.viewportStart = maxStart;
|
|
879
|
+
hasMoreAbove = this.viewportStart > 0;
|
|
880
|
+
tentativeCount = availableRows - (hasMoreAbove ? 1 : 0);
|
|
881
|
+
hasMoreBelow = this.viewportStart + tentativeCount < tickets.length;
|
|
882
|
+
ticketsToShow = Math.max(
|
|
883
|
+
0,
|
|
884
|
+
tentativeCount - (hasMoreBelow ? 1 : 0)
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
if (hasMoreAbove) {
|
|
888
|
+
const moreCount = this.viewportStart;
|
|
889
|
+
output += ` ... ${moreCount} more above
|
|
890
|
+
`;
|
|
891
|
+
}
|
|
892
|
+
const visibleTickets = tickets.slice(
|
|
893
|
+
this.viewportStart,
|
|
894
|
+
this.viewportStart + ticketsToShow
|
|
895
|
+
);
|
|
896
|
+
for (const ticket of visibleTickets) {
|
|
897
|
+
const dot = renderDot(ticket.status);
|
|
898
|
+
output += `${dot} ${ticket.filename}
|
|
899
|
+
`;
|
|
900
|
+
}
|
|
901
|
+
if (hasMoreBelow) {
|
|
902
|
+
const moreCount = tickets.length - (this.viewportStart + ticketsToShow);
|
|
903
|
+
output += ` ... ${moreCount} more below
|
|
904
|
+
`;
|
|
905
|
+
}
|
|
906
|
+
renderedTicketCount = (hasMoreAbove ? 1 : 0) + visibleTickets.length + (hasMoreBelow ? 1 : 0);
|
|
907
|
+
}
|
|
908
|
+
output += "-".repeat(50) + "\n";
|
|
909
|
+
const msgLines = Math.max(
|
|
910
|
+
0,
|
|
911
|
+
this.config.messageLines - this.messages.length
|
|
912
|
+
);
|
|
913
|
+
for (const msg of this.messages) {
|
|
914
|
+
output += `${msg}
|
|
915
|
+
`;
|
|
916
|
+
}
|
|
917
|
+
for (let i = 0; i < msgLines; i++) {
|
|
918
|
+
output += "\n";
|
|
919
|
+
}
|
|
920
|
+
this.renderedLines = 2 + renderedTicketCount + this.config.messageLines;
|
|
921
|
+
this.config.stream.write(output);
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
var PlainDisplay = class {
|
|
925
|
+
static {
|
|
926
|
+
__name(this, "PlainDisplay");
|
|
927
|
+
}
|
|
928
|
+
startWave(waveNum, _totalWaves, tickets) {
|
|
929
|
+
console.log(
|
|
930
|
+
`
|
|
931
|
+
Wave ${waveNum}: ` + tickets.map((t) => t.filename).join(", ")
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
setTicketStatus(_num, _status) {
|
|
935
|
+
}
|
|
936
|
+
log(message) {
|
|
937
|
+
console.log(message);
|
|
938
|
+
}
|
|
939
|
+
cleanup() {
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
function createDisplay(stream) {
|
|
943
|
+
const s = stream ?? process.stdout;
|
|
944
|
+
if (s.isTTY) {
|
|
945
|
+
return new StatusDisplay({ stream: s });
|
|
946
|
+
}
|
|
947
|
+
return new PlainDisplay();
|
|
948
|
+
}
|
|
949
|
+
__name(createDisplay, "createDisplay");
|
|
950
|
+
|
|
951
|
+
// src/orchestrator.ts
|
|
952
|
+
function formatErrorMessage(error) {
|
|
953
|
+
return error instanceof Error ? error.message : String(error);
|
|
954
|
+
}
|
|
955
|
+
__name(formatErrorMessage, "formatErrorMessage");
|
|
956
|
+
var TICKET_RE = /^(\d+)-(.+)\.md$/;
|
|
957
|
+
function parseTicketFilename(filename) {
|
|
958
|
+
const m = filename.match(TICKET_RE);
|
|
959
|
+
if (!m) return null;
|
|
960
|
+
return { num: Number(m[1]), slug: m[2] };
|
|
961
|
+
}
|
|
962
|
+
__name(parseTicketFilename, "parseTicketFilename");
|
|
963
|
+
function parseStatus(body) {
|
|
964
|
+
const m = body.match(/\*\*Status:\*\*\s*(.+)/i);
|
|
965
|
+
return m ? m[1].trim().toLowerCase() : "";
|
|
966
|
+
}
|
|
967
|
+
__name(parseStatus, "parseStatus");
|
|
968
|
+
function parseBlockers(body) {
|
|
969
|
+
const m = body.match(/\*\*Blocked by:\*\*\s*(.+)/i);
|
|
970
|
+
if (!m) return [];
|
|
971
|
+
const text = m[1];
|
|
972
|
+
if (/none/i.test(text)) return [];
|
|
973
|
+
const nums = [];
|
|
974
|
+
const re = /(\d+)/g;
|
|
975
|
+
let match;
|
|
976
|
+
while (match = re.exec(text)) {
|
|
977
|
+
nums.push(Number(match[1]));
|
|
978
|
+
}
|
|
979
|
+
return nums;
|
|
980
|
+
}
|
|
981
|
+
__name(parseBlockers, "parseBlockers");
|
|
982
|
+
function scanTickets(dir) {
|
|
983
|
+
const files = readdirSync2(dir).filter((f) => TICKET_RE.test(f)).sort((a, b) => a.localeCompare(
|
|
984
|
+
b,
|
|
985
|
+
void 0,
|
|
986
|
+
{ numeric: true, sensitivity: "base" }
|
|
987
|
+
));
|
|
988
|
+
return files.map((filename) => {
|
|
989
|
+
const parsed = parseTicketFilename(filename);
|
|
990
|
+
const filepath = join3(dir, filename);
|
|
991
|
+
const body = readFileSync3(filepath, "utf8");
|
|
992
|
+
return {
|
|
993
|
+
num: parsed.num,
|
|
994
|
+
slug: parsed.slug,
|
|
995
|
+
filename,
|
|
996
|
+
filepath,
|
|
997
|
+
body,
|
|
998
|
+
status: parseStatus(body),
|
|
999
|
+
blockers: parseBlockers(body)
|
|
1000
|
+
};
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
__name(scanTickets, "scanTickets");
|
|
1004
|
+
function findReadyTickets(tickets) {
|
|
1005
|
+
const doneNums = new Set(
|
|
1006
|
+
tickets.filter((t) => t.status === "done").map((t) => t.num)
|
|
1007
|
+
);
|
|
1008
|
+
return tickets.filter((t) => {
|
|
1009
|
+
if (t.status !== "ready-for-agent") return false;
|
|
1010
|
+
return t.blockers.every((b) => doneNums.has(b));
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
__name(findReadyTickets, "findReadyTickets");
|
|
1014
|
+
var COMPLETED_RE = /^\[x\]\s+(.+)$/i;
|
|
1015
|
+
function checkItems(ticket, completedItems) {
|
|
1016
|
+
if (completedItems.length === 0) return;
|
|
1017
|
+
let body = ticket.body;
|
|
1018
|
+
for (const item of completedItems) {
|
|
1019
|
+
const escaped = item.replace(
|
|
1020
|
+
/[.*+?^${}()|[\]\\]/g,
|
|
1021
|
+
"\\$&"
|
|
1022
|
+
);
|
|
1023
|
+
body = body.replace(
|
|
1024
|
+
new RegExp(
|
|
1025
|
+
`- \\[ \\] ${escaped}`,
|
|
1026
|
+
"i"
|
|
1027
|
+
),
|
|
1028
|
+
`- [x] ${item}`
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
writeFileSync3(ticket.filepath, body);
|
|
1032
|
+
ticket.body = body;
|
|
1033
|
+
}
|
|
1034
|
+
__name(checkItems, "checkItems");
|
|
1035
|
+
function markDone(ticket) {
|
|
1036
|
+
const updated = ticket.body.replace(
|
|
1037
|
+
/\*\*Status:\*\*\s*.+/i,
|
|
1038
|
+
"**Status:** done"
|
|
1039
|
+
);
|
|
1040
|
+
writeFileSync3(ticket.filepath, updated);
|
|
1041
|
+
ticket.status = "done";
|
|
1042
|
+
ticket.body = updated;
|
|
1043
|
+
}
|
|
1044
|
+
__name(markDone, "markDone");
|
|
1045
|
+
function renderPrompt(specBody, ticket) {
|
|
1046
|
+
return [
|
|
1047
|
+
"---- SPEC CONTEXT ----",
|
|
1048
|
+
specBody,
|
|
1049
|
+
"",
|
|
1050
|
+
"---- TICKET ----",
|
|
1051
|
+
ticket.filename,
|
|
1052
|
+
ticket.body,
|
|
1053
|
+
"",
|
|
1054
|
+
"---- AGENT INSTRUCTIONS ----",
|
|
1055
|
+
"You are an autonomous coding agent. Implement the ticket.",
|
|
1056
|
+
"",
|
|
1057
|
+
"Time is limited. Follow these rules:",
|
|
1058
|
+
"- Spend at most 2 minutes reading existing code. Read only the files directly relevant to the ticket.",
|
|
1059
|
+
"- Do NOT research external APIs or services via web search. Use the spec and ticket body as your sole reference for API shapes.",
|
|
1060
|
+
"- Start writing code as soon as you understand the immediate context. You can read more files as needed while implementing.",
|
|
1061
|
+
"- Commit your work when finished.",
|
|
1062
|
+
"- After completing each checklist item in the ticket, print a line: [x] <item text> (matching the checklist text exactly).",
|
|
1063
|
+
""
|
|
1064
|
+
].join("\n");
|
|
1065
|
+
}
|
|
1066
|
+
__name(renderPrompt, "renderPrompt");
|
|
1067
|
+
function listTickets(tickets) {
|
|
1068
|
+
console.log("\nTickets:");
|
|
1069
|
+
for (const t of tickets) {
|
|
1070
|
+
const blockerStr = t.blockers.length > 0 ? `blocked by [${t.blockers.join(", ")}]` : "no blockers";
|
|
1071
|
+
console.log(
|
|
1072
|
+
` ${t.filename} (${t.status}) ${blockerStr}`
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
__name(listTickets, "listTickets");
|
|
1077
|
+
function* simulateWaves(tickets) {
|
|
1078
|
+
const sim = new Map(
|
|
1079
|
+
tickets.map((t) => [t.num, t.status])
|
|
1080
|
+
);
|
|
1081
|
+
let wave = 1;
|
|
1082
|
+
while (true) {
|
|
1083
|
+
const doneNums = new Set(
|
|
1084
|
+
[...sim.entries()].filter(([, s]) => s === "done").map(([n]) => n)
|
|
1085
|
+
);
|
|
1086
|
+
const ready = [...tickets].filter((t) => {
|
|
1087
|
+
if (sim.get(t.num) !== "ready-for-agent") {
|
|
1088
|
+
return false;
|
|
1089
|
+
}
|
|
1090
|
+
return t.blockers.every(
|
|
1091
|
+
(b) => doneNums.has(b)
|
|
1092
|
+
);
|
|
1093
|
+
});
|
|
1094
|
+
if (ready.length === 0) break;
|
|
1095
|
+
yield { wave, ready };
|
|
1096
|
+
for (const t of ready) {
|
|
1097
|
+
sim.set(t.num, "done");
|
|
1098
|
+
}
|
|
1099
|
+
wave++;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
__name(simulateWaves, "simulateWaves");
|
|
1103
|
+
function detectDeadlocks(tickets) {
|
|
1104
|
+
const doneNums = /* @__PURE__ */ new Set();
|
|
1105
|
+
const ready = /* @__PURE__ */ new Set();
|
|
1106
|
+
for (const t of tickets) {
|
|
1107
|
+
if (t.status === "ready-for-agent") {
|
|
1108
|
+
ready.add(t.num);
|
|
1109
|
+
} else if (t.status === "done") {
|
|
1110
|
+
doneNums.add(t.num);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
let changed = true;
|
|
1114
|
+
while (changed) {
|
|
1115
|
+
changed = false;
|
|
1116
|
+
for (const num of ready) {
|
|
1117
|
+
const ticket = tickets.find(
|
|
1118
|
+
(t) => t.num === num
|
|
1119
|
+
);
|
|
1120
|
+
if (ticket.blockers.every(
|
|
1121
|
+
(b) => doneNums.has(b)
|
|
1122
|
+
)) {
|
|
1123
|
+
doneNums.add(num);
|
|
1124
|
+
ready.delete(num);
|
|
1125
|
+
changed = true;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
return [...ready].map(
|
|
1130
|
+
(num) => tickets.find((t) => t.num === num)
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
__name(detectDeadlocks, "detectDeadlocks");
|
|
1134
|
+
var PROVIDER_ARGS = {
|
|
1135
|
+
claude: [
|
|
1136
|
+
"--print",
|
|
1137
|
+
"--verbose",
|
|
1138
|
+
"--output-format",
|
|
1139
|
+
"stream-json",
|
|
1140
|
+
"--dangerously-skip-permissions"
|
|
1141
|
+
],
|
|
1142
|
+
codex: ["exec", "--full-auto"],
|
|
1143
|
+
pi: [
|
|
1144
|
+
"-p",
|
|
1145
|
+
"--verbose",
|
|
1146
|
+
"--approve",
|
|
1147
|
+
"--no-session",
|
|
1148
|
+
"--mode",
|
|
1149
|
+
"json"
|
|
1150
|
+
]
|
|
1151
|
+
};
|
|
1152
|
+
function getProviderArgs(cmd) {
|
|
1153
|
+
const name = basename2(cmd);
|
|
1154
|
+
return PROVIDER_ARGS[name] ?? [];
|
|
1155
|
+
}
|
|
1156
|
+
__name(getProviderArgs, "getProviderArgs");
|
|
1157
|
+
function formatStreamLine(line) {
|
|
1158
|
+
let obj;
|
|
1159
|
+
try {
|
|
1160
|
+
obj = JSON.parse(line);
|
|
1161
|
+
} catch {
|
|
1162
|
+
return line;
|
|
1163
|
+
}
|
|
1164
|
+
if (obj.type === "assistant") {
|
|
1165
|
+
const msg = obj.message;
|
|
1166
|
+
const content = msg?.content;
|
|
1167
|
+
if (!content) return null;
|
|
1168
|
+
for (const block of content) {
|
|
1169
|
+
if (block.type === "text") {
|
|
1170
|
+
return String(block.text);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
return null;
|
|
1174
|
+
}
|
|
1175
|
+
if (obj.type === "result") {
|
|
1176
|
+
const sub = obj.subtype ?? "done";
|
|
1177
|
+
const cost = obj.total_cost_usd;
|
|
1178
|
+
const costStr = cost != null ? ` ($${cost.toFixed(2)})` : "";
|
|
1179
|
+
return `${sub}${costStr}`;
|
|
1180
|
+
}
|
|
1181
|
+
if (obj.type === "system") {
|
|
1182
|
+
const sub = obj.subtype;
|
|
1183
|
+
if (sub === "api_retry") {
|
|
1184
|
+
const error = obj.error;
|
|
1185
|
+
return `retry: ${error ?? "unknown"}`;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
return null;
|
|
1189
|
+
}
|
|
1190
|
+
__name(formatStreamLine, "formatStreamLine");
|
|
1191
|
+
function createPiStreamFormatter() {
|
|
1192
|
+
const calls = /* @__PURE__ */ new Map();
|
|
1193
|
+
return (line) => {
|
|
1194
|
+
let obj;
|
|
1195
|
+
try {
|
|
1196
|
+
obj = JSON.parse(line);
|
|
1197
|
+
} catch {
|
|
1198
|
+
return line;
|
|
1199
|
+
}
|
|
1200
|
+
const evt = obj.assistantMessageEvent;
|
|
1201
|
+
if (!evt) return null;
|
|
1202
|
+
const type = evt.type;
|
|
1203
|
+
const ci = evt.contentIndex ?? -1;
|
|
1204
|
+
if (type === "text_delta") {
|
|
1205
|
+
return evt.delta ?? null;
|
|
1206
|
+
}
|
|
1207
|
+
if (type === "toolcall_start") {
|
|
1208
|
+
calls.set(ci, {
|
|
1209
|
+
name: evt.toolName ?? "?",
|
|
1210
|
+
buf: ""
|
|
1211
|
+
});
|
|
1212
|
+
return null;
|
|
1213
|
+
}
|
|
1214
|
+
if (type === "toolcall_delta" && calls.has(ci)) {
|
|
1215
|
+
calls.get(ci).buf += evt.delta ?? "";
|
|
1216
|
+
return null;
|
|
1217
|
+
}
|
|
1218
|
+
if (type === "toolcall_end" && calls.has(ci)) {
|
|
1219
|
+
const call = calls.get(ci);
|
|
1220
|
+
calls.delete(ci);
|
|
1221
|
+
let label = "";
|
|
1222
|
+
try {
|
|
1223
|
+
const args = JSON.parse(call.buf);
|
|
1224
|
+
label = String(
|
|
1225
|
+
args.command ?? args.file_path ?? args.query ?? ""
|
|
1226
|
+
).split("\n")[0].slice(0, 72);
|
|
1227
|
+
} catch {
|
|
1228
|
+
}
|
|
1229
|
+
return `[${call.name}] ${label}`;
|
|
1230
|
+
}
|
|
1231
|
+
return null;
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
__name(createPiStreamFormatter, "createPiStreamFormatter");
|
|
1235
|
+
function writePromptFile(prompt) {
|
|
1236
|
+
const name = `nightralph-${Date.now()}-${Math.random().toString(36).slice(2)}.md`;
|
|
1237
|
+
const filePath = join3(tmpdir(), name);
|
|
1238
|
+
writeFileSync3(filePath, prompt);
|
|
1239
|
+
return filePath;
|
|
1240
|
+
}
|
|
1241
|
+
__name(writePromptFile, "writePromptFile");
|
|
1242
|
+
function cleanupPromptFile(filePath) {
|
|
1243
|
+
try {
|
|
1244
|
+
unlinkSync(filePath);
|
|
1245
|
+
} catch {
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
__name(cleanupPromptFile, "cleanupPromptFile");
|
|
1249
|
+
function spawnAgent(opts) {
|
|
1250
|
+
const args = [
|
|
1251
|
+
...getProviderArgs(opts.agentCmd)
|
|
1252
|
+
];
|
|
1253
|
+
if (opts.model) {
|
|
1254
|
+
args.push("--model", opts.model);
|
|
1255
|
+
}
|
|
1256
|
+
const providerName = basename2(opts.agentCmd);
|
|
1257
|
+
const isClaude = providerName === "claude";
|
|
1258
|
+
const isPi = providerName === "pi";
|
|
1259
|
+
const prefix = opts.label ? ` [${opts.label}] ` : " ";
|
|
1260
|
+
let promptFile = null;
|
|
1261
|
+
if (isPi) {
|
|
1262
|
+
promptFile = writePromptFile(opts.prompt);
|
|
1263
|
+
args.push(`@${promptFile}`);
|
|
1264
|
+
}
|
|
1265
|
+
const logStream = createWriteStream(
|
|
1266
|
+
opts.logPath,
|
|
1267
|
+
{ flags: "w" }
|
|
1268
|
+
);
|
|
1269
|
+
const proc = spawn(opts.agentCmd, args, {
|
|
1270
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1271
|
+
cwd: opts.cwd
|
|
1272
|
+
});
|
|
1273
|
+
proc.stdout.pipe(logStream);
|
|
1274
|
+
const piFormatter = isPi ? createPiStreamFormatter() : null;
|
|
1275
|
+
const completedItems = [];
|
|
1276
|
+
function collectCompleted(text) {
|
|
1277
|
+
const match = text.match(COMPLETED_RE);
|
|
1278
|
+
if (match) completedItems.push(match[1]);
|
|
1279
|
+
}
|
|
1280
|
+
__name(collectCompleted, "collectCompleted");
|
|
1281
|
+
const rl = createInterface({ input: proc.stdout });
|
|
1282
|
+
rl.on("line", (line) => {
|
|
1283
|
+
if (isClaude) {
|
|
1284
|
+
const formatted = formatStreamLine(line);
|
|
1285
|
+
if (formatted !== null) {
|
|
1286
|
+
collectCompleted(formatted);
|
|
1287
|
+
const output = `${prefix}${formatted}`;
|
|
1288
|
+
if (opts.onLine) {
|
|
1289
|
+
opts.onLine(output);
|
|
1290
|
+
} else {
|
|
1291
|
+
console.log(output);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
} else if (piFormatter) {
|
|
1295
|
+
const formatted = piFormatter(line);
|
|
1296
|
+
if (formatted !== null) {
|
|
1297
|
+
collectCompleted(formatted);
|
|
1298
|
+
const output = `${prefix}${formatted}`;
|
|
1299
|
+
if (opts.onLine) {
|
|
1300
|
+
opts.onLine(output);
|
|
1301
|
+
} else {
|
|
1302
|
+
console.log(output);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
} else {
|
|
1306
|
+
collectCompleted(line);
|
|
1307
|
+
const output = `${prefix}${line}`;
|
|
1308
|
+
if (opts.onLine) {
|
|
1309
|
+
opts.onLine(output);
|
|
1310
|
+
} else {
|
|
1311
|
+
console.log(output);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
});
|
|
1315
|
+
proc.stderr.pipe(logStream);
|
|
1316
|
+
const rlErr = createInterface({ input: proc.stderr });
|
|
1317
|
+
rlErr.on("line", (line) => {
|
|
1318
|
+
const output = `${prefix}${line}`;
|
|
1319
|
+
if (opts.onLine) {
|
|
1320
|
+
opts.onLine(output);
|
|
1321
|
+
} else {
|
|
1322
|
+
console.error(output);
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
const timeout = setTimeout(() => {
|
|
1326
|
+
if (!proc.killed) {
|
|
1327
|
+
console.warn(
|
|
1328
|
+
`Agent did not exit in ${opts.timeout}s; killing.`
|
|
1329
|
+
);
|
|
1330
|
+
proc.kill("SIGKILL");
|
|
1331
|
+
}
|
|
1332
|
+
}, opts.timeout * 1e3);
|
|
1333
|
+
if (!promptFile) {
|
|
1334
|
+
proc.stdin.write(opts.prompt);
|
|
1335
|
+
}
|
|
1336
|
+
proc.stdin.end();
|
|
1337
|
+
return new Promise((resolve) => {
|
|
1338
|
+
proc.on("close", (code) => {
|
|
1339
|
+
clearTimeout(timeout);
|
|
1340
|
+
rl.close();
|
|
1341
|
+
rlErr.close();
|
|
1342
|
+
logStream.end();
|
|
1343
|
+
if (promptFile) cleanupPromptFile(promptFile);
|
|
1344
|
+
resolve({
|
|
1345
|
+
exitCode: code ?? 1,
|
|
1346
|
+
completedItems
|
|
1347
|
+
});
|
|
1348
|
+
});
|
|
1349
|
+
proc.on("error", (err) => {
|
|
1350
|
+
clearTimeout(timeout);
|
|
1351
|
+
rl.close();
|
|
1352
|
+
rlErr.close();
|
|
1353
|
+
logStream.end();
|
|
1354
|
+
if (promptFile) cleanupPromptFile(promptFile);
|
|
1355
|
+
console.error(
|
|
1356
|
+
"Agent process error:",
|
|
1357
|
+
formatErrorMessage(err)
|
|
1358
|
+
);
|
|
1359
|
+
resolve({ exitCode: 1, completedItems });
|
|
1360
|
+
});
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
__name(spawnAgent, "spawnAgent");
|
|
1364
|
+
function dryRun(tickets, specBody) {
|
|
1365
|
+
listTickets(tickets);
|
|
1366
|
+
console.log("\nSimulated wave order:");
|
|
1367
|
+
for (const { wave, ready } of simulateWaves(tickets)) {
|
|
1368
|
+
console.log(
|
|
1369
|
+
` Wave ${wave}: ` + ready.map((t) => t.filename).join(", ")
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
const stuck = detectDeadlocks(tickets);
|
|
1373
|
+
if (stuck.length > 0) {
|
|
1374
|
+
console.log(
|
|
1375
|
+
"\nDeadlocked tickets: " + stuck.map((t) => t.filename).join(", ")
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
const firstReady = findReadyTickets(tickets);
|
|
1379
|
+
if (firstReady.length > 0) {
|
|
1380
|
+
console.log(
|
|
1381
|
+
`
|
|
1382
|
+
Prompt for first ready ticket (${firstReady[0].filename}):
|
|
1383
|
+
`
|
|
1384
|
+
);
|
|
1385
|
+
console.log(
|
|
1386
|
+
renderPrompt(specBody, firstReady[0])
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
__name(dryRun, "dryRun");
|
|
1391
|
+
function dryRunIsolated(opts) {
|
|
1392
|
+
listTickets(opts.tickets);
|
|
1393
|
+
console.log("\nSimulated wave order:");
|
|
1394
|
+
for (const { wave, ready } of simulateWaves(
|
|
1395
|
+
opts.tickets
|
|
1396
|
+
)) {
|
|
1397
|
+
console.log(
|
|
1398
|
+
` Wave ${wave}: ` + ready.map((t) => t.filename).join(", ")
|
|
1399
|
+
);
|
|
1400
|
+
for (const t of ready) {
|
|
1401
|
+
const num = String(t.num).padStart(2, "0");
|
|
1402
|
+
const branchName = `${opts.repoName}/${num}-${t.slug}`;
|
|
1403
|
+
const worktreePath = `../${opts.repoName}.${num}-${t.slug}`;
|
|
1404
|
+
console.log(` worktree: ${worktreePath}`);
|
|
1405
|
+
console.log(` branch: ${branchName}`);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
const stuck = detectDeadlocks(opts.tickets);
|
|
1409
|
+
if (stuck.length > 0) {
|
|
1410
|
+
console.log(
|
|
1411
|
+
"\nDeadlocked tickets: " + stuck.map((t) => t.filename).join(", ")
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
const firstReady = findReadyTickets(opts.tickets);
|
|
1415
|
+
if (firstReady.length > 0) {
|
|
1416
|
+
console.log(
|
|
1417
|
+
`
|
|
1418
|
+
Prompt for first ready ticket (${firstReady[0].filename}):
|
|
1419
|
+
`
|
|
1420
|
+
);
|
|
1421
|
+
console.log(
|
|
1422
|
+
renderPrompt(opts.specBody, firstReady[0])
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
const strategyDesc = opts.conflictStrategy === "respawn" ? "respawn (re-run agent on conflict)" : "stop (stop merging on conflict)";
|
|
1426
|
+
console.log(
|
|
1427
|
+
`
|
|
1428
|
+
Merge strategy: ${strategyDesc}`
|
|
1429
|
+
);
|
|
1430
|
+
console.log(`Progress file: ${opts.progressPath}`);
|
|
1431
|
+
}
|
|
1432
|
+
__name(dryRunIsolated, "dryRunIsolated");
|
|
1433
|
+
async function respawnAndRetryMerge(opts) {
|
|
1434
|
+
await updateWorktreeFromBase(
|
|
1435
|
+
opts.worktreePath,
|
|
1436
|
+
opts.baseBranch
|
|
1437
|
+
);
|
|
1438
|
+
const msg1 = ` ${opts.ticket.filename}: respawning agent to resolve merge conflict`;
|
|
1439
|
+
if (opts.onLine) {
|
|
1440
|
+
opts.onLine(msg1);
|
|
1441
|
+
} else {
|
|
1442
|
+
console.log(msg1);
|
|
1443
|
+
}
|
|
1444
|
+
const prompt = renderPrompt(opts.specBody, opts.ticket);
|
|
1445
|
+
const logPath = join3(
|
|
1446
|
+
opts.logsDir,
|
|
1447
|
+
opts.ticket.filename.replace(/\.md$/, ".respawn.log")
|
|
1448
|
+
);
|
|
1449
|
+
const label = opts.ticket.filename.replace(
|
|
1450
|
+
/\.md$/,
|
|
1451
|
+
""
|
|
1452
|
+
);
|
|
1453
|
+
const result = await spawnAgent({
|
|
1454
|
+
prompt,
|
|
1455
|
+
agentCmd: opts.agentCmd,
|
|
1456
|
+
model: opts.model,
|
|
1457
|
+
timeout: opts.timeout,
|
|
1458
|
+
logPath,
|
|
1459
|
+
cwd: opts.worktreePath,
|
|
1460
|
+
label,
|
|
1461
|
+
onLine: opts.onLine
|
|
1462
|
+
});
|
|
1463
|
+
if (result.exitCode !== 0) {
|
|
1464
|
+
const msg2 = ` ${opts.ticket.filename}: respawned agent exited ${result.exitCode}`;
|
|
1465
|
+
if (opts.onLine) {
|
|
1466
|
+
opts.onLine(msg2);
|
|
1467
|
+
} else {
|
|
1468
|
+
console.log(msg2);
|
|
1469
|
+
}
|
|
1470
|
+
return false;
|
|
1471
|
+
}
|
|
1472
|
+
const merged = await mergeBranch(
|
|
1473
|
+
opts.repoRoot,
|
|
1474
|
+
opts.branch
|
|
1475
|
+
);
|
|
1476
|
+
if (!merged) {
|
|
1477
|
+
const msg3 = ` ${opts.ticket.filename}: merge still conflicted after respawn`;
|
|
1478
|
+
if (opts.onLine) {
|
|
1479
|
+
opts.onLine(msg3);
|
|
1480
|
+
} else {
|
|
1481
|
+
console.log(msg3);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
return merged;
|
|
1485
|
+
}
|
|
1486
|
+
__name(respawnAndRetryMerge, "respawnAndRetryMerge");
|
|
1487
|
+
async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, display) {
|
|
1488
|
+
mkdirSync2(logsDir, { recursive: true });
|
|
1489
|
+
const d = display ?? createDisplay();
|
|
1490
|
+
let totalWaves = 0;
|
|
1491
|
+
for (const _ of simulateWaves(tickets)) totalWaves++;
|
|
1492
|
+
let totalCompleted = 0;
|
|
1493
|
+
let waveNum = 0;
|
|
1494
|
+
while (true) {
|
|
1495
|
+
const ready = findReadyTickets(tickets);
|
|
1496
|
+
if (ready.length === 0) break;
|
|
1497
|
+
waveNum++;
|
|
1498
|
+
d.startWave(
|
|
1499
|
+
waveNum,
|
|
1500
|
+
totalWaves,
|
|
1501
|
+
ready.map((t) => ({
|
|
1502
|
+
num: t.num,
|
|
1503
|
+
filename: t.filename
|
|
1504
|
+
}))
|
|
1505
|
+
);
|
|
1506
|
+
const results = await Promise.allSettled(
|
|
1507
|
+
ready.map(async (t) => {
|
|
1508
|
+
const prompt = renderPrompt(specBody, t);
|
|
1509
|
+
const logPath = join3(
|
|
1510
|
+
logsDir,
|
|
1511
|
+
t.filename.replace(/\.md$/, ".log")
|
|
1512
|
+
);
|
|
1513
|
+
const label = t.filename.replace(
|
|
1514
|
+
/\.md$/,
|
|
1515
|
+
""
|
|
1516
|
+
);
|
|
1517
|
+
d.setTicketStatus(t.num, "in-progress");
|
|
1518
|
+
d.log(` Starting ${t.filename}`);
|
|
1519
|
+
const result = await spawnAgent({
|
|
1520
|
+
prompt,
|
|
1521
|
+
agentCmd,
|
|
1522
|
+
model,
|
|
1523
|
+
timeout,
|
|
1524
|
+
logPath,
|
|
1525
|
+
label,
|
|
1526
|
+
onLine: /* @__PURE__ */ __name((line) => d.log(line), "onLine")
|
|
1527
|
+
});
|
|
1528
|
+
return { ticket: t, result };
|
|
1529
|
+
})
|
|
1530
|
+
);
|
|
1531
|
+
let waveCompleted = 0;
|
|
1532
|
+
for (const r of results) {
|
|
1533
|
+
if (r.status === "rejected") continue;
|
|
1534
|
+
const { ticket, result } = r.value;
|
|
1535
|
+
if (result.exitCode !== 0) {
|
|
1536
|
+
d.setTicketStatus(ticket.num, "failed");
|
|
1537
|
+
d.log(
|
|
1538
|
+
` ${ticket.filename}: agent exited ${result.exitCode}`
|
|
1539
|
+
);
|
|
1540
|
+
continue;
|
|
1541
|
+
}
|
|
1542
|
+
checkItems(ticket, result.completedItems);
|
|
1543
|
+
markDone(ticket);
|
|
1544
|
+
waveCompleted++;
|
|
1545
|
+
totalCompleted++;
|
|
1546
|
+
d.setTicketStatus(ticket.num, "done");
|
|
1547
|
+
d.log(
|
|
1548
|
+
` ${ticket.filename}: done`
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1551
|
+
if (waveCompleted === 0) {
|
|
1552
|
+
d.log(
|
|
1553
|
+
"\nNo tickets completed this wave. Stopping."
|
|
1554
|
+
);
|
|
1555
|
+
break;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
const remaining = tickets.filter(
|
|
1559
|
+
(t) => t.status === "ready-for-agent"
|
|
1560
|
+
);
|
|
1561
|
+
d.log(
|
|
1562
|
+
`
|
|
1563
|
+
Finished: ${totalCompleted} completed` + (remaining.length > 0 ? `, ${remaining.length} remaining` : "")
|
|
1564
|
+
);
|
|
1565
|
+
d.cleanup();
|
|
1566
|
+
return { allDone: remaining.length === 0 };
|
|
1567
|
+
}
|
|
1568
|
+
__name(runWaves, "runWaves");
|
|
1569
|
+
async function runWavesIsolated(opts) {
|
|
1570
|
+
mkdirSync2(opts.logsDir, { recursive: true });
|
|
1571
|
+
const d = opts.display ?? createDisplay();
|
|
1572
|
+
let totalWaves = 0;
|
|
1573
|
+
for (const _ of simulateWaves(opts.tickets)) totalWaves++;
|
|
1574
|
+
const repoRoot = getRepoRoot();
|
|
1575
|
+
const repoName = getRepoName();
|
|
1576
|
+
const baseBranch = getCurrentBranch();
|
|
1577
|
+
const progressPath = join3(
|
|
1578
|
+
dirname(opts.logsDir),
|
|
1579
|
+
"progress.md"
|
|
1580
|
+
);
|
|
1581
|
+
const progress = mergeProgress(
|
|
1582
|
+
generateProgress(opts.featureName, opts.tickets),
|
|
1583
|
+
readProgress(progressPath)
|
|
1584
|
+
);
|
|
1585
|
+
writeProgress(progressPath, progress);
|
|
1586
|
+
await commitProgress(
|
|
1587
|
+
repoRoot,
|
|
1588
|
+
progressPath,
|
|
1589
|
+
`nightralph: start ${opts.featureName}`
|
|
1590
|
+
);
|
|
1591
|
+
let totalCompleted = 0;
|
|
1592
|
+
let waveNum = 0;
|
|
1593
|
+
while (true) {
|
|
1594
|
+
const ready = findReadyTickets(opts.tickets);
|
|
1595
|
+
if (ready.length === 0) break;
|
|
1596
|
+
waveNum++;
|
|
1597
|
+
d.startWave(
|
|
1598
|
+
waveNum,
|
|
1599
|
+
totalWaves,
|
|
1600
|
+
ready.map((t) => ({
|
|
1601
|
+
num: t.num,
|
|
1602
|
+
filename: t.filename
|
|
1603
|
+
}))
|
|
1604
|
+
);
|
|
1605
|
+
const worktreeSettled = await Promise.allSettled(
|
|
1606
|
+
ready.map((t) => createWorktree({
|
|
1607
|
+
repoRoot,
|
|
1608
|
+
repoName,
|
|
1609
|
+
baseBranch,
|
|
1610
|
+
ticket: t
|
|
1611
|
+
}))
|
|
1612
|
+
);
|
|
1613
|
+
const activeTickets = [];
|
|
1614
|
+
const worktrees = [];
|
|
1615
|
+
for (let i = 0; i < worktreeSettled.length; i++) {
|
|
1616
|
+
const settled = worktreeSettled[i];
|
|
1617
|
+
const ticket = ready[i];
|
|
1618
|
+
if (settled.status === "rejected") {
|
|
1619
|
+
d.setTicketStatus(ticket.num, "failed");
|
|
1620
|
+
d.log(
|
|
1621
|
+
` ${ticket.filename}: failed to create worktree: ${formatErrorMessage(settled.reason)}`
|
|
1622
|
+
);
|
|
1623
|
+
continue;
|
|
1624
|
+
}
|
|
1625
|
+
activeTickets.push(ticket);
|
|
1626
|
+
worktrees.push(settled.value);
|
|
1627
|
+
}
|
|
1628
|
+
const results = await Promise.allSettled(
|
|
1629
|
+
activeTickets.map((t, i) => {
|
|
1630
|
+
const prompt = renderPrompt(opts.specBody, t);
|
|
1631
|
+
const logPath = join3(
|
|
1632
|
+
opts.logsDir,
|
|
1633
|
+
t.filename.replace(/\.md$/, ".log")
|
|
1634
|
+
);
|
|
1635
|
+
const label = t.filename.replace(
|
|
1636
|
+
/\.md$/,
|
|
1637
|
+
""
|
|
1638
|
+
);
|
|
1639
|
+
d.setTicketStatus(t.num, "in-progress");
|
|
1640
|
+
d.log(` Starting ${t.filename}`);
|
|
1641
|
+
return spawnAgent({
|
|
1642
|
+
prompt,
|
|
1643
|
+
agentCmd: opts.agentCmd,
|
|
1644
|
+
model: opts.model,
|
|
1645
|
+
timeout: opts.timeout,
|
|
1646
|
+
logPath,
|
|
1647
|
+
cwd: worktrees[i].worktreePath,
|
|
1648
|
+
label,
|
|
1649
|
+
onLine: /* @__PURE__ */ __name((line) => d.log(line), "onLine")
|
|
1650
|
+
});
|
|
1651
|
+
})
|
|
1652
|
+
);
|
|
1653
|
+
let waveCompleted = 0;
|
|
1654
|
+
const successfulBranches = [];
|
|
1655
|
+
const ticketsByNum = /* @__PURE__ */ new Map();
|
|
1656
|
+
const worktreePathsByNum = /* @__PURE__ */ new Map();
|
|
1657
|
+
for (let i = 0; i < results.length; i++) {
|
|
1658
|
+
const r = results[i];
|
|
1659
|
+
if (r.status === "rejected") continue;
|
|
1660
|
+
const agentResult = r.value;
|
|
1661
|
+
const ticket = activeTickets[i];
|
|
1662
|
+
const worktree = worktrees[i];
|
|
1663
|
+
if (agentResult.exitCode !== 0) {
|
|
1664
|
+
d.setTicketStatus(ticket.num, "failed");
|
|
1665
|
+
d.log(
|
|
1666
|
+
` ${ticket.filename}: agent exited ${agentResult.exitCode}`
|
|
1667
|
+
);
|
|
1668
|
+
checkItems(ticket, agentResult.completedItems);
|
|
1669
|
+
checkTicket(progress, ticket.num, {
|
|
1670
|
+
branch: worktree.branchName,
|
|
1671
|
+
exitCode: agentResult.exitCode,
|
|
1672
|
+
done: false
|
|
1673
|
+
});
|
|
1674
|
+
writeProgress(progressPath, progress);
|
|
1675
|
+
await commitProgress(
|
|
1676
|
+
repoRoot,
|
|
1677
|
+
progressPath,
|
|
1678
|
+
`nightralph: ${ticket.filename} failed`
|
|
1679
|
+
);
|
|
1680
|
+
try {
|
|
1681
|
+
await removeWorktree(worktree.worktreePath);
|
|
1682
|
+
} catch (err) {
|
|
1683
|
+
d.log(
|
|
1684
|
+
` Failed to remove worktree ${worktree.worktreePath}: ` + formatErrorMessage(err)
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
1687
|
+
continue;
|
|
1688
|
+
}
|
|
1689
|
+
if (isDirty(worktree.worktreePath)) {
|
|
1690
|
+
const committed = await commitDirty(
|
|
1691
|
+
worktree.worktreePath,
|
|
1692
|
+
`nightralph: auto-commit ${ticket.filename} remaining changes`
|
|
1693
|
+
);
|
|
1694
|
+
if (committed) {
|
|
1695
|
+
d.log(
|
|
1696
|
+
` ${ticket.filename}: auto-committed remaining changes`
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
if (!hasNewCommits(
|
|
1701
|
+
repoRoot,
|
|
1702
|
+
baseBranch,
|
|
1703
|
+
worktree.branchName
|
|
1704
|
+
)) {
|
|
1705
|
+
d.setTicketStatus(ticket.num, "failed");
|
|
1706
|
+
d.log(
|
|
1707
|
+
` ${ticket.filename}: agent exited 0 but made no changes; leaving ticket ready-for-agent`
|
|
1708
|
+
);
|
|
1709
|
+
continue;
|
|
1710
|
+
}
|
|
1711
|
+
checkItems(ticket, agentResult.completedItems);
|
|
1712
|
+
d.log(
|
|
1713
|
+
` ${ticket.filename}: agent finished, awaiting merge`
|
|
1714
|
+
);
|
|
1715
|
+
ticketsByNum.set(ticket.num, ticket);
|
|
1716
|
+
worktreePathsByNum.set(
|
|
1717
|
+
ticket.num,
|
|
1718
|
+
worktree.worktreePath
|
|
1719
|
+
);
|
|
1720
|
+
successfulBranches.push({
|
|
1721
|
+
branch: worktree.branchName,
|
|
1722
|
+
ticketNum: ticket.num,
|
|
1723
|
+
worktreePath: worktree.worktreePath
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
if (successfulBranches.length > 0) {
|
|
1727
|
+
const mergeResult = await mergeWave({
|
|
1728
|
+
repoRoot,
|
|
1729
|
+
baseBranch,
|
|
1730
|
+
branches: successfulBranches,
|
|
1731
|
+
strategy: opts.conflictStrategy
|
|
1732
|
+
});
|
|
1733
|
+
for (const branchResult of mergeResult.merged) {
|
|
1734
|
+
const ticket = ticketsByNum.get(
|
|
1735
|
+
branchResult.ticketNum
|
|
1736
|
+
);
|
|
1737
|
+
if (!ticket) continue;
|
|
1738
|
+
if (branchResult.merged) {
|
|
1739
|
+
markDone(ticket);
|
|
1740
|
+
waveCompleted++;
|
|
1741
|
+
totalCompleted++;
|
|
1742
|
+
d.setTicketStatus(ticket.num, "done");
|
|
1743
|
+
d.log(
|
|
1744
|
+
` ${ticket.filename}: done`
|
|
1745
|
+
);
|
|
1746
|
+
checkTicket(progress, ticket.num, {
|
|
1747
|
+
branch: branchResult.branch,
|
|
1748
|
+
exitCode: 0
|
|
1749
|
+
});
|
|
1750
|
+
writeProgress(progressPath, progress);
|
|
1751
|
+
await commitProgress(
|
|
1752
|
+
repoRoot,
|
|
1753
|
+
progressPath,
|
|
1754
|
+
`nightralph: ${ticket.filename} done`
|
|
1755
|
+
);
|
|
1756
|
+
const worktreePath = worktreePathsByNum.get(
|
|
1757
|
+
ticket.num
|
|
1758
|
+
);
|
|
1759
|
+
if (worktreePath) {
|
|
1760
|
+
try {
|
|
1761
|
+
await removeWorktree(worktreePath);
|
|
1762
|
+
} catch (err) {
|
|
1763
|
+
d.log(
|
|
1764
|
+
` Failed to remove worktree ${worktreePath}: ` + formatErrorMessage(err)
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
} else {
|
|
1769
|
+
const worktreePath = worktreePathsByNum.get(
|
|
1770
|
+
ticket.num
|
|
1771
|
+
);
|
|
1772
|
+
const recovered = opts.conflictStrategy === "respawn" && worktreePath ? await respawnAndRetryMerge({
|
|
1773
|
+
repoRoot,
|
|
1774
|
+
baseBranch,
|
|
1775
|
+
branch: branchResult.branch,
|
|
1776
|
+
ticket,
|
|
1777
|
+
worktreePath,
|
|
1778
|
+
specBody: opts.specBody,
|
|
1779
|
+
agentCmd: opts.agentCmd,
|
|
1780
|
+
model: opts.model,
|
|
1781
|
+
timeout: opts.timeout,
|
|
1782
|
+
logsDir: opts.logsDir,
|
|
1783
|
+
onLine: /* @__PURE__ */ __name((msg) => d.log(msg), "onLine")
|
|
1784
|
+
}) : false;
|
|
1785
|
+
if (recovered) {
|
|
1786
|
+
markDone(ticket);
|
|
1787
|
+
waveCompleted++;
|
|
1788
|
+
totalCompleted++;
|
|
1789
|
+
d.setTicketStatus(ticket.num, "done");
|
|
1790
|
+
d.log(
|
|
1791
|
+
` ${ticket.filename}: done (resolved via respawn)`
|
|
1792
|
+
);
|
|
1793
|
+
checkTicket(progress, ticket.num, {
|
|
1794
|
+
branch: branchResult.branch,
|
|
1795
|
+
exitCode: 0
|
|
1796
|
+
});
|
|
1797
|
+
writeProgress(progressPath, progress);
|
|
1798
|
+
await commitProgress(
|
|
1799
|
+
repoRoot,
|
|
1800
|
+
progressPath,
|
|
1801
|
+
`nightralph: ${ticket.filename} done (respawn)`
|
|
1802
|
+
);
|
|
1803
|
+
if (worktreePath) {
|
|
1804
|
+
try {
|
|
1805
|
+
await removeWorktree(worktreePath);
|
|
1806
|
+
} catch (err) {
|
|
1807
|
+
d.log(
|
|
1808
|
+
` Failed to remove worktree ${worktreePath}: ` + formatErrorMessage(err)
|
|
1809
|
+
);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
} else {
|
|
1813
|
+
d.log(
|
|
1814
|
+
` ${ticket.filename}: merge failed, staying ready-for-agent`
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
if (mergeResult.stoppedAt) {
|
|
1820
|
+
d.log(
|
|
1821
|
+
`
|
|
1822
|
+
Merge stopped at ${mergeResult.stoppedAt.branch}`
|
|
1823
|
+
);
|
|
1824
|
+
for (const sb of successfulBranches) {
|
|
1825
|
+
const ticket = ticketsByNum.get(sb.ticketNum);
|
|
1826
|
+
if (ticket && ticket.status === "done") continue;
|
|
1827
|
+
try {
|
|
1828
|
+
await removeWorktree(sb.worktreePath);
|
|
1829
|
+
} catch (err) {
|
|
1830
|
+
d.log(
|
|
1831
|
+
` Failed to remove worktree ${sb.worktreePath}: ` + formatErrorMessage(err)
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
break;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
if (waveCompleted === 0) {
|
|
1839
|
+
d.log(
|
|
1840
|
+
"\nNo tickets completed this wave. Stopping."
|
|
1841
|
+
);
|
|
1842
|
+
break;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
const remaining = opts.tickets.filter(
|
|
1846
|
+
(t) => t.status === "ready-for-agent"
|
|
1847
|
+
);
|
|
1848
|
+
d.log(
|
|
1849
|
+
`
|
|
1850
|
+
Finished: ${totalCompleted} completed` + (remaining.length > 0 ? `, ${remaining.length} remaining` : "")
|
|
1851
|
+
);
|
|
1852
|
+
d.cleanup();
|
|
1853
|
+
return { allDone: remaining.length === 0 };
|
|
1854
|
+
}
|
|
1855
|
+
__name(runWavesIsolated, "runWavesIsolated");
|
|
1856
|
+
|
|
1857
|
+
// src/resolve.ts
|
|
1858
|
+
import { existsSync as existsSync3, readdirSync as readdirSync3 } from "node:fs";
|
|
1859
|
+
import { dirname as dirname2, join as join4 } from "node:path";
|
|
1860
|
+
import { fileURLToPath } from "node:url";
|
|
1861
|
+
import { select } from "@inquirer/prompts";
|
|
1862
|
+
function getScriptDir(metaUrl) {
|
|
1863
|
+
return dirname2(fileURLToPath(metaUrl));
|
|
1864
|
+
}
|
|
1865
|
+
__name(getScriptDir, "getScriptDir");
|
|
1866
|
+
function findFeatureDirs(cwd) {
|
|
1867
|
+
const scratchDir = join4(cwd, ".scratch");
|
|
1868
|
+
if (!existsSync3(scratchDir)) return [];
|
|
1869
|
+
const entries = readdirSync3(
|
|
1870
|
+
scratchDir,
|
|
1871
|
+
{ withFileTypes: true }
|
|
1872
|
+
);
|
|
1873
|
+
const results = [];
|
|
1874
|
+
for (const entry of entries) {
|
|
1875
|
+
if (!entry.isDirectory()) continue;
|
|
1876
|
+
const issuesDir = join4(
|
|
1877
|
+
scratchDir,
|
|
1878
|
+
entry.name,
|
|
1879
|
+
"issues"
|
|
1880
|
+
);
|
|
1881
|
+
const specFile = join4(
|
|
1882
|
+
scratchDir,
|
|
1883
|
+
entry.name,
|
|
1884
|
+
"spec.md"
|
|
1885
|
+
);
|
|
1886
|
+
if (existsSync3(issuesDir)) {
|
|
1887
|
+
results.push({
|
|
1888
|
+
name: entry.name,
|
|
1889
|
+
dir: issuesDir,
|
|
1890
|
+
spec: specFile
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
return results;
|
|
1895
|
+
}
|
|
1896
|
+
__name(findFeatureDirs, "findFeatureDirs");
|
|
1897
|
+
async function resolveIssuesDir(specName, cwd = process.cwd()) {
|
|
1898
|
+
if (specName) {
|
|
1899
|
+
const featureDir = join4(cwd, ".scratch", specName);
|
|
1900
|
+
return {
|
|
1901
|
+
dir: join4(featureDir, "issues"),
|
|
1902
|
+
spec: join4(featureDir, "spec.md")
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
const features = findFeatureDirs(cwd);
|
|
1906
|
+
if (features.length === 0) {
|
|
1907
|
+
console.error(
|
|
1908
|
+
"No .scratch/<feature>/issues/ directories found.\nProvide --spec <feature-name>."
|
|
1909
|
+
);
|
|
1910
|
+
process.exit(2);
|
|
1911
|
+
}
|
|
1912
|
+
let chosen;
|
|
1913
|
+
if (features.length === 1) {
|
|
1914
|
+
chosen = features[0];
|
|
1915
|
+
console.log(`Using feature: ${chosen.name}`);
|
|
1916
|
+
} else {
|
|
1917
|
+
const name = await select({
|
|
1918
|
+
message: "Which feature?",
|
|
1919
|
+
choices: features.map((f) => ({
|
|
1920
|
+
name: f.name,
|
|
1921
|
+
value: f.name
|
|
1922
|
+
}))
|
|
1923
|
+
});
|
|
1924
|
+
chosen = features.find((f) => f.name === name);
|
|
1925
|
+
}
|
|
1926
|
+
return { dir: chosen.dir, spec: chosen.spec };
|
|
1927
|
+
}
|
|
1928
|
+
__name(resolveIssuesDir, "resolveIssuesDir");
|
|
1929
|
+
|
|
1930
|
+
// src/index.ts
|
|
1931
|
+
var scriptDir = getScriptDir(import.meta.url);
|
|
25
1932
|
yargs(hideBin(process.argv)).scriptName("nightralph").usage("$0 <command>").command(
|
|
26
1933
|
"setup",
|
|
27
1934
|
"Install bundled skills and doc templates",
|
|
@@ -31,7 +1938,7 @@ yargs(hideBin(process.argv)).scriptName("nightralph").usage("$0 <command>").comm
|
|
|
31
1938
|
default: false
|
|
32
1939
|
}),
|
|
33
1940
|
async (argv) => {
|
|
34
|
-
const tracker = await
|
|
1941
|
+
const tracker = await select2({
|
|
35
1942
|
message: "Issue tracker format",
|
|
36
1943
|
choices: [
|
|
37
1944
|
{
|
|
@@ -55,8 +1962,8 @@ yargs(hideBin(process.argv)).scriptName("nightralph").usage("$0 <command>").comm
|
|
|
55
1962
|
}
|
|
56
1963
|
}
|
|
57
1964
|
setup({
|
|
58
|
-
bundledSkillsDir:
|
|
59
|
-
docsTemplatesDir:
|
|
1965
|
+
bundledSkillsDir: join5(scriptDir, "skills"),
|
|
1966
|
+
docsTemplatesDir: join5(
|
|
60
1967
|
scriptDir,
|
|
61
1968
|
"docs-templates"
|
|
62
1969
|
),
|
|
@@ -127,15 +2034,15 @@ async function runExecute(argv) {
|
|
|
127
2034
|
const { dir, spec } = await resolveIssuesDir(
|
|
128
2035
|
argv.spec
|
|
129
2036
|
);
|
|
130
|
-
if (!
|
|
2037
|
+
if (!existsSync4(dir)) {
|
|
131
2038
|
console.error(`Directory not found: ${dir}`);
|
|
132
2039
|
process.exit(2);
|
|
133
2040
|
}
|
|
134
|
-
if (!
|
|
2041
|
+
if (!existsSync4(spec)) {
|
|
135
2042
|
console.error(`Spec file not found: ${spec}`);
|
|
136
2043
|
process.exit(2);
|
|
137
2044
|
}
|
|
138
|
-
const specBody =
|
|
2045
|
+
const specBody = readFileSync4(spec, "utf8");
|
|
139
2046
|
const tickets = scanTickets(dir);
|
|
140
2047
|
if (tickets.length === 0) {
|
|
141
2048
|
console.log("No ticket files found. Exiting.");
|
|
@@ -156,12 +2063,12 @@ async function runExecute(argv) {
|
|
|
156
2063
|
(b) => knownNums.has(b)
|
|
157
2064
|
);
|
|
158
2065
|
}
|
|
159
|
-
const logsDir =
|
|
2066
|
+
const logsDir = join5(dirname3(dir), "logs");
|
|
160
2067
|
const timeout = argv.timeout ?? 300;
|
|
161
2068
|
const featureName = extractFeatureName(spec);
|
|
162
2069
|
const conflictStrategy = argv.stopOnConflict ? "stop" : "respawn";
|
|
163
|
-
const progressPath =
|
|
164
|
-
|
|
2070
|
+
const progressPath = join5(
|
|
2071
|
+
dirname3(dir),
|
|
165
2072
|
"progress.md"
|
|
166
2073
|
);
|
|
167
2074
|
if (argv.dryRun) {
|