nightralph 0.0.53 → 0.0.54
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 +82 -9
- package/dist/index.js +1947 -230
- package/dist/index.js.map +4 -4
- package/dist/learnings.js +356 -0
- package/dist/learnings.js.map +7 -0
- package/dist/meta.json +232 -12
- package/dist/orchestrator.js +442 -109
- package/dist/orchestrator.js.map +3 -3
- package/dist/preflight.js +34 -0
- package/dist/preflight.js.map +7 -0
- package/dist/progress.js +5 -4
- package/dist/progress.js.map +2 -2
- package/dist/retry.js +83 -0
- package/dist/retry.js.map +7 -0
- package/dist/review.js +662 -0
- package/dist/review.js.map +7 -0
- package/dist/rules.js +54 -0
- package/dist/rules.js.map +7 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/learnings.d.ts +68 -0
- package/dist/src/learnings.d.ts.map +1 -0
- package/dist/src/orchestrator.d.ts +14 -11
- package/dist/src/orchestrator.d.ts.map +1 -1
- package/dist/src/preflight.d.ts +6 -0
- package/dist/src/preflight.d.ts.map +1 -0
- package/dist/src/progress.d.ts +1 -1
- package/dist/src/progress.d.ts.map +1 -1
- package/dist/src/retry.d.ts +26 -0
- package/dist/src/retry.d.ts.map +1 -0
- package/dist/src/review.d.ts +127 -0
- package/dist/src/review.d.ts.map +1 -0
- package/dist/src/rules.d.ts +10 -0
- package/dist/src/rules.d.ts.map +1 -0
- package/dist/src/testcmd.d.ts +1 -0
- package/dist/src/testcmd.d.ts.map +1 -1
- package/dist/src/worktree.d.ts +30 -1
- package/dist/src/worktree.d.ts.map +1 -1
- package/dist/testcmd.js +13 -0
- package/dist/testcmd.js.map +2 -2
- package/dist/worktree.js +236 -32
- package/dist/worktree.js.map +2 -2
- package/package.json +4 -3
package/dist/orchestrator.js
CHANGED
|
@@ -7,10 +7,11 @@ import {
|
|
|
7
7
|
readFileSync,
|
|
8
8
|
writeFileSync,
|
|
9
9
|
unlinkSync,
|
|
10
|
-
readdirSync
|
|
10
|
+
readdirSync,
|
|
11
|
+
statSync
|
|
11
12
|
} from "node:fs";
|
|
12
13
|
import { tmpdir } from "node:os";
|
|
13
|
-
import { join, dirname, basename } from "node:path";
|
|
14
|
+
import { join, dirname, basename, relative } from "node:path";
|
|
14
15
|
import { createInterface } from "node:readline";
|
|
15
16
|
import { promisify } from "node:util";
|
|
16
17
|
const execFileAsync = promisify(execFile);
|
|
@@ -23,7 +24,8 @@ import {
|
|
|
23
24
|
buildBranchName,
|
|
24
25
|
hasNewCommits,
|
|
25
26
|
isDirty,
|
|
26
|
-
commitDirty
|
|
27
|
+
commitDirty,
|
|
28
|
+
headCommit
|
|
27
29
|
} from "./worktree.js";
|
|
28
30
|
import {
|
|
29
31
|
generateProgress,
|
|
@@ -45,6 +47,43 @@ import {
|
|
|
45
47
|
ticketLabel
|
|
46
48
|
} from "./display.js";
|
|
47
49
|
import { runTestCmd } from "./testcmd.js";
|
|
50
|
+
import {
|
|
51
|
+
readDocsRule,
|
|
52
|
+
installRule,
|
|
53
|
+
testGateRule,
|
|
54
|
+
LINT_RULE,
|
|
55
|
+
integrityRule,
|
|
56
|
+
CONFINEMENT_RULES,
|
|
57
|
+
commitRule,
|
|
58
|
+
notesRule,
|
|
59
|
+
agentsMdRule
|
|
60
|
+
} from "./rules.js";
|
|
61
|
+
import { runReviewPass, renderReviewSummary } from "./review.js";
|
|
62
|
+
import {
|
|
63
|
+
EFFORT_LADDERS,
|
|
64
|
+
escalateEffort,
|
|
65
|
+
retryDelayFor,
|
|
66
|
+
runWithRetries,
|
|
67
|
+
sleep
|
|
68
|
+
} from "./retry.js";
|
|
69
|
+
import {
|
|
70
|
+
EFFORT_LADDERS as EFFORT_LADDERS2,
|
|
71
|
+
ALL_EFFORT_LEVELS,
|
|
72
|
+
escalateEffort as escalateEffort2,
|
|
73
|
+
retryDelayFor as retryDelayFor2
|
|
74
|
+
} from "./retry.js";
|
|
75
|
+
import {
|
|
76
|
+
LOG_TAIL_LINES,
|
|
77
|
+
appendEntry,
|
|
78
|
+
collectNotes,
|
|
79
|
+
ensureNotesIgnored,
|
|
80
|
+
learningsPathFor,
|
|
81
|
+
readEntries,
|
|
82
|
+
renderEntry,
|
|
83
|
+
renderHistory,
|
|
84
|
+
selectHistory,
|
|
85
|
+
tailLines
|
|
86
|
+
} from "./learnings.js";
|
|
48
87
|
function formatErrorMessage(error) {
|
|
49
88
|
return error instanceof Error ? error.message : String(error);
|
|
50
89
|
}
|
|
@@ -337,11 +376,23 @@ function stripSpecSection(specBody, heading) {
|
|
|
337
376
|
return out.join("\n");
|
|
338
377
|
}
|
|
339
378
|
__name(stripSpecSection, "stripSpecSection");
|
|
379
|
+
function reviewSpecBody(specBody, specBodies) {
|
|
380
|
+
if (!specBodies || specBodies.size <= 1) return specBody;
|
|
381
|
+
return [...specBodies].map(([name, body]) => `# Feature: ${name}
|
|
382
|
+
|
|
383
|
+
${body}`).join("\n\n");
|
|
384
|
+
}
|
|
385
|
+
__name(reviewSpecBody, "reviewSpecBody");
|
|
340
386
|
function lookupSpec(specBodies, fallback, feature) {
|
|
341
387
|
if (!feature || !specBodies) return fallback;
|
|
342
388
|
return specBodies.get(feature) ?? fallback;
|
|
343
389
|
}
|
|
344
390
|
__name(lookupSpec, "lookupSpec");
|
|
391
|
+
function historyBlock(history) {
|
|
392
|
+
if (!history) return [];
|
|
393
|
+
return [history, ""];
|
|
394
|
+
}
|
|
395
|
+
__name(historyBlock, "historyBlock");
|
|
345
396
|
function renderPrompt(specBody, ticket, opts) {
|
|
346
397
|
return [
|
|
347
398
|
"---- SPEC CONTEXT ----",
|
|
@@ -351,27 +402,29 @@ function renderPrompt(specBody, ticket, opts) {
|
|
|
351
402
|
ticket.filename,
|
|
352
403
|
ticket.body,
|
|
353
404
|
"",
|
|
405
|
+
...historyBlock(opts.history),
|
|
354
406
|
"---- AGENT INSTRUCTIONS ----",
|
|
355
407
|
"You are an autonomous coding agent. Implement the ticket using /tdd.",
|
|
356
408
|
"",
|
|
357
409
|
"Follow these rules:",
|
|
358
|
-
"
|
|
410
|
+
readDocsRule("ticket"),
|
|
359
411
|
"- Work ONLY on this ticket. Do not fix unrelated problems you notice.",
|
|
360
412
|
"- Do NOT research external APIs or services via web search. Use the spec and ticket body as your sole reference for API shapes.",
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
"
|
|
366
|
-
|
|
367
|
-
"
|
|
413
|
+
...installRule(opts.installCmd),
|
|
414
|
+
"- Use /tdd for every checklist item. For each item, write a failing test at the seams named in the spec's Testing Decisions section, then write only enough code to make it pass, then refactor. For a checklist item that is pure scaffolding, config, or docs (where a test adds no value), skip test-first and record that decision in your notes file.",
|
|
415
|
+
testGateRule(opts.testCmd, "ticket"),
|
|
416
|
+
LINT_RULE,
|
|
417
|
+
integrityRule("checklist item"),
|
|
418
|
+
...CONFINEMENT_RULES,
|
|
419
|
+
commitRule("checklist item"),
|
|
420
|
+
notesRule("checklist item", "ticket"),
|
|
368
421
|
"- After completing each checklist item in the ticket, print a line: [x] <item text> (matching the checklist text exactly).",
|
|
369
|
-
"
|
|
422
|
+
agentsMdRule("ticket"),
|
|
370
423
|
""
|
|
371
424
|
].join("\n");
|
|
372
425
|
}
|
|
373
426
|
__name(renderPrompt, "renderPrompt");
|
|
374
|
-
function renderMergePrompt(specBody, ticket) {
|
|
427
|
+
function renderMergePrompt(specBody, ticket, opts = {}) {
|
|
375
428
|
return [
|
|
376
429
|
"---- SPEC CONTEXT ----",
|
|
377
430
|
specBody,
|
|
@@ -392,6 +445,7 @@ function renderMergePrompt(specBody, ticket) {
|
|
|
392
445
|
"4. Commit the resolved files.",
|
|
393
446
|
"",
|
|
394
447
|
"Do NOT re-implement the ticket from scratch.",
|
|
448
|
+
...installRule(opts.installCmd),
|
|
395
449
|
"Do NOT read or modify files outside your current directory (the worktree).",
|
|
396
450
|
"Do NOT run git push."
|
|
397
451
|
].join("\n");
|
|
@@ -505,42 +559,6 @@ function getProviderArgs(cmd) {
|
|
|
505
559
|
return PROVIDER_ARGS[name] ?? [];
|
|
506
560
|
}
|
|
507
561
|
__name(getProviderArgs, "getProviderArgs");
|
|
508
|
-
const EFFORT_LADDERS = {
|
|
509
|
-
pi: [
|
|
510
|
-
"off",
|
|
511
|
-
"minimal",
|
|
512
|
-
"low",
|
|
513
|
-
"medium",
|
|
514
|
-
"high",
|
|
515
|
-
"xhigh",
|
|
516
|
-
"max"
|
|
517
|
-
],
|
|
518
|
-
claude: [
|
|
519
|
-
"low",
|
|
520
|
-
"medium",
|
|
521
|
-
"high",
|
|
522
|
-
"xhigh",
|
|
523
|
-
"max"
|
|
524
|
-
],
|
|
525
|
-
codex: ["low", "medium", "high"]
|
|
526
|
-
};
|
|
527
|
-
const ALL_EFFORT_LEVELS = [
|
|
528
|
-
"off",
|
|
529
|
-
"minimal",
|
|
530
|
-
"low",
|
|
531
|
-
"medium",
|
|
532
|
-
"high",
|
|
533
|
-
"xhigh",
|
|
534
|
-
"max"
|
|
535
|
-
];
|
|
536
|
-
function escalateEffort(provider, level) {
|
|
537
|
-
if (!level) return "high";
|
|
538
|
-
const ladder = EFFORT_LADDERS[provider];
|
|
539
|
-
const i = ladder.indexOf(level);
|
|
540
|
-
if (i === -1) return level;
|
|
541
|
-
return ladder[Math.min(i + 1, ladder.length - 1)];
|
|
542
|
-
}
|
|
543
|
-
__name(escalateEffort, "escalateEffort");
|
|
544
562
|
function formatStreamLine(line) {
|
|
545
563
|
let obj;
|
|
546
564
|
try {
|
|
@@ -867,16 +885,21 @@ function spawnAgent(opts) {
|
|
|
867
885
|
}
|
|
868
886
|
rl.close();
|
|
869
887
|
rlErr.close();
|
|
870
|
-
|
|
888
|
+
const msg = "agent process error: " + formatErrorMessage(err);
|
|
889
|
+
logStream.write(`nightralph: ${msg}
|
|
890
|
+
`);
|
|
871
891
|
if (promptFile) cleanupPromptFile(promptFile);
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
892
|
+
if (opts.onLine) {
|
|
893
|
+
opts.onLine(`${prefix}${msg}`);
|
|
894
|
+
} else {
|
|
895
|
+
console.error(`${prefix}${msg}`);
|
|
896
|
+
}
|
|
897
|
+
logStream.end(() => {
|
|
898
|
+
resolve({
|
|
899
|
+
exitCode: 1,
|
|
900
|
+
completedItems,
|
|
901
|
+
timedOut
|
|
902
|
+
});
|
|
880
903
|
});
|
|
881
904
|
});
|
|
882
905
|
});
|
|
@@ -896,6 +919,8 @@ function dryRun(tickets, specBody, testCmd) {
|
|
|
896
919
|
"\nDeadlocked tickets: " + stuck.map((t) => t.filename).join(", ")
|
|
897
920
|
);
|
|
898
921
|
}
|
|
922
|
+
console.log(`
|
|
923
|
+
Test command: ${testCmd || "(none)"}`);
|
|
899
924
|
const firstReady = findReadyTickets(tickets);
|
|
900
925
|
if (firstReady.length > 0) {
|
|
901
926
|
console.log(
|
|
@@ -951,14 +976,20 @@ Prompt for first ready ticket (${firstReady[0].filename}):
|
|
|
951
976
|
renderPrompt(
|
|
952
977
|
opts.specBody,
|
|
953
978
|
firstReady[0],
|
|
954
|
-
{
|
|
979
|
+
{
|
|
980
|
+
testCmd: opts.testCmd,
|
|
981
|
+
installCmd: opts.installCmd
|
|
982
|
+
}
|
|
955
983
|
)
|
|
956
984
|
);
|
|
957
985
|
}
|
|
958
986
|
const strategyDesc = opts.conflictStrategy === "respawn" ? "respawn (re-run agent on conflict)" : "stop (stop merging on conflict)";
|
|
959
987
|
console.log(
|
|
960
988
|
`
|
|
961
|
-
Test command: ${opts.testCmd}`
|
|
989
|
+
Test command: ${opts.testCmd || "(none)"}`
|
|
990
|
+
);
|
|
991
|
+
console.log(
|
|
992
|
+
`Install: ${opts.installCmd || "(none)"}`
|
|
962
993
|
);
|
|
963
994
|
console.log(
|
|
964
995
|
`Effort: ${opts.effort ?? "(provider default)"}`
|
|
@@ -979,7 +1010,34 @@ Merge strategy: ${strategyDesc}`
|
|
|
979
1010
|
console.log(`Progress file: ${opts.progressPath}`);
|
|
980
1011
|
}
|
|
981
1012
|
__name(dryRunIsolated, "dryRunIsolated");
|
|
1013
|
+
async function runInstall(opts) {
|
|
1014
|
+
const result = await runTestCmd({
|
|
1015
|
+
cmd: opts.installCmd,
|
|
1016
|
+
cwd: opts.worktreePath,
|
|
1017
|
+
timeout: opts.timeout,
|
|
1018
|
+
registry: opts.registry
|
|
1019
|
+
});
|
|
1020
|
+
const installLogPath = attemptLogPath(
|
|
1021
|
+
opts.logsDir,
|
|
1022
|
+
opts.ticket,
|
|
1023
|
+
opts.attempt ?? 1,
|
|
1024
|
+
".install.log"
|
|
1025
|
+
);
|
|
1026
|
+
writeFileSync(installLogPath, result.output);
|
|
1027
|
+
if (result.exitCode !== 0) {
|
|
1028
|
+
opts.log(
|
|
1029
|
+
` ${opts.ticket.filename}: install failed (exit ${result.exitCode}); see ${installLogPath}`
|
|
1030
|
+
);
|
|
1031
|
+
return false;
|
|
1032
|
+
}
|
|
1033
|
+
return true;
|
|
1034
|
+
}
|
|
1035
|
+
__name(runInstall, "runInstall");
|
|
982
1036
|
async function runTestGate(opts) {
|
|
1037
|
+
if (opts.testCmd === "") {
|
|
1038
|
+
opts.log(` ${opts.ticket.filename}: no test gate`);
|
|
1039
|
+
return true;
|
|
1040
|
+
}
|
|
983
1041
|
const result = await runTestCmd({
|
|
984
1042
|
cmd: opts.testCmd,
|
|
985
1043
|
cwd: opts.worktreePath,
|
|
@@ -1021,9 +1079,22 @@ async function respawnAndRetryMerge(opts) {
|
|
|
1021
1079
|
} else {
|
|
1022
1080
|
console.log(msg1);
|
|
1023
1081
|
}
|
|
1082
|
+
if (opts.installCmd) {
|
|
1083
|
+
const installed = await runInstall({
|
|
1084
|
+
installCmd: opts.installCmd,
|
|
1085
|
+
ticket: opts.ticket,
|
|
1086
|
+
worktreePath: opts.worktreePath,
|
|
1087
|
+
timeout: opts.timeout,
|
|
1088
|
+
logsDir: opts.logsDir,
|
|
1089
|
+
log,
|
|
1090
|
+
registry: runningAgents
|
|
1091
|
+
});
|
|
1092
|
+
if (!installed) return false;
|
|
1093
|
+
}
|
|
1024
1094
|
const prompt = renderMergePrompt(
|
|
1025
1095
|
opts.specBody,
|
|
1026
|
-
opts.ticket
|
|
1096
|
+
opts.ticket,
|
|
1097
|
+
{ installCmd: opts.installCmd }
|
|
1027
1098
|
);
|
|
1028
1099
|
const logPath = join(
|
|
1029
1100
|
opts.logsDir,
|
|
@@ -1122,20 +1193,31 @@ async function runPool(items, limit, fn) {
|
|
|
1122
1193
|
return results;
|
|
1123
1194
|
}
|
|
1124
1195
|
__name(runPool, "runPool");
|
|
1125
|
-
function retryDelayFor(base, attempt) {
|
|
1126
|
-
return base * 2 ** (attempt - 2);
|
|
1127
|
-
}
|
|
1128
|
-
__name(retryDelayFor, "retryDelayFor");
|
|
1129
|
-
function sleep(ms) {
|
|
1130
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1131
|
-
}
|
|
1132
|
-
__name(sleep, "sleep");
|
|
1133
1196
|
function attemptLogPath(logsDir, ticket, attempt, ext) {
|
|
1134
1197
|
const stem = ticket.filename.replace(/\.md$/, "");
|
|
1135
1198
|
const suffix = attempt > 1 ? `.attempt${attempt}` : "";
|
|
1136
1199
|
return join(logsDir, `${stem}${suffix}${ext}`);
|
|
1137
1200
|
}
|
|
1138
1201
|
__name(attemptLogPath, "attemptLogPath");
|
|
1202
|
+
function buildHistory(learningsPath, ticket, d) {
|
|
1203
|
+
try {
|
|
1204
|
+
return renderHistory(selectHistory(
|
|
1205
|
+
readEntries(learningsPath),
|
|
1206
|
+
{
|
|
1207
|
+
key: ticketKey(ticket.feature, ticket.num),
|
|
1208
|
+
blockerKeys: ticket.blockers.map((b) => {
|
|
1209
|
+
return ticketKey(ticket.feature, b);
|
|
1210
|
+
})
|
|
1211
|
+
}
|
|
1212
|
+
));
|
|
1213
|
+
} catch (err) {
|
|
1214
|
+
d.log(
|
|
1215
|
+
` ${ticket.filename}: could not read learnings: ${formatErrorMessage(err)}`
|
|
1216
|
+
);
|
|
1217
|
+
return "";
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
__name(buildHistory, "buildHistory");
|
|
1139
1221
|
async function runTicketAttempt(opts) {
|
|
1140
1222
|
const { ticket, d } = opts;
|
|
1141
1223
|
let worktree;
|
|
@@ -1154,6 +1236,38 @@ async function runTicketAttempt(opts) {
|
|
|
1154
1236
|
"in-progress",
|
|
1155
1237
|
ticket.feature
|
|
1156
1238
|
);
|
|
1239
|
+
if (opts.installCmd) {
|
|
1240
|
+
const installed = await runInstall({
|
|
1241
|
+
installCmd: opts.installCmd,
|
|
1242
|
+
ticket,
|
|
1243
|
+
worktreePath: worktree.worktreePath,
|
|
1244
|
+
timeout: opts.timeout,
|
|
1245
|
+
logsDir: opts.logsDir,
|
|
1246
|
+
attempt: opts.attempt,
|
|
1247
|
+
log: /* @__PURE__ */ __name((msg) => d.log(msg), "log"),
|
|
1248
|
+
registry: runningAgents
|
|
1249
|
+
});
|
|
1250
|
+
if (!installed) {
|
|
1251
|
+
await removeWorktreeQuietly(
|
|
1252
|
+
worktree.worktreePath,
|
|
1253
|
+
d
|
|
1254
|
+
);
|
|
1255
|
+
return {
|
|
1256
|
+
kind: "failed",
|
|
1257
|
+
reason: "install",
|
|
1258
|
+
branchName: worktree.branchName,
|
|
1259
|
+
exitCode: 1,
|
|
1260
|
+
completedItems: []
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
try {
|
|
1265
|
+
ensureNotesIgnored(worktree.worktreePath);
|
|
1266
|
+
} catch (err) {
|
|
1267
|
+
d.log(
|
|
1268
|
+
` ${ticket.filename}: could not set up the notes directory: ${formatErrorMessage(err)}`
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1157
1271
|
const ticketSpec = lookupSpec(
|
|
1158
1272
|
opts.specBodies,
|
|
1159
1273
|
opts.specBody,
|
|
@@ -1163,7 +1277,17 @@ async function runTicketAttempt(opts) {
|
|
|
1163
1277
|
prompt: renderPrompt(
|
|
1164
1278
|
ticketSpec,
|
|
1165
1279
|
ticket,
|
|
1166
|
-
{
|
|
1280
|
+
{
|
|
1281
|
+
testCmd: opts.testCmd,
|
|
1282
|
+
installCmd: opts.installCmd,
|
|
1283
|
+
// Rebuilt every attempt, so a retry sees the
|
|
1284
|
+
// entry its previous attempt just wrote
|
|
1285
|
+
history: buildHistory(
|
|
1286
|
+
opts.learningsPath,
|
|
1287
|
+
ticket,
|
|
1288
|
+
d
|
|
1289
|
+
)
|
|
1290
|
+
}
|
|
1167
1291
|
),
|
|
1168
1292
|
agentCmd: opts.agentCmd,
|
|
1169
1293
|
model: opts.model,
|
|
@@ -1180,6 +1304,20 @@ async function runTicketAttempt(opts) {
|
|
|
1180
1304
|
effort: opts.effort,
|
|
1181
1305
|
onLine: /* @__PURE__ */ __name((line) => d.log(line), "onLine")
|
|
1182
1306
|
});
|
|
1307
|
+
let notes = null;
|
|
1308
|
+
try {
|
|
1309
|
+
notes = collectNotes(worktree.worktreePath);
|
|
1310
|
+
} catch (err) {
|
|
1311
|
+
d.log(
|
|
1312
|
+
` ${ticket.filename}: could not collect notes: ${formatErrorMessage(err)}`
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
const agentRun = {
|
|
1316
|
+
exitCode: agentResult.exitCode,
|
|
1317
|
+
completedItems: agentResult.completedItems,
|
|
1318
|
+
timedOut: agentResult.timedOut,
|
|
1319
|
+
notes
|
|
1320
|
+
};
|
|
1183
1321
|
if (agentResult.timedOut) {
|
|
1184
1322
|
d.log(
|
|
1185
1323
|
` ${ticket.filename}: timed out; keeping the work it committed`
|
|
@@ -1194,7 +1332,8 @@ async function runTicketAttempt(opts) {
|
|
|
1194
1332
|
reason: "exit",
|
|
1195
1333
|
branchName: worktree.branchName,
|
|
1196
1334
|
exitCode: agentResult.exitCode,
|
|
1197
|
-
completedItems: agentResult.completedItems
|
|
1335
|
+
completedItems: agentResult.completedItems,
|
|
1336
|
+
agentRun
|
|
1198
1337
|
};
|
|
1199
1338
|
}
|
|
1200
1339
|
if (isDirty(worktree.worktreePath)) {
|
|
@@ -1217,7 +1356,8 @@ async function runTicketAttempt(opts) {
|
|
|
1217
1356
|
return {
|
|
1218
1357
|
kind: "no-commits",
|
|
1219
1358
|
worktreePath: worktree.worktreePath,
|
|
1220
|
-
branchName: worktree.branchName
|
|
1359
|
+
branchName: worktree.branchName,
|
|
1360
|
+
agentRun
|
|
1221
1361
|
};
|
|
1222
1362
|
}
|
|
1223
1363
|
const passed = await runTestGate({
|
|
@@ -1240,44 +1380,130 @@ async function runTicketAttempt(opts) {
|
|
|
1240
1380
|
reason: "tests",
|
|
1241
1381
|
branchName: worktree.branchName,
|
|
1242
1382
|
exitCode: 1,
|
|
1243
|
-
completedItems: agentResult.completedItems
|
|
1383
|
+
completedItems: agentResult.completedItems,
|
|
1384
|
+
agentRun
|
|
1244
1385
|
};
|
|
1245
1386
|
}
|
|
1246
1387
|
return {
|
|
1247
1388
|
kind: "ready",
|
|
1248
1389
|
worktreePath: worktree.worktreePath,
|
|
1249
1390
|
branchName: worktree.branchName,
|
|
1250
|
-
completedItems: agentResult.completedItems
|
|
1391
|
+
completedItems: agentResult.completedItems,
|
|
1392
|
+
agentRun
|
|
1251
1393
|
};
|
|
1252
1394
|
}
|
|
1253
1395
|
__name(runTicketAttempt, "runTicketAttempt");
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1396
|
+
function readLogTail(path) {
|
|
1397
|
+
try {
|
|
1398
|
+
return tailLines(readFileSync(path, "utf8"), LOG_TAIL_LINES);
|
|
1399
|
+
} catch {
|
|
1400
|
+
return "";
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
__name(readLogTail, "readLogTail");
|
|
1404
|
+
function attemptEntry(opts) {
|
|
1405
|
+
const { logsDir, ticket, attempt, effort, outcome } = opts;
|
|
1406
|
+
const base = {
|
|
1407
|
+
key: ticketKey(ticket.feature, ticket.num),
|
|
1408
|
+
label: ticketLabel(ticket),
|
|
1409
|
+
attempt,
|
|
1410
|
+
now: /* @__PURE__ */ new Date()
|
|
1264
1411
|
};
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1412
|
+
const logFor = /* @__PURE__ */ __name((ext) => {
|
|
1413
|
+
const path = attemptLogPath(logsDir, ticket, attempt, ext);
|
|
1414
|
+
return { logName: basename(path), logTail: readLogTail(path) };
|
|
1415
|
+
}, "logFor");
|
|
1416
|
+
switch (outcome.kind) {
|
|
1417
|
+
case "ready":
|
|
1418
|
+
return {
|
|
1419
|
+
...base,
|
|
1420
|
+
outcome: "done",
|
|
1421
|
+
run: outcome.agentRun,
|
|
1422
|
+
effort
|
|
1423
|
+
};
|
|
1424
|
+
case "failed":
|
|
1425
|
+
switch (outcome.reason) {
|
|
1426
|
+
case "exit":
|
|
1427
|
+
return {
|
|
1428
|
+
...base,
|
|
1429
|
+
outcome: "exit",
|
|
1430
|
+
run: outcome.agentRun,
|
|
1431
|
+
effort
|
|
1432
|
+
};
|
|
1433
|
+
case "tests":
|
|
1434
|
+
return {
|
|
1435
|
+
...base,
|
|
1436
|
+
outcome: "tests",
|
|
1437
|
+
run: outcome.agentRun,
|
|
1438
|
+
effort,
|
|
1439
|
+
...logFor(".test.log")
|
|
1440
|
+
};
|
|
1441
|
+
case "install":
|
|
1442
|
+
return {
|
|
1443
|
+
...base,
|
|
1444
|
+
outcome: "install",
|
|
1445
|
+
...logFor(".install.log")
|
|
1446
|
+
};
|
|
1447
|
+
default: {
|
|
1448
|
+
const _exhaustive = outcome;
|
|
1449
|
+
return _exhaustive;
|
|
1450
|
+
}
|
|
1274
1451
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1452
|
+
case "no-commits":
|
|
1453
|
+
return {
|
|
1454
|
+
...base,
|
|
1455
|
+
outcome: "no-commits",
|
|
1456
|
+
run: outcome.agentRun,
|
|
1457
|
+
effort
|
|
1458
|
+
};
|
|
1459
|
+
case "error":
|
|
1460
|
+
case "worktree-error":
|
|
1461
|
+
return {
|
|
1462
|
+
...base,
|
|
1463
|
+
outcome: "error",
|
|
1464
|
+
error: formatErrorMessage(outcome.error)
|
|
1465
|
+
};
|
|
1466
|
+
default: {
|
|
1467
|
+
const _exhaustive = outcome;
|
|
1468
|
+
return _exhaustive;
|
|
1280
1469
|
}
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
__name(attemptEntry, "attemptEntry");
|
|
1473
|
+
function recordAttempt(opts) {
|
|
1474
|
+
try {
|
|
1475
|
+
const entry = attemptEntry(opts);
|
|
1476
|
+
if (!entry) return;
|
|
1477
|
+
appendEntry(opts.learningsPath, renderEntry(entry));
|
|
1478
|
+
} catch (err) {
|
|
1479
|
+
opts.d.log(
|
|
1480
|
+
` ${opts.ticket.filename}: could not record learnings: ${formatErrorMessage(err)}`
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
__name(recordAttempt, "recordAttempt");
|
|
1485
|
+
async function runTicketAttempts(opts) {
|
|
1486
|
+
const { ticket, d } = opts;
|
|
1487
|
+
return runWithRetries({
|
|
1488
|
+
maxAttempts: opts.retries + 1,
|
|
1489
|
+
retryDelay: opts.retryDelay,
|
|
1490
|
+
agentCmd: opts.agentCmd,
|
|
1491
|
+
effort: opts.effort,
|
|
1492
|
+
label: ticket.filename,
|
|
1493
|
+
log: /* @__PURE__ */ __name((msg) => d.log(msg), "log")
|
|
1494
|
+
}, async (attempt, effort) => {
|
|
1495
|
+
const record = /* @__PURE__ */ __name((o) => {
|
|
1496
|
+
recordAttempt({
|
|
1497
|
+
learningsPath: opts.learningsPath,
|
|
1498
|
+
logsDir: opts.logsDir,
|
|
1499
|
+
ticket,
|
|
1500
|
+
attempt,
|
|
1501
|
+
effort,
|
|
1502
|
+
outcome: o,
|
|
1503
|
+
d
|
|
1504
|
+
});
|
|
1505
|
+
}, "record");
|
|
1506
|
+
let outcome;
|
|
1281
1507
|
try {
|
|
1282
1508
|
outcome = await runTicketAttempt({
|
|
1283
1509
|
...opts,
|
|
@@ -1288,25 +1514,46 @@ async function runTicketAttempts(opts) {
|
|
|
1288
1514
|
d.log(
|
|
1289
1515
|
` ${ticket.filename}: attempt ${attempt} threw: ${formatErrorMessage(error)}`
|
|
1290
1516
|
);
|
|
1291
|
-
|
|
1517
|
+
const errorOutcome = { kind: "error", error };
|
|
1518
|
+
record(errorOutcome);
|
|
1519
|
+
return { result: errorOutcome, stop: true };
|
|
1292
1520
|
}
|
|
1521
|
+
record(outcome);
|
|
1293
1522
|
if (outcome.kind === "ready") {
|
|
1294
1523
|
d.setTicketStatus(
|
|
1295
1524
|
ticket.num,
|
|
1296
1525
|
"awaiting-merge",
|
|
1297
1526
|
ticket.feature
|
|
1298
1527
|
);
|
|
1299
|
-
return outcome;
|
|
1528
|
+
return { result: outcome, stop: true };
|
|
1300
1529
|
}
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1530
|
+
return {
|
|
1531
|
+
result: outcome,
|
|
1532
|
+
stop: outcome.kind === "worktree-error"
|
|
1533
|
+
};
|
|
1534
|
+
});
|
|
1304
1535
|
}
|
|
1305
1536
|
__name(runTicketAttempts, "runTicketAttempts");
|
|
1537
|
+
function failureNote(reason) {
|
|
1538
|
+
if (reason === "tests") return "tests failed";
|
|
1539
|
+
if (reason === "install") return "install failed";
|
|
1540
|
+
return "failed";
|
|
1541
|
+
}
|
|
1542
|
+
__name(failureNote, "failureNote");
|
|
1543
|
+
function isRegularFile(path) {
|
|
1544
|
+
try {
|
|
1545
|
+
return statSync(path).isFile();
|
|
1546
|
+
} catch {
|
|
1547
|
+
return false;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
__name(isRegularFile, "isRegularFile");
|
|
1306
1551
|
async function saveProgress(repoRoot, progressPath, progress, message, d) {
|
|
1307
1552
|
writeProgress(progressPath, progress);
|
|
1553
|
+
const learningsPath = learningsPathFor(progressPath);
|
|
1554
|
+
const paths = isRegularFile(learningsPath) ? [progressPath, learningsPath] : progressPath;
|
|
1308
1555
|
try {
|
|
1309
|
-
await commitProgress(repoRoot,
|
|
1556
|
+
await commitProgress(repoRoot, paths, message);
|
|
1310
1557
|
} catch (err) {
|
|
1311
1558
|
d.log(
|
|
1312
1559
|
" Failed to commit progress: " + formatErrorMessage(err)
|
|
@@ -1582,6 +1829,7 @@ async function runWavesIsolated(opts) {
|
|
|
1582
1829
|
"Working tree is dirty; commit or stash changes before running"
|
|
1583
1830
|
);
|
|
1584
1831
|
}
|
|
1832
|
+
const launchCommit = headCommit(repoRoot);
|
|
1585
1833
|
const features = new Set(
|
|
1586
1834
|
opts.tickets.map((t) => t.feature).filter(Boolean)
|
|
1587
1835
|
);
|
|
@@ -1685,13 +1933,34 @@ async function runWavesIsolated(opts) {
|
|
|
1685
1933
|
timeout: opts.timeout,
|
|
1686
1934
|
logsDir: opts.logsDir,
|
|
1687
1935
|
testCmd: opts.testCmd,
|
|
1936
|
+
installCmd: opts.installCmd,
|
|
1688
1937
|
maxTurns: opts.maxTurns,
|
|
1689
1938
|
effort: opts.effort,
|
|
1939
|
+
learningsPath: learningsPathFor(
|
|
1940
|
+
tracker.pathFor(t.feature)
|
|
1941
|
+
),
|
|
1690
1942
|
retries,
|
|
1691
1943
|
retryDelay,
|
|
1692
1944
|
d
|
|
1693
1945
|
})
|
|
1694
1946
|
);
|
|
1947
|
+
const touched = new Set(ready.map((t) => {
|
|
1948
|
+
return learningsPathFor(tracker.pathFor(t.feature));
|
|
1949
|
+
}));
|
|
1950
|
+
for (const path of touched) {
|
|
1951
|
+
if (!isRegularFile(path)) continue;
|
|
1952
|
+
try {
|
|
1953
|
+
await commitProgress(
|
|
1954
|
+
repoRoot,
|
|
1955
|
+
path,
|
|
1956
|
+
"nightralph: record attempt learnings"
|
|
1957
|
+
);
|
|
1958
|
+
} catch (err) {
|
|
1959
|
+
d.log(
|
|
1960
|
+
" Failed to commit learnings: " + formatErrorMessage(err)
|
|
1961
|
+
);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1695
1964
|
const outcomes = settled.map(
|
|
1696
1965
|
(r) => r.status === "fulfilled" ? r.value : { kind: "error", error: r.reason }
|
|
1697
1966
|
);
|
|
@@ -1795,7 +2064,7 @@ async function runWavesIsolated(opts) {
|
|
|
1795
2064
|
repoRoot,
|
|
1796
2065
|
tracker.pathFor(ticket.feature),
|
|
1797
2066
|
tracker.dataFor(ticket.feature),
|
|
1798
|
-
`nightralph: ${ticket.filename} ` + (outcome.reason
|
|
2067
|
+
`nightralph: ${ticket.filename} ` + failureNote(outcome.reason),
|
|
1799
2068
|
d
|
|
1800
2069
|
);
|
|
1801
2070
|
continue;
|
|
@@ -1831,6 +2100,7 @@ async function runWavesIsolated(opts) {
|
|
|
1831
2100
|
branches: successfulBranches,
|
|
1832
2101
|
strategy: opts.conflictStrategy
|
|
1833
2102
|
});
|
|
2103
|
+
const doneThisWave = [];
|
|
1834
2104
|
for (const branchResult of mergeResult.merged) {
|
|
1835
2105
|
const bk = branchToKey.get(
|
|
1836
2106
|
branchResult.branch
|
|
@@ -1839,6 +2109,7 @@ async function runWavesIsolated(opts) {
|
|
|
1839
2109
|
if (!ticket) continue;
|
|
1840
2110
|
if (branchResult.merged) {
|
|
1841
2111
|
markDone(ticket);
|
|
2112
|
+
doneThisWave.push(ticket);
|
|
1842
2113
|
waveCompleted++;
|
|
1843
2114
|
totalCompleted++;
|
|
1844
2115
|
d.setTicketStatus(
|
|
@@ -1888,11 +2159,13 @@ async function runWavesIsolated(opts) {
|
|
|
1888
2159
|
timeout: opts.timeout,
|
|
1889
2160
|
logsDir: opts.logsDir,
|
|
1890
2161
|
testCmd: opts.testCmd,
|
|
2162
|
+
installCmd: opts.installCmd,
|
|
1891
2163
|
maxTurns: opts.maxTurns,
|
|
1892
2164
|
onLine: /* @__PURE__ */ __name((msg) => d.log(msg), "onLine")
|
|
1893
2165
|
}) : false;
|
|
1894
2166
|
if (recovered) {
|
|
1895
2167
|
markDone(ticket);
|
|
2168
|
+
doneThisWave.push(ticket);
|
|
1896
2169
|
waveCompleted++;
|
|
1897
2170
|
totalCompleted++;
|
|
1898
2171
|
d.setTicketStatus(
|
|
@@ -1948,6 +2221,28 @@ async function runWavesIsolated(opts) {
|
|
|
1948
2221
|
exitCode: 0,
|
|
1949
2222
|
done: false
|
|
1950
2223
|
});
|
|
2224
|
+
try {
|
|
2225
|
+
appendEntry(
|
|
2226
|
+
learningsPathFor(
|
|
2227
|
+
tracker.pathFor(ticket.feature)
|
|
2228
|
+
),
|
|
2229
|
+
renderEntry({
|
|
2230
|
+
outcome: "merge",
|
|
2231
|
+
key: ticketKey(
|
|
2232
|
+
ticket.feature,
|
|
2233
|
+
ticket.num
|
|
2234
|
+
),
|
|
2235
|
+
label: ticketLabel(ticket),
|
|
2236
|
+
attempt: 0,
|
|
2237
|
+
now: /* @__PURE__ */ new Date(),
|
|
2238
|
+
stderr: branchResult.stderr ?? ""
|
|
2239
|
+
})
|
|
2240
|
+
);
|
|
2241
|
+
} catch (err) {
|
|
2242
|
+
d.log(
|
|
2243
|
+
` ${ticket.filename}: could not record learnings: ` + formatErrorMessage(err)
|
|
2244
|
+
);
|
|
2245
|
+
}
|
|
1951
2246
|
await saveProgress(
|
|
1952
2247
|
repoRoot,
|
|
1953
2248
|
tracker.pathFor(ticket.feature),
|
|
@@ -1958,6 +2253,7 @@ async function runWavesIsolated(opts) {
|
|
|
1958
2253
|
}
|
|
1959
2254
|
}
|
|
1960
2255
|
}
|
|
2256
|
+
await commitTicketFiles(repoRoot, doneThisWave, d);
|
|
1961
2257
|
if (mergeResult.stoppedAt) {
|
|
1962
2258
|
d.log(
|
|
1963
2259
|
`
|
|
@@ -2000,6 +2296,41 @@ No tickets completed this wave. ${remaining2.length} ready ticket${remaining2.le
|
|
|
2000
2296
|
}
|
|
2001
2297
|
}
|
|
2002
2298
|
if (waveNum > 0) reportBlocked(d, opts.tickets, failedKeys);
|
|
2299
|
+
let reviewLine = null;
|
|
2300
|
+
if (opts.review) {
|
|
2301
|
+
try {
|
|
2302
|
+
const outcome = await runReviewPass({
|
|
2303
|
+
repoRoot,
|
|
2304
|
+
repoName,
|
|
2305
|
+
baseBranch,
|
|
2306
|
+
reviewPath: opts.review.reviewPath,
|
|
2307
|
+
forkedFrom: opts.review.forkedFrom,
|
|
2308
|
+
launchCommit,
|
|
2309
|
+
specBody: reviewSpecBody(
|
|
2310
|
+
opts.specBody,
|
|
2311
|
+
opts.specBodies
|
|
2312
|
+
),
|
|
2313
|
+
agentCmd: opts.agentCmd,
|
|
2314
|
+
model: opts.model,
|
|
2315
|
+
timeout: opts.timeout,
|
|
2316
|
+
logsDir: opts.logsDir,
|
|
2317
|
+
testCmd: opts.testCmd,
|
|
2318
|
+
installCmd: opts.installCmd,
|
|
2319
|
+
maxTurns: opts.maxTurns,
|
|
2320
|
+
effort: opts.effort,
|
|
2321
|
+
retries,
|
|
2322
|
+
retryDelay,
|
|
2323
|
+
log: /* @__PURE__ */ __name((msg) => d.log(msg), "log")
|
|
2324
|
+
}, { spawnAgent, registry: runningAgents });
|
|
2325
|
+
reviewLine = renderReviewSummary(
|
|
2326
|
+
outcome,
|
|
2327
|
+
relative(repoRoot, opts.review.reviewPath)
|
|
2328
|
+
);
|
|
2329
|
+
} catch (err) {
|
|
2330
|
+
d.log(`
|
|
2331
|
+
Review failed: ${formatErrorMessage(err)}`);
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2003
2334
|
const remaining = opts.tickets.filter(
|
|
2004
2335
|
(t) => t.status === "ready-for-agent"
|
|
2005
2336
|
);
|
|
@@ -2008,8 +2339,10 @@ No tickets completed this wave. ${remaining2.length} ready ticket${remaining2.le
|
|
|
2008
2339
|
totalCompleted,
|
|
2009
2340
|
failedKeys
|
|
2010
2341
|
));
|
|
2342
|
+
if (reviewLine) d.log(reviewLine);
|
|
2343
|
+
d.log(`Work is on ${baseBranch}`);
|
|
2011
2344
|
d.cleanup();
|
|
2012
|
-
return { allDone: remaining.length === 0 };
|
|
2345
|
+
return { allDone: remaining.length === 0, branch: baseBranch };
|
|
2013
2346
|
}
|
|
2014
2347
|
__name(runWavesIsolated, "runWavesIsolated");
|
|
2015
2348
|
export {
|
|
@@ -2018,7 +2351,7 @@ export {
|
|
|
2018
2351
|
DEFAULT_CONCURRENCY,
|
|
2019
2352
|
DEFAULT_RETRIES,
|
|
2020
2353
|
DEFAULT_RETRY_DELAY,
|
|
2021
|
-
EFFORT_LADDERS,
|
|
2354
|
+
EFFORT_LADDERS2 as EFFORT_LADDERS,
|
|
2022
2355
|
HUMAN_STATUS,
|
|
2023
2356
|
TICKET_RE,
|
|
2024
2357
|
TOOL_USE_RE,
|
|
@@ -2026,7 +2359,7 @@ export {
|
|
|
2026
2359
|
createPiStreamFormatter,
|
|
2027
2360
|
dryRun,
|
|
2028
2361
|
dryRunIsolated,
|
|
2029
|
-
escalateEffort,
|
|
2362
|
+
escalateEffort2 as escalateEffort,
|
|
2030
2363
|
findBlockedTickets,
|
|
2031
2364
|
findReadyTickets,
|
|
2032
2365
|
formatErrorMessage,
|
|
@@ -2042,7 +2375,7 @@ export {
|
|
|
2042
2375
|
renderFinishSummary,
|
|
2043
2376
|
renderMergePrompt,
|
|
2044
2377
|
renderPrompt,
|
|
2045
|
-
retryDelayFor,
|
|
2378
|
+
retryDelayFor2 as retryDelayFor,
|
|
2046
2379
|
runPool,
|
|
2047
2380
|
runWaves,
|
|
2048
2381
|
runWavesIsolated,
|