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.
Files changed (42) hide show
  1. package/README.md +82 -9
  2. package/dist/index.js +1947 -230
  3. package/dist/index.js.map +4 -4
  4. package/dist/learnings.js +356 -0
  5. package/dist/learnings.js.map +7 -0
  6. package/dist/meta.json +232 -12
  7. package/dist/orchestrator.js +442 -109
  8. package/dist/orchestrator.js.map +3 -3
  9. package/dist/preflight.js +34 -0
  10. package/dist/preflight.js.map +7 -0
  11. package/dist/progress.js +5 -4
  12. package/dist/progress.js.map +2 -2
  13. package/dist/retry.js +83 -0
  14. package/dist/retry.js.map +7 -0
  15. package/dist/review.js +662 -0
  16. package/dist/review.js.map +7 -0
  17. package/dist/rules.js +54 -0
  18. package/dist/rules.js.map +7 -0
  19. package/dist/src/index.d.ts.map +1 -1
  20. package/dist/src/learnings.d.ts +68 -0
  21. package/dist/src/learnings.d.ts.map +1 -0
  22. package/dist/src/orchestrator.d.ts +14 -11
  23. package/dist/src/orchestrator.d.ts.map +1 -1
  24. package/dist/src/preflight.d.ts +6 -0
  25. package/dist/src/preflight.d.ts.map +1 -0
  26. package/dist/src/progress.d.ts +1 -1
  27. package/dist/src/progress.d.ts.map +1 -1
  28. package/dist/src/retry.d.ts +26 -0
  29. package/dist/src/retry.d.ts.map +1 -0
  30. package/dist/src/review.d.ts +127 -0
  31. package/dist/src/review.d.ts.map +1 -0
  32. package/dist/src/rules.d.ts +10 -0
  33. package/dist/src/rules.d.ts.map +1 -0
  34. package/dist/src/testcmd.d.ts +1 -0
  35. package/dist/src/testcmd.d.ts.map +1 -1
  36. package/dist/src/worktree.d.ts +30 -1
  37. package/dist/src/worktree.d.ts.map +1 -1
  38. package/dist/testcmd.js +13 -0
  39. package/dist/testcmd.js.map +2 -2
  40. package/dist/worktree.js +236 -32
  41. package/dist/worktree.js.map +2 -2
  42. package/package.json +4 -3
@@ -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
- "- Read the files directly relevant to the ticket before writing code. Also read any AGENTS.md in or above the directories you will touch, plus CONTEXT.md and the relevant docs/adr/ entries if they exist. If they do not exist, proceed silently.",
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
- "- 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.",
362
- `- While iterating, run only the tests relevant to what you changed. Run \`${opts.testCmd}\` in full once, as the final gate before your last commit. Do not commit with failing tests. The orchestrator runs the same command after you exit and rejects the ticket if it fails.`,
363
- "- If the project has a lint script, run it and fix what it reports.",
364
- "- INTEGRITY: never delete, skip, weaken, or write tautological tests to force a green result. Only report a checklist item done when its real tests genuinely pass. If an item cannot be completed honestly, leave it unchecked and say what blocked you.",
365
- "- Do NOT read or modify files outside your current directory (the worktree).",
366
- "- Do NOT run git push.",
367
- "- Commit after each checklist item, and make sure everything is committed when finished.",
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
- "- Before your final commit, record reusable learnings in the nearest AGENTS.md (in or above the directories you modified; prefer that over the repo root). Good additions: module conventions, gotchas, files that must change together, how to test that area, environment requirements. Do NOT add ticket-specific details or temporary debugging notes. Only write when you have knowledge that would help future work in that directory.",
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
- logStream.end();
888
+ const msg = "agent process error: " + formatErrorMessage(err);
889
+ logStream.write(`nightralph: ${msg}
890
+ `);
871
891
  if (promptFile) cleanupPromptFile(promptFile);
872
- console.error(
873
- "Agent process error:",
874
- formatErrorMessage(err)
875
- );
876
- resolve({
877
- exitCode: 1,
878
- completedItems,
879
- timedOut
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
- { testCmd: opts.testCmd }
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
- { testCmd: opts.testCmd }
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
- async function runTicketAttempts(opts) {
1255
- const { ticket, d } = opts;
1256
- const maxAttempts = opts.retries + 1;
1257
- const providerName = basename(opts.agentCmd);
1258
- const isKnown = providerName in EFFORT_LADDERS;
1259
- let effort = opts.effort;
1260
- let outcome = {
1261
- kind: "no-commits",
1262
- worktreePath: "",
1263
- branchName: ""
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
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1266
- if (attempt === 1) {
1267
- d.log(` Starting ${ticket.filename}`);
1268
- } else {
1269
- if (isKnown) {
1270
- effort = escalateEffort(
1271
- providerName,
1272
- effort
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
- const delay = retryDelayFor(opts.retryDelay, attempt);
1276
- d.log(
1277
- ` ${ticket.filename}: retrying (attempt ${attempt}/${maxAttempts}` + (isKnown && effort ? `, effort ${effort}` : "") + (delay > 0 ? `, waiting ${delay}s` : "") + ")"
1278
- );
1279
- if (delay > 0) await sleep(delay * 1e3);
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
- return { kind: "error", error };
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
- if (outcome.kind === "worktree-error") return outcome;
1302
- }
1303
- return outcome;
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, progressPath, message);
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 === "tests" ? "tests failed" : "failed"),
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,