nightralph 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +106 -33
  2. package/dist/index.js +80 -22
  3. package/dist/index.js.map +2 -2
  4. package/dist/merge.js +194 -0
  5. package/dist/merge.js.map +7 -0
  6. package/dist/meta.json +183 -17
  7. package/dist/orchestrator.js +459 -44
  8. package/dist/orchestrator.js.map +2 -2
  9. package/dist/progress.js +164 -0
  10. package/dist/progress.js.map +7 -0
  11. package/dist/resolve.js +16 -8
  12. package/dist/resolve.js.map +2 -2
  13. package/dist/setup.js +33 -5
  14. package/dist/setup.js.map +2 -2
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/merge.d.ts +25 -0
  17. package/dist/src/merge.d.ts.map +1 -0
  18. package/dist/src/orchestrator.d.ts +27 -3
  19. package/dist/src/orchestrator.d.ts.map +1 -1
  20. package/dist/src/progress.d.ts +27 -0
  21. package/dist/src/progress.d.ts.map +1 -0
  22. package/dist/src/resolve.d.ts +2 -1
  23. package/dist/src/resolve.d.ts.map +1 -1
  24. package/dist/src/setup.d.ts +1 -0
  25. package/dist/src/setup.d.ts.map +1 -1
  26. package/dist/src/worktree.d.ts +18 -0
  27. package/dist/src/worktree.d.ts.map +1 -0
  28. package/dist/worktree.js +139 -0
  29. package/dist/worktree.js.map +7 -0
  30. package/package.json +8 -4
  31. package/.claude/settings.local.json +0 -5
  32. package/.github/dependabot.yml +0 -28
  33. package/.github/workflows/nodejs.yml +0 -30
  34. package/AGENTS.md +0 -60
  35. package/CLAUDE.md +0 -1
  36. package/PROMPT.md +0 -95
  37. package/WIP.md +0 -5
  38. package/dist/test/integration.test.js +0 -848
  39. package/dist/test/integration.test.js.map +0 -7
  40. package/dist/test/orchestrator.test.js +0 -749
  41. package/dist/test/orchestrator.test.js.map +0 -7
  42. package/dist/test/setup.test.js +0 -574
  43. package/dist/test/setup.test.js.map +0 -7
  44. package/docs/README.md +0 -12
  45. package/docs/agents/domain.md +0 -36
  46. package/docs/agents/issue-tracker.md +0 -30
  47. package/docs/agents/triage-labels.md +0 -15
  48. package/docs/implementation-plans/2026-08-27-issue-tracker-choice/phase-2-future-work.md +0 -144
  49. package/docs/implementation-plans/2026-08-27-issue-tracker-choice/phase_01.md +0 -292
  50. package/docs/implementation-plans/2026-08-27-issue-tracker-choice/phase_02.md +0 -212
  51. package/docs/implementation-plans/2026-08-27-issue-tracker-choice/test-requirements.md +0 -203
  52. package/docs/skills.md +0 -58
  53. package/docs/test-plans/2026-08-27-issue-tracker-choice.md +0 -95
  54. package/eslint.config.js +0 -142
  55. package/lib.es5.d.ts +0 -19
  56. package/progress.log +0 -190
  57. package/ralph_claude.sh +0 -263
  58. package/scripts/clone-skills.sh +0 -52
  59. package/specs/prd.json +0 -172
  60. package/src/docs-templates/domain.md +0 -36
  61. package/src/docs-templates/issue-tracker-github.md +0 -228
  62. package/src/docs-templates/issue-tracker.md +0 -30
  63. package/src/docs-templates/triage-labels.md +0 -15
  64. package/src/index.ts +0 -184
  65. package/src/orchestrator.ts +0 -324
  66. package/src/resolve.ts +0 -73
  67. package/src/setup.ts +0 -157
  68. package/src/skills/domain-modeling/ADR-FORMAT.md +0 -47
  69. package/src/skills/domain-modeling/CONTEXT-FORMAT.md +0 -60
  70. package/src/skills/domain-modeling/SKILL.md +0 -74
  71. package/src/skills/domain-modeling/agents/openai.yaml +0 -3
  72. package/src/skills/grill/SKILL.md +0 -7
  73. package/src/skills/grill/agents/openai.yaml +0 -5
  74. package/src/skills/grilling/SKILL.md +0 -28
  75. package/src/skills/grilling/agents/openai.yaml +0 -3
  76. package/src/skills/tdd/SKILL.md +0 -38
  77. package/src/skills/tdd/agents/openai.yaml +0 -3
  78. package/src/skills/tdd/mocking.md +0 -59
  79. package/src/skills/tdd/tests.md +0 -77
  80. package/src/skills/to-spec/SKILL.md +0 -75
  81. package/src/skills/to-spec/agents/openai.yaml +0 -5
  82. package/src/skills/to-tickets/SKILL.md +0 -105
  83. package/src/skills/to-tickets/agents/openai.yaml +0 -5
  84. package/src/skills/upstream.json +0 -5
  85. package/tasks/prd-integration-test-coverage.md +0 -243
  86. package/test/integration.test.ts +0 -636
  87. package/test/mock-agent-echo-args.js +0 -4
  88. package/test/mock-agent-fail.js +0 -3
  89. package/test/mock-agent-pass.js +0 -3
  90. package/test/orchestrator.test.ts +0 -609
  91. package/test/setup.test.ts +0 -417
  92. package/tsconfig.build.json +0 -7
  93. package/tsconfig.json +0 -31
