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 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
- import { existsSync, readFileSync } from "node:fs";
5
- import { dirname, join } from "node:path";
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
- import { setup, checkGhAuth } from "./setup.js";
10
+ import { select as select2 } from "@inquirer/prompts";
11
+
12
+ // src/setup.ts
10
13
  import {
11
- scanTickets,
12
- dryRun,
13
- dryRunIsolated,
14
- runWaves,
15
- runWavesIsolated
16
- } from "./orchestrator.js";
17
- import { createDisplay } from "./display.js";
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
- findFeatureDirs,
20
- resolveIssuesDir,
21
- getScriptDir
22
- } from "./resolve.js";
23
- import { getRepoRoot, getRepoName } from "./worktree.js";
24
- const scriptDir = getScriptDir(import.meta.url);
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 select({
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: join(scriptDir, "skills"),
59
- docsTemplatesDir: join(
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 (!existsSync(dir)) {
2037
+ if (!existsSync4(dir)) {
131
2038
  console.error(`Directory not found: ${dir}`);
132
2039
  process.exit(2);
133
2040
  }
134
- if (!existsSync(spec)) {
2041
+ if (!existsSync4(spec)) {
135
2042
  console.error(`Spec file not found: ${spec}`);
136
2043
  process.exit(2);
137
2044
  }
138
- const specBody = readFileSync(spec, "utf8");
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 = join(dirname(dir), "logs");
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 = join(
164
- dirname(dir),
2070
+ const progressPath = join5(
2071
+ dirname3(dir),
165
2072
  "progress.md"
166
2073
  );
167
2074
  if (argv.dryRun) {