nightralph 0.0.54 → 0.0.56

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/dist/resolve.js CHANGED
@@ -5,7 +5,16 @@ import { dirname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { checkbox, Separator } from "@inquirer/prompts";
7
7
  import { isSpecComplete } from "./progress.js";
8
- import { scanTickets, findReadyTickets } from "./orchestrator.js";
8
+ import {
9
+ scanTickets,
10
+ findReadyTickets,
11
+ blockerLabel
12
+ } from "./orchestrator.js";
13
+ import { ticketLabel } from "./display.js";
14
+ import {
15
+ parseBlockerField,
16
+ resolveBlockers
17
+ } from "./blockers.js";
9
18
  function getScriptDir(metaUrl) {
10
19
  return dirname(fileURLToPath(metaUrl));
11
20
  }
@@ -47,6 +56,86 @@ function findFeatureDirs(cwd) {
47
56
  return results;
48
57
  }
49
58
  __name(findFeatureDirs, "findFeatureDirs");
59
+ function knownFeatureNames(scratchDir) {
60
+ const names = /* @__PURE__ */ new Set();
61
+ const walk = /* @__PURE__ */ __name((rel) => {
62
+ const dir = rel === "" ? scratchDir : join(scratchDir, rel);
63
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
64
+ if (!e.isDirectory() || e.name === "issues") continue;
65
+ const name = rel === "" ? e.name : `${rel}/${e.name}`;
66
+ if (existsSync(join(scratchDir, name, "issues"))) {
67
+ names.add(name);
68
+ }
69
+ walk(name);
70
+ }
71
+ }, "walk");
72
+ if (existsSync(scratchDir)) walk("");
73
+ return names;
74
+ }
75
+ __name(knownFeatureNames, "knownFeatureNames");
76
+ function loadSelection(scratchDir, names) {
77
+ const known = knownFeatureNames(scratchDir);
78
+ const issuesDir = /* @__PURE__ */ __name((name) => join(scratchDir, name, "issues"), "issuesDir");
79
+ const tickets = [];
80
+ const selected = /* @__PURE__ */ new Map();
81
+ for (const name of new Set(names)) {
82
+ const dir = issuesDir(name);
83
+ const found = existsSync(dir) ? scanTickets(dir, name, known) : [];
84
+ selected.set(name, new Set(found.map((t) => t.num)));
85
+ tickets.push(...found);
86
+ }
87
+ const outside = /* @__PURE__ */ new Map();
88
+ const lookup = /* @__PURE__ */ __name((feature, num) => {
89
+ if (!known.has(feature)) return void 0;
90
+ let statuses = outside.get(feature);
91
+ if (!statuses) {
92
+ statuses = new Map(
93
+ scanTickets(issuesDir(feature), feature).map((t) => [t.num, t.status])
94
+ );
95
+ outside.set(feature, statuses);
96
+ }
97
+ return statuses.get(num);
98
+ }, "lookup");
99
+ const warnings = [];
100
+ for (const t of tickets) {
101
+ const parsed = parseBlockerField(t.body, known);
102
+ t.blockers = resolveBlockers(
103
+ parsed.refs,
104
+ t.feature,
105
+ selected,
106
+ lookup
107
+ );
108
+ const ticket = ticketLabel(t);
109
+ for (const word of parsed.typos) {
110
+ warnings.push({ ticket, kind: "typo", text: word });
111
+ }
112
+ for (const ref of t.blockers) {
113
+ if (ref.where !== "missing") continue;
114
+ warnings.push({
115
+ ticket,
116
+ kind: "missing",
117
+ text: blockerLabel(ref)
118
+ });
119
+ }
120
+ }
121
+ return { tickets, warnings };
122
+ }
123
+ __name(loadSelection, "loadSelection");
124
+ function featureCounts(scratchDir, name) {
125
+ const { tickets, warnings } = loadSelection(scratchDir, [name]);
126
+ return {
127
+ ready: findReadyTickets(tickets).length,
128
+ total: tickets.length,
129
+ badBlockers: warnings.filter((w) => w.kind === "missing").length
130
+ };
131
+ }
132
+ __name(featureCounts, "featureCounts");
133
+ function featureChoiceLabel(scratchDir, name) {
134
+ const c = featureCounts(scratchDir, name);
135
+ const bad = c.badBlockers === 0 ? "" : `, ${c.badBlockers} bad blocker` + (c.badBlockers === 1 ? "" : "s");
136
+ return `${name} (${c.ready}/${c.total} ready${bad})`;
137
+ }
138
+ __name(featureChoiceLabel, "featureChoiceLabel");
50
139
  const ALL_VALUE = "__all__";
