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
package/dist/review.js ADDED
@@ -0,0 +1,662 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { basename, dirname, join, relative } from "node:path";
5
+ import {
6
+ NOTES_PATH,
7
+ formatDate,
8
+ collectNotes,
9
+ ensureNotesIgnored,
10
+ appendEntry
11
+ } from "./learnings.js";
12
+ import { runTestCmd } from "./testcmd.js";
13
+ import { commitProgress } from "./progress.js";
14
+ import { runWithRetries } from "./retry.js";
15
+ import {
16
+ createNamedWorktree,
17
+ deleteBranch,
18
+ removeWorktree,
19
+ commitDirty,
20
+ hasNewCommits,
21
+ isDirty,
22
+ hasChangesOutside,
23
+ headCommit,
24
+ isAncestor,
25
+ mergeBase
26
+ } from "./worktree.js";
27
+ import { mergeBranch } from "./merge.js";
28
+ import {
29
+ readDocsRule,
30
+ installRule,
31
+ testGateRule,
32
+ LINT_RULE,
33
+ integrityRule,
34
+ CONFINEMENT_RULES,
35
+ commitRule,
36
+ notesRule,
37
+ agentsMdRule
38
+ } from "./rules.js";
39
+ const FINDINGS_FILE = "findings.md";
40
+ const FINDINGS_PATH = `${dirname(NOTES_PATH)}/${FINDINGS_FILE}`;
41
+ const NO_FINDINGS = "No findings.";
42
+ const REVIEW_EXCLUDE = ".scratch";
43
+ const REVIEW_MARKER = "---- REVIEW INSTRUCTIONS ----";
44
+ const FIX_MARKER = "---- REVIEW FINDINGS ----";
45
+ function reviewExcludeFor(repoRoot, reviewPath) {
46
+ let dir = dirname(reviewPath);
47
+ while (dirname(dir) !== dir) {
48
+ if (basename(dir) === ".scratch") {
49
+ const rel = relative(repoRoot, dir);
50
+ if (rel && !rel.startsWith("..")) return rel;
51
+ break;
52
+ }
53
+ dir = dirname(dir);
54
+ }
55
+ return REVIEW_EXCLUDE;
56
+ }
57
+ __name(reviewExcludeFor, "reviewExcludeFor");
58
+ function reviewDiffCommand(from, to, exclude = REVIEW_EXCLUDE) {
59
+ return `git diff ${from}..${to} -- . ':(exclude)${exclude}'`;
60
+ }
61
+ __name(reviewDiffCommand, "reviewDiffCommand");
62
+ function renderReviewPrompt(opts) {
63
+ return [
64
+ "---- SPEC CONTEXT ----",
65
+ opts.specBody,
66
+ "",
67
+ REVIEW_MARKER,
68
+ "You are reviewing work that autonomous agents merged into this branch, one ticket at a time. Review the combined change as a whole.",
69
+ "",
70
+ "The change to review:",
71
+ "",
72
+ ` ${reviewDiffCommand(opts.from, opts.to, opts.exclude)}`,
73
+ "",
74
+ "Follow these rules:",
75
+ "- Read any AGENTS.md in or above the directories the change touches, plus CONTEXT.md and the relevant docs/adr/ entries if they exist. Treat their contracts and the spec above as the standard the code must meet.",
76
+ "- Look for correctness bugs, missing or weak tests, and code that breaks a documented contract. Ignore style preferences.",
77
+ "- You may run the tests or the type checker to confirm a suspicion.",
78
+ "- Do NOT edit, stage, or commit code. Anything you change here is discarded.",
79
+ ...CONFINEMENT_RULES,
80
+ "",
81
+ `Write your findings to \`${FINDINGS_PATH}\` (create the directory), most severe first, in exactly this form:`,
82
+ "",
83
+ "- [ ] <one-line summary of the finding>",
84
+ " <file:line, and a concrete input or state that goes wrong>",
85
+ "",
86
+ "If you find nothing worth fixing, write the single line: " + NO_FINDINGS,
87
+ "Never git add or commit that file; the orchestrator collects it.",
88
+ ""
89
+ ].join("\n");
90
+ }
91
+ __name(renderReviewPrompt, "renderReviewPrompt");
92
+ const FIX_FAILURE_TEXT = {
93
+ install: "dependency install failed",
94
+ exit: "the agent exited with an error",
95
+ "no-commits": "the agent made no commits",
96
+ tests: "the test command failed after the agent finished",
97
+ merge: "the fix branch did not merge cleanly"
98
+ };
99
+ function findingLines(findings) {
100
+ return findings.flatMap((f) => [
101
+ `- [ ] ${f.summary}`,
102
+ ...f.detail ? f.detail.split("\n").map((l) => ` ${l}`) : []
103
+ ]);
104
+ }
105
+ __name(findingLines, "findingLines");
106
+ function previousBlock(previous) {
107
+ if (!previous) return [];
108
+ return [
109
+ "---- PREVIOUS FIX ATTEMPT ----",
110
+ `It failed: ${FIX_FAILURE_TEXT[previous.reason]}.`,
111
+ previous.notes ?? "It left no notes.",
112
+ ""
113
+ ];
114
+ }
115
+ __name(previousBlock, "previousBlock");
116
+ function renderFixPrompt(opts) {
117
+ return [
118
+ "---- SPEC CONTEXT ----",
119
+ opts.specBody,
120
+ "",
121
+ FIX_MARKER,
122
+ ...findingLines(opts.findings),
123
+ "",
124
+ ...previousBlock(opts.previous),
125
+ "---- AGENT INSTRUCTIONS ----",
126
+ "You are an autonomous coding agent. Fix the review findings above using /tdd.",
127
+ "",
128
+ "Follow these rules:",
129
+ readDocsRule("findings"),
130
+ "- Fix ONLY the listed findings. Do not fix unrelated problems you notice.",
131
+ ...installRule(opts.installCmd),
132
+ "- Use /tdd for every finding: write a failing test that shows the problem, then write only enough code to make it pass, then refactor.",
133
+ testGateRule(opts.testCmd, "fix"),
134
+ LINT_RULE,
135
+ integrityRule("finding"),
136
+ ...CONFINEMENT_RULES,
137
+ commitRule("finding"),
138
+ notesRule("finding", "fix"),
139
+ "- After fixing each finding, print a line: [x] <finding summary> (matching the summary text exactly).",
140
+ "- If a finding is wrong or cannot be fixed honestly, leave it unchecked and explain why in your notes file.",
141
+ agentsMdRule("fix"),
142
+ ""
143
+ ].join("\n");
144
+ }
145
+ __name(renderFixPrompt, "renderFixPrompt");
146
+ const SENTINEL_PREFIX = "<!-- nightralph:review";
147
+ const SENTINEL_RE = /^<!-- nightralph:review (.*) -->$/;
148
+ const SHA_RE = /^[0-9a-f]{4,64}$/;
149
+ const FIX_HEADING = "### Fix";
150
+ const FINDING_RE = /^ ?[-*+]\s+\[([ xX])\]\s+(.+)$/;
151
+ const REVIEW_STATUSES = [
152
+ "clean",
153
+ "fixed",
154
+ "unresolved",
155
+ "review-failed"
156
+ ];
157
+ function isReviewStatus(value) {
158
+ return REVIEW_STATUSES.includes(value);
159
+ }
160
+ __name(isReviewStatus, "isReviewStatus");
161
+ function collapse(s) {
162
+ return s.replace(/\s+/g, " ").trim();
163
+ }
164
+ __name(collapse, "collapse");
165
+ function normalize(s) {
166
+ return collapse(s).toLowerCase();
167
+ }
168
+ __name(normalize, "normalize");
169
+ function sanitize(text) {
170
+ return text.replace(
171
+ new RegExp(SENTINEL_PREFIX, "g"),
172
+ "<!-- nightralph-review"
173
+ );
174
+ }
175
+ __name(sanitize, "sanitize");
176
+ function splitLines(text) {
177
+ return text.split("\n").map((line) => {
178
+ return line.endsWith("\r") ? line.slice(0, -1) : line;
179
+ });
180
+ }
181
+ __name(splitLines, "splitLines");
182
+ function parseFindings(text) {
183
+ const findings = [];
184
+ const details = [];
185
+ for (const line of splitLines(text)) {
186
+ const m = line.match(FINDING_RE);
187
+ if (m) {
188
+ findings.push({
189
+ summary: collapse(m[2]),
190
+ detail: "",
191
+ checked: m[1] !== " "
192
+ });
193
+ details.push([]);
194
+ } else if (findings.length > 0 && line.trim()) {
195
+ details[details.length - 1].push(line.trim());
196
+ }
197
+ }
198
+ if (findings.length > 0) {
199
+ return findings.map((f, i) => {
200
+ return { ...f, detail: details[i].join("\n") };
201
+ });
202
+ }
203
+ const clean = splitLines(text).some((line) => {
204
+ return normalize(line) === NO_FINDINGS.toLowerCase();
205
+ });
206
+ return clean ? [] : null;
207
+ }
208
+ __name(parseFindings, "parseFindings");
209
+ function markFixed(findings, completedItems) {
210
+ const done = new Set(completedItems.map(normalize));
211
+ return findings.map((f) => {
212
+ return { ...f, checked: done.has(normalize(f.summary)) };
213
+ });
214
+ }
215
+ __name(markFixed, "markFixed");
216
+ function headline(input) {
217
+ const n = input.findings.length;
218
+ const fixed = input.findings.filter((f) => f.checked).length;
219
+ switch (input.status) {
220
+ case "clean":
221
+ return "no findings";
222
+ case "fixed":
223
+ return `${fixed} of ${n} fixed`;
224
+ case "unresolved":
225
+ return `${n} finding${n === 1 ? "" : "s"}, fix failed`;
226
+ case "review-failed":
227
+ return "review failed";
228
+ default: {
229
+ const _exhaustive = input.status;
230
+ return _exhaustive;
231
+ }
232
+ }
233
+ }
234
+ __name(headline, "headline");
235
+ function renderFindings(input) {
236
+ if (input.status === "review-failed") {
237
+ return ["The reviewer left no usable findings file."];
238
+ }
239
+ if (input.findings.length === 0) return [NO_FINDINGS];
240
+ return input.findings.flatMap((f) => {
241
+ const box = f.checked ? "[x]" : "[ ]";
242
+ const detail = f.detail ? f.detail.split("\n").map((l) => ` ${sanitize(l)}`) : [];
243
+ return [`- ${box} ${sanitize(collapse(f.summary))}`, ...detail];
244
+ });
245
+ }
246
+ __name(renderFindings, "renderFindings");
247
+ function renderFix(fix) {
248
+ if (!fix) return [];
249
+ return [
250
+ "",
251
+ FIX_HEADING,
252
+ `- attempts: ${fix.attempts} of ${fix.maxAttempts}`,
253
+ ...fix.effort ? [`- effort: ${fix.effort}`] : [],
254
+ `- agent exit code: ${fix.exitCode ?? "none"}`,
255
+ `- result: ${sanitize(fix.result)}`,
256
+ "",
257
+ fix.notes ? sanitize(fix.notes) : "The fixer left no notes."
258
+ ];
259
+ }
260
+ __name(renderFix, "renderFix");
261
+ function renderReviewEntry(input) {
262
+ return [
263
+ `${SENTINEL_PREFIX} from=${input.from} to=${input.to} status=${input.status} -->`,
264
+ `## ${formatDate(input.now)} -- review ${input.from.slice(0, 7)}..${input.to.slice(0, 7)} -- ${headline(input)}`,
265
+ ...renderFindings(input),
266
+ ...renderFix(input.fix),
267
+ "---",
268
+ ""
269
+ ].join("\n");
270
+ }
271
+ __name(renderReviewEntry, "renderReviewEntry");
272
+ function parseEntry(sentinel, bodyLines) {
273
+ const attrs = {};
274
+ for (const part of sentinel.split(/\s+/).filter(Boolean)) {
275
+ const idx = part.indexOf("=");
276
+ if (idx > 0) attrs[part.slice(0, idx)] = part.slice(idx + 1);
277
+ }
278
+ const { from, to, status } = attrs;
279
+ if (!from || !SHA_RE.test(from)) return null;
280
+ if (!to || !SHA_RE.test(to)) return null;
281
+ if (!status || !isReviewStatus(status)) return null;
282
+ const lines = [...bodyLines];
283
+ while (lines.length > 0 && !lines[lines.length - 1].trim()) {
284
+ lines.pop();
285
+ }
286
+ if (lines[lines.length - 1] === "---") lines.pop();
287
+ const fixAt = lines.indexOf(FIX_HEADING);
288
+ const findingLines2 = fixAt === -1 ? lines : lines.slice(0, fixAt);
289
+ return {
290
+ from,
291
+ to,
292
+ status,
293
+ findings: parseFindings(findingLines2.join("\n")) ?? [],
294
+ body: lines.join("\n").trim()
295
+ };
296
+ }
297
+ __name(parseEntry, "parseEntry");
298
+ function parseReviewEntries(text) {
299
+ const entries = [];
300
+ let sentinel = null;
301
+ let body = [];
302
+ const flush = /* @__PURE__ */ __name(() => {
303
+ if (sentinel === null) return;
304
+ const entry = parseEntry(sentinel, body);
305
+ if (entry) entries.push(entry);
306
+ }, "flush");
307
+ for (const line of splitLines(text)) {
308
+ const m = line.match(SENTINEL_RE);
309
+ if (m) {
310
+ flush();
311
+ sentinel = m[1];
312
+ body = [];
313
+ } else if (sentinel !== null) {
314
+ body.push(line);
315
+ }
316
+ }
317
+ flush();
318
+ return entries;
319
+ }
320
+ __name(parseReviewEntries, "parseReviewEntries");
321
+ function selectReviewBase(entries, opts) {
322
+ const last = [...entries].reverse().find((e) => {
323
+ return e.status !== "review-failed";
324
+ });
325
+ if (last && opts.isAncestor(last.to)) return last.to;
326
+ return opts.forkPoint ?? opts.launchCommit;
327
+ }
328
+ __name(selectReviewBase, "selectReviewBase");
329
+ function renderReviewSummary(outcome, reviewPath) {
330
+ const see = ` -- see ${reviewPath}`;
331
+ const plural = outcome.findings === 1 ? "" : "s";
332
+ switch (outcome.status) {
333
+ case "skipped":
334
+ return null;
335
+ case "clean":
336
+ return `Review: no findings${see}`;
337
+ case "fixed":
338
+ return `Review: ${outcome.findings} finding${plural}, ${outcome.fixed} fixed${see}`;
339
+ case "unresolved":
340
+ return `Review: ${outcome.findings} finding${plural}, fix failed${see}`;
341
+ case "review-failed":
342
+ return `Review: the reviewer failed${see}`;
343
+ default: {
344
+ const _exhaustive = outcome.status;
345
+ return _exhaustive;
346
+ }
347
+ }
348
+ }
349
+ __name(renderReviewSummary, "renderReviewSummary");
350
+ function logPath(logsDir, stem, attempt, ext) {
351
+ const suffix = attempt > 1 ? `.attempt${attempt}` : "";
352
+ return join(logsDir, `${stem}${suffix}${ext}`);
353
+ }
354
+ __name(logPath, "logPath");
355
+ async function runStep(opts, deps, cmd, cwd, path) {
356
+ const result = await runTestCmd({
357
+ cmd,
358
+ cwd,
359
+ timeout: opts.timeout,
360
+ registry: deps.registry
361
+ });
362
+ writeFileSync(path, result.output);
363
+ return result.exitCode === 0;
364
+ }
365
+ __name(runStep, "runStep");
366
+ async function discardWorktree(opts, wt) {
367
+ try {
368
+ await removeWorktree(wt.worktreePath, { force: true });
369
+ await deleteBranch(opts.repoRoot, wt.branchName);
370
+ } catch (err) {
371
+ opts.log(
372
+ ` Failed to remove ${wt.worktreePath}: ` + (err instanceof Error ? err.message : String(err))
373
+ );
374
+ }
375
+ }
376
+ __name(discardWorktree, "discardWorktree");
377
+ async function runReviewer(opts, deps, range) {
378
+ return runWithRetries({
379
+ maxAttempts: opts.retries + 1,
380
+ retryDelay: opts.retryDelay,
381
+ agentCmd: opts.agentCmd,
382
+ effort: opts.effort,
383
+ label: "review",
384
+ log: opts.log
385
+ }, async (attempt, effort) => {
386
+ const wt = await createNamedWorktree({
387
+ repoRoot: opts.repoRoot,
388
+ repoName: opts.repoName,
389
+ baseBranch: opts.baseBranch,
390
+ name: "review"
391
+ });
392
+ try {
393
+ if (opts.installCmd && !await runStep(
394
+ opts,
395
+ deps,
396
+ opts.installCmd,
397
+ wt.worktreePath,
398
+ logPath(opts.logsDir, "review", attempt, ".install.log")
399
+ )) {
400
+ opts.log(" review: install failed");
401
+ return { result: null, stop: false };
402
+ }
403
+ ensureNotesIgnored(wt.worktreePath);
404
+ await deps.spawnAgent({
405
+ prompt: renderReviewPrompt({
406
+ specBody: opts.specBody,
407
+ ...range,
408
+ exclude: reviewExcludeFor(
409
+ opts.repoRoot,
410
+ opts.reviewPath
411
+ )
412
+ }),
413
+ agentCmd: opts.agentCmd,
414
+ model: opts.model,
415
+ timeout: opts.timeout,
416
+ logPath: logPath(opts.logsDir, "review", attempt, ".log"),
417
+ cwd: wt.worktreePath,
418
+ label: "review",
419
+ maxTurns: opts.maxTurns,
420
+ effort,
421
+ onLine: opts.log
422
+ });
423
+ const text = collectNotes(wt.worktreePath, FINDINGS_FILE);
424
+ const findings = text === null ? null : parseFindings(text);
425
+ if (findings === null) {
426
+ opts.log(" review: no usable findings file");
427
+ }
428
+ return { result: findings, stop: findings !== null };
429
+ } finally {
430
+ await discardWorktree(opts, wt);
431
+ }
432
+ });
433
+ }
434
+ __name(runReviewer, "runReviewer");
435
+ async function runFixer(opts, deps, findings) {
436
+ const unchecked = findings.map((f) => ({ ...f, checked: false }));
437
+ let previous;
438
+ return runWithRetries({
439
+ maxAttempts: opts.retries + 1,
440
+ retryDelay: opts.retryDelay,
441
+ agentCmd: opts.agentCmd,
442
+ effort: opts.effort,
443
+ label: "review fix",
444
+ log: opts.log
445
+ }, async (attempt, effort) => {
446
+ const fail = /* @__PURE__ */ __name((reason, exitCode, notes) => {
447
+ previous = { reason, notes };
448
+ opts.log(` review fix: ${FIX_FAILURE_TEXT[reason]}`);
449
+ return {
450
+ result: {
451
+ merged: false,
452
+ findings: unchecked,
453
+ attempts: attempt,
454
+ effort,
455
+ exitCode,
456
+ result: FIX_FAILURE_TEXT[reason],
457
+ notes
458
+ },
459
+ stop: false
460
+ };
461
+ }, "fail");
462
+ const wt = await createNamedWorktree({
463
+ repoRoot: opts.repoRoot,
464
+ repoName: opts.repoName,
465
+ baseBranch: opts.baseBranch,
466
+ name: "review-fix"
467
+ });
468
+ try {
469
+ if (opts.installCmd && !await runStep(
470
+ opts,
471
+ deps,
472
+ opts.installCmd,
473
+ wt.worktreePath,
474
+ logPath(
475
+ opts.logsDir,
476
+ "review-fix",
477
+ attempt,
478
+ ".install.log"
479
+ )
480
+ )) {
481
+ return fail("install", null, null);
482
+ }
483
+ ensureNotesIgnored(wt.worktreePath);
484
+ const agent = await deps.spawnAgent({
485
+ prompt: renderFixPrompt({
486
+ specBody: opts.specBody,
487
+ findings,
488
+ testCmd: opts.testCmd,
489
+ installCmd: opts.installCmd,
490
+ previous
491
+ }),
492
+ agentCmd: opts.agentCmd,
493
+ model: opts.model,
494
+ timeout: opts.timeout,
495
+ logPath: logPath(
496
+ opts.logsDir,
497
+ "review-fix",
498
+ attempt,
499
+ ".log"
500
+ ),
501
+ cwd: wt.worktreePath,
502
+ label: "review-fix",
503
+ maxTurns: opts.maxTurns,
504
+ effort,
505
+ onLine: opts.log
506
+ });
507
+ const notes = collectNotes(wt.worktreePath);
508
+ if (!agent.timedOut && agent.exitCode !== 0) {
509
+ return fail("exit", agent.exitCode, notes);
510
+ }
511
+ if (isDirty(wt.worktreePath)) {
512
+ await commitDirty(
513
+ wt.worktreePath,
514
+ "nightralph: auto-commit review fix remaining changes"
515
+ );
516
+ }
517
+ if (!hasNewCommits(
518
+ opts.repoRoot,
519
+ opts.baseBranch,
520
+ wt.branchName
521
+ )) {
522
+ return fail("no-commits", agent.exitCode, notes);
523
+ }
524
+ if (opts.testCmd === "") {
525
+ opts.log(" review fix: no test gate");
526
+ } else if (!await runStep(
527
+ opts,
528
+ deps,
529
+ opts.testCmd,
530
+ wt.worktreePath,
531
+ logPath(
532
+ opts.logsDir,
533
+ "review-fix",
534
+ attempt,
535
+ ".test.log"
536
+ )
537
+ )) {
538
+ return fail("tests", agent.exitCode, notes);
539
+ }
540
+ const merged = await mergeBranch(
541
+ opts.repoRoot,
542
+ wt.branchName
543
+ );
544
+ if (!merged.success) {
545
+ return fail(
546
+ "merge",
547
+ agent.exitCode,
548
+ [notes, merged.stderr].filter(Boolean).join("\n\n") || null
549
+ );
550
+ }
551
+ opts.log(" review fix: merged");
552
+ return {
553
+ result: {
554
+ merged: true,
555
+ findings: markFixed(findings, agent.completedItems),
556
+ attempts: attempt,
557
+ effort,
558
+ exitCode: agent.exitCode,
559
+ result: "merged",
560
+ notes
561
+ },
562
+ stop: true
563
+ };
564
+ } finally {
565
+ await discardWorktree(opts, wt);
566
+ }
567
+ });
568
+ }
569
+ __name(runFixer, "runFixer");
570
+ function readReviewEntries(path) {
571
+ if (!existsSync(path)) return [];
572
+ return parseReviewEntries(readFileSync(path, "utf8"));
573
+ }
574
+ __name(readReviewEntries, "readReviewEntries");
575
+ async function runReviewPass(opts, deps) {
576
+ const now = opts.now ?? (() => /* @__PURE__ */ new Date());
577
+ const to = headCommit(opts.repoRoot);
578
+ const forkPoint = opts.forkedFrom ? mergeBase(opts.repoRoot, opts.forkedFrom, to) : null;
579
+ const from = selectReviewBase(readReviewEntries(opts.reviewPath), {
580
+ isAncestor: /* @__PURE__ */ __name((sha) => isAncestor(opts.repoRoot, sha, to), "isAncestor"),
581
+ forkPoint,
582
+ launchCommit: opts.launchCommit
583
+ });
584
+ const exclude = reviewExcludeFor(opts.repoRoot, opts.reviewPath);
585
+ if (!hasChangesOutside(opts.repoRoot, from, to, exclude)) {
586
+ opts.log("\nReview: nothing new to review");
587
+ return { status: "skipped", findings: 0, fixed: 0 };
588
+ }
589
+ opts.log(`
590
+ Reviewing ${from.slice(0, 7)}..${to.slice(0, 7)}`);
591
+ const findings = await runReviewer(opts, deps, { from, to });
592
+ let entry;
593
+ if (findings === null) {
594
+ entry = {
595
+ from,
596
+ to,
597
+ status: "review-failed",
598
+ now: now(),
599
+ findings: []
600
+ };
601
+ } else if (findings.length === 0) {
602
+ entry = { from, to, status: "clean", now: now(), findings: [] };
603
+ } else {
604
+ opts.log(
605
+ `Review: ${findings.length} finding${findings.length === 1 ? "" : "s"}`
606
+ );
607
+ const fix = await runFixer(opts, deps, findings);
608
+ entry = {
609
+ from,
610
+ // A merged fix counts as reviewed
611
+ to: fix.merged ? headCommit(opts.repoRoot) : to,
612
+ status: fix.merged ? "fixed" : "unresolved",
613
+ now: now(),
614
+ findings: fix.findings,
615
+ fix: {
616
+ attempts: fix.attempts,
617
+ maxAttempts: opts.retries + 1,
618
+ effort: fix.effort,
619
+ exitCode: fix.exitCode,
620
+ result: fix.result,
621
+ notes: fix.notes
622
+ }
623
+ };
624
+ }
625
+ appendEntry(opts.reviewPath, renderReviewEntry(entry));
626
+ await commitProgress(
627
+ opts.repoRoot,
628
+ opts.reviewPath,
629
+ "nightralph: record review"
630
+ );
631
+ return {
632
+ status: entry.status,
633
+ findings: entry.findings.length,
634
+ fixed: entry.findings.filter((f) => f.checked).length
635
+ };
636
+ }
637
+ __name(runReviewPass, "runReviewPass");
638
+ export {
639
+ FINDINGS_FILE,
640
+ FINDINGS_PATH,
641
+ FIX_FAILURE_TEXT,
642
+ FIX_MARKER,
643
+ NO_FINDINGS,
644
+ REVIEW_EXCLUDE,
645
+ REVIEW_MARKER,
646
+ REVIEW_STATUSES,
647
+ markFixed,
648
+ parseFindings,
649
+ parseReviewEntries,
650
+ readReviewEntries,
651
+ renderFixPrompt,
652
+ renderReviewEntry,
653
+ renderReviewPrompt,
654
+ renderReviewSummary,
655
+ reviewDiffCommand,
656
+ reviewExcludeFor,
657
+ runFixer,
658
+ runReviewPass,
659
+ runReviewer,
660
+ selectReviewBase
661
+ };
662
+ //# sourceMappingURL=review.js.map