@@ -8,7 +8,29 @@ import {
8
8
  writeFileSync,
9
9
  readdirSync
10
10
  } from "node:fs";
11
- import { join } from "node:path";
11
+ import { join, dirname, basename } from "node:path";
12
+ import {
13
+ getRepoRoot,
14
+ getRepoName,
15
+ getCurrentBranch,
16
+ createWorktree,
17
+ removeWorktree,
18
+ hasNewCommits,
19
+ isDirty
20
+ } from "./worktree.js";
21
+ import {
22
+ generateProgress,
23
+ readProgress,
24
+ mergeProgress,
25
+ writeProgress,
26
+ commitProgress,
27
+ checkTicket
28
+ } from "./progress.js";
29
+ import {
30
+ mergeBranch,
31
+ mergeWave,
32
+ updateWorktreeFromBase
33
+ } from "./merge.js";
12
34
  function formatErrorMessage(error) {
13
35
  return error instanceof Error ? error.message : String(error);
14
36
  }
@@ -63,7 +85,7 @@ function scanTickets(dir) {
63
85
  __name(scanTickets, "scanTickets");
64
86
  function findReadyTickets(tickets) {
65
87
  const doneNums = new Set(
66
- tickets.filter((t) => t.status !== "ready-for-agent").map((t) => t.num)
88
+ tickets.filter((t) => t.status === "done").map((t) => t.num)
67
89
  );
68
90
  return tickets.filter((t) => {
69
91
  if (t.status !== "ready-for-agent") return false;
@@ -96,8 +118,86 @@ function renderPrompt(specBody, ticket) {
96
118
  ].join("\n");
97
119
  }
98
120
  __name(renderPrompt, "renderPrompt");
121
+ function listTickets(tickets) {
122
+ console.log("\nTickets:");
123
+ for (const t of tickets) {
124
+ const blockerStr = t.blockers.length > 0 ? `blocked by [${t.blockers.join(", ")}]` : "no blockers";
125
+ console.log(
126
+ ` ${t.filename} (${t.status}) ${blockerStr}`
127
+ );
128
+ }
129
+ }
130
+ __name(listTickets, "listTickets");
131
+ function* simulateWaves(tickets) {
132
+ const sim = new Map(
133
+ tickets.map((t) => [t.num, t.status])
134
+ );
135
+ let wave = 1;
136
+ while (true) {
137
+ const doneNums = new Set(
138
+ [...sim.entries()].filter(([, s]) => s === "done").map(([n]) => n)
139
+ );
140
+ const ready = [...tickets].filter((t) => {
141
+ if (sim.get(t.num) !== "ready-for-agent") {
142
+ return false;
143
+ }
144
+ return t.blockers.every(
145
+ (b) => doneNums.has(b)
146
+ );
147
+ });
148
+ if (ready.length === 0) break;
149
+ yield { wave, ready };
150
+ for (const t of ready) {
151
+ sim.set(t.num, "done");
152
+ }
153
+ wave++;
154
+ }
155
+ }
156
+ __name(simulateWaves, "simulateWaves");
157
+ function detectDeadlocks(tickets) {
158
+ const doneNums = /* @__PURE__ */ new Set();
159
+ const ready = /* @__PURE__ */ new Set();
160
+ for (const t of tickets) {
161
+ if (t.status === "ready-for-agent") {
162
+ ready.add(t.num);
163
+ } else if (t.status === "done") {
164
+ doneNums.add(t.num);
165
+ }
166
+ }
167
+ let changed = true;
168
+ while (changed) {
169
+ changed = false;
170
+ for (const num of ready) {
171
+ const ticket = tickets.find(
172
+ (t) => t.num === num
173
+ );
174
+ if (ticket.blockers.every(
175
+ (b) => doneNums.has(b)
176
+ )) {
177
+ doneNums.add(num);
178
+ ready.delete(num);
179
+ changed = true;
180
+ }
181
+ }
182
+ }
183
+ return [...ready].map(
184
+ (num) => tickets.find((t) => t.num === num)
185
+ );
186
+ }
187
+ __name(detectDeadlocks, "detectDeadlocks");
188
+ const PROVIDER_ARGS = {
189
+ claude: ["--print", "--dangerously-skip-permissions"],
190
+ codex: ["exec", "--full-auto"]
191
+ };
192
+ function getProviderArgs(cmd) {
193
+ const name = basename(cmd);
194
+ return PROVIDER_ARGS[name] ?? [];
195
+ }
196
+ __name(getProviderArgs, "getProviderArgs");
99
197
  function spawnAgent(opts) {
100
- const args = [];
198
+ const args = [
199
+ ...getProviderArgs(opts.agentCmd)
200
+ ];
101
201
  if (opts.model) {
102
202
  args.push("--model", opts.model);
103
203
  }
@@ -106,18 +206,19 @@ function spawnAgent(opts) {
106
206
  { flags: "w" }
107
207
  );
108
208
  const proc = spawn(opts.agentCmd, args, {
109
- stdio: ["pipe", "pipe", "pipe"]
209
+ stdio: ["pipe", "pipe", "pipe"],
210
+ cwd: opts.cwd
110
211
  });
111
212
  proc.stdout.pipe(logStream);
112
213
  proc.stderr.pipe(logStream);
113
214
  const timeout = setTimeout(() => {
114
215
  if (!proc.killed) {
115
216
  console.warn(
116
- `Agent did not exit in ${opts.timeoutSecs}s; killing.`
217
+ `Agent did not exit in ${opts.timeout}s; killing.`
117
218
  );
118
219
  proc.kill("SIGKILL");
119
220
  }
120
- }, opts.timeoutSecs * 1e3);
221
+ }, opts.timeout * 1e3);
121
222
  proc.stdin.write(opts.prompt);
122
223
  proc.stdin.end();
123
224
  return new Promise((resolve) => {
@@ -139,48 +240,56 @@ function spawnAgent(opts) {
139
240
  }
140
241
  __name(spawnAgent, "spawnAgent");
141
242
  function dryRun(tickets, specBody) {
142
- console.log("\nTickets:");
143
- for (const t of tickets) {
144
- const blockerStr = t.blockers.length > 0 ? `blocked by [${t.blockers.join(", ")}]` : "no blockers";
243
+ listTickets(tickets);
244
+ console.log("\nSimulated wave order:");
245
+ for (const { wave, ready } of simulateWaves(tickets)) {
145
246
  console.log(
146
- ` ${t.filename} (${t.status}) ${blockerStr}`
247
+ ` Wave ${wave}: ` + ready.map((t) => t.filename).join(", ")
147
248
  );
148
249
  }
149
- console.log("\nSimulated wave order:");
150
- const sim = new Map(
151
- tickets.map((t) => [t.num, t.status])
152
- );
153
- let wave = 1;
154
- while (true) {
155
- const doneNums = new Set(
156
- [...sim.entries()].filter(([, s]) => s !== "ready-for-agent").map(([n]) => n)
250
+ const stuck = detectDeadlocks(tickets);
251
+ if (stuck.length > 0) {
252
+ console.log(
253
+ "\nDeadlocked tickets: " + stuck.map((t) => t.filename).join(", ")
254
+ );
255
+ }
256
+ const firstReady = findReadyTickets(tickets);
257
+ if (firstReady.length > 0) {
258
+ console.log(
259
+ `
260
+ Prompt for first ready ticket (${firstReady[0].filename}):
261
+ `
157
262
  );
158
- const ready = tickets.filter((t) => {
159
- if (sim.get(t.num) !== "ready-for-agent") {
160
- return false;
161
- }
162
- return t.blockers.every(
163
- (b) => doneNums.has(b)
164
- );
165
- });
166
- if (ready.length === 0) break;
263
+ console.log(
264
+ renderPrompt(specBody, firstReady[0])
265
+ );
266
+ }
267
+ }
268
+ __name(dryRun, "dryRun");
269
+ function dryRunIsolated(opts) {
270
+ listTickets(opts.tickets);
271
+ console.log("\nSimulated wave order:");
272
+ for (const { wave, ready } of simulateWaves(
273
+ opts.tickets
274
+ )) {
167
275
  console.log(
168
276
  ` Wave ${wave}: ` + ready.map((t) => t.filename).join(", ")
169
277
  );
170
278
  for (const t of ready) {
171
- sim.set(t.num, "done");
279
+ const num = String(t.num).padStart(2, "0");
280
+ const branchName = `${opts.repoName}/${num}-${t.slug}`;
281
+ const worktreePath = `../${opts.repoName}.${num}-${t.slug}`;
282
+ console.log(` worktree: ${worktreePath}`);
283
+ console.log(` branch: ${branchName}`);
172
284
  }
173
- wave++;
174
285
  }
175
- const stuck = tickets.filter(
176
- (t) => sim.get(t.num) === "ready-for-agent"
177
- );
286
+ const stuck = detectDeadlocks(opts.tickets);
178
287
  if (stuck.length > 0) {
179
288
  console.log(
180
289
  "\nDeadlocked tickets: " + stuck.map((t) => t.filename).join(", ")
181
290
  );
182
291
  }
183
- const firstReady = findReadyTickets(tickets);
292
+ const firstReady = findReadyTickets(opts.tickets);
184
293
  if (firstReady.length > 0) {
185
294
  console.log(
186
295
  `
@@ -188,12 +297,57 @@ Prompt for first ready ticket (${firstReady[0].filename}):
188
297
  `
189
298
  );
190
299
  console.log(
191
- renderPrompt(specBody, firstReady[0])
300
+ renderPrompt(opts.specBody, firstReady[0])
192
301
  );
193
302
  }
303
+ const strategyDesc = opts.conflictStrategy === "respawn" ? "respawn (re-run agent on conflict)" : "stop (stop merging on conflict)";
304
+ console.log(
305
+ `
306
+ Merge strategy: ${strategyDesc}`
307
+ );
308
+ console.log(`Progress file: ${opts.progressPath}`);
194
309
  }
195
- __name(dryRun, "dryRun");
196
- async function runWaves(tickets, specBody, agentCmd, model, timeoutSecs, logsDir) {
310
+ __name(dryRunIsolated, "dryRunIsolated");
311
+ async function respawnAndRetryMerge(opts) {
312
+ await updateWorktreeFromBase(
313
+ opts.worktreePath,
314
+ opts.baseBranch
315
+ );
316
+ console.log(
317
+ ` ${opts.ticket.filename}: respawning agent to resolve merge conflict`
318
+ );
319
+ const prompt = renderPrompt(opts.specBody, opts.ticket);
320
+ const logPath = join(
321
+ opts.logsDir,
322
+ opts.ticket.filename.replace(/\.md$/, ".respawn.log")
323
+ );
324
+ const exitCode = await spawnAgent({
325
+ prompt,
326
+ agentCmd: opts.agentCmd,
327
+ model: opts.model,
328
+ timeout: opts.timeout,
329
+ logPath,
330
+ cwd: opts.worktreePath
331
+ });
332
+ if (exitCode !== 0) {
333
+ console.log(
334
+ ` ${opts.ticket.filename}: respawned agent exited ${exitCode}`
335
+ );
336
+ return false;
337
+ }
338
+ const merged = await mergeBranch(
339
+ opts.repoRoot,
340
+ opts.branch
341
+ );
342
+ if (!merged) {
343
+ console.log(
344
+ ` ${opts.ticket.filename}: merge still conflicted after respawn`
345
+ );
346
+ }
347
+ return merged;
348
+ }
349
+ __name(respawnAndRetryMerge, "respawnAndRetryMerge");
350
+ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir) {
197
351
  mkdirSync(logsDir, { recursive: true });
198
352
  let totalCompleted = 0;
199
353
  let waveNum = 0;
@@ -217,7 +371,7 @@ Wave ${waveNum}: ` + ready.map((t) => t.filename).join(", ")
217
371
  prompt,
218
372
  agentCmd,
219
373
  model,
220
- timeoutSecs,
374
+ timeout,
221
375
  logPath
222
376
  });
223
377
  return { ticket: t, exitCode: code };
@@ -227,17 +381,274 @@ Wave ${waveNum}: ` + ready.map((t) => t.filename).join(", ")
227
381
  for (const r of results) {
228
382
  if (r.status === "rejected") continue;
229
383
  const { ticket, exitCode } = r.value;
230
- if (exitCode === 0) {
231
- markDone(ticket);
232
- waveCompleted++;
233
- totalCompleted++;
384
+ if (exitCode !== 0) {
385
+ console.log(
386
+ ` ${ticket.filename}: agent exited ${exitCode}`
387
+ );
388
+ continue;
389
+ }
390
+ markDone(ticket);
391
+ waveCompleted++;
392
+ totalCompleted++;
393
+ console.log(
394
+ ` ${ticket.filename}: done`
395
+ );
396
+ }
397
+ if (waveCompleted === 0) {
398
+ console.log(
399
+ "\nNo tickets completed this wave. Stopping."
400
+ );
401
+ break;
402
+ }
403
+ }
404
+ const remaining = tickets.filter(
405
+ (t) => t.status === "ready-for-agent"
406
+ );
407
+ console.log(
408
+ `
409
+ Finished: ${totalCompleted} completed` + (remaining.length > 0 ? `, ${remaining.length} remaining` : "")
410
+ );
411
+ return { allDone: remaining.length === 0 };
412
+ }
413
+ __name(runWaves, "runWaves");
414
+ async function runWavesIsolated(opts) {
415
+ mkdirSync(opts.logsDir, { recursive: true });
416
+ const repoRoot = getRepoRoot();
417
+ const repoName = getRepoName();
418
+ const baseBranch = getCurrentBranch();
419
+ const progressPath = join(
420
+ dirname(opts.logsDir),
421
+ "progress.md"
422
+ );
423
+ const progress = mergeProgress(
424
+ generateProgress(opts.featureName, opts.tickets),
425
+ readProgress(progressPath)
426
+ );
427
+ writeProgress(progressPath, progress);
428
+ await commitProgress(
429
+ repoRoot,
430
+ progressPath,
431
+ `nightralph: start ${opts.featureName}`
432
+ );
433
+ let totalCompleted = 0;
434
+ let waveNum = 0;
435
+ while (true) {
436
+ const ready = findReadyTickets(opts.tickets);
437
+ if (ready.length === 0) break;
438
+ waveNum++;
439
+ console.log(
440
+ `
441
+ Wave ${waveNum}: ` + ready.map((t) => t.filename).join(", ")
442
+ );
443
+ const worktreeSettled = await Promise.allSettled(
444
+ ready.map((t) => createWorktree({
445
+ repoRoot,
446
+ repoName,
447
+ baseBranch,
448
+ ticket: t
449
+ }))
450
+ );
451
+ const activeTickets = [];
452
+ const worktrees = [];
453
+ for (let i = 0; i < worktreeSettled.length; i++) {
454
+ const settled = worktreeSettled[i];
455
+ const ticket = ready[i];
456
+ if (settled.status === "rejected") {
234
457
  console.log(
235
- ` ${ticket.filename}: done`
458
+ ` ${ticket.filename}: failed to create worktree: ${formatErrorMessage(settled.reason)}`
236
459
  );
460
+ continue;
461
+ }
462
+ activeTickets.push(ticket);
463
+ worktrees.push(settled.value);
464
+ }
465
+ const results = await Promise.allSettled(
466
+ activeTickets.map((t, i) => {
467
+ const prompt = renderPrompt(opts.specBody, t);
468
+ const logPath = join(
469
+ opts.logsDir,
470
+ t.filename.replace(/\.md$/, ".log")
471
+ );
472
+ console.log(` Starting ${t.filename}`);
473
+ return spawnAgent({
474
+ prompt,
475
+ agentCmd: opts.agentCmd,
476
+ model: opts.model,
477
+ timeout: opts.timeout,
478
+ logPath,
479
+ cwd: worktrees[i].worktreePath
480
+ });
481
+ })
482
+ );
483
+ let waveCompleted = 0;
484
+ const successfulBranches = [];
485
+ const ticketsByNum = /* @__PURE__ */ new Map();
486
+ const worktreePathsByNum = /* @__PURE__ */ new Map();
487
+ for (let i = 0; i < results.length; i++) {
488
+ const r = results[i];
489
+ if (r.status === "rejected") continue;
490
+ const exitCode = r.value;
491
+ const ticket = activeTickets[i];
492
+ const worktree = worktrees[i];
493
+ if (exitCode === 0 && !hasNewCommits(
494
+ repoRoot,
495
+ baseBranch,
496
+ worktree.branchName
497
+ )) {
498
+ console.log(
499
+ ` ${ticket.filename}: agent exited 0 but made no commits; leaving ticket ready-for-agent`
500
+ );
501
+ } else if (exitCode === 0 && isDirty(
502
+ worktree.worktreePath
503
+ )) {
504
+ console.log(
505
+ ` ${ticket.filename}: agent left uncommitted changes in the worktree; leaving ticket ready-for-agent`
506
+ );
507
+ } else if (exitCode === 0) {
508
+ console.log(
509
+ ` ${ticket.filename}: agent finished, awaiting merge`
510
+ );
511
+ ticketsByNum.set(ticket.num, ticket);
512
+ worktreePathsByNum.set(
513
+ ticket.num,
514
+ worktree.worktreePath
515
+ );
516
+ successfulBranches.push({
517
+ branch: worktree.branchName,
518
+ ticketNum: ticket.num,
519
+ worktreePath: worktree.worktreePath
520
+ });
237
521
  } else {
238
522
  console.log(
239
523
  ` ${ticket.filename}: agent exited ${exitCode}`
240
524
  );
525
+ checkTicket(progress, ticket.num, {
526
+ branch: worktree.branchName,
527
+ exitCode,
528
+ done: false
529
+ });
530
+ writeProgress(progressPath, progress);
531
+ await commitProgress(
532
+ repoRoot,
533
+ progressPath,
534
+ `nightralph: ${ticket.filename} failed`
535
+ );
536
+ try {
537
+ await removeWorktree(worktree.worktreePath);
538
+ } catch (err) {
539
+ console.error(
540
+ ` Failed to remove worktree ${worktree.worktreePath}: ` + formatErrorMessage(err)
541
+ );
542
+ }
543
+ }
544
+ }
545
+ if (successfulBranches.length > 0) {
546
+ const mergeResult = await mergeWave({
547
+ repoRoot,
548
+ baseBranch,
549
+ branches: successfulBranches,
550
+ strategy: opts.conflictStrategy
551
+ });
552
+ for (const branchResult of mergeResult.merged) {
553
+ const ticket = ticketsByNum.get(
554
+ branchResult.ticketNum
555
+ );
556
+ if (!ticket) continue;
557
+ if (branchResult.merged) {
558
+ markDone(ticket);
559
+ waveCompleted++;
560
+ totalCompleted++;
561
+ console.log(
562
+ ` ${ticket.filename}: done`
563
+ );
564
+ checkTicket(progress, ticket.num, {
565
+ branch: branchResult.branch,
566
+ exitCode: 0
567
+ });
568
+ writeProgress(progressPath, progress);
569
+ await commitProgress(
570
+ repoRoot,
571
+ progressPath,
572
+ `nightralph: ${ticket.filename} done`
573
+ );
574
+ const worktreePath = worktreePathsByNum.get(
575
+ ticket.num
576
+ );
577
+ if (worktreePath) {
578
+ try {
579
+ await removeWorktree(worktreePath);
580
+ } catch (err) {
581
+ console.error(
582
+ ` Failed to remove worktree ${worktreePath}: ` + formatErrorMessage(err)
583
+ );
584
+ }
585
+ }
586
+ } else {
587
+ const worktreePath = worktreePathsByNum.get(
588
+ ticket.num
589
+ );
590
+ const recovered = opts.conflictStrategy === "respawn" && worktreePath ? await respawnAndRetryMerge({
591
+ repoRoot,
592
+ baseBranch,
593
+ branch: branchResult.branch,
594
+ ticket,
595
+ worktreePath,
596
+ specBody: opts.specBody,
597
+ agentCmd: opts.agentCmd,
598
+ model: opts.model,
599
+ timeout: opts.timeout,
600
+ logsDir: opts.logsDir
601
+ }) : false;
602
+ if (recovered) {
603
+ markDone(ticket);
604
+ waveCompleted++;
605
+ totalCompleted++;
606
+ console.log(
607
+ ` ${ticket.filename}: done (resolved via respawn)`
608
+ );
609
+ checkTicket(progress, ticket.num, {
610
+ branch: branchResult.branch,
611
+ exitCode: 0
612
+ });
613
+ writeProgress(progressPath, progress);
614
+ await commitProgress(
615
+ repoRoot,
616
+ progressPath,
617
+ `nightralph: ${ticket.filename} done (respawn)`
618
+ );
619
+ if (worktreePath) {
620
+ try {
621
+ await removeWorktree(worktreePath);
622
+ } catch (err) {
623
+ console.error(
624
+ ` Failed to remove worktree ${worktreePath}: ` + formatErrorMessage(err)
625
+ );
626
+ }
627
+ }
628
+ } else {
629
+ console.log(
630
+ ` ${ticket.filename}: merge failed, staying ready-for-agent`
631
+ );
632
+ }
633
+ }
634
+ }
635
+ if (mergeResult.stoppedAt) {
636
+ console.log(
637
+ `
638
+ Merge stopped at ${mergeResult.stoppedAt.branch}`
639
+ );
640
+ for (const sb of successfulBranches) {
641
+ const ticket = ticketsByNum.get(sb.ticketNum);
642
+ if (ticket && ticket.status === "done") continue;
643
+ try {
644
+ await removeWorktree(sb.worktreePath);
645
+ } catch (err) {
646
+ console.error(
647
+ ` Failed to remove worktree ${sb.worktreePath}: ` + formatErrorMessage(err)
648
+ );
649
+ }
650
+ }
651
+ break;
241
652
  }
242
653
  }
243
654
  if (waveCompleted === 0) {
@@ -247,26 +658,30 @@ Wave ${waveNum}: ` + ready.map((t) => t.filename).join(", ")
247
658
  break;
248
659
  }
249
660
  }
250
- const remaining = tickets.filter(
661
+ const remaining = opts.tickets.filter(
251
662
  (t) => t.status === "ready-for-agent"
252
663
  );
253
664
  console.log(
254
665
  `
255
666
  Finished: ${totalCompleted} completed` + (remaining.length > 0 ? `, ${remaining.length} remaining` : "")
256
667
  );
668
+ return { allDone: remaining.length === 0 };
257
669
  }
258
- __name(runWaves, "runWaves");
670
+ __name(runWavesIsolated, "runWavesIsolated");
259
671
  export {
260
672
  TICKET_RE,
261
673
  dryRun,
674
+ dryRunIsolated,
262
675
  findReadyTickets,
263
676
  formatErrorMessage,
677
+ getProviderArgs,
264
678
  markDone,
265
679
  parseBlockers,
266
680
  parseStatus,
267
681
  parseTicketFilename,
268
682
  renderPrompt,
269
683
  runWaves,
684
+ runWavesIsolated,
270
685
  scanTickets,
271
686
  spawnAgent
272
687
  };