nightralph 0.0.2 → 0.0.4
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 +113 -34
- 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 +189 -17
- package/dist/orchestrator.js +538 -45
- 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 +29 -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,30 @@ 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 { createInterface } from "node:readline";
|
|
13
|
+
import {
|
|
14
|
+
getRepoRoot,
|
|
15
|
+
getRepoName,
|
|
16
|
+
getCurrentBranch,
|
|
17
|
+
createWorktree,
|
|
18
|
+
removeWorktree,
|
|
19
|
+
hasNewCommits,
|
|
20
|
+
isDirty
|
|
21
|
+
} from "./worktree.js";
|
|
22
|
+
import {
|
|
23
|
+
generateProgress,
|
|
24
|
+
readProgress,
|
|
25
|
+
mergeProgress,
|
|
26
|
+
writeProgress,
|
|
27
|
+
commitProgress,
|
|
28
|
+
checkTicket
|
|
29
|
+
} from "./progress.js";
|
|
30
|
+
import {
|
|
31
|
+
mergeBranch,
|
|
32
|
+
mergeWave,
|
|
33
|
+
updateWorktreeFromBase
|
|
34
|
+
} from "./merge.js";
|
|
12
35
|
function formatErrorMessage(error) {
|
|
13
36
|
return error instanceof Error ? error.message : String(error);
|
|
14
37
|
}
|
|
@@ -63,7 +86,7 @@ function scanTickets(dir) {
|
|
|
63
86
|
__name(scanTickets, "scanTickets");
|
|
64
87
|
function findReadyTickets(tickets) {
|
|
65
88
|
const doneNums = new Set(
|
|
66
|
-
tickets.filter((t) => t.status
|
|
89
|
+
tickets.filter((t) => t.status === "done").map((t) => t.num)
|
|
67
90
|
);
|
|
68
91
|
return tickets.filter((t) => {
|
|
69
92
|
if (t.status !== "ready-for-agent") return false;
|
|
@@ -96,38 +119,178 @@ function renderPrompt(specBody, ticket) {
|
|
|
96
119
|
].join("\n");
|
|
97
120
|
}
|
|
98
121
|
__name(renderPrompt, "renderPrompt");
|
|
122
|
+
function listTickets(tickets) {
|
|
123
|
+
console.log("\nTickets:");
|
|
124
|
+
for (const t of tickets) {
|
|
125
|
+
const blockerStr = t.blockers.length > 0 ? `blocked by [${t.blockers.join(", ")}]` : "no blockers";
|
|
126
|
+
console.log(
|
|
127
|
+
` ${t.filename} (${t.status}) ${blockerStr}`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
__name(listTickets, "listTickets");
|
|
132
|
+
function* simulateWaves(tickets) {
|
|
133
|
+
const sim = new Map(
|
|
134
|
+
tickets.map((t) => [t.num, t.status])
|
|
135
|
+
);
|
|
136
|
+
let wave = 1;
|
|
137
|
+
while (true) {
|
|
138
|
+
const doneNums = new Set(
|
|
139
|
+
[...sim.entries()].filter(([, s]) => s === "done").map(([n]) => n)
|
|
140
|
+
);
|
|
141
|
+
const ready = [...tickets].filter((t) => {
|
|
142
|
+
if (sim.get(t.num) !== "ready-for-agent") {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return t.blockers.every(
|
|
146
|
+
(b) => doneNums.has(b)
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
if (ready.length === 0) break;
|
|
150
|
+
yield { wave, ready };
|
|
151
|
+
for (const t of ready) {
|
|
152
|
+
sim.set(t.num, "done");
|
|
153
|
+
}
|
|
154
|
+
wave++;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
__name(simulateWaves, "simulateWaves");
|
|
158
|
+
function detectDeadlocks(tickets) {
|
|
159
|
+
const doneNums = /* @__PURE__ */ new Set();
|
|
160
|
+
const ready = /* @__PURE__ */ new Set();
|
|
161
|
+
for (const t of tickets) {
|
|
162
|
+
if (t.status === "ready-for-agent") {
|
|
163
|
+
ready.add(t.num);
|
|
164
|
+
} else if (t.status === "done") {
|
|
165
|
+
doneNums.add(t.num);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
let changed = true;
|
|
169
|
+
while (changed) {
|
|
170
|
+
changed = false;
|
|
171
|
+
for (const num of ready) {
|
|
172
|
+
const ticket = tickets.find(
|
|
173
|
+
(t) => t.num === num
|
|
174
|
+
);
|
|
175
|
+
if (ticket.blockers.every(
|
|
176
|
+
(b) => doneNums.has(b)
|
|
177
|
+
)) {
|
|
178
|
+
doneNums.add(num);
|
|
179
|
+
ready.delete(num);
|
|
180
|
+
changed = true;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return [...ready].map(
|
|
185
|
+
(num) => tickets.find((t) => t.num === num)
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
__name(detectDeadlocks, "detectDeadlocks");
|
|
189
|
+
const PROVIDER_ARGS = {
|
|
190
|
+
claude: [
|
|
191
|
+
"--print",
|
|
192
|
+
"--verbose",
|
|
193
|
+
"--output-format",
|
|
194
|
+
"stream-json",
|
|
195
|
+
"--dangerously-skip-permissions"
|
|
196
|
+
],
|
|
197
|
+
codex: ["exec", "--full-auto"]
|
|
198
|
+
};
|
|
199
|
+
function getProviderArgs(cmd) {
|
|
200
|
+
const name = basename(cmd);
|
|
201
|
+
return PROVIDER_ARGS[name] ?? [];
|
|
202
|
+
}
|
|
203
|
+
__name(getProviderArgs, "getProviderArgs");
|
|
204
|
+
function formatStreamLine(line) {
|
|
205
|
+
let obj;
|
|
206
|
+
try {
|
|
207
|
+
obj = JSON.parse(line);
|
|
208
|
+
} catch {
|
|
209
|
+
return line;
|
|
210
|
+
}
|
|
211
|
+
if (obj.type === "assistant") {
|
|
212
|
+
const msg = obj.message;
|
|
213
|
+
const content = msg?.content;
|
|
214
|
+
if (!content) return null;
|
|
215
|
+
for (const block of content) {
|
|
216
|
+
if (block.type === "text") {
|
|
217
|
+
return String(block.text);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
if (obj.type === "result") {
|
|
223
|
+
const sub = obj.subtype ?? "done";
|
|
224
|
+
const cost = obj.total_cost_usd;
|
|
225
|
+
const costStr = cost != null ? ` ($${cost.toFixed(2)})` : "";
|
|
226
|
+
return `${sub}${costStr}`;
|
|
227
|
+
}
|
|
228
|
+
if (obj.type === "system") {
|
|
229
|
+
const sub = obj.subtype;
|
|
230
|
+
if (sub === "api_retry") {
|
|
231
|
+
const error = obj.error;
|
|
232
|
+
return `retry: ${error ?? "unknown"}`;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
__name(formatStreamLine, "formatStreamLine");
|
|
99
238
|
function spawnAgent(opts) {
|
|
100
|
-
const args = [
|
|
239
|
+
const args = [
|
|
240
|
+
...getProviderArgs(opts.agentCmd)
|
|
241
|
+
];
|
|
101
242
|
if (opts.model) {
|
|
102
243
|
args.push("--model", opts.model);
|
|
103
244
|
}
|
|
245
|
+
const isClaude = basename(opts.agentCmd) === "claude";
|
|
246
|
+
const prefix = opts.label ? ` [${opts.label}] ` : " ";
|
|
104
247
|
const logStream = createWriteStream(
|
|
105
248
|
opts.logPath,
|
|
106
249
|
{ flags: "w" }
|
|
107
250
|
);
|
|
108
251
|
const proc = spawn(opts.agentCmd, args, {
|
|
109
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
252
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
253
|
+
cwd: opts.cwd
|
|
110
254
|
});
|
|
111
255
|
proc.stdout.pipe(logStream);
|
|
256
|
+
const rl = createInterface({ input: proc.stdout });
|
|
257
|
+
rl.on("line", (line) => {
|
|
258
|
+
if (isClaude) {
|
|
259
|
+
const formatted = formatStreamLine(line);
|
|
260
|
+
if (formatted !== null) {
|
|
261
|
+
console.log(`${prefix}${formatted}`);
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
console.log(`${prefix}${line}`);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
112
267
|
proc.stderr.pipe(logStream);
|
|
268
|
+
const rlErr = createInterface({ input: proc.stderr });
|
|
269
|
+
rlErr.on("line", (line) => {
|
|
270
|
+
console.error(`${prefix}${line}`);
|
|
271
|
+
});
|
|
113
272
|
const timeout = setTimeout(() => {
|
|
114
273
|
if (!proc.killed) {
|
|
115
274
|
console.warn(
|
|
116
|
-
`Agent did not exit in ${opts.
|
|
275
|
+
`Agent did not exit in ${opts.timeout}s; killing.`
|
|
117
276
|
);
|
|
118
277
|
proc.kill("SIGKILL");
|
|
119
278
|
}
|
|
120
|
-
}, opts.
|
|
279
|
+
}, opts.timeout * 1e3);
|
|
121
280
|
proc.stdin.write(opts.prompt);
|
|
122
281
|
proc.stdin.end();
|
|
123
282
|
return new Promise((resolve) => {
|
|
124
283
|
proc.on("close", (code) => {
|
|
125
284
|
clearTimeout(timeout);
|
|
285
|
+
rl.close();
|
|
286
|
+
rlErr.close();
|
|
126
287
|
logStream.end();
|
|
127
288
|
resolve(code ?? 1);
|
|
128
289
|
});
|
|
129
290
|
proc.on("error", (err) => {
|
|
130
291
|
clearTimeout(timeout);
|
|
292
|
+
rl.close();
|
|
293
|
+
rlErr.close();
|
|
131
294
|
logStream.end();
|
|
132
295
|
console.error(
|
|
133
296
|
"Agent process error:",
|
|
@@ -139,48 +302,56 @@ function spawnAgent(opts) {
|
|
|
139
302
|
}
|
|
140
303
|
__name(spawnAgent, "spawnAgent");
|
|
141
304
|
function dryRun(tickets, specBody) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
305
|
+
listTickets(tickets);
|
|
306
|
+
console.log("\nSimulated wave order:");
|
|
307
|
+
for (const { wave, ready } of simulateWaves(tickets)) {
|
|
145
308
|
console.log(
|
|
146
|
-
` ${
|
|
309
|
+
` Wave ${wave}: ` + ready.map((t) => t.filename).join(", ")
|
|
147
310
|
);
|
|
148
311
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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)
|
|
312
|
+
const stuck = detectDeadlocks(tickets);
|
|
313
|
+
if (stuck.length > 0) {
|
|
314
|
+
console.log(
|
|
315
|
+
"\nDeadlocked tickets: " + stuck.map((t) => t.filename).join(", ")
|
|
157
316
|
);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
317
|
+
}
|
|
318
|
+
const firstReady = findReadyTickets(tickets);
|
|
319
|
+
if (firstReady.length > 0) {
|
|
320
|
+
console.log(
|
|
321
|
+
`
|
|
322
|
+
Prompt for first ready ticket (${firstReady[0].filename}):
|
|
323
|
+
`
|
|
324
|
+
);
|
|
325
|
+
console.log(
|
|
326
|
+
renderPrompt(specBody, firstReady[0])
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
__name(dryRun, "dryRun");
|
|
331
|
+
function dryRunIsolated(opts) {
|
|
332
|
+
listTickets(opts.tickets);
|
|
333
|
+
console.log("\nSimulated wave order:");
|
|
334
|
+
for (const { wave, ready } of simulateWaves(
|
|
335
|
+
opts.tickets
|
|
336
|
+
)) {
|
|
167
337
|
console.log(
|
|
168
338
|
` Wave ${wave}: ` + ready.map((t) => t.filename).join(", ")
|
|
169
339
|
);
|
|
170
340
|
for (const t of ready) {
|
|
171
|
-
|
|
341
|
+
const num = String(t.num).padStart(2, "0");
|
|
342
|
+
const branchName = `${opts.repoName}/${num}-${t.slug}`;
|
|
343
|
+
const worktreePath = `../${opts.repoName}.${num}-${t.slug}`;
|
|
344
|
+
console.log(` worktree: ${worktreePath}`);
|
|
345
|
+
console.log(` branch: ${branchName}`);
|
|
172
346
|
}
|
|
173
|
-
wave++;
|
|
174
347
|
}
|
|
175
|
-
const stuck = tickets
|
|
176
|
-
(t) => sim.get(t.num) === "ready-for-agent"
|
|
177
|
-
);
|
|
348
|
+
const stuck = detectDeadlocks(opts.tickets);
|
|
178
349
|
if (stuck.length > 0) {
|
|
179
350
|
console.log(
|
|
180
351
|
"\nDeadlocked tickets: " + stuck.map((t) => t.filename).join(", ")
|
|
181
352
|
);
|
|
182
353
|
}
|
|
183
|
-
const firstReady = findReadyTickets(tickets);
|
|
354
|
+
const firstReady = findReadyTickets(opts.tickets);
|
|
184
355
|
if (firstReady.length > 0) {
|
|
185
356
|
console.log(
|
|
186
357
|
`
|
|
@@ -188,12 +359,62 @@ Prompt for first ready ticket (${firstReady[0].filename}):
|
|
|
188
359
|
`
|
|
189
360
|
);
|
|
190
361
|
console.log(
|
|
191
|
-
renderPrompt(specBody, firstReady[0])
|
|
362
|
+
renderPrompt(opts.specBody, firstReady[0])
|
|
192
363
|
);
|
|
193
364
|
}
|
|
365
|
+
const strategyDesc = opts.conflictStrategy === "respawn" ? "respawn (re-run agent on conflict)" : "stop (stop merging on conflict)";
|
|
366
|
+
console.log(
|
|
367
|
+
`
|
|
368
|
+
Merge strategy: ${strategyDesc}`
|
|
369
|
+
);
|
|
370
|
+
console.log(`Progress file: ${opts.progressPath}`);
|
|
194
371
|
}
|
|
195
|
-
__name(
|
|
196
|
-
async function
|
|
372
|
+
__name(dryRunIsolated, "dryRunIsolated");
|
|
373
|
+
async function respawnAndRetryMerge(opts) {
|
|
374
|
+
await updateWorktreeFromBase(
|
|
375
|
+
opts.worktreePath,
|
|
376
|
+
opts.baseBranch
|
|
377
|
+
);
|
|
378
|
+
console.log(
|
|
379
|
+
` ${opts.ticket.filename}: respawning agent to resolve merge conflict`
|
|
380
|
+
);
|
|
381
|
+
const prompt = renderPrompt(opts.specBody, opts.ticket);
|
|
382
|
+
const logPath = join(
|
|
383
|
+
opts.logsDir,
|
|
384
|
+
opts.ticket.filename.replace(/\.md$/, ".respawn.log")
|
|
385
|
+
);
|
|
386
|
+
const label = opts.ticket.filename.replace(
|
|
387
|
+
/\.md$/,
|
|
388
|
+
""
|
|
389
|
+
);
|
|
390
|
+
const exitCode = await spawnAgent({
|
|
391
|
+
prompt,
|
|
392
|
+
agentCmd: opts.agentCmd,
|
|
393
|
+
model: opts.model,
|
|
394
|
+
timeout: opts.timeout,
|
|
395
|
+
logPath,
|
|
396
|
+
cwd: opts.worktreePath,
|
|
397
|
+
label
|
|
398
|
+
});
|
|
399
|
+
if (exitCode !== 0) {
|
|
400
|
+
console.log(
|
|
401
|
+
` ${opts.ticket.filename}: respawned agent exited ${exitCode}`
|
|
402
|
+
);
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
const merged = await mergeBranch(
|
|
406
|
+
opts.repoRoot,
|
|
407
|
+
opts.branch
|
|
408
|
+
);
|
|
409
|
+
if (!merged) {
|
|
410
|
+
console.log(
|
|
411
|
+
` ${opts.ticket.filename}: merge still conflicted after respawn`
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
return merged;
|
|
415
|
+
}
|
|
416
|
+
__name(respawnAndRetryMerge, "respawnAndRetryMerge");
|
|
417
|
+
async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir) {
|
|
197
418
|
mkdirSync(logsDir, { recursive: true });
|
|
198
419
|
let totalCompleted = 0;
|
|
199
420
|
let waveNum = 0;
|
|
@@ -212,13 +433,18 @@ Wave ${waveNum}: ` + ready.map((t) => t.filename).join(", ")
|
|
|
212
433
|
logsDir,
|
|
213
434
|
t.filename.replace(/\.md$/, ".log")
|
|
214
435
|
);
|
|
436
|
+
const label = t.filename.replace(
|
|
437
|
+
/\.md$/,
|
|
438
|
+
""
|
|
439
|
+
);
|
|
215
440
|
console.log(` Starting ${t.filename}`);
|
|
216
441
|
const code = await spawnAgent({
|
|
217
442
|
prompt,
|
|
218
443
|
agentCmd,
|
|
219
444
|
model,
|
|
220
|
-
|
|
221
|
-
logPath
|
|
445
|
+
timeout,
|
|
446
|
+
logPath,
|
|
447
|
+
label
|
|
222
448
|
});
|
|
223
449
|
return { ticket: t, exitCode: code };
|
|
224
450
|
})
|
|
@@ -227,17 +453,279 @@ Wave ${waveNum}: ` + ready.map((t) => t.filename).join(", ")
|
|
|
227
453
|
for (const r of results) {
|
|
228
454
|
if (r.status === "rejected") continue;
|
|
229
455
|
const { ticket, exitCode } = r.value;
|
|
230
|
-
if (exitCode
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
456
|
+
if (exitCode !== 0) {
|
|
457
|
+
console.log(
|
|
458
|
+
` ${ticket.filename}: agent exited ${exitCode}`
|
|
459
|
+
);
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
markDone(ticket);
|
|
463
|
+
waveCompleted++;
|
|
464
|
+
totalCompleted++;
|
|
465
|
+
console.log(
|
|
466
|
+
` ${ticket.filename}: done`
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
if (waveCompleted === 0) {
|
|
470
|
+
console.log(
|
|
471
|
+
"\nNo tickets completed this wave. Stopping."
|
|
472
|
+
);
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
const remaining = tickets.filter(
|
|
477
|
+
(t) => t.status === "ready-for-agent"
|
|
478
|
+
);
|
|
479
|
+
console.log(
|
|
480
|
+
`
|
|
481
|
+
Finished: ${totalCompleted} completed` + (remaining.length > 0 ? `, ${remaining.length} remaining` : "")
|
|
482
|
+
);
|
|
483
|
+
return { allDone: remaining.length === 0 };
|
|
484
|
+
}
|
|
485
|
+
__name(runWaves, "runWaves");
|
|
486
|
+
async function runWavesIsolated(opts) {
|
|
487
|
+
mkdirSync(opts.logsDir, { recursive: true });
|
|
488
|
+
const repoRoot = getRepoRoot();
|
|
489
|
+
const repoName = getRepoName();
|
|
490
|
+
const baseBranch = getCurrentBranch();
|
|
491
|
+
const progressPath = join(
|
|
492
|
+
dirname(opts.logsDir),
|
|
493
|
+
"progress.md"
|
|
494
|
+
);
|
|
495
|
+
const progress = mergeProgress(
|
|
496
|
+
generateProgress(opts.featureName, opts.tickets),
|
|
497
|
+
readProgress(progressPath)
|
|
498
|
+
);
|
|
499
|
+
writeProgress(progressPath, progress);
|
|
500
|
+
await commitProgress(
|
|
501
|
+
repoRoot,
|
|
502
|
+
progressPath,
|
|
503
|
+
`nightralph: start ${opts.featureName}`
|
|
504
|
+
);
|
|
505
|
+
let totalCompleted = 0;
|
|
506
|
+
let waveNum = 0;
|
|
507
|
+
while (true) {
|
|
508
|
+
const ready = findReadyTickets(opts.tickets);
|
|
509
|
+
if (ready.length === 0) break;
|
|
510
|
+
waveNum++;
|
|
511
|
+
console.log(
|
|
512
|
+
`
|
|
513
|
+
Wave ${waveNum}: ` + ready.map((t) => t.filename).join(", ")
|
|
514
|
+
);
|
|
515
|
+
const worktreeSettled = await Promise.allSettled(
|
|
516
|
+
ready.map((t) => createWorktree({
|
|
517
|
+
repoRoot,
|
|
518
|
+
repoName,
|
|
519
|
+
baseBranch,
|
|
520
|
+
ticket: t
|
|
521
|
+
}))
|
|
522
|
+
);
|
|
523
|
+
const activeTickets = [];
|
|
524
|
+
const worktrees = [];
|
|
525
|
+
for (let i = 0; i < worktreeSettled.length; i++) {
|
|
526
|
+
const settled = worktreeSettled[i];
|
|
527
|
+
const ticket = ready[i];
|
|
528
|
+
if (settled.status === "rejected") {
|
|
234
529
|
console.log(
|
|
235
|
-
` ${ticket.filename}:
|
|
530
|
+
` ${ticket.filename}: failed to create worktree: ${formatErrorMessage(settled.reason)}`
|
|
531
|
+
);
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
activeTickets.push(ticket);
|
|
535
|
+
worktrees.push(settled.value);
|
|
536
|
+
}
|
|
537
|
+
const results = await Promise.allSettled(
|
|
538
|
+
activeTickets.map((t, i) => {
|
|
539
|
+
const prompt = renderPrompt(opts.specBody, t);
|
|
540
|
+
const logPath = join(
|
|
541
|
+
opts.logsDir,
|
|
542
|
+
t.filename.replace(/\.md$/, ".log")
|
|
236
543
|
);
|
|
544
|
+
const label = t.filename.replace(
|
|
545
|
+
/\.md$/,
|
|
546
|
+
""
|
|
547
|
+
);
|
|
548
|
+
console.log(` Starting ${t.filename}`);
|
|
549
|
+
return spawnAgent({
|
|
550
|
+
prompt,
|
|
551
|
+
agentCmd: opts.agentCmd,
|
|
552
|
+
model: opts.model,
|
|
553
|
+
timeout: opts.timeout,
|
|
554
|
+
logPath,
|
|
555
|
+
cwd: worktrees[i].worktreePath,
|
|
556
|
+
label
|
|
557
|
+
});
|
|
558
|
+
})
|
|
559
|
+
);
|
|
560
|
+
let waveCompleted = 0;
|
|
561
|
+
const successfulBranches = [];
|
|
562
|
+
const ticketsByNum = /* @__PURE__ */ new Map();
|
|
563
|
+
const worktreePathsByNum = /* @__PURE__ */ new Map();
|
|
564
|
+
for (let i = 0; i < results.length; i++) {
|
|
565
|
+
const r = results[i];
|
|
566
|
+
if (r.status === "rejected") continue;
|
|
567
|
+
const exitCode = r.value;
|
|
568
|
+
const ticket = activeTickets[i];
|
|
569
|
+
const worktree = worktrees[i];
|
|
570
|
+
if (exitCode === 0 && !hasNewCommits(
|
|
571
|
+
repoRoot,
|
|
572
|
+
baseBranch,
|
|
573
|
+
worktree.branchName
|
|
574
|
+
)) {
|
|
575
|
+
console.log(
|
|
576
|
+
` ${ticket.filename}: agent exited 0 but made no commits; leaving ticket ready-for-agent`
|
|
577
|
+
);
|
|
578
|
+
} else if (exitCode === 0 && isDirty(
|
|
579
|
+
worktree.worktreePath
|
|
580
|
+
)) {
|
|
581
|
+
console.log(
|
|
582
|
+
` ${ticket.filename}: agent left uncommitted changes in the worktree; leaving ticket ready-for-agent`
|
|
583
|
+
);
|
|
584
|
+
} else if (exitCode === 0) {
|
|
585
|
+
console.log(
|
|
586
|
+
` ${ticket.filename}: agent finished, awaiting merge`
|
|
587
|
+
);
|
|
588
|
+
ticketsByNum.set(ticket.num, ticket);
|
|
589
|
+
worktreePathsByNum.set(
|
|
590
|
+
ticket.num,
|
|
591
|
+
worktree.worktreePath
|
|
592
|
+
);
|
|
593
|
+
successfulBranches.push({
|
|
594
|
+
branch: worktree.branchName,
|
|
595
|
+
ticketNum: ticket.num,
|
|
596
|
+
worktreePath: worktree.worktreePath
|
|
597
|
+
});
|
|
237
598
|
} else {
|
|
238
599
|
console.log(
|
|
239
600
|
` ${ticket.filename}: agent exited ${exitCode}`
|
|
240
601
|
);
|
|
602
|
+
checkTicket(progress, ticket.num, {
|
|
603
|
+
branch: worktree.branchName,
|
|
604
|
+
exitCode,
|
|
605
|
+
done: false
|
|
606
|
+
});
|
|
607
|
+
writeProgress(progressPath, progress);
|
|
608
|
+
await commitProgress(
|
|
609
|
+
repoRoot,
|
|
610
|
+
progressPath,
|
|
611
|
+
`nightralph: ${ticket.filename} failed`
|
|
612
|
+
);
|
|
613
|
+
try {
|
|
614
|
+
await removeWorktree(worktree.worktreePath);
|
|
615
|
+
} catch (err) {
|
|
616
|
+
console.error(
|
|
617
|
+
` Failed to remove worktree ${worktree.worktreePath}: ` + formatErrorMessage(err)
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (successfulBranches.length > 0) {
|
|
623
|
+
const mergeResult = await mergeWave({
|
|
624
|
+
repoRoot,
|
|
625
|
+
baseBranch,
|
|
626
|
+
branches: successfulBranches,
|
|
627
|
+
strategy: opts.conflictStrategy
|
|
628
|
+
});
|
|
629
|
+
for (const branchResult of mergeResult.merged) {
|
|
630
|
+
const ticket = ticketsByNum.get(
|
|
631
|
+
branchResult.ticketNum
|
|
632
|
+
);
|
|
633
|
+
if (!ticket) continue;
|
|
634
|
+
if (branchResult.merged) {
|
|
635
|
+
markDone(ticket);
|
|
636
|
+
waveCompleted++;
|
|
637
|
+
totalCompleted++;
|
|
638
|
+
console.log(
|
|
639
|
+
` ${ticket.filename}: done`
|
|
640
|
+
);
|
|
641
|
+
checkTicket(progress, ticket.num, {
|
|
642
|
+
branch: branchResult.branch,
|
|
643
|
+
exitCode: 0
|
|
644
|
+
});
|
|
645
|
+
writeProgress(progressPath, progress);
|
|
646
|
+
await commitProgress(
|
|
647
|
+
repoRoot,
|
|
648
|
+
progressPath,
|
|
649
|
+
`nightralph: ${ticket.filename} done`
|
|
650
|
+
);
|
|
651
|
+
const worktreePath = worktreePathsByNum.get(
|
|
652
|
+
ticket.num
|
|
653
|
+
);
|
|
654
|
+
if (worktreePath) {
|
|
655
|
+
try {
|
|
656
|
+
await removeWorktree(worktreePath);
|
|
657
|
+
} catch (err) {
|
|
658
|
+
console.error(
|
|
659
|
+
` Failed to remove worktree ${worktreePath}: ` + formatErrorMessage(err)
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
} else {
|
|
664
|
+
const worktreePath = worktreePathsByNum.get(
|
|
665
|
+
ticket.num
|
|
666
|
+
);
|
|
667
|
+
const recovered = opts.conflictStrategy === "respawn" && worktreePath ? await respawnAndRetryMerge({
|
|
668
|
+
repoRoot,
|
|
669
|
+
baseBranch,
|
|
670
|
+
branch: branchResult.branch,
|
|
671
|
+
ticket,
|
|
672
|
+
worktreePath,
|
|
673
|
+
specBody: opts.specBody,
|
|
674
|
+
agentCmd: opts.agentCmd,
|
|
675
|
+
model: opts.model,
|
|
676
|
+
timeout: opts.timeout,
|
|
677
|
+
logsDir: opts.logsDir
|
|
678
|
+
}) : false;
|
|
679
|
+
if (recovered) {
|
|
680
|
+
markDone(ticket);
|
|
681
|
+
waveCompleted++;
|
|
682
|
+
totalCompleted++;
|
|
683
|
+
console.log(
|
|
684
|
+
` ${ticket.filename}: done (resolved via respawn)`
|
|
685
|
+
);
|
|
686
|
+
checkTicket(progress, ticket.num, {
|
|
687
|
+
branch: branchResult.branch,
|
|
688
|
+
exitCode: 0
|
|
689
|
+
});
|
|
690
|
+
writeProgress(progressPath, progress);
|
|
691
|
+
await commitProgress(
|
|
692
|
+
repoRoot,
|
|
693
|
+
progressPath,
|
|
694
|
+
`nightralph: ${ticket.filename} done (respawn)`
|
|
695
|
+
);
|
|
696
|
+
if (worktreePath) {
|
|
697
|
+
try {
|
|
698
|
+
await removeWorktree(worktreePath);
|
|
699
|
+
} catch (err) {
|
|
700
|
+
console.error(
|
|
701
|
+
` Failed to remove worktree ${worktreePath}: ` + formatErrorMessage(err)
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
} else {
|
|
706
|
+
console.log(
|
|
707
|
+
` ${ticket.filename}: merge failed, staying ready-for-agent`
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
if (mergeResult.stoppedAt) {
|
|
713
|
+
console.log(
|
|
714
|
+
`
|
|
715
|
+
Merge stopped at ${mergeResult.stoppedAt.branch}`
|
|
716
|
+
);
|
|
717
|
+
for (const sb of successfulBranches) {
|
|
718
|
+
const ticket = ticketsByNum.get(sb.ticketNum);
|
|
719
|
+
if (ticket && ticket.status === "done") continue;
|
|
720
|
+
try {
|
|
721
|
+
await removeWorktree(sb.worktreePath);
|
|
722
|
+
} catch (err) {
|
|
723
|
+
console.error(
|
|
724
|
+
` Failed to remove worktree ${sb.worktreePath}: ` + formatErrorMessage(err)
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
break;
|
|
241
729
|
}
|
|
242
730
|
}
|
|
243
731
|
if (waveCompleted === 0) {
|
|
@@ -247,26 +735,31 @@ Wave ${waveNum}: ` + ready.map((t) => t.filename).join(", ")
|
|
|
247
735
|
break;
|
|
248
736
|
}
|
|
249
737
|
}
|
|
250
|
-
const remaining = tickets.filter(
|
|
738
|
+
const remaining = opts.tickets.filter(
|
|
251
739
|
(t) => t.status === "ready-for-agent"
|
|
252
740
|
);
|
|
253
741
|
console.log(
|
|
254
742
|
`
|
|
255
743
|
Finished: ${totalCompleted} completed` + (remaining.length > 0 ? `, ${remaining.length} remaining` : "")
|
|
256
744
|
);
|
|
745
|
+
return { allDone: remaining.length === 0 };
|
|
257
746
|
}
|
|
258
|
-
__name(
|
|
747
|
+
__name(runWavesIsolated, "runWavesIsolated");
|
|
259
748
|
export {
|
|
260
749
|
TICKET_RE,
|
|
261
750
|
dryRun,
|
|
751
|
+
dryRunIsolated,
|
|
262
752
|
findReadyTickets,
|
|
263
753
|
formatErrorMessage,
|
|
754
|
+
formatStreamLine,
|
|
755
|
+
getProviderArgs,
|
|
264
756
|
markDone,
|
|
265
757
|
parseBlockers,
|
|
266
758
|
parseStatus,
|
|
267
759
|
parseTicketFilename,
|
|
268
760
|
renderPrompt,
|
|
269
761
|
runWaves,
|
|
762
|
+
runWavesIsolated,
|
|
270
763
|
scanTickets,
|
|
271
764
|
spawnAgent
|
|
272
765
|
};
|