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.
- package/README.md +106 -33
- package/dist/index.js +80 -22
- package/dist/index.js.map +2 -2
- package/dist/merge.js +194 -0
- package/dist/merge.js.map +7 -0
- package/dist/meta.json +183 -17
- package/dist/orchestrator.js +459 -44
- package/dist/orchestrator.js.map +2 -2
- package/dist/progress.js +164 -0
- package/dist/progress.js.map +7 -0
- package/dist/resolve.js +16 -8
- package/dist/resolve.js.map +2 -2
- package/dist/setup.js +33 -5
- package/dist/setup.js.map +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/merge.d.ts +25 -0
- package/dist/src/merge.d.ts.map +1 -0
- package/dist/src/orchestrator.d.ts +27 -3
- package/dist/src/orchestrator.d.ts.map +1 -1
- package/dist/src/progress.d.ts +27 -0
- package/dist/src/progress.d.ts.map +1 -0
- package/dist/src/resolve.d.ts +2 -1
- package/dist/src/resolve.d.ts.map +1 -1
- package/dist/src/setup.d.ts +1 -0
- package/dist/src/setup.d.ts.map +1 -1
- package/dist/src/worktree.d.ts +18 -0
- package/dist/src/worktree.d.ts.map +1 -0
- package/dist/worktree.js +139 -0
- package/dist/worktree.js.map +7 -0
- package/package.json +8 -4
- package/.claude/settings.local.json +0 -5
- package/.github/dependabot.yml +0 -28
- package/.github/workflows/nodejs.yml +0 -30
- package/AGENTS.md +0 -60
- package/CLAUDE.md +0 -1
- package/PROMPT.md +0 -95
- package/WIP.md +0 -5
- package/dist/test/integration.test.js +0 -848
- package/dist/test/integration.test.js.map +0 -7
- package/dist/test/orchestrator.test.js +0 -749
- package/dist/test/orchestrator.test.js.map +0 -7
- package/dist/test/setup.test.js +0 -574
- package/dist/test/setup.test.js.map +0 -7
- package/docs/README.md +0 -12
- package/docs/agents/domain.md +0 -36
- package/docs/agents/issue-tracker.md +0 -30
- package/docs/agents/triage-labels.md +0 -15
- package/docs/implementation-plans/2026-08-27-issue-tracker-choice/phase-2-future-work.md +0 -144
- package/docs/implementation-plans/2026-08-27-issue-tracker-choice/phase_01.md +0 -292
- package/docs/implementation-plans/2026-08-27-issue-tracker-choice/phase_02.md +0 -212
- package/docs/implementation-plans/2026-08-27-issue-tracker-choice/test-requirements.md +0 -203
- package/docs/skills.md +0 -58
- package/docs/test-plans/2026-08-27-issue-tracker-choice.md +0 -95
- package/eslint.config.js +0 -142
- package/lib.es5.d.ts +0 -19
- package/progress.log +0 -190
- package/ralph_claude.sh +0 -263
- package/scripts/clone-skills.sh +0 -52
- package/specs/prd.json +0 -172
- package/src/docs-templates/domain.md +0 -36
- package/src/docs-templates/issue-tracker-github.md +0 -228
- package/src/docs-templates/issue-tracker.md +0 -30
- package/src/docs-templates/triage-labels.md +0 -15
- package/src/index.ts +0 -184
- package/src/orchestrator.ts +0 -324
- package/src/resolve.ts +0 -73
- package/src/setup.ts +0 -157
- package/src/skills/domain-modeling/ADR-FORMAT.md +0 -47
- package/src/skills/domain-modeling/CONTEXT-FORMAT.md +0 -60
- package/src/skills/domain-modeling/SKILL.md +0 -74
- package/src/skills/domain-modeling/agents/openai.yaml +0 -3
- package/src/skills/grill/SKILL.md +0 -7
- package/src/skills/grill/agents/openai.yaml +0 -5
- package/src/skills/grilling/SKILL.md +0 -28
- package/src/skills/grilling/agents/openai.yaml +0 -3
- package/src/skills/tdd/SKILL.md +0 -38
- package/src/skills/tdd/agents/openai.yaml +0 -3
- package/src/skills/tdd/mocking.md +0 -59
- package/src/skills/tdd/tests.md +0 -77
- package/src/skills/to-spec/SKILL.md +0 -75
- package/src/skills/to-spec/agents/openai.yaml +0 -5
- package/src/skills/to-tickets/SKILL.md +0 -105
- package/src/skills/to-tickets/agents/openai.yaml +0 -5
- package/src/skills/upstream.json +0 -5
- package/tasks/prd-integration-test-coverage.md +0 -243
- package/test/integration.test.ts +0 -636
- package/test/mock-agent-echo-args.js +0 -4
- package/test/mock-agent-fail.js +0 -3
- package/test/mock-agent-pass.js +0 -3
- package/test/orchestrator.test.ts +0 -609
- package/test/setup.test.ts +0 -417
- package/tsconfig.build.json +0 -7
- package/tsconfig.json +0 -31
package/dist/orchestrator.js
CHANGED
|
@@ -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
|
|
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.
|
|
217
|
+
`Agent did not exit in ${opts.timeout}s; killing.`
|
|
117
218
|
);
|
|
118
219
|
proc.kill("SIGKILL");
|
|
119
220
|
}
|
|
120
|
-
}, opts.
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
243
|
+
listTickets(tickets);
|
|
244
|
+
console.log("\nSimulated wave order:");
|
|
245
|
+
for (const { wave, ready } of simulateWaves(tickets)) {
|
|
145
246
|
console.log(
|
|
146
|
-
` ${
|
|
247
|
+
` Wave ${wave}: ` + ready.map((t) => t.filename).join(", ")
|
|
147
248
|
);
|
|
148
249
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
196
|
-
async function
|
|
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
|
-
|
|
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
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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}:
|
|
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(
|
|
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
|
};
|