51
140
  async function resolveIssuesDir(specName, cwd = process.cwd()) {
52
141
  if (specName) {
@@ -69,14 +158,11 @@ async function resolveIssuesDir(specName, cwd = process.cwd()) {
69
158
  console.log(`Using feature: ${f.name}`);
70
159
  return [{ dir: f.dir, spec: f.spec, name: f.name }];
71
160
  }
72
- const featureChoices = features.map((f) => {
73
- const tickets = scanTickets(f.dir);
74
- const ready = findReadyTickets(tickets).length;
75
- return {
76
- name: `${f.name} (${ready}/${tickets.length} ready)`,
77
- value: f.name
78
- };
79
- });
161
+ const scratchDir = join(cwd, ".scratch");
162
+ const featureChoices = features.map((f) => ({
163
+ name: featureChoiceLabel(scratchDir, f.name),
164
+ value: f.name
165
+ }));
80
166
  while (true) {
81
167
  const selected = await checkbox({
82
168
  message: "Which features?",
@@ -101,8 +187,11 @@ async function resolveIssuesDir(specName, cwd = process.cwd()) {
101
187
  }
102
188
  __name(resolveIssuesDir, "resolveIssuesDir");
103
189
  export {
190
+ featureChoiceLabel,
191
+ featureCounts,
104
192
  findFeatureDirs,
105
193
  getScriptDir,
194
+ loadSelection,
106
195
  resolveIssuesDir
107
196
  };
108
197
  //# sourceMappingURL=resolve.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/resolve.ts"],
4
- "sourcesContent": ["import { existsSync, readdirSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { checkbox, Separator } from '@inquirer/prompts'\nimport { isSpecComplete } from './progress.js'\nimport { scanTickets, findReadyTickets } from './orchestrator.js'\n\nexport type FeatureSelection = {\n dir:string;\n spec:string;\n name:string;\n}\n\nexport function getScriptDir (metaUrl:string):string {\n return dirname(fileURLToPath(metaUrl))\n}\n\nexport function findFeatureDirs (cwd:string):{\n name:string;\n dir:string;\n spec:string\n}[] {\n const scratchDir = join(cwd, '.scratch')\n if (!existsSync(scratchDir)) return []\n const entries = readdirSync(\n scratchDir, { withFileTypes:true }\n )\n const results:{ name:string; dir:string; spec:string }[] = []\n for (const entry of entries) {\n if (!entry.isDirectory()) continue\n const issuesDir = join(\n scratchDir, entry.name, 'issues'\n )\n const specFile = join(\n scratchDir, entry.name, 'spec.md'\n )\n if (!existsSync(issuesDir)) continue\n const progressFile = join(\n scratchDir, entry.name, 'progress.md'\n )\n if (isSpecComplete(progressFile)) continue\n results.push({\n name:entry.name,\n dir:issuesDir,\n spec:specFile,\n })\n }\n results.sort((a, b) => a.name.localeCompare(b.name))\n return results\n}\n\nconst ALL_VALUE = '__all__'\n\nexport async function resolveIssuesDir (\n specName:string|undefined,\n cwd:string = process.cwd(),\n):Promise<FeatureSelection[]> {\n if (specName) {\n const featureDir = join(cwd, '.scratch', specName)\n return [{\n dir:join(featureDir, 'issues'),\n spec:join(featureDir, 'spec.md'),\n name:specName,\n }]\n }\n\n const features = findFeatureDirs(cwd)\n\n if (features.length === 0) {\n console.error(\n 'No .scratch/<feature>/issues/ directories ' +\n 'found.\\nProvide --spec <feature-name>.'\n )\n process.exit(2)\n }\n\n if (features.length === 1) {\n const f = features[0]\n console.log(`Using feature: ${f.name}`)\n return [{ dir:f.dir, spec:f.spec, name:f.name }]\n }\n\n const featureChoices = features.map(f => {\n const tickets = scanTickets(f.dir)\n const ready = findReadyTickets(tickets).length\n return {\n name:`${f.name} (${ready}/${tickets.length} ready)`,\n value:f.name,\n }\n })\n\n while (true) {\n const selected = await checkbox({\n message:'Which features?',\n choices:[\n { name:'All', value:ALL_VALUE },\n new Separator(),\n ...featureChoices,\n ],\n })\n\n if (selected.length === 0) {\n console.log('Select at least one feature')\n continue\n }\n\n const names = selected.includes(ALL_VALUE) ?\n features.map(f => f.name) :\n selected\n\n return names.map(name => {\n const f = features.find(\n ff => ff.name === name\n )!\n return { dir:f.dir, spec:f.spec, name:f.name }\n })\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { existsSync, readdirSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { checkbox, Separator } from '@inquirer/prompts'\nimport { isSpecComplete } from './progress.js'\nimport {\n scanTickets,\n findReadyTickets,\n blockerLabel,\n type Ticket,\n} from './orchestrator.js'\nimport { ticketLabel } from './display.js'\nimport {\n parseBlockerField,\n resolveBlockers,\n type BlockerWarning,\n} from './blockers.js'\n\nexport type { BlockerWarning }\n\nexport type FeatureSelection = {\n dir:string;\n spec:string;\n name:string;\n}\n\nexport function getScriptDir (metaUrl:string):string {\n return dirname(fileURLToPath(metaUrl))\n}\n\nexport function findFeatureDirs (cwd:string):{\n name:string;\n dir:string;\n spec:string\n}[] {\n const scratchDir = join(cwd, '.scratch')\n if (!existsSync(scratchDir)) return []\n const entries = readdirSync(\n scratchDir, { withFileTypes:true }\n )\n const results:{ name:string; dir:string; spec:string }[] = []\n for (const entry of entries) {\n if (!entry.isDirectory()) continue\n const issuesDir = join(\n scratchDir, entry.name, 'issues'\n )\n const specFile = join(\n scratchDir, entry.name, 'spec.md'\n )\n if (!existsSync(issuesDir)) continue\n const progressFile = join(\n scratchDir, entry.name, 'progress.md'\n )\n if (isSpecComplete(progressFile)) continue\n results.push({\n name:entry.name,\n dir:issuesDir,\n spec:specFile,\n })\n }\n results.sort((a, b) => a.name.localeCompare(b.name))\n return results\n}\n\nexport type LoadedSelection = {\n tickets:Ticket[];\n warnings:BlockerWarning[];\n}\n\n// Names of the features under scratchDir: dirs with an issues\n// subdir, complete or not, at any depth. A nested feature's name\n// is its path, e.g. `rollout/billing`, the same name `--spec`\n// takes.\nfunction knownFeatureNames (scratchDir:string):Set<string> {\n const names = new Set<string>()\n const walk = (rel:string) => {\n const dir = rel === '' ? scratchDir : join(scratchDir, rel)\n for (const e of readdirSync(dir, { withFileTypes:true })) {\n if (!e.isDirectory() || e.name === 'issues') continue\n const name = rel === '' ? e.name : `${rel}/${e.name}`\n if (existsSync(join(scratchDir, name, 'issues'))) {\n names.add(name)\n }\n walk(name)\n }\n }\n if (existsSync(scratchDir)) walk('')\n return names\n}\n\n/**\n * Tickets of the named features with every blocker resolved, and\n * the problems found on the way. An unselected feature is scanned\n * only when a ref points into it, once.\n */\nexport function loadSelection (\n scratchDir:string,\n names:ReadonlyArray<string>\n):LoadedSelection {\n const known = knownFeatureNames(scratchDir)\n const issuesDir = (name:string) => join(scratchDir, name, 'issues')\n\n const tickets:Ticket[] = []\n const selected = new Map<string, Set<number>>()\n for (const name of new Set(names)) {\n const dir = issuesDir(name)\n const found = existsSync(dir) ?\n scanTickets(dir, name, known) :\n []\n selected.set(name, new Set(found.map(t => t.num)))\n tickets.push(...found)\n }\n\n const outside = new Map<string, Map<number, string>>()\n const lookup = (feature:string, num:number) => {\n if (!known.has(feature)) return undefined\n let statuses = outside.get(feature)\n if (!statuses) {\n statuses = new Map(\n scanTickets(issuesDir(feature), feature)\n .map(t => [t.num, t.status])\n )\n outside.set(feature, statuses)\n }\n return statuses.get(num)\n }\n\n const warnings:BlockerWarning[] = []\n for (const t of tickets) {\n const parsed = parseBlockerField(t.body, known)\n t.blockers = resolveBlockers(\n parsed.refs, t.feature, selected, lookup\n )\n const ticket = ticketLabel(t)\n for (const word of parsed.typos) {\n warnings.push({ ticket, kind:'typo', text:word })\n }\n for (const ref of t.blockers) {\n if (ref.where !== 'missing') continue\n warnings.push({\n ticket, kind:'missing', text:blockerLabel(ref),\n })\n }\n }\n return { tickets, warnings }\n}\n\nexport type FeatureCounts = {\n ready:number;\n total:number;\n badBlockers:number; // refs that match no ticket\n}\n\n/**\n * Counts for one feature as if it ran alone: the same\n * loadSelection a `--spec <name>` run uses.\n */\nexport function featureCounts (\n scratchDir:string,\n name:string\n):FeatureCounts {\n const { tickets, warnings } = loadSelection(scratchDir, [name])\n return {\n ready:findReadyTickets(tickets).length,\n total:tickets.length,\n badBlockers:warnings.filter(w => w.kind === 'missing').length,\n }\n}\n\nexport function featureChoiceLabel (\n scratchDir:string,\n name:string\n):string {\n const c = featureCounts(scratchDir, name)\n const bad = c.badBlockers === 0 ?\n '' :\n `, ${c.badBlockers} bad blocker` +\n (c.badBlockers === 1 ? '' : 's')\n return `${name} (${c.ready}/${c.total} ready${bad})`\n}\n\nconst ALL_VALUE = '__all__'\n\nexport async function resolveIssuesDir (\n specName:string|undefined,\n cwd:string = process.cwd(),\n):Promise<FeatureSelection[]> {\n if (specName) {\n const featureDir = join(cwd, '.scratch', specName)\n return [{\n dir:join(featureDir, 'issues'),\n spec:join(featureDir, 'spec.md'),\n name:specName,\n }]\n }\n\n const features = findFeatureDirs(cwd)\n\n if (features.length === 0) {\n console.error(\n 'No .scratch/<feature>/issues/ directories ' +\n 'found.\\nProvide --spec <feature-name>.'\n )\n process.exit(2)\n }\n\n if (features.length === 1) {\n const f = features[0]\n console.log(`Using feature: ${f.name}`)\n return [{ dir:f.dir, spec:f.spec, name:f.name }]\n }\n\n const scratchDir = join(cwd, '.scratch')\n const featureChoices = features.map(f => ({\n name:featureChoiceLabel(scratchDir, f.name),\n value:f.name,\n }))\n\n while (true) {\n const selected = await checkbox({\n message:'Which features?',\n choices:[\n { name:'All', value:ALL_VALUE },\n new Separator(),\n ...featureChoices,\n ],\n })\n\n if (selected.length === 0) {\n console.log('Select at least one feature')\n continue\n }\n\n const names = selected.includes(ALL_VALUE) ?\n features.map(f => f.name) :\n selected\n\n return names.map(name => {\n const f = features.find(\n ff => ff.name === name\n )!\n return { dir:f.dir, spec:f.spec, name:f.name }\n })\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/dist/review.js CHANGED
@@ -28,6 +28,7 @@ import { mergeBranch } from "./merge.js";
28
28
  import {
29
29
  readDocsRule,
30
30
  installRule,
31
+ subagentRule,
31
32
  testGateRule,
32
33
  LINT_RULE,
33
34
  integrityRule,
@@ -75,6 +76,7 @@ function renderReviewPrompt(opts) {
75
76
  "- 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
77
  "- Look for correctness bugs, missing or weak tests, and code that breaks a documented contract. Ignore style preferences.",
77
78
  "- You may run the tests or the type checker to confirm a suspicion.",
79
+ ...subagentRule(opts.preferSubagent, true),
78
80
  "- Do NOT edit, stage, or commit code. Anything you change here is discarded.",
79
81
  ...CONFINEMENT_RULES,
80
82
  "",
@@ -129,6 +131,7 @@ function renderFixPrompt(opts) {
129
131
  readDocsRule("findings"),
130
132
  "- Fix ONLY the listed findings. Do not fix unrelated problems you notice.",
131
133
  ...installRule(opts.installCmd),
134
+ ...subagentRule(opts.preferSubagent),
132
135
  "- 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
136
  testGateRule(opts.testCmd, "fix"),
134
137
  LINT_RULE,
@@ -408,7 +411,8 @@ async function runReviewer(opts, deps, range) {
408
411
  exclude: reviewExcludeFor(
409
412
  opts.repoRoot,
410
413
  opts.reviewPath
411
- )
414
+ ),
415
+ preferSubagent: opts.preferSubagent
412
416
  }),
413
417
  agentCmd: opts.agentCmd,
414
418
  model: opts.model,
@@ -418,6 +422,7 @@ async function runReviewer(opts, deps, range) {
418
422
  label: "review",
419
423
  maxTurns: opts.maxTurns,
420
424
  effort,
425
+ preferSubagent: opts.preferSubagent,
421
426
  onLine: opts.log
422
427
  });
423
428
  const text = collectNotes(wt.worktreePath, FINDINGS_FILE);
@@ -487,7 +492,8 @@ async function runFixer(opts, deps, findings) {
487
492
  findings,
488
493
  testCmd: opts.testCmd,
489
494
  installCmd: opts.installCmd,
490
- previous
495
+ previous,
496
+ preferSubagent: opts.preferSubagent
491
497
  }),
492
498
  agentCmd: opts.agentCmd,
493
499
  model: opts.model,
@@ -502,6 +508,7 @@ async function runFixer(opts, deps, findings) {
502
508
  label: "review-fix",
503
509
  maxTurns: opts.maxTurns,
504
510
  effort,
511
+ preferSubagent: opts.preferSubagent,
505
512
  onLine: opts.log
506
513
  });
507
514
  const notes = collectNotes(wt.worktreePath);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/review.ts"],
4
- "sourcesContent": ["import { existsSync, readFileSync, writeFileSync } from 'node:fs'\nimport { basename, dirname, join, relative } from 'node:path'\nimport {\n NOTES_PATH,\n formatDate,\n collectNotes,\n ensureNotesIgnored,\n appendEntry,\n} from './learnings.js'\nimport { runTestCmd } from './testcmd.js'\nimport { commitProgress } from './progress.js'\nimport { runWithRetries, type EffortLevel } from './retry.js'\nimport {\n createNamedWorktree,\n deleteBranch,\n removeWorktree,\n commitDirty,\n hasNewCommits,\n isDirty,\n hasChangesOutside,\n headCommit,\n isAncestor,\n mergeBase,\n} from './worktree.js'\nimport { mergeBranch } from './merge.js'\nimport type { AgentResult } from './orchestrator.js'\nimport {\n readDocsRule,\n installRule,\n testGateRule,\n LINT_RULE,\n integrityRule,\n CONFINEMENT_RULES,\n commitRule,\n notesRule,\n agentsMdRule,\n} from './rules.js'\n\nexport const FINDINGS_FILE = 'findings.md'\nexport const FINDINGS_PATH = `${dirname(NOTES_PATH)}/${FINDINGS_FILE}`\nexport const NO_FINDINGS = 'No findings.'\n// Bookkeeping lives here (tickets, progress, learnings, the\n// review log), so the review never looks at it.\nexport const REVIEW_EXCLUDE = '.scratch'\n\nexport const REVIEW_MARKER = '---- REVIEW INSTRUCTIONS ----'\nexport const FIX_MARKER = '---- REVIEW FINDINGS ----'\n\n// The bookkeeping directory for a review log: the nearest ancestor\n// of reviewPath named .scratch, relative to the repo root, so a run\n// launched from a subdirectory excludes the right path. Falls back\n// to REVIEW_EXCLUDE when reviewPath is not under a .scratch.\nexport function reviewExcludeFor (\n repoRoot:string,\n reviewPath:string\n):string {\n let dir = dirname(reviewPath)\n while (dirname(dir) !== dir) {\n if (basename(dir) === '.scratch') {\n const rel = relative(repoRoot, dir)\n if (rel && !rel.startsWith('..')) return rel\n break\n }\n dir = dirname(dir)\n }\n return REVIEW_EXCLUDE\n}\n\nexport function reviewDiffCommand (\n from:string,\n to:string,\n exclude:string = REVIEW_EXCLUDE\n):string {\n return `git diff ${from}..${to} -- . ':(exclude)${exclude}'`\n}\n\nexport function renderReviewPrompt (opts:{\n specBody:string\n from:string\n to:string\n exclude?:string\n}):string {\n return [\n '---- SPEC CONTEXT ----',\n opts.specBody,\n '',\n REVIEW_MARKER,\n 'You are reviewing work that autonomous agents merged ' +\n 'into this branch, one ticket at a time. Review the ' +\n 'combined change as a whole.',\n '',\n 'The change to review:',\n '',\n ` ${reviewDiffCommand(opts.from, opts.to, opts.exclude)}`,\n '',\n 'Follow these rules:',\n '- Read any AGENTS.md in or above the directories the ' +\n 'change touches, plus CONTEXT.md and the relevant ' +\n 'docs/adr/ entries if they exist. Treat their ' +\n 'contracts and the spec above as the standard the ' +\n 'code must meet.',\n '- Look for correctness bugs, missing or weak tests, ' +\n 'and code that breaks a documented contract. Ignore ' +\n 'style preferences.',\n '- You may run the tests or the type checker to confirm ' +\n 'a suspicion.',\n '- Do NOT edit, stage, or commit code. Anything you ' +\n 'change here is discarded.',\n ...CONFINEMENT_RULES,\n '',\n `Write your findings to \\`${FINDINGS_PATH}\\` (create the ` +\n 'directory), most severe first, in exactly this form:',\n '',\n '- [ ] <one-line summary of the finding>',\n ' <file:line, and a concrete input or state that goes ' +\n 'wrong>',\n '',\n 'If you find nothing worth fixing, write the single line: ' +\n NO_FINDINGS,\n 'Never git add or commit that file; the orchestrator ' +\n 'collects it.',\n '',\n ].join('\\n')\n}\n\nexport type FixFailure =\n 'install'|'exit'|'no-commits'|'tests'|'merge'\n\nexport const FIX_FAILURE_TEXT:Record<FixFailure, string> = {\n install:'dependency install failed',\n exit:'the agent exited with an error',\n 'no-commits':'the agent made no commits',\n tests:'the test command failed after the agent finished',\n merge:'the fix branch did not merge cleanly',\n}\n\nfunction findingLines (findings:ReadonlyArray<Finding>):string[] {\n return findings.flatMap(f => [\n `- [ ] ${f.summary}`,\n ...(f.detail ?\n f.detail.split('\\n').map(l => ` ${l}`) :\n []),\n ])\n}\n\nfunction previousBlock (\n previous?:{ reason:FixFailure; notes:string|null }\n):string[] {\n if (!previous) return []\n return [\n '---- PREVIOUS FIX ATTEMPT ----',\n `It failed: ${FIX_FAILURE_TEXT[previous.reason]}.`,\n previous.notes ?? 'It left no notes.',\n '',\n ]\n}\n\nexport function renderFixPrompt (opts:{\n specBody:string\n findings:ReadonlyArray<Finding>\n testCmd:string\n installCmd?:string\n previous?:{ reason:FixFailure; notes:string|null }\n}):string {\n return [\n '---- SPEC CONTEXT ----',\n opts.specBody,\n '',\n FIX_MARKER,\n ...findingLines(opts.findings),\n '',\n ...previousBlock(opts.previous),\n '---- AGENT INSTRUCTIONS ----',\n 'You are an autonomous coding agent. Fix the review ' +\n 'findings above using /tdd.',\n '',\n 'Follow these rules:',\n readDocsRule('findings'),\n '- Fix ONLY the listed findings. Do not fix unrelated ' +\n 'problems you notice.',\n ...installRule(opts.installCmd),\n '- Use /tdd for every finding: write a failing test that ' +\n 'shows the problem, then write only enough code to ' +\n 'make it pass, then refactor.',\n testGateRule(opts.testCmd, 'fix'),\n LINT_RULE,\n integrityRule('finding'),\n ...CONFINEMENT_RULES,\n commitRule('finding'),\n notesRule('finding', 'fix'),\n '- After fixing each finding, print a line: ' +\n '[x] <finding summary> (matching the summary ' +\n 'text exactly).',\n '- If a finding is wrong or cannot be fixed honestly, ' +\n 'leave it unchecked and explain why in your notes ' +\n 'file.',\n agentsMdRule('fix'),\n '',\n ].join('\\n')\n}\n\nconst SENTINEL_PREFIX = '<!-- nightralph:review'\nconst SENTINEL_RE = /^<!-- nightralph:review (.*) -->$/\nconst SHA_RE = /^[0-9a-f]{4,64}$/\nconst FIX_HEADING = '### Fix'\nconst FINDING_RE = /^ ?[-*+]\\s+\\[([ xX])\\]\\s+(.+)$/\n\nexport const REVIEW_STATUSES = [\n 'clean', 'fixed', 'unresolved', 'review-failed',\n] as const\n\nexport type ReviewStatus = typeof REVIEW_STATUSES[number]\n\nexport type Finding = {\n summary:string\n detail:string\n checked:boolean\n}\n\nexport type FixRecord = {\n attempts:number\n maxAttempts:number\n effort?:string\n exitCode:number|null\n result:string\n notes:string|null\n}\n\nexport type ReviewEntryInput = {\n from:string\n to:string\n status:ReviewStatus\n now:Date\n findings:Finding[]\n fix?:FixRecord\n}\n\nexport type ReviewEntry = {\n from:string\n to:string\n status:ReviewStatus\n findings:Finding[]\n body:string\n}\n\nexport type ReviewOutcome = {\n status:ReviewStatus|'skipped'\n findings:number\n fixed:number\n}\n\nfunction isReviewStatus (value:string):value is ReviewStatus {\n return (REVIEW_STATUSES as readonly string[]).includes(value)\n}\n\nfunction collapse (s:string):string {\n return s.replace(/\\s+/g, ' ').trim()\n}\n\nfunction normalize (s:string):string {\n return collapse(s).toLowerCase()\n}\n\nfunction sanitize (text:string):string {\n return text.replace(\n new RegExp(SENTINEL_PREFIX, 'g'),\n '<!-- nightralph-review'\n )\n}\n\nfunction splitLines (text:string):string[] {\n return text.split('\\n').map(line => {\n return line.endsWith('\\r') ? line.slice(0, -1) : line\n })\n}\n\n// The reviewer's checklist: `- [ ] summary` lines, each followed\n// by its detail lines. [] for an explicit \"No findings.\", null\n// when the text is neither, which fails the review attempt.\nexport function parseFindings (text:string):Finding[]|null {\n const findings:Finding[] = []\n const details:string[][] = []\n for (const line of splitLines(text)) {\n const m = line.match(FINDING_RE)\n if (m) {\n findings.push({\n summary:collapse(m[2]),\n detail:'',\n checked:m[1] !== ' ',\n })\n details.push([])\n } else if (findings.length > 0 && line.trim()) {\n details[details.length - 1].push(line.trim())\n }\n }\n if (findings.length > 0) {\n return findings.map((f, i) => {\n return { ...f, detail:details[i].join('\\n') }\n })\n }\n const clean = splitLines(text).some(line => {\n return normalize(line) === NO_FINDINGS.toLowerCase()\n })\n return clean ? [] : null\n}\n\n// Checks off the findings the fixer echoed as `[x] <summary>`,\n// compared loosely like ticket checklist items.\nexport function markFixed (\n findings:ReadonlyArray<Finding>,\n completedItems:ReadonlyArray<string>\n):Finding[] {\n const done = new Set(completedItems.map(normalize))\n return findings.map(f => {\n return { ...f, checked:done.has(normalize(f.summary)) }\n })\n}\n\nfunction headline (input:ReviewEntryInput):string {\n const n = input.findings.length\n const fixed = input.findings.filter(f => f.checked).length\n switch (input.status) {\n case 'clean': return 'no findings'\n case 'fixed': return `${fixed} of ${n} fixed`\n case 'unresolved':\n return `${n} finding${n === 1 ? '' : 's'}, fix failed`\n case 'review-failed': return 'review failed'\n default: {\n const _exhaustive:never = input.status\n return _exhaustive\n }\n }\n}\n\nfunction renderFindings (input:ReviewEntryInput):string[] {\n if (input.status === 'review-failed') {\n return ['The reviewer left no usable findings file.']\n }\n if (input.findings.length === 0) return [NO_FINDINGS]\n return input.findings.flatMap(f => {\n const box = f.checked ? '[x]' : '[ ]'\n const detail = f.detail ?\n f.detail.split('\\n').map(l => ` ${sanitize(l)}`) :\n []\n return [`- ${box} ${sanitize(collapse(f.summary))}`, ...detail]\n })\n}\n\nfunction renderFix (fix?:FixRecord):string[] {\n if (!fix) return []\n return [\n '',\n FIX_HEADING,\n `- attempts: ${fix.attempts} of ${fix.maxAttempts}`,\n ...(fix.effort ? [`- effort: ${fix.effort}`] : []),\n `- agent exit code: ${fix.exitCode ?? 'none'}`,\n `- result: ${sanitize(fix.result)}`,\n '',\n fix.notes ? sanitize(fix.notes) : 'The fixer left no notes.',\n ]\n}\n\nexport function renderReviewEntry (input:ReviewEntryInput):string {\n return [\n `${SENTINEL_PREFIX} from=${input.from} to=${input.to} ` +\n `status=${input.status} -->`,\n `## ${formatDate(input.now)} -- review ` +\n `${input.from.slice(0, 7)}..${input.to.slice(0, 7)} ` +\n `-- ${headline(input)}`,\n ...renderFindings(input),\n ...renderFix(input.fix),\n '---',\n '',\n ].join('\\n')\n}\n\nfunction parseEntry (\n sentinel:string,\n bodyLines:string[]\n):ReviewEntry|null {\n const attrs:Record<string, string> = {}\n for (const part of sentinel.split(/\\s+/).filter(Boolean)) {\n const idx = part.indexOf('=')\n if (idx > 0) attrs[part.slice(0, idx)] = part.slice(idx + 1)\n }\n const { from, to, status } = attrs\n if (!from || !SHA_RE.test(from)) return null\n if (!to || !SHA_RE.test(to)) return null\n if (!status || !isReviewStatus(status)) return null\n\n const lines = [...bodyLines]\n while (lines.length > 0 && !lines[lines.length - 1].trim()) {\n lines.pop()\n }\n if (lines[lines.length - 1] === '---') lines.pop()\n const fixAt = lines.indexOf(FIX_HEADING)\n const findingLines = fixAt === -1 ? lines : lines.slice(0, fixAt)\n return {\n from,\n to,\n status,\n findings:parseFindings(findingLines.join('\\n')) ?? [],\n body:lines.join('\\n').trim(),\n }\n}\n\n// Malformed entries are dropped, not thrown; text before the\n// first sentinel is ignored.\nexport function parseReviewEntries (text:string):ReviewEntry[] {\n const entries:ReviewEntry[] = []\n let sentinel:string|null = null\n let body:string[] = []\n const flush = ():void => {\n if (sentinel === null) return\n const entry = parseEntry(sentinel, body)\n if (entry) entries.push(entry)\n }\n for (const line of splitLines(text)) {\n const m = line.match(SENTINEL_RE)\n if (m) {\n flush()\n sentinel = m[1]\n body = []\n } else if (sentinel !== null) {\n body.push(line)\n }\n }\n flush()\n return entries\n}\n\n// Where the review range starts: the last reviewed commit while\n// it is still on this branch, else the fork point from the\n// default branch, else the commit the run started from.\nexport function selectReviewBase (\n entries:ReadonlyArray<ReviewEntry>,\n opts:{\n isAncestor:(sha:string) => boolean\n forkPoint:string|null\n launchCommit:string\n }\n):string {\n const last = [...entries].reverse().find(e => {\n return e.status !== 'review-failed'\n })\n if (last && opts.isAncestor(last.to)) return last.to\n return opts.forkPoint ?? opts.launchCommit\n}\n\n// One line for the finish summary; null when no review ran.\nexport function renderReviewSummary (\n outcome:ReviewOutcome,\n reviewPath:string\n):string|null {\n const see = ` -- see ${reviewPath}`\n const plural = outcome.findings === 1 ? '' : 's'\n switch (outcome.status) {\n case 'skipped': return null\n case 'clean': return `Review: no findings${see}`\n case 'fixed':\n return `Review: ${outcome.findings} finding${plural}, ` +\n `${outcome.fixed} fixed${see}`\n case 'unresolved':\n return `Review: ${outcome.findings} finding${plural}, ` +\n `fix failed${see}`\n case 'review-failed':\n return `Review: the reviewer failed${see}`\n default: {\n const _exhaustive:never = outcome.status\n return _exhaustive\n }\n }\n}\n\nexport type SpawnAgentFn = (opts:{\n prompt:string\n agentCmd:string\n model:string|null\n timeout:number\n logPath:string\n cwd?:string\n label?:string\n maxTurns?:number\n effort?:EffortLevel\n onLine?:(line:string) => void\n}) => Promise<AgentResult>\n\nexport type ReviewDeps = {\n spawnAgent:SpawnAgentFn\n // running child pids, so SIGINT also kills installs and tests\n registry?:Set<number>\n}\n\nexport type ReviewPassOpts = {\n repoRoot:string\n repoName:string\n // the branch the run merged into; the fix merges here too\n baseBranch:string\n reviewPath:string\n // the default branch, when baseBranch is an integration branch\n forkedFrom?:string\n launchCommit:string\n specBody:string\n agentCmd:string\n model:string|null\n timeout:number\n logsDir:string\n testCmd:string\n installCmd?:string\n maxTurns?:number\n effort?:EffortLevel\n retries:number\n retryDelay:number\n log:(msg:string) => void\n now?:() => Date\n}\n\n// Same naming as ticket logs: the first attempt keeps the plain\n// name, later attempts get their own file.\nfunction logPath (\n logsDir:string,\n stem:string,\n attempt:number,\n ext:string\n):string {\n const suffix = attempt > 1 ? `.attempt${attempt}` : ''\n return join(logsDir, `${stem}${suffix}${ext}`)\n}\n\n// Runs a shell step (install or tests) in a worktree, writing its\n// output to `path`; true only on exit 0.\nasync function runStep (\n opts:ReviewPassOpts,\n deps:ReviewDeps,\n cmd:string,\n cwd:string,\n path:string\n):Promise<boolean> {\n const result = await runTestCmd({\n cmd,\n cwd,\n timeout:opts.timeout,\n registry:deps.registry,\n })\n writeFileSync(path, result.output)\n return result.exitCode === 0\n}\n\nasync function discardWorktree (\n opts:ReviewPassOpts,\n wt:{ worktreePath:string; branchName:string }\n):Promise<void> {\n try {\n await removeWorktree(wt.worktreePath, { force:true })\n await deleteBranch(opts.repoRoot, wt.branchName)\n } catch (err) {\n opts.log(\n ` Failed to remove ${wt.worktreePath}: ` +\n (err instanceof Error ? err.message : String(err))\n )\n }\n}\n\n// Asks an agent to review from..to and returns its findings:\n// [] when clean, null when no attempt produced a usable findings\n// file. The worktree and anything the reviewer changed in it are\n// always discarded.\nexport async function runReviewer (\n opts:ReviewPassOpts,\n deps:ReviewDeps,\n range:{ from:string; to:string }\n):Promise<Finding[]|null> {\n return runWithRetries({\n maxAttempts:opts.retries + 1,\n retryDelay:opts.retryDelay,\n agentCmd:opts.agentCmd,\n effort:opts.effort,\n label:'review',\n log:opts.log,\n }, async (attempt, effort) => {\n const wt = await createNamedWorktree({\n repoRoot:opts.repoRoot,\n repoName:opts.repoName,\n baseBranch:opts.baseBranch,\n name:'review',\n })\n try {\n if (opts.installCmd && !(await runStep(\n opts, deps, opts.installCmd, wt.worktreePath,\n logPath(opts.logsDir, 'review', attempt, '.install.log')\n ))) {\n opts.log(' review: install failed')\n return { result:null, stop:false }\n }\n ensureNotesIgnored(wt.worktreePath)\n await deps.spawnAgent({\n prompt:renderReviewPrompt({\n specBody:opts.specBody,\n ...range,\n exclude:reviewExcludeFor(\n opts.repoRoot, opts.reviewPath\n ),\n }),\n agentCmd:opts.agentCmd,\n model:opts.model,\n timeout:opts.timeout,\n logPath:logPath(opts.logsDir, 'review', attempt, '.log'),\n cwd:wt.worktreePath,\n label:'review',\n maxTurns:opts.maxTurns,\n effort,\n onLine:opts.log,\n })\n const text = collectNotes(wt.worktreePath, FINDINGS_FILE)\n const findings = text === null ? null : parseFindings(text)\n if (findings === null) {\n opts.log(' review: no usable findings file')\n }\n return { result:findings, stop:findings !== null }\n } finally {\n await discardWorktree(opts, wt)\n }\n })\n}\n\nexport type FixResult = {\n merged:boolean\n // checked per the fixer's [x] lines when merged; all\n // unchecked otherwise\n findings:Finding[]\n attempts:number\n effort?:EffortLevel\n exitCode:number|null\n result:string\n notes:string|null\n}\n\n// Asks an agent to fix the findings in a fresh worktree off the\n// branch tip. Its work merges only after a green test gate;\n// every failure is retried like a ticket.\nexport async function runFixer (\n opts:ReviewPassOpts,\n deps:ReviewDeps,\n findings:ReadonlyArray<Finding>\n):Promise<FixResult> {\n const unchecked = findings.map(f => ({ ...f, checked:false }))\n let previous:{ reason:FixFailure; notes:string|null }|undefined\n\n return runWithRetries<FixResult>({\n maxAttempts:opts.retries + 1,\n retryDelay:opts.retryDelay,\n agentCmd:opts.agentCmd,\n effort:opts.effort,\n label:'review fix',\n log:opts.log,\n }, async (attempt, effort) => {\n const fail = (\n reason:FixFailure,\n exitCode:number|null,\n notes:string|null\n ) => {\n previous = { reason, notes }\n opts.log(` review fix: ${FIX_FAILURE_TEXT[reason]}`)\n return {\n result:{\n merged:false,\n findings:unchecked,\n attempts:attempt,\n effort,\n exitCode,\n result:FIX_FAILURE_TEXT[reason],\n notes,\n },\n stop:false,\n }\n }\n\n const wt = await createNamedWorktree({\n repoRoot:opts.repoRoot,\n repoName:opts.repoName,\n baseBranch:opts.baseBranch,\n name:'review-fix',\n })\n try {\n if (opts.installCmd && !(await runStep(\n opts, deps, opts.installCmd, wt.worktreePath,\n logPath(\n opts.logsDir, 'review-fix', attempt,\n '.install.log'\n )\n ))) {\n return fail('install', null, null)\n }\n ensureNotesIgnored(wt.worktreePath)\n const agent = await deps.spawnAgent({\n prompt:renderFixPrompt({\n specBody:opts.specBody,\n findings,\n testCmd:opts.testCmd,\n installCmd:opts.installCmd,\n previous,\n }),\n agentCmd:opts.agentCmd,\n model:opts.model,\n timeout:opts.timeout,\n logPath:logPath(\n opts.logsDir, 'review-fix', attempt, '.log'\n ),\n cwd:wt.worktreePath,\n label:'review-fix',\n maxTurns:opts.maxTurns,\n effort,\n onLine:opts.log,\n })\n // Before any auto-commit, so notes never get committed\n const notes = collectNotes(wt.worktreePath)\n\n if (!agent.timedOut && agent.exitCode !== 0) {\n return fail('exit', agent.exitCode, notes)\n }\n if (isDirty(wt.worktreePath)) {\n await commitDirty(\n wt.worktreePath,\n 'nightralph: auto-commit review fix ' +\n 'remaining changes'\n )\n }\n if (!hasNewCommits(\n opts.repoRoot, opts.baseBranch, wt.branchName\n )) {\n return fail('no-commits', agent.exitCode, notes)\n }\n if (opts.testCmd === '') {\n opts.log(' review fix: no test gate')\n } else if (!(await runStep(\n opts, deps, opts.testCmd, wt.worktreePath,\n logPath(\n opts.logsDir, 'review-fix', attempt, '.test.log'\n )\n ))) {\n return fail('tests', agent.exitCode, notes)\n }\n const merged = await mergeBranch(\n opts.repoRoot, wt.branchName\n )\n if (!merged.success) {\n return fail(\n 'merge',\n agent.exitCode,\n [notes, merged.stderr].filter(Boolean)\n .join('\\n\\n') || null\n )\n }\n opts.log(' review fix: merged')\n return {\n result:{\n merged:true,\n findings:markFixed(findings, agent.completedItems),\n attempts:attempt,\n effort,\n exitCode:agent.exitCode,\n result:'merged',\n notes,\n },\n stop:true,\n }\n } finally {\n await discardWorktree(opts, wt)\n }\n })\n}\n\nexport function readReviewEntries (path:string):ReviewEntry[] {\n if (!existsSync(path)) return []\n return parseReviewEntries(readFileSync(path, 'utf8'))\n}\n\n// Reviews everything since the last reviewed commit, runs one fix\n// pass when there are findings, and appends and commits one entry\n// to the review log. Skips entirely when the range only touches\n// bookkeeping under REVIEW_EXCLUDE.\nexport async function runReviewPass (\n opts:ReviewPassOpts,\n deps:ReviewDeps\n):Promise<ReviewOutcome> {\n const now = opts.now ?? (() => new Date())\n const to = headCommit(opts.repoRoot)\n const forkPoint = opts.forkedFrom ?\n mergeBase(opts.repoRoot, opts.forkedFrom, to) :\n null\n const from = selectReviewBase(readReviewEntries(opts.reviewPath), {\n isAncestor:sha => isAncestor(opts.repoRoot, sha, to),\n forkPoint,\n launchCommit:opts.launchCommit,\n })\n const exclude = reviewExcludeFor(opts.repoRoot, opts.reviewPath)\n if (!hasChangesOutside(opts.repoRoot, from, to, exclude)) {\n opts.log('\\nReview: nothing new to review')\n return { status:'skipped', findings:0, fixed:0 }\n }\n\n opts.log(`\\nReviewing ${from.slice(0, 7)}..${to.slice(0, 7)}`)\n const findings = await runReviewer(opts, deps, { from, to })\n\n let entry:ReviewEntryInput\n if (findings === null) {\n entry = {\n from,\n to,\n status:'review-failed',\n now:now(),\n findings:[]\n }\n } else if (findings.length === 0) {\n entry = { from, to, status:'clean', now:now(), findings:[] }\n } else {\n opts.log(\n `Review: ${findings.length} finding` +\n `${findings.length === 1 ? '' : 's'}`\n )\n const fix = await runFixer(opts, deps, findings)\n entry = {\n from,\n // A merged fix counts as reviewed\n to:fix.merged ? headCommit(opts.repoRoot) : to,\n status:fix.merged ? 'fixed' : 'unresolved',\n now:now(),\n findings:fix.findings,\n fix:{\n attempts:fix.attempts,\n maxAttempts:opts.retries + 1,\n effort:fix.effort,\n exitCode:fix.exitCode,\n result:fix.result,\n notes:fix.notes,\n },\n }\n }\n\n appendEntry(opts.reviewPath, renderReviewEntry(entry))\n await commitProgress(\n opts.repoRoot, opts.reviewPath, 'nightralph: record review'\n )\n return {\n status:entry.status,\n findings:entry.findings.length,\n fixed:entry.findings.filter(f => f.checked).length,\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { existsSync, readFileSync, writeFileSync } from 'node:fs'\nimport { basename, dirname, join, relative } from 'node:path'\nimport {\n NOTES_PATH,\n formatDate,\n collectNotes,\n ensureNotesIgnored,\n appendEntry,\n} from './learnings.js'\nimport { runTestCmd } from './testcmd.js'\nimport { commitProgress } from './progress.js'\nimport { runWithRetries, type EffortLevel } from './retry.js'\nimport {\n createNamedWorktree,\n deleteBranch,\n removeWorktree,\n commitDirty,\n hasNewCommits,\n isDirty,\n hasChangesOutside,\n headCommit,\n isAncestor,\n mergeBase,\n} from './worktree.js'\nimport { mergeBranch } from './merge.js'\nimport type { AgentResult } from './orchestrator.js'\nimport {\n readDocsRule,\n installRule,\n subagentRule,\n testGateRule,\n LINT_RULE,\n integrityRule,\n CONFINEMENT_RULES,\n commitRule,\n notesRule,\n agentsMdRule,\n} from './rules.js'\n\nexport const FINDINGS_FILE = 'findings.md'\nexport const FINDINGS_PATH = `${dirname(NOTES_PATH)}/${FINDINGS_FILE}`\nexport const NO_FINDINGS = 'No findings.'\n// Bookkeeping lives here (tickets, progress, learnings, the\n// review log), so the review never looks at it.\nexport const REVIEW_EXCLUDE = '.scratch'\n\nexport const REVIEW_MARKER = '---- REVIEW INSTRUCTIONS ----'\nexport const FIX_MARKER = '---- REVIEW FINDINGS ----'\n\n// The bookkeeping directory for a review log: the nearest ancestor\n// of reviewPath named .scratch, relative to the repo root, so a run\n// launched from a subdirectory excludes the right path. Falls back\n// to REVIEW_EXCLUDE when reviewPath is not under a .scratch.\nexport function reviewExcludeFor (\n repoRoot:string,\n reviewPath:string\n):string {\n let dir = dirname(reviewPath)\n while (dirname(dir) !== dir) {\n if (basename(dir) === '.scratch') {\n const rel = relative(repoRoot, dir)\n if (rel && !rel.startsWith('..')) return rel\n break\n }\n dir = dirname(dir)\n }\n return REVIEW_EXCLUDE\n}\n\nexport function reviewDiffCommand (\n from:string,\n to:string,\n exclude:string = REVIEW_EXCLUDE\n):string {\n return `git diff ${from}..${to} -- . ':(exclude)${exclude}'`\n}\n\nexport function renderReviewPrompt (opts:{\n specBody:string\n from:string\n to:string\n exclude?:string\n preferSubagent?:string\n}):string {\n return [\n '---- SPEC CONTEXT ----',\n opts.specBody,\n '',\n REVIEW_MARKER,\n 'You are reviewing work that autonomous agents merged ' +\n 'into this branch, one ticket at a time. Review the ' +\n 'combined change as a whole.',\n '',\n 'The change to review:',\n '',\n ` ${reviewDiffCommand(opts.from, opts.to, opts.exclude)}`,\n '',\n 'Follow these rules:',\n '- Read any AGENTS.md in or above the directories the ' +\n 'change touches, plus CONTEXT.md and the relevant ' +\n 'docs/adr/ entries if they exist. Treat their ' +\n 'contracts and the spec above as the standard the ' +\n 'code must meet.',\n '- Look for correctness bugs, missing or weak tests, ' +\n 'and code that breaks a documented contract. Ignore ' +\n 'style preferences.',\n '- You may run the tests or the type checker to confirm ' +\n 'a suspicion.',\n ...subagentRule(opts.preferSubagent, true),\n '- Do NOT edit, stage, or commit code. Anything you ' +\n 'change here is discarded.',\n ...CONFINEMENT_RULES,\n '',\n `Write your findings to \\`${FINDINGS_PATH}\\` (create the ` +\n 'directory), most severe first, in exactly this form:',\n '',\n '- [ ] <one-line summary of the finding>',\n ' <file:line, and a concrete input or state that goes ' +\n 'wrong>',\n '',\n 'If you find nothing worth fixing, write the single line: ' +\n NO_FINDINGS,\n 'Never git add or commit that file; the orchestrator ' +\n 'collects it.',\n '',\n ].join('\\n')\n}\n\nexport type FixFailure =\n 'install'|'exit'|'no-commits'|'tests'|'merge'\n\nexport const FIX_FAILURE_TEXT:Record<FixFailure, string> = {\n install:'dependency install failed',\n exit:'the agent exited with an error',\n 'no-commits':'the agent made no commits',\n tests:'the test command failed after the agent finished',\n merge:'the fix branch did not merge cleanly',\n}\n\nfunction findingLines (findings:ReadonlyArray<Finding>):string[] {\n return findings.flatMap(f => [\n `- [ ] ${f.summary}`,\n ...(f.detail ?\n f.detail.split('\\n').map(l => ` ${l}`) :\n []),\n ])\n}\n\nfunction previousBlock (\n previous?:{ reason:FixFailure; notes:string|null }\n):string[] {\n if (!previous) return []\n return [\n '---- PREVIOUS FIX ATTEMPT ----',\n `It failed: ${FIX_FAILURE_TEXT[previous.reason]}.`,\n previous.notes ?? 'It left no notes.',\n '',\n ]\n}\n\nexport function renderFixPrompt (opts:{\n specBody:string\n findings:ReadonlyArray<Finding>\n testCmd:string\n installCmd?:string\n preferSubagent?:string\n previous?:{ reason:FixFailure; notes:string|null }\n}):string {\n return [\n '---- SPEC CONTEXT ----',\n opts.specBody,\n '',\n FIX_MARKER,\n ...findingLines(opts.findings),\n '',\n ...previousBlock(opts.previous),\n '---- AGENT INSTRUCTIONS ----',\n 'You are an autonomous coding agent. Fix the review ' +\n 'findings above using /tdd.',\n '',\n 'Follow these rules:',\n readDocsRule('findings'),\n '- Fix ONLY the listed findings. Do not fix unrelated ' +\n 'problems you notice.',\n ...installRule(opts.installCmd),\n ...subagentRule(opts.preferSubagent),\n '- Use /tdd for every finding: write a failing test that ' +\n 'shows the problem, then write only enough code to ' +\n 'make it pass, then refactor.',\n testGateRule(opts.testCmd, 'fix'),\n LINT_RULE,\n integrityRule('finding'),\n ...CONFINEMENT_RULES,\n commitRule('finding'),\n notesRule('finding', 'fix'),\n '- After fixing each finding, print a line: ' +\n '[x] <finding summary> (matching the summary ' +\n 'text exactly).',\n '- If a finding is wrong or cannot be fixed honestly, ' +\n 'leave it unchecked and explain why in your notes ' +\n 'file.',\n agentsMdRule('fix'),\n '',\n ].join('\\n')\n}\n\nconst SENTINEL_PREFIX = '<!-- nightralph:review'\nconst SENTINEL_RE = /^<!-- nightralph:review (.*) -->$/\nconst SHA_RE = /^[0-9a-f]{4,64}$/\nconst FIX_HEADING = '### Fix'\nconst FINDING_RE = /^ ?[-*+]\\s+\\[([ xX])\\]\\s+(.+)$/\n\nexport const REVIEW_STATUSES = [\n 'clean', 'fixed', 'unresolved', 'review-failed',\n] as const\n\nexport type ReviewStatus = typeof REVIEW_STATUSES[number]\n\nexport type Finding = {\n summary:string\n detail:string\n checked:boolean\n}\n\nexport type FixRecord = {\n attempts:number\n maxAttempts:number\n effort?:string\n exitCode:number|null\n result:string\n notes:string|null\n}\n\nexport type ReviewEntryInput = {\n from:string\n to:string\n status:ReviewStatus\n now:Date\n findings:Finding[]\n fix?:FixRecord\n}\n\nexport type ReviewEntry = {\n from:string\n to:string\n status:ReviewStatus\n findings:Finding[]\n body:string\n}\n\nexport type ReviewOutcome = {\n status:ReviewStatus|'skipped'\n findings:number\n fixed:number\n}\n\nfunction isReviewStatus (value:string):value is ReviewStatus {\n return (REVIEW_STATUSES as readonly string[]).includes(value)\n}\n\nfunction collapse (s:string):string {\n return s.replace(/\\s+/g, ' ').trim()\n}\n\nfunction normalize (s:string):string {\n return collapse(s).toLowerCase()\n}\n\nfunction sanitize (text:string):string {\n return text.replace(\n new RegExp(SENTINEL_PREFIX, 'g'),\n '<!-- nightralph-review'\n )\n}\n\nfunction splitLines (text:string):string[] {\n return text.split('\\n').map(line => {\n return line.endsWith('\\r') ? line.slice(0, -1) : line\n })\n}\n\n// The reviewer's checklist: `- [ ] summary` lines, each followed\n// by its detail lines. [] for an explicit \"No findings.\", null\n// when the text is neither, which fails the review attempt.\nexport function parseFindings (text:string):Finding[]|null {\n const findings:Finding[] = []\n const details:string[][] = []\n for (const line of splitLines(text)) {\n const m = line.match(FINDING_RE)\n if (m) {\n findings.push({\n summary:collapse(m[2]),\n detail:'',\n checked:m[1] !== ' ',\n })\n details.push([])\n } else if (findings.length > 0 && line.trim()) {\n details[details.length - 1].push(line.trim())\n }\n }\n if (findings.length > 0) {\n return findings.map((f, i) => {\n return { ...f, detail:details[i].join('\\n') }\n })\n }\n const clean = splitLines(text).some(line => {\n return normalize(line) === NO_FINDINGS.toLowerCase()\n })\n return clean ? [] : null\n}\n\n// Checks off the findings the fixer echoed as `[x] <summary>`,\n// compared loosely like ticket checklist items.\nexport function markFixed (\n findings:ReadonlyArray<Finding>,\n completedItems:ReadonlyArray<string>\n):Finding[] {\n const done = new Set(completedItems.map(normalize))\n return findings.map(f => {\n return { ...f, checked:done.has(normalize(f.summary)) }\n })\n}\n\nfunction headline (input:ReviewEntryInput):string {\n const n = input.findings.length\n const fixed = input.findings.filter(f => f.checked).length\n switch (input.status) {\n case 'clean': return 'no findings'\n case 'fixed': return `${fixed} of ${n} fixed`\n case 'unresolved':\n return `${n} finding${n === 1 ? '' : 's'}, fix failed`\n case 'review-failed': return 'review failed'\n default: {\n const _exhaustive:never = input.status\n return _exhaustive\n }\n }\n}\n\nfunction renderFindings (input:ReviewEntryInput):string[] {\n if (input.status === 'review-failed') {\n return ['The reviewer left no usable findings file.']\n }\n if (input.findings.length === 0) return [NO_FINDINGS]\n return input.findings.flatMap(f => {\n const box = f.checked ? '[x]' : '[ ]'\n const detail = f.detail ?\n f.detail.split('\\n').map(l => ` ${sanitize(l)}`) :\n []\n return [`- ${box} ${sanitize(collapse(f.summary))}`, ...detail]\n })\n}\n\nfunction renderFix (fix?:FixRecord):string[] {\n if (!fix) return []\n return [\n '',\n FIX_HEADING,\n `- attempts: ${fix.attempts} of ${fix.maxAttempts}`,\n ...(fix.effort ? [`- effort: ${fix.effort}`] : []),\n `- agent exit code: ${fix.exitCode ?? 'none'}`,\n `- result: ${sanitize(fix.result)}`,\n '',\n fix.notes ? sanitize(fix.notes) : 'The fixer left no notes.',\n ]\n}\n\nexport function renderReviewEntry (input:ReviewEntryInput):string {\n return [\n `${SENTINEL_PREFIX} from=${input.from} to=${input.to} ` +\n `status=${input.status} -->`,\n `## ${formatDate(input.now)} -- review ` +\n `${input.from.slice(0, 7)}..${input.to.slice(0, 7)} ` +\n `-- ${headline(input)}`,\n ...renderFindings(input),\n ...renderFix(input.fix),\n '---',\n '',\n ].join('\\n')\n}\n\nfunction parseEntry (\n sentinel:string,\n bodyLines:string[]\n):ReviewEntry|null {\n const attrs:Record<string, string> = {}\n for (const part of sentinel.split(/\\s+/).filter(Boolean)) {\n const idx = part.indexOf('=')\n if (idx > 0) attrs[part.slice(0, idx)] = part.slice(idx + 1)\n }\n const { from, to, status } = attrs\n if (!from || !SHA_RE.test(from)) return null\n if (!to || !SHA_RE.test(to)) return null\n if (!status || !isReviewStatus(status)) return null\n\n const lines = [...bodyLines]\n while (lines.length > 0 && !lines[lines.length - 1].trim()) {\n lines.pop()\n }\n if (lines[lines.length - 1] === '---') lines.pop()\n const fixAt = lines.indexOf(FIX_HEADING)\n const findingLines = fixAt === -1 ? lines : lines.slice(0, fixAt)\n return {\n from,\n to,\n status,\n findings:parseFindings(findingLines.join('\\n')) ?? [],\n body:lines.join('\\n').trim(),\n }\n}\n\n// Malformed entries are dropped, not thrown; text before the\n// first sentinel is ignored.\nexport function parseReviewEntries (text:string):ReviewEntry[] {\n const entries:ReviewEntry[] = []\n let sentinel:string|null = null\n let body:string[] = []\n const flush = ():void => {\n if (sentinel === null) return\n const entry = parseEntry(sentinel, body)\n if (entry) entries.push(entry)\n }\n for (const line of splitLines(text)) {\n const m = line.match(SENTINEL_RE)\n if (m) {\n flush()\n sentinel = m[1]\n body = []\n } else if (sentinel !== null) {\n body.push(line)\n }\n }\n flush()\n return entries\n}\n\n// Where the review range starts: the last reviewed commit while\n// it is still on this branch, else the fork point from the\n// default branch, else the commit the run started from.\nexport function selectReviewBase (\n entries:ReadonlyArray<ReviewEntry>,\n opts:{\n isAncestor:(sha:string) => boolean\n forkPoint:string|null\n launchCommit:string\n }\n):string {\n const last = [...entries].reverse().find(e => {\n return e.status !== 'review-failed'\n })\n if (last && opts.isAncestor(last.to)) return last.to\n return opts.forkPoint ?? opts.launchCommit\n}\n\n// One line for the finish summary; null when no review ran.\nexport function renderReviewSummary (\n outcome:ReviewOutcome,\n reviewPath:string\n):string|null {\n const see = ` -- see ${reviewPath}`\n const plural = outcome.findings === 1 ? '' : 's'\n switch (outcome.status) {\n case 'skipped': return null\n case 'clean': return `Review: no findings${see}`\n case 'fixed':\n return `Review: ${outcome.findings} finding${plural}, ` +\n `${outcome.fixed} fixed${see}`\n case 'unresolved':\n return `Review: ${outcome.findings} finding${plural}, ` +\n `fix failed${see}`\n case 'review-failed':\n return `Review: the reviewer failed${see}`\n default: {\n const _exhaustive:never = outcome.status\n return _exhaustive\n }\n }\n}\n\nexport type SpawnAgentFn = (opts:{\n prompt:string\n agentCmd:string\n model:string|null\n timeout:number\n logPath:string\n cwd?:string\n label?:string\n maxTurns?:number\n effort?:EffortLevel\n // claude only: model for subagents; unset = off\n preferSubagent?:string\n onLine?:(line:string) => void\n}) => Promise<AgentResult>\n\nexport type ReviewDeps = {\n spawnAgent:SpawnAgentFn\n // running child pids, so SIGINT also kills installs and tests\n registry?:Set<number>\n}\n\nexport type ReviewPassOpts = {\n repoRoot:string\n repoName:string\n // the branch the run merged into; the fix merges here too\n baseBranch:string\n reviewPath:string\n // the default branch, when baseBranch is an integration branch\n forkedFrom?:string\n launchCommit:string\n specBody:string\n agentCmd:string\n model:string|null\n timeout:number\n logsDir:string\n testCmd:string\n installCmd?:string\n maxTurns?:number\n effort?:EffortLevel\n // claude only: model for subagents; unset = off\n preferSubagent?:string\n retries:number\n retryDelay:number\n log:(msg:string) => void\n now?:() => Date\n}\n\n// Same naming as ticket logs: the first attempt keeps the plain\n// name, later attempts get their own file.\nfunction logPath (\n logsDir:string,\n stem:string,\n attempt:number,\n ext:string\n):string {\n const suffix = attempt > 1 ? `.attempt${attempt}` : ''\n return join(logsDir, `${stem}${suffix}${ext}`)\n}\n\n// Runs a shell step (install or tests) in a worktree, writing its\n// output to `path`; true only on exit 0.\nasync function runStep (\n opts:ReviewPassOpts,\n deps:ReviewDeps,\n cmd:string,\n cwd:string,\n path:string\n):Promise<boolean> {\n const result = await runTestCmd({\n cmd,\n cwd,\n timeout:opts.timeout,\n registry:deps.registry,\n })\n writeFileSync(path, result.output)\n return result.exitCode === 0\n}\n\nasync function discardWorktree (\n opts:ReviewPassOpts,\n wt:{ worktreePath:string; branchName:string }\n):Promise<void> {\n try {\n await removeWorktree(wt.worktreePath, { force:true })\n await deleteBranch(opts.repoRoot, wt.branchName)\n } catch (err) {\n opts.log(\n ` Failed to remove ${wt.worktreePath}: ` +\n (err instanceof Error ? err.message : String(err))\n )\n }\n}\n\n// Asks an agent to review from..to and returns its findings:\n// [] when clean, null when no attempt produced a usable findings\n// file. The worktree and anything the reviewer changed in it are\n// always discarded.\nexport async function runReviewer (\n opts:ReviewPassOpts,\n deps:ReviewDeps,\n range:{ from:string; to:string }\n):Promise<Finding[]|null> {\n return runWithRetries({\n maxAttempts:opts.retries + 1,\n retryDelay:opts.retryDelay,\n agentCmd:opts.agentCmd,\n effort:opts.effort,\n label:'review',\n log:opts.log,\n }, async (attempt, effort) => {\n const wt = await createNamedWorktree({\n repoRoot:opts.repoRoot,\n repoName:opts.repoName,\n baseBranch:opts.baseBranch,\n name:'review',\n })\n try {\n if (opts.installCmd && !(await runStep(\n opts, deps, opts.installCmd, wt.worktreePath,\n logPath(opts.logsDir, 'review', attempt, '.install.log')\n ))) {\n opts.log(' review: install failed')\n return { result:null, stop:false }\n }\n ensureNotesIgnored(wt.worktreePath)\n await deps.spawnAgent({\n prompt:renderReviewPrompt({\n specBody:opts.specBody,\n ...range,\n exclude:reviewExcludeFor(\n opts.repoRoot, opts.reviewPath\n ),\n preferSubagent:opts.preferSubagent,\n }),\n agentCmd:opts.agentCmd,\n model:opts.model,\n timeout:opts.timeout,\n logPath:logPath(opts.logsDir, 'review', attempt, '.log'),\n cwd:wt.worktreePath,\n label:'review',\n maxTurns:opts.maxTurns,\n effort,\n preferSubagent:opts.preferSubagent,\n onLine:opts.log,\n })\n const text = collectNotes(wt.worktreePath, FINDINGS_FILE)\n const findings = text === null ? null : parseFindings(text)\n if (findings === null) {\n opts.log(' review: no usable findings file')\n }\n return { result:findings, stop:findings !== null }\n } finally {\n await discardWorktree(opts, wt)\n }\n })\n}\n\nexport type FixResult = {\n merged:boolean\n // checked per the fixer's [x] lines when merged; all\n // unchecked otherwise\n findings:Finding[]\n attempts:number\n effort?:EffortLevel\n exitCode:number|null\n result:string\n notes:string|null\n}\n\n// Asks an agent to fix the findings in a fresh worktree off the\n// branch tip. Its work merges only after a green test gate;\n// every failure is retried like a ticket.\nexport async function runFixer (\n opts:ReviewPassOpts,\n deps:ReviewDeps,\n findings:ReadonlyArray<Finding>\n):Promise<FixResult> {\n const unchecked = findings.map(f => ({ ...f, checked:false }))\n let previous:{ reason:FixFailure; notes:string|null }|undefined\n\n return runWithRetries<FixResult>({\n maxAttempts:opts.retries + 1,\n retryDelay:opts.retryDelay,\n agentCmd:opts.agentCmd,\n effort:opts.effort,\n label:'review fix',\n log:opts.log,\n }, async (attempt, effort) => {\n const fail = (\n reason:FixFailure,\n exitCode:number|null,\n notes:string|null\n ) => {\n previous = { reason, notes }\n opts.log(` review fix: ${FIX_FAILURE_TEXT[reason]}`)\n return {\n result:{\n merged:false,\n findings:unchecked,\n attempts:attempt,\n effort,\n exitCode,\n result:FIX_FAILURE_TEXT[reason],\n notes,\n },\n stop:false,\n }\n }\n\n const wt = await createNamedWorktree({\n repoRoot:opts.repoRoot,\n repoName:opts.repoName,\n baseBranch:opts.baseBranch,\n name:'review-fix',\n })\n try {\n if (opts.installCmd && !(await runStep(\n opts, deps, opts.installCmd, wt.worktreePath,\n logPath(\n opts.logsDir, 'review-fix', attempt,\n '.install.log'\n )\n ))) {\n return fail('install', null, null)\n }\n ensureNotesIgnored(wt.worktreePath)\n const agent = await deps.spawnAgent({\n prompt:renderFixPrompt({\n specBody:opts.specBody,\n findings,\n testCmd:opts.testCmd,\n installCmd:opts.installCmd,\n previous,\n preferSubagent:opts.preferSubagent,\n }),\n agentCmd:opts.agentCmd,\n model:opts.model,\n timeout:opts.timeout,\n logPath:logPath(\n opts.logsDir, 'review-fix', attempt, '.log'\n ),\n cwd:wt.worktreePath,\n label:'review-fix',\n maxTurns:opts.maxTurns,\n effort,\n preferSubagent:opts.preferSubagent,\n onLine:opts.log,\n })\n // Before any auto-commit, so notes never get committed\n const notes = collectNotes(wt.worktreePath)\n\n if (!agent.timedOut && agent.exitCode !== 0) {\n return fail('exit', agent.exitCode, notes)\n }\n if (isDirty(wt.worktreePath)) {\n await commitDirty(\n wt.worktreePath,\n 'nightralph: auto-commit review fix ' +\n 'remaining changes'\n )\n }\n if (!hasNewCommits(\n opts.repoRoot, opts.baseBranch, wt.branchName\n )) {\n return fail('no-commits', agent.exitCode, notes)\n }\n if (opts.testCmd === '') {\n opts.log(' review fix: no test gate')\n } else if (!(await runStep(\n opts, deps, opts.testCmd, wt.worktreePath,\n logPath(\n opts.logsDir, 'review-fix', attempt, '.test.log'\n )\n ))) {\n return fail('tests', agent.exitCode, notes)\n }\n const merged = await mergeBranch(\n opts.repoRoot, wt.branchName\n )\n if (!merged.success) {\n return fail(\n 'merge',\n agent.exitCode,\n [notes, merged.stderr].filter(Boolean)\n .join('\\n\\n') || null\n )\n }\n opts.log(' review fix: merged')\n return {\n result:{\n merged:true,\n findings:markFixed(findings, agent.completedItems),\n attempts:attempt,\n effort,\n exitCode:agent.exitCode,\n result:'merged',\n notes,\n },\n stop:true,\n }\n } finally {\n await discardWorktree(opts, wt)\n }\n })\n}\n\nexport function readReviewEntries (path:string):ReviewEntry[] {\n if (!existsSync(path)) return []\n return parseReviewEntries(readFileSync(path, 'utf8'))\n}\n\n// Reviews everything since the last reviewed commit, runs one fix\n// pass when there are findings, and appends and commits one entry\n// to the review log. Skips entirely when the range only touches\n// bookkeeping under REVIEW_EXCLUDE.\nexport async function runReviewPass (\n opts:ReviewPassOpts,\n deps:ReviewDeps\n):Promise<ReviewOutcome> {\n const now = opts.now ?? (() => new Date())\n const to = headCommit(opts.repoRoot)\n const forkPoint = opts.forkedFrom ?\n mergeBase(opts.repoRoot, opts.forkedFrom, to) :\n null\n const from = selectReviewBase(readReviewEntries(opts.reviewPath), {\n isAncestor:sha => isAncestor(opts.repoRoot, sha, to),\n forkPoint,\n launchCommit:opts.launchCommit,\n })\n const exclude = reviewExcludeFor(opts.repoRoot, opts.reviewPath)\n if (!hasChangesOutside(opts.repoRoot, from, to, exclude)) {\n opts.log('\\nReview: nothing new to review')\n return { status:'skipped', findings:0, fixed:0 }\n }\n\n opts.log(`\\nReviewing ${from.slice(0, 7)}..${to.slice(0, 7)}`)\n const findings = await runReviewer(opts, deps, { from, to })\n\n let entry:ReviewEntryInput\n if (findings === null) {\n entry = {\n from,\n to,\n status:'review-failed',\n now:now(),\n findings:[]\n }\n } else if (findings.length === 0) {\n entry = { from, to, status:'clean', now:now(), findings:[] }\n } else {\n opts.log(\n `Review: ${findings.length} finding` +\n `${findings.length === 1 ? '' : 's'}`\n )\n const fix = await runFixer(opts, deps, findings)\n entry = {\n from,\n // A merged fix counts as reviewed\n to:fix.merged ? headCommit(opts.repoRoot) : to,\n status:fix.merged ? 'fixed' : 'unresolved',\n now:now(),\n findings:fix.findings,\n fix:{\n attempts:fix.attempts,\n maxAttempts:opts.retries + 1,\n effort:fix.effort,\n exitCode:fix.exitCode,\n result:fix.result,\n notes:fix.notes,\n },\n }\n }\n\n appendEntry(opts.reviewPath, renderReviewEntry(entry))\n await commitProgress(\n opts.repoRoot, opts.reviewPath, 'nightralph: record review'\n )\n return {\n status:entry.status,\n findings:entry.findings.length,\n fixed:entry.findings.filter(f => f.checked).length,\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["findingLines"]
7
7
  }
package/dist/rules.js CHANGED
@@ -12,6 +12,18 @@ function installRule(installCmd) {
12
12
  ];
13
13
  }
14
14
  __name(installRule, "installRule");
15
+ function subagentRule(preferSubagent, readOnly = false) {
16
+ if (!preferSubagent) return [];
17
+ return [
18
+ `- Hand read-only work to subagents: exploring the codebase, searching code, and experiments or test runs whose output would flood your context. Subagents default to the ${preferSubagent} model.`,
19
+ "- A subagent starts with no context. Brief each one with the objective, what you already know, its boundaries, and the reply you expect (file:line references, or pass/fail with the relevant output).",
20
+ ...readOnly ? [] : [
21
+ "- Subagents never edit files or run git commands that write. You make every edit and every commit."
22
+ ],
23
+ "- Do not delegate a lookup that takes a single tool call."
24
+ ];
25
+ }
26
+ __name(subagentRule, "subagentRule");
15
27
  function testGateRule(testCmd, subject) {
16
28
  if (testCmd === "") {
17
29
  return `- Before each commit, run the tests relevant to what you changed. Do not commit with failing tests. The orchestrator runs no test command for this ${subject}.`;
@@ -49,6 +61,7 @@ export {
49
61
  integrityRule,
50
62
  notesRule,
51
63
  readDocsRule,
64
+ subagentRule,
52
65
  testGateRule
53
66
  };
54
67
  //# sourceMappingURL=rules.js.map
package/dist/rules.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/rules.ts"],
4
- "sourcesContent": ["import { NOTES_PATH } from './learnings.js'\n\n// Rule lines shared by the agent prompts. `unit` is what the agent\n// commits one at a time (\"checklist item\", \"finding\"); `subject` is\n// what the orchestrator accepts or rejects (\"ticket\", \"fix\").\n\nexport function readDocsRule (subject:string):string {\n return '- Read the files directly relevant to the ' +\n `${subject} before writing code. Also read any ` +\n 'AGENTS.md in or above the directories you ' +\n 'will touch, plus CONTEXT.md and the ' +\n 'relevant docs/adr/ entries if they exist. ' +\n 'If they do not exist, proceed silently.'\n}\n\n// The rule for agents in a worktree the orchestrator has\n// already installed dependencies into. Empty without one.\nexport function installRule (installCmd?:string):string[] {\n if (!installCmd) return []\n return [\n '- Dependencies are already installed with ' +\n `\\`${installCmd}\\`. Do NOT reinstall them ` +\n 'with any other command; to reinstall, run ' +\n 'that same command. To add a dependency, use ' +\n 'the package manager that command uses, so ' +\n 'its lockfile stays in sync.',\n ]\n}\n\nexport function testGateRule (testCmd:string, subject:string):string {\n if (testCmd === '') {\n return '- Before each commit, run the tests relevant ' +\n 'to what you changed. Do not commit with failing ' +\n 'tests. The orchestrator runs no test command ' +\n `for this ${subject}.`\n }\n return '- While iterating, run only the tests ' +\n 'relevant to what you changed. Run ' +\n `\\`${testCmd}\\` in full once, as the ` +\n 'final gate before your last commit. Do ' +\n 'not commit with failing tests. The ' +\n 'orchestrator runs the same command after ' +\n `you exit and rejects the ${subject} if it ` +\n 'fails.'\n}\n\nexport const LINT_RULE = '- If the project has a lint script, run it ' +\n 'and fix what it reports.'\n\nexport function integrityRule (unit:string):string {\n return '- INTEGRITY: never delete, skip, weaken, or ' +\n 'write tautological tests to force a green ' +\n `result. Only report a ${unit} done ` +\n 'when its real tests genuinely pass. If an ' +\n 'item cannot be completed honestly, leave ' +\n 'it unchecked and record what blocked you ' +\n 'in your notes file.'\n}\n\nexport const CONFINEMENT_RULES:string[] = [\n '- Do NOT read or modify files outside ' +\n 'your current directory (the worktree).',\n '- Do NOT run git push.',\n]\n\nexport function commitRule (unit:string):string {\n return `- Commit after each ${unit}, and make ` +\n 'sure everything is committed when finished, ' +\n 'except your notes file.'\n}\n\nexport function notesRule (unit:string, subject:string):string {\n return `- Keep notes for this attempt in \\`${NOTES_PATH}\\` ` +\n '(create the directory). Update it after each ' +\n `${unit} and as soon as you hit a ` +\n 'blocker, not only at the end. Record what you ' +\n 'implemented, files changed, how you read any ' +\n 'vague acceptance criteria, what blocked you, and ' +\n `anything the next attempt at this ${subject} should ` +\n 'know. Never commit, git add, or delete this ' +\n 'file; the orchestrator collects it.'\n}\n\nexport function agentsMdRule (subject:string):string {\n return '- Before your final commit, record reusable ' +\n 'learnings in the nearest AGENTS.md (in or ' +\n 'above the directories you modified; ' +\n 'prefer that over the repo root). Good ' +\n 'additions: module conventions, gotchas, ' +\n 'files that must change together, how to ' +\n 'test that area, environment requirements. ' +\n `Do NOT add ${subject}-specific details or ` +\n 'temporary debugging notes. Only write ' +\n 'when you have knowledge that would help ' +\n 'future work in that directory. Reusable ' +\n 'patterns go in AGENTS.md; anything ' +\n `specific to this ${subject} goes in your ` +\n 'notes file.'\n}\n"],
5
- "mappings": ";;AAAA,SAAS,kBAAkB;AAMpB,SAAS,aAAc,SAAuB;AACjD,SAAO,6CACA,OAAO;AAKlB;AAPgB;AAWT,SAAS,YAAa,YAA6B;AACtD,MAAI,CAAC,WAAY,QAAO,CAAC;AACzB,SAAO;AAAA,IACH,+CACS,UAAU;AAAA,EAKvB;AACJ;AAVgB;AAYT,SAAS,aAAc,SAAgB,SAAuB;AACjE,MAAI,YAAY,IAAI;AAChB,WAAO,sJAGS,OAAO;AAAA,EAC3B;AACA,SAAO,6EAEE,OAAO,uKAIgB,OAAO;AAE3C;AAfgB;AAiBT,MAAM,YAAY;AAGlB,SAAS,cAAe,MAAoB;AAC/C,SAAO,+GAEsB,IAAI;AAKrC;AARgB;AAUT,MAAM,oBAA6B;AAAA,EACtC;AAAA,EAEA;AACJ;AAEO,SAAS,WAAY,MAAoB;AAC5C,SAAO,uBAAuB,IAAI;AAGtC;AAJgB;AAMT,SAAS,UAAW,MAAa,SAAuB;AAC3D,SAAO,sCAAsC,UAAU,mDAEhD,IAAI,2MAI8B,OAAO;AAGpD;AAVgB;AAYT,SAAS,aAAc,SAAuB;AACjD,SAAO,wSAOW,OAAO,kMAKD,OAAO;AAEnC;AAfgB;",
4
+ "sourcesContent": ["import { NOTES_PATH } from './learnings.js'\n\n// Rule lines shared by the agent prompts. `unit` is what the agent\n// commits one at a time (\"checklist item\", \"finding\"); `subject` is\n// what the orchestrator accepts or rejects (\"ticket\", \"fix\").\n\nexport function readDocsRule (subject:string):string {\n return '- Read the files directly relevant to the ' +\n `${subject} before writing code. Also read any ` +\n 'AGENTS.md in or above the directories you ' +\n 'will touch, plus CONTEXT.md and the ' +\n 'relevant docs/adr/ entries if they exist. ' +\n 'If they do not exist, proceed silently.'\n}\n\n// The rule for agents in a worktree the orchestrator has\n// already installed dependencies into. Empty without one.\nexport function installRule (installCmd?:string):string[] {\n if (!installCmd) return []\n return [\n '- Dependencies are already installed with ' +\n `\\`${installCmd}\\`. Do NOT reinstall them ` +\n 'with any other command; to reinstall, run ' +\n 'that same command. To add a dependency, use ' +\n 'the package manager that command uses, so ' +\n 'its lockfile stays in sync.',\n ]\n}\n\n// The delegation rule for agents whose subagents default to a\n// cheaper model (CLAUDE_CODE_SUBAGENT_MODEL). Empty without one.\n// `readOnly` drops the edit/commit line, for the reviewer, which\n// never edits.\nexport function subagentRule (\n preferSubagent?:string,\n readOnly = false\n):string[] {\n if (!preferSubagent) return []\n return [\n '- Hand read-only work to subagents: exploring the ' +\n 'codebase, searching code, and experiments or test ' +\n 'runs whose output would flood your context. ' +\n `Subagents default to the ${preferSubagent} model.`,\n '- A subagent starts with no context. Brief each one ' +\n 'with the objective, what you already know, its ' +\n 'boundaries, and the reply you expect (file:line ' +\n 'references, or pass/fail with the relevant output).',\n ...(readOnly ? [] : [\n '- Subagents never edit files or run git commands ' +\n 'that write. You make every edit and every commit.',\n ]),\n '- Do not delegate a lookup that takes a single tool call.',\n ]\n}\n\nexport function testGateRule (testCmd:string, subject:string):string {\n if (testCmd === '') {\n return '- Before each commit, run the tests relevant ' +\n 'to what you changed. Do not commit with failing ' +\n 'tests. The orchestrator runs no test command ' +\n `for this ${subject}.`\n }\n return '- While iterating, run only the tests ' +\n 'relevant to what you changed. Run ' +\n `\\`${testCmd}\\` in full once, as the ` +\n 'final gate before your last commit. Do ' +\n 'not commit with failing tests. The ' +\n 'orchestrator runs the same command after ' +\n `you exit and rejects the ${subject} if it ` +\n 'fails.'\n}\n\nexport const LINT_RULE = '- If the project has a lint script, run it ' +\n 'and fix what it reports.'\n\nexport function integrityRule (unit:string):string {\n return '- INTEGRITY: never delete, skip, weaken, or ' +\n 'write tautological tests to force a green ' +\n `result. Only report a ${unit} done ` +\n 'when its real tests genuinely pass. If an ' +\n 'item cannot be completed honestly, leave ' +\n 'it unchecked and record what blocked you ' +\n 'in your notes file.'\n}\n\nexport const CONFINEMENT_RULES:string[] = [\n '- Do NOT read or modify files outside ' +\n 'your current directory (the worktree).',\n '- Do NOT run git push.',\n]\n\nexport function commitRule (unit:string):string {\n return `- Commit after each ${unit}, and make ` +\n 'sure everything is committed when finished, ' +\n 'except your notes file.'\n}\n\nexport function notesRule (unit:string, subject:string):string {\n return `- Keep notes for this attempt in \\`${NOTES_PATH}\\` ` +\n '(create the directory). Update it after each ' +\n `${unit} and as soon as you hit a ` +\n 'blocker, not only at the end. Record what you ' +\n 'implemented, files changed, how you read any ' +\n 'vague acceptance criteria, what blocked you, and ' +\n `anything the next attempt at this ${subject} should ` +\n 'know. Never commit, git add, or delete this ' +\n 'file; the orchestrator collects it.'\n}\n\nexport function agentsMdRule (subject:string):string {\n return '- Before your final commit, record reusable ' +\n 'learnings in the nearest AGENTS.md (in or ' +\n 'above the directories you modified; ' +\n 'prefer that over the repo root). Good ' +\n 'additions: module conventions, gotchas, ' +\n 'files that must change together, how to ' +\n 'test that area, environment requirements. ' +\n `Do NOT add ${subject}-specific details or ` +\n 'temporary debugging notes. Only write ' +\n 'when you have knowledge that would help ' +\n 'future work in that directory. Reusable ' +\n 'patterns go in AGENTS.md; anything ' +\n `specific to this ${subject} goes in your ` +\n 'notes file.'\n}\n"],
5
+ "mappings": ";;AAAA,SAAS,kBAAkB;AAMpB,SAAS,aAAc,SAAuB;AACjD,SAAO,6CACA,OAAO;AAKlB;AAPgB;AAWT,SAAS,YAAa,YAA6B;AACtD,MAAI,CAAC,WAAY,QAAO,CAAC;AACzB,SAAO;AAAA,IACH,+CACS,UAAU;AAAA,EAKvB;AACJ;AAVgB;AAgBT,SAAS,aACZ,gBACA,WAAW,OACJ;AACP,MAAI,CAAC,eAAgB,QAAO,CAAC;AAC7B,SAAO;AAAA,IACH,4KAGgC,cAAc;AAAA,IAC9C;AAAA,IAIA,GAAI,WAAW,CAAC,IAAI;AAAA,MAChB;AAAA,IAEJ;AAAA,IACA;AAAA,EACJ;AACJ;AApBgB;AAsBT,SAAS,aAAc,SAAgB,SAAuB;AACjE,MAAI,YAAY,IAAI;AAChB,WAAO,sJAGS,OAAO;AAAA,EAC3B;AACA,SAAO,6EAEE,OAAO,uKAIgB,OAAO;AAE3C;AAfgB;AAiBT,MAAM,YAAY;AAGlB,SAAS,cAAe,MAAoB;AAC/C,SAAO,+GAEsB,IAAI;AAKrC;AARgB;AAUT,MAAM,oBAA6B;AAAA,EACtC;AAAA,EAEA;AACJ;AAEO,SAAS,WAAY,MAAoB;AAC5C,SAAO,uBAAuB,IAAI;AAGtC;AAJgB;AAMT,SAAS,UAAW,MAAa,SAAuB;AAC3D,SAAO,sCAAsC,UAAU,mDAEhD,IAAI,2MAI8B,OAAO;AAGpD;AAVgB;AAYT,SAAS,aAAc,SAAuB;AACjD,SAAO,wSAOW,OAAO,kMAKD,OAAO;AAEnC;AAfgB;",
6
6
  "names": []
7
7
  }
@@ -44,7 +44,7 @@ Give each ticket its **blocking edges**: the other tickets that must complete be
44
44
  Present the proposed breakdown as a numbered list. For each ticket, show:
45
45
 
46
46
  - **Title**: short descriptive name
47
- - **Blocked by**: which other tickets (if any) must complete first
47
+ - **Blocked by**: which other tickets (if any) must complete first, by number, or `<feature> <NN>` for a ticket in another feature
48
48
  - **What it delivers**: the end-to-end behaviour this ticket makes work
49
49
 
50
50
  Ask the user:
@@ -59,10 +59,10 @@ Iterate until the user approves the breakdown.
59
59
 
60
60
  Publish the approved tickets. **How** depends on the tracker `npx nightralph init` configured; the tickets are the same either way, only the shape of the blocking edges changes:
61
61
 
62
- - **Local files** → write one file per ticket under `.scratch/<feature-slug>/issues/<NN>-<slug>.md`, numbered from `01` in dependency order (blockers first). Each file's "Blocked by" lists the numbers/titles it depends on. Use the per-ticket file template below: one ticket per file, never a single combined file.
62
+ - **Local files** -> write one file per ticket under `.scratch/<feature-slug>/issues/<NN>-<slug>.md`, numbered from `01` in dependency order (blockers first). Each file's "Blocked by" lists the numbers of the tickets it depends on. A blocker in another feature is written `<feature> <NN>`, where `<feature>` is that feature's `.scratch/<feature>/` directory name (a path such as `rollout/billing` for a nested feature), repeated before each of its numbers (`newsletter-audit 15, newsletter-audit 16`). List every number: `03-05` means 3 and 5, not 3 through 5, and a feature name before it applies to both. A note in parentheses is ignored. The list may wrap onto the next lines; it ends at a blank line, the next `**Field:**` line, a heading or a checklist item. Use the per-ticket file template below: one ticket per file, never a single combined file.
63
63
  - Set every local ticket to `**Status:** ready-for-agent` so nightralph can
64
64
  run it immediately. Do not publish local tickets as `needs-triage`.
65
- - **A real issue tracker (GitHub, Linear, …)** → publish one issue per ticket in dependency order (blockers first) so each ticket's blocking edges can reference real identifiers. Use the platform's native blocking / sub-issue relationship where it has one; otherwise set each ticket's "Blocked by" to the blocking issues. Apply the `ready-for-agent` triage label unless instructed otherwise; the tickets are agent-grabbable by construction.
65
+ - **A real issue tracker (GitHub, Linear, …)** -> publish one issue per ticket in dependency order (blockers first) so each ticket's blocking edges can reference real identifiers. Use the platform's native blocking / sub-issue relationship where it has one; otherwise set each ticket's "Blocked by" to the blocking issues. Apply the `ready-for-agent` triage label unless instructed otherwise; the tickets are agent-grabbable by construction.
66
66
 
67
67
  Work the **frontier**: any ticket whose blockers are all done. For a purely linear chain that means top to bottom.
68
68
 
@@ -74,7 +74,7 @@ Do NOT close or modify any parent issue.
74
74
 
75
75
  **What to build:** the end-to-end behaviour this ticket makes work, from the user's perspective, not a layer-by-layer implementation list.
76
76
 
77
- **Blocked by:** the numbers/titles of the tickets that gate this one, or "None (can start immediately)".
77
+ **Blocked by:** the numbers of the tickets that gate this one, and `<feature> <NN>` for one in another feature, each optionally followed by a note in parentheses; or "None (can start immediately)".
78
78
 
79
79
  **Status:** ready-for-agent
80
80
 
@@ -100,7 +100,7 @@ The end-to-end behaviour this ticket makes work, from the user's perspective, no
100
100
 
101
101
  ## Blocked by
102
102
 
103
- - A reference to each blocking ticket, or "None (can start immediately)".
103
+ - The tracker identifier of each blocking ticket, or "None (can start immediately)".
104
104
 
105
105
  </issue-template>
106
106
 
@@ -0,0 +1,45 @@
1
+ export type RawRef = {
2
+ feature?: string;
3
+ num: number;
4
+ };
5
+ export type BlockerParse = {
6
+ refs: RawRef[];
7
+ typos: string[];
8
+ };
9
+ /**
10
+ * Reads the **Blocked by:** field of a ticket body. A number
11
+ * belongs to the feature named by the word directly before it
12
+ * when that word is in `knownFeatures` (the names of
13
+ * `.scratch/<name>/issues` dirs, `<name>` possibly nested, as in
14
+ * `rollout/billing`); otherwise to the ticket's own feature.
15
+ * Feature-shaped words that name no feature are returned as
16
+ * `typos`.
17
+ */
18
+ export declare function parseBlockerField(body: string, knownFeatures: ReadonlySet<string>): BlockerParse;
19
+ export type BlockerWhere = 'run' | 'done' | 'pending' | 'missing';
20
+ export type BlockerRef = {
21
+ feature: string;
22
+ num: number;
23
+ where: BlockerWhere;
24
+ status?: string;
25
+ };
26
+ /**
27
+ * Classifies raw refs. `selected` maps each feature in the run to
28
+ * its ticket numbers; `lookup` returns the status of a ticket in a
29
+ * feature outside the run, or undefined when there is none.
30
+ * De-duplicates by (feature, num) after the own feature is filled
31
+ * in.
32
+ */
33
+ export declare function resolveBlockers(raw: ReadonlyArray<RawRef>, ownFeature: string, selected: ReadonlyMap<string, ReadonlySet<number>>, lookup: (feature: string, num: number) => string | undefined): BlockerRef[];
34
+ /**
35
+ * Refs for a bare scan, before the run is known: every ref is
36
+ * `run`, so one that matches no ticket in the list is never
37
+ * satisfied.
38
+ */
39
+ export declare function provisionalRefs(raw: ReadonlyArray<RawRef>, ownFeature: string): BlockerRef[];
40
+ export type BlockerWarning = {
41
+ ticket: string;
42
+ kind: 'missing' | 'typo';
43
+ text: string;
44
+ };
45
+ //# sourceMappingURL=blockers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blockers.d.ts","sourceRoot":"","sources":["../../src/blockers.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,MAAM,GAAG;IACjB,OAAO,CAAC,EAAC,MAAM,CAAC;IAChB,GAAG,EAAC,MAAM,CAAC;CACd,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACvB,IAAI,EAAC,MAAM,EAAE,CAAC;IACd,KAAK,EAAC,MAAM,EAAE,CAAC;CAClB,CAAA;AA8FD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC7B,IAAI,EAAC,MAAM,EACX,aAAa,EAAC,WAAW,CAAC,MAAM,CAAC,GACnC,YAAY,CAkCb;AAED,MAAM,MAAM,YAAY,GAAG,KAAK,GAAC,MAAM,GAAC,SAAS,GAAC,SAAS,CAAA;AAE3D,MAAM,MAAM,UAAU,GAAG;IACrB,OAAO,EAAC,MAAM,CAAC;IACf,GAAG,EAAC,MAAM,CAAC;IAKX,KAAK,EAAC,YAAY,CAAC;IACnB,MAAM,CAAC,EAAC,MAAM,CAAC;CAClB,CAAA;AAQD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC3B,GAAG,EAAC,aAAa,CAAC,MAAM,CAAC,EACzB,UAAU,EAAC,MAAM,EACjB,QAAQ,EAAC,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,EACjD,MAAM,EAAC,CAAC,OAAO,EAAC,MAAM,EAAE,GAAG,EAAC,MAAM,KAAK,MAAM,GAAC,SAAS,GACzD,UAAU,EAAE,CA8Bb;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC3B,GAAG,EAAC,aAAa,CAAC,MAAM,CAAC,EACzB,UAAU,EAAC,MAAM,GACnB,UAAU,EAAE,CAWb;AAED,MAAM,MAAM,cAAc,GAAG;IACzB,MAAM,EAAC,MAAM,CAAC;IACd,IAAI,EAAC,SAAS,GAAC,MAAM,CAAC;IACtB,IAAI,EAAC,MAAM,CAAC;CACf,CAAA"}