@jphutchins/code-review 0.1.0-alpha.6 → 0.1.0-alpha.7

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/index.js CHANGED
@@ -53,15 +53,138 @@ var computeCost = (models, prices, warn = defaultWarn) => {
53
53
  };
54
54
  };
55
55
 
56
+ // src/patch.ts
57
+ var HUNK_HEADER_RE = /^@@ -(\d+)(?:,\d+)? \+\d+(?:,\d+)? @@/;
58
+ var hunkOldStart = (line) => {
59
+ const raw = HUNK_HEADER_RE.exec(line)?.[1];
60
+ return raw !== void 0 ? Number(raw) : null;
61
+ };
62
+ var classifyBodyLine = (line) => {
63
+ if (line.startsWith(" ")) return { kind: "context", text: line.slice(1) };
64
+ if (line.startsWith("-")) return { kind: "removed", text: line.slice(1) };
65
+ if (line.startsWith("+")) return { kind: "added", text: line.slice(1) };
66
+ return null;
67
+ };
68
+ var trimmedMiddle = (body) => {
69
+ const first = body.findIndex((l) => l.kind !== "context");
70
+ if (first === -1) return [];
71
+ const last = body.findLastIndex((l) => l.kind !== "context");
72
+ return body.slice(first, last + 1);
73
+ };
74
+ var isContiguousChange = (middle) => {
75
+ if (middle.some((l) => l.kind === "context")) return false;
76
+ const firstAdded = middle.findIndex((l) => l.kind === "added");
77
+ if (firstAdded === -1) return true;
78
+ return middle.slice(0, firstAdded).every((l) => l.kind === "removed") && middle.slice(firstAdded).every((l) => l.kind === "added");
79
+ };
80
+ var removedRange = (body, oldStart) => body.reduce(
81
+ (acc, line) => line.kind === "added" ? acc : {
82
+ lineNumber: acc.lineNumber + 1,
83
+ firstRemoved: line.kind === "removed" && acc.firstRemoved === null ? acc.lineNumber : acc.firstRemoved,
84
+ lastRemoved: line.kind === "removed" ? acc.lineNumber : acc.lastRemoved
85
+ },
86
+ { lineNumber: oldStart, firstRemoved: null, lastRemoved: null }
87
+ );
88
+ var drop = (reason) => ({
89
+ kind: "drop",
90
+ reason
91
+ });
92
+ var parseHunk = (patch) => {
93
+ const rawLines = patch.split("\n");
94
+ const lines = rawLines.length > 0 && rawLines[rawLines.length - 1] === "" ? rawLines.slice(0, -1) : rawLines;
95
+ const headerHits = lines.reduce(
96
+ (acc, line, index) => {
97
+ const oldStart = hunkOldStart(line);
98
+ return oldStart !== null ? [...acc, { index, oldStart }] : acc;
99
+ },
100
+ []
101
+ );
102
+ if (headerHits.length !== 1) {
103
+ return drop(`expected exactly one hunk, got ${String(headerHits.length)}`);
104
+ }
105
+ const hit = headerHits[0];
106
+ if (hit === void 0) return drop("malformed hunk header");
107
+ const bodyRaw = lines.slice(hit.index + 1).filter((line) => !line.startsWith("\\"));
108
+ const classified = bodyRaw.map(classifyBodyLine);
109
+ if (classified.some((line) => line === null)) return drop("malformed hunk body line");
110
+ const body = classified.filter((line) => line !== null);
111
+ return { kind: "ok", oldStart: hit.oldStart, body };
112
+ };
113
+ var validatePatch = (patch, fileLines) => {
114
+ const parsed = parseHunk(patch);
115
+ if (parsed.kind === "drop") return parsed;
116
+ const { oldStart, body } = parsed;
117
+ const oldSideTexts = body.filter((l) => l.kind !== "added").map((l) => l.text);
118
+ const expected = fileLines.slice(oldStart - 1, oldStart - 1 + oldSideTexts.length);
119
+ const oldSideMatches = expected.length === oldSideTexts.length && expected.every((line, i) => line === oldSideTexts[i]);
120
+ if (!oldSideMatches) {
121
+ return drop(
122
+ `patch context does not match the file at lines ${String(oldStart)}..${String(oldStart + oldSideTexts.length - 1)}`
123
+ );
124
+ }
125
+ if (!isContiguousChange(trimmedMiddle(body))) {
126
+ return drop("change is not a single contiguous block");
127
+ }
128
+ const removedCount = body.filter((l) => l.kind === "removed").length;
129
+ const addedCount = body.filter((l) => l.kind === "added").length;
130
+ if (removedCount === 0 && addedCount === 0) return drop("hunk contains no changes");
131
+ if (removedCount === 0) return drop("pure insertion has no range to anchor a suggestion");
132
+ const { firstRemoved, lastRemoved } = removedRange(body, oldStart);
133
+ if (firstRemoved === null || lastRemoved === null) return drop("malformed hunk body");
134
+ return { startLine: firstRemoved, endLine: lastRemoved };
135
+ };
136
+ var patchToSuggestion = (patch) => {
137
+ const parsed = parseHunk(patch);
138
+ if (parsed.kind === "drop") return parsed;
139
+ const { body } = parsed;
140
+ if (!isContiguousChange(trimmedMiddle(body))) {
141
+ return drop("change is not a single contiguous block");
142
+ }
143
+ const removedCount = body.filter((l) => l.kind === "removed").length;
144
+ const addedLines = body.filter((l) => l.kind === "added");
145
+ if (removedCount === 0 && addedLines.length === 0) return drop("hunk contains no changes");
146
+ if (removedCount === 0) return drop("pure insertion can't be expressed as a suggestion");
147
+ return addedLines.map((l) => l.text).join("\n");
148
+ };
149
+
150
+ // src/surface.ts
151
+ var severityEmoji = (s) => {
152
+ switch (s) {
153
+ case "critical":
154
+ return "\u{1F534}";
155
+ case "major":
156
+ return "\u{1F7E0}";
157
+ case "minor":
158
+ return "\u{1F535}";
159
+ case "nit":
160
+ return "\u26AA";
161
+ default:
162
+ return "\u2753";
163
+ }
164
+ };
165
+ var EMBED_LIMIT = 4e4;
166
+ var AGENTS_STOP_DIRECTIVE = "<!-- AGENTS: STOP \u2014 do not parse the prose below; decode this findings JSON and read schema_version first. -->";
167
+ var findingsPointer = (findings, jsonUrl, limit = EMBED_LIMIT) => {
168
+ const b64 = Buffer.from(JSON.stringify(findings), "utf-8").toString("base64");
169
+ const marker = b64.length <= limit ? `<!-- code-review:findings-json;base64 ${b64} -->` : jsonUrl ? `<!-- code-review:findings-json ${jsonUrl} -->` : "";
170
+ return marker ? `${AGENTS_STOP_DIRECTIVE}
171
+ ${marker}` : "";
172
+ };
173
+ var escapeFence = (text) => text.replace(/```/g, "`` ` ``");
174
+ var projectPatch = (patch) => {
175
+ if (patch === null || patch === void 0) return { kind: "none" };
176
+ const lowered = patchToSuggestion(patch);
177
+ return typeof lowered === "string" ? { kind: "suggestion", text: escapeFence(lowered) } : { kind: "patch", raw: escapeFence(patch) };
178
+ };
179
+
56
180
  // src/render.ts
57
- var escapeBackticks = (text) => text.replace(/```/g, "`` ` ``");
58
181
  var escapePipes = (text) => text.replace(/\|/g, "\\|");
59
182
  var escapeCodeBackticks = (text) => text.replace(/`/g, "-");
60
183
  var sanitizeFinding = (f) => ({
61
184
  ...f,
62
185
  title: escapePipes(f.title),
63
186
  path: escapeCodeBackticks(f.path),
64
- suggestion: f.suggestion ? escapeBackticks(f.suggestion) : f.suggestion
187
+ patchProjection: projectPatch(f.patch)
65
188
  });
66
189
  var emptySeverityCounts = () => ({
67
190
  critical: 0,
@@ -73,21 +196,20 @@ var computeSeverityCounts = (findings) => findings.reduce(
73
196
  (acc, f) => f.severity in acc ? { ...acc, [f.severity]: acc[f.severity] + 1 } : acc,
74
197
  emptySeverityCounts()
75
198
  );
76
- var EMBED_LIMIT = 4e4;
77
199
  var render = (input) => {
78
200
  const eta = new Eta({ autoTrim: false });
79
201
  const usageAvailable = input.envelope !== null;
80
202
  const costReport = input.envelope ? computeCost(input.envelope.models, input.prices) : null;
203
+ const pricesProvided = input.pricesProvided ?? true;
81
204
  const route = input.route ?? input.envelope?.route ?? null;
82
205
  const effort = input.effort ?? input.envelope?.effort ?? null;
83
206
  const modelNames = input.envelope ? input.envelope.models.map((m) => m.model).join(", ") : "";
84
- const findingsB64 = Buffer.from(JSON.stringify(input.findings), "utf-8").toString("base64");
85
- const embeddedFindings = findingsB64.length <= EMBED_LIMIT ? findingsB64 : null;
86
207
  return eta.renderString(input.template, {
87
208
  findings: input.findings,
88
209
  envelope: input.envelope,
89
210
  usageAvailable,
90
211
  costReport,
212
+ pricesProvided,
91
213
  route,
92
214
  effort,
93
215
  modelNames,
@@ -98,10 +220,12 @@ var render = (input) => {
98
220
  inlineDisposition: input.inlineDisposition ?? null,
99
221
  runUrl: input.runUrl ?? null,
100
222
  jsonUrl: input.jsonUrl ?? null,
101
- embeddedFindings,
223
+ findingsPointer: input.findingsPointer ?? findingsPointer(input.findings, input.jsonUrl),
102
224
  reviewUrl: input.reviewUrl ?? null,
103
225
  formatTokens: (n) => Number.isFinite(n) && n >= 0 ? n.toLocaleString("en-US") : "\u2014",
104
- formatCost: (n) => Number.isFinite(n) ? n > 0 && n.toFixed(2) === "0.00" ? "<$0.01" : `$${n.toFixed(2)}` : "\u2014",
226
+ // Cost cells render N/A (never a false $0.00) when no real price map was provided — there are
227
+ // real tokens spent, we simply have no rates to price them (SPEC §6.2).
228
+ formatCost: (n) => !pricesProvided ? "N/A" : Number.isFinite(n) ? n > 0 && n.toFixed(2) === "0.00" ? "<$0.01" : `$${n.toFixed(2)}` : "\u2014",
105
229
  formatDuration: (ms) => {
106
230
  if (!Number.isFinite(ms) || ms < 0) return "\u2014";
107
231
  const s = Math.round(ms / 1e3);
@@ -119,20 +243,7 @@ var render = (input) => {
119
243
  return `\u2753 ${v}`;
120
244
  }
121
245
  },
122
- severityEmoji: (s) => {
123
- switch (s) {
124
- case "critical":
125
- return "\u{1F534}";
126
- case "major":
127
- return "\u{1F7E0}";
128
- case "minor":
129
- return "\u{1F535}";
130
- case "nit":
131
- return "\u26AA";
132
- default:
133
- return "\u2753";
134
- }
135
- }
246
+ severityEmoji
136
247
  });
137
248
  };
138
249
  var key = (path, line) => `${path}:${String(line)}`;
@@ -189,56 +300,32 @@ var partitionFindings = (findings, index) => {
189
300
  };
190
301
 
191
302
  // src/inline.ts
192
- var escapeBackticks2 = (text) => text.replace(/```/g, "`` ` ``");
193
- var severityEmoji = (s) => {
194
- switch (s) {
195
- case "critical":
196
- return "\u{1F534}";
197
- case "major":
198
- return "\u{1F7E0}";
199
- case "minor":
200
- return "\u{1F535}";
201
- case "nit":
202
- return "\u26AA";
203
- default:
204
- return "\u2753";
205
- }
206
- };
207
- var severityHeader = (f) => `${severityEmoji(f.severity)} **${f.severity}** \u2014 ${f.title}`;
208
- var jsonUrlMarker = (jsonUrl) => jsonUrl ? `<!-- code-review:findings-json ${jsonUrl} -->` : void 0;
209
303
  var formatModels = (models) => models.length > 0 ? models.map((m) => `\`${m}\``).join("/") : "an AI model";
210
- var buildCommentBody = (f, jsonUrl) => {
211
- const marker = jsonUrlMarker(jsonUrl);
212
- const parts = [...marker ? [marker] : [], severityHeader(f), f.body];
213
- if (f.suggestion !== null && f.suggestion !== void 0) {
214
- const safe = escapeBackticks2(f.suggestion);
215
- parts.push(`\`\`\`suggestion
216
- ${safe}
217
- \`\`\``);
218
- }
219
- return parts.join("\n\n");
220
- };
221
- var renderCommentBody = (f, eta, template, modelsText, jsonUrl) => {
222
- return eta.renderString(template, {
304
+ var renderCommentBody = (f, eta, template, modelsText, jsonUrl, pointer) => (
305
+ // Eta.renderString returns string | Promise<string>; with autoTrim:false it's always sync.
306
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
307
+ eta.renderString(template, {
223
308
  ...f,
224
- suggestion: f.suggestion !== null && f.suggestion !== void 0 ? escapeBackticks2(f.suggestion) : null,
309
+ patchProjection: projectPatch(f.patch),
225
310
  severityEmoji,
226
311
  modelsText,
227
- jsonUrl: jsonUrl ?? null
228
- });
229
- };
230
- var buildInlineComments = (findings, diff, context = {}) => {
231
- const { inlineTemplate, models = [], jsonUrl } = context;
312
+ jsonUrl: jsonUrl ?? null,
313
+ findingsPointer: pointer
314
+ })
315
+ );
316
+ var buildInlineComments = (findings, diff, context) => {
317
+ const { inlineTemplate, models = [], jsonUrl, findings: fullFindings } = context;
232
318
  const index = indexDiff(diff);
233
319
  const { inDiff, strays } = partitionFindings(findings, index);
234
- const eta = inlineTemplate ? new Eta({ autoTrim: false }) : null;
320
+ const eta = new Eta({ autoTrim: false });
235
321
  const modelsText = formatModels(models);
322
+ const pointer = context.findingsPointer ?? (fullFindings ? findingsPointer(fullFindings, jsonUrl) : "");
236
323
  const comments = inDiff.map((f) => {
237
324
  const comment = {
238
325
  path: f.path,
239
326
  line: f.end_line,
240
327
  side: defaultSide(f.side),
241
- body: eta && inlineTemplate ? renderCommentBody(f, eta, inlineTemplate, modelsText, jsonUrl) : buildCommentBody(f, jsonUrl)
328
+ body: renderCommentBody(f, eta, inlineTemplate, modelsText, jsonUrl, pointer)
242
329
  };
243
330
  if (f.start_line < f.end_line) {
244
331
  return {
@@ -292,16 +379,16 @@ var FindingShape = t.intersection([
292
379
  end_line: LineNumber,
293
380
  severity: SeverityCodec,
294
381
  title: t.string,
295
- body: t.string
382
+ description: t.string,
383
+ reasoning: t.string,
384
+ confidence: Confidence
296
385
  }),
297
386
  t.partial({
298
387
  side: SideCodec,
299
- suggestion: t.union([t.string, t.null]),
300
- confidence: Confidence,
301
388
  code: t.string,
302
389
  code_url: t.string,
303
- reasoning: t.string,
304
- patch: t.string
390
+ recommendation: t.string,
391
+ patch: t.union([t.string, t.null])
305
392
  })
306
393
  ]);
307
394
  var EndGeStart = t.refinement(
@@ -377,7 +464,13 @@ var TestSummaryCodec = t.intersection([
377
464
  failures: t.array(TestFailureCodec)
378
465
  })
379
466
  ]);
380
- var DEFAULT_SCHEMA_VERSION = "0.3.0";
467
+ var DEFAULT_SCHEMA_VERSION = "0.4.0";
468
+ var noticeFindings = (summary) => ({
469
+ schema_version: DEFAULT_SCHEMA_VERSION,
470
+ summary,
471
+ verdict: "comment",
472
+ findings: []
473
+ });
381
474
 
382
475
  // src/validate.ts
383
476
  var addFormats = _addFormats;
@@ -438,20 +531,12 @@ var formatMarkdown = (md) => {
438
531
  var identity = (decoded) => decoded;
439
532
  var findingsTable = [
440
533
  {
441
- minor: "0.3",
534
+ minor: "0.4",
442
535
  defaultVersion: DEFAULT_SCHEMA_VERSION,
443
536
  schemaFile: "findings.schema.json",
444
537
  codec: FindingsCodec,
445
538
  normalize: identity,
446
539
  latest: true
447
- },
448
- {
449
- minor: "0.2",
450
- defaultVersion: "0.2.0",
451
- schemaFile: "v0.2/findings.schema.json",
452
- codec: FindingsCodec,
453
- normalize: identity,
454
- latest: false
455
540
  }
456
541
  ];
457
542
  var triageTable = [
@@ -590,12 +675,6 @@ var checkLongSuggestions = (comments) => {
590
675
  });
591
676
  return { comments: adjusted, longFiles };
592
677
  };
593
- var noticeFindings = (message) => ({
594
- schema_version: DEFAULT_SCHEMA_VERSION,
595
- summary: `### \u26A0\uFE0F ${message}`,
596
- verdict: "comment",
597
- findings: []
598
- });
599
678
  var loadFindings = (path) => {
600
679
  let raw;
601
680
  try {
@@ -665,9 +744,12 @@ var parseHtmlUrl = (raw) => {
665
744
  return void 0;
666
745
  }
667
746
  };
668
- var postInlineReview = async (repo, prNumber, headSha, comments, stickyUrl, ghApi) => {
747
+ var postInlineReview = async (repo, prNumber, headSha, comments, stickyUrl, marker, ghApi) => {
669
748
  const sha7 = headSha.slice(0, 7);
670
- const pointer = stickyUrl ? `\u{1F916} Automated code review for \`${sha7}\` \u2014 see the [summary comment](${stickyUrl}) for the verdict, walkthrough, and cost.` : `\u{1F916} Automated code review for \`${sha7}\` \u2014 see the summary comment for the verdict, walkthrough, and cost.`;
749
+ const linkLine = stickyUrl ? `\u{1F916} Automated code review for \`${sha7}\` \u2014 see the [summary comment](${stickyUrl}) for the verdict, walkthrough, and cost.` : `\u{1F916} Automated code review for \`${sha7}\` \u2014 see the summary comment for the verdict, walkthrough, and cost.`;
750
+ const pointer = marker ? `${marker}
751
+
752
+ ${linkLine}` : linkLine;
671
753
  const body = JSON.stringify({
672
754
  body: pointer,
673
755
  commit_id: headSha,
@@ -807,12 +889,13 @@ var post = async (input, ghApi = runGhApi) => {
807
889
  throw new Error(`Price map at ${input.pricesPath} does not match the expected shape`);
808
890
  }
809
891
  const template = readFileSync(input.templatePath, "utf-8");
810
- const inlineTemplate = input.inlineTemplatePath ? readFileSync(input.inlineTemplatePath, "utf-8") : void 0;
892
+ const inlineTemplate = readFileSync(input.inlineTemplatePath, "utf-8");
811
893
  const renderNotice = (message) => formatMarkdown(
812
894
  render({
813
- findings: noticeFindings(message),
895
+ findings: noticeFindings(`### \u26A0\uFE0F ${message}`),
814
896
  envelope: null,
815
897
  prices: decodedPrices.right,
898
+ pricesProvided: input.pricesProvided,
816
899
  template,
817
900
  route: input.route,
818
901
  reviewedSha: input.headSha,
@@ -851,6 +934,7 @@ var post = async (input, ghApi = runGhApi) => {
851
934
  findings,
852
935
  envelope: null,
853
936
  prices: decodedPrices.right,
937
+ pricesProvided: input.pricesProvided,
854
938
  template,
855
939
  route: input.route,
856
940
  reviewedSha: input.headSha,
@@ -867,10 +951,11 @@ var post = async (input, ghApi = runGhApi) => {
867
951
  );
868
952
  process.exit(0);
869
953
  }
954
+ const findingsMarker = findingsPointer(findings, input.jsonUrl);
870
955
  const { comments: rawComments, strays } = buildInlineComments(findings.findings, diff, {
871
956
  inlineTemplate,
872
957
  models: envelope.models.map((m) => m.model),
873
- jsonUrl: input.jsonUrl
958
+ findingsPointer: findingsMarker
874
959
  });
875
960
  const { comments, longFiles } = checkLongSuggestions(rawComments);
876
961
  for (const wf of longFiles) {
@@ -881,11 +966,12 @@ var post = async (input, ghApi = runGhApi) => {
881
966
  }
882
967
  const botReviews = comments.length > 0 ? await fetchBotReviews(input.repo, prNumber, input.botLogin, ghApi) : [];
883
968
  const alreadyReviewedThisSha = botReviews.some((r) => r.commitId === input.headSha);
884
- const inlineDisposition = comments.length > 0 ? alreadyReviewedThisSha ? { kind: "suppressed-existing-review", sha: input.headSha } : { kind: "posted", count: comments.length, sha: input.headSha } : strays.length > 0 ? { kind: "none-in-diff" } : void 0;
969
+ const initialDisposition = comments.length > 0 ? alreadyReviewedThisSha ? { kind: "suppressed-existing-review", sha: input.headSha } : void 0 : strays.length > 0 ? { kind: "none-in-diff" } : void 0;
885
970
  const commonRenderInput = {
886
971
  findings,
887
972
  envelope,
888
973
  prices: decodedPrices.right,
974
+ pricesProvided: input.pricesProvided,
889
975
  template,
890
976
  route: input.route,
891
977
  reviewedSha: input.headSha,
@@ -893,9 +979,9 @@ var post = async (input, ghApi = runGhApi) => {
893
979
  testReport,
894
980
  severityCounts: computeSeverityCounts(findings.findings),
895
981
  strays,
896
- inlineDisposition,
897
982
  runUrl: input.runUrl,
898
- jsonUrl: input.jsonUrl
983
+ jsonUrl: input.jsonUrl,
984
+ findingsPointer: findingsMarker
899
985
  };
900
986
  const longFilesNote = longFiles.length > 0 ? `
901
987
 
@@ -903,8 +989,14 @@ var post = async (input, ghApi = runGhApi) => {
903
989
 
904
990
  > **Note:** ${String(longFiles.length)} suggestion(s) exceeded GitHub's ~10-line inline suggestion limit and were omitted from the inline comments; the affected findings remain in the review.
905
991
  ` : "";
906
- const renderBody = (reviewUrl2) => formatMarkdown(render({ ...commonRenderInput, reviewUrl: reviewUrl2 }) + longFilesNote);
907
- const stickyRef = await upsertSticky(input.repo, prNumber, existingSticky, renderBody(), ghApi);
992
+ const renderBody = (inlineDisposition, reviewUrl2) => formatMarkdown(render({ ...commonRenderInput, inlineDisposition, reviewUrl: reviewUrl2 }) + longFilesNote);
993
+ const stickyRef = await upsertSticky(
994
+ input.repo,
995
+ prNumber,
996
+ existingSticky,
997
+ renderBody(initialDisposition),
998
+ ghApi
999
+ );
908
1000
  if (comments.length === 0) return;
909
1001
  if (alreadyReviewedThisSha) {
910
1002
  process.stderr.write(
@@ -923,15 +1015,26 @@ var post = async (input, ghApi = runGhApi) => {
923
1015
  input.headSha,
924
1016
  comments,
925
1017
  stickyRef?.url,
1018
+ findingsMarker,
926
1019
  ghApi
927
1020
  );
928
1021
  process.stderr.write(
929
1022
  `Posted ${String(comments.length)} inline comments on PR #${String(prNumber)}
930
1023
  `
931
1024
  );
932
- if (stickyRef !== null && reviewUrl !== void 0) {
1025
+ if (stickyRef !== null) {
1026
+ const confirmedDisposition = {
1027
+ kind: "posted",
1028
+ count: comments.length,
1029
+ sha: input.headSha
1030
+ };
933
1031
  try {
934
- await patchComment(input.repo, stickyRef.id, renderBody(reviewUrl), ghApi);
1032
+ await patchComment(
1033
+ input.repo,
1034
+ stickyRef.id,
1035
+ renderBody(confirmedDisposition, reviewUrl),
1036
+ ghApi
1037
+ );
935
1038
  process.stderr.write(`Linked sticky comment #${String(stickyRef.id)} to the review
936
1039
  `);
937
1040
  } catch (err) {
@@ -1263,24 +1366,42 @@ var mapModelUsage = (modelUsage) => Object.entries(modelUsage).map(([model, entr
1263
1366
  ...entry.cacheReadInputTokens !== void 0 ? { cache_read_tokens: entry.cacheReadInputTokens } : {},
1264
1367
  ...entry.cacheCreationInputTokens !== void 0 ? { cache_write_tokens: entry.cacheCreationInputTokens } : {}
1265
1368
  }));
1369
+ var findingsOutcome = (native, agentFilePath) => {
1370
+ const ladder = extractStructured({ kind: "findings", native, agentFilePath });
1371
+ if (ladder.kind !== "ok")
1372
+ return { kind: "telemetry-only", reason: describeLadderFailure(ladder) };
1373
+ const resolution = resolve("findings", ladder.candidate);
1374
+ return resolution.kind === "ok" ? { kind: "ok", version: resolution.version, findings: resolution.value } : {
1375
+ kind: "telemetry-only",
1376
+ reason: "internal error: the extraction ladder validated a candidate the registry then rejected"
1377
+ };
1378
+ };
1266
1379
  var adaptClaudeCode = (native, agentFilePath, meta) => {
1267
- const outcome = extractStructured({ kind: "findings", native, agentFilePath });
1268
- if (outcome.kind !== "ok") {
1269
- return left(describeLadderFailure(outcome));
1270
- }
1271
- const resolution = resolve("findings", outcome.candidate);
1272
- return resolution.kind === "ok" ? right({
1273
- schema_version: resolution.version,
1274
- findings: resolution.value,
1380
+ const telemetry = {
1275
1381
  models: mapModelUsage(native.modelUsage),
1276
1382
  turns: native.num_turns,
1277
1383
  duration_ms: native.duration_ms,
1278
1384
  vendor_cost_usd: native.total_cost_usd ?? null,
1279
1385
  ...meta.route ? { route: meta.route } : {},
1280
1386
  ...meta.effort ? { effort: meta.effort } : {}
1281
- }) : left(
1282
- "internal error: the extraction ladder validated a candidate the registry then rejected"
1283
- );
1387
+ };
1388
+ const outcome = findingsOutcome(native, agentFilePath);
1389
+ switch (outcome.kind) {
1390
+ case "ok":
1391
+ return right({
1392
+ schema_version: outcome.version,
1393
+ findings: outcome.findings,
1394
+ ...telemetry
1395
+ });
1396
+ case "telemetry-only":
1397
+ return right({
1398
+ schema_version: DEFAULT_SCHEMA_VERSION,
1399
+ findings: noticeFindings(`### \u26A0\uFE0F Review did not complete
1400
+
1401
+ ${outcome.reason}`),
1402
+ ...telemetry
1403
+ });
1404
+ }
1284
1405
  };
1285
1406
  var adapt = (adapterName, native, agentFilePath, meta = {}) => {
1286
1407
  switch (adapterName) {
@@ -1294,83 +1415,86 @@ var adapt = (adapterName, native, agentFilePath, meta = {}) => {
1294
1415
  }
1295
1416
  }
1296
1417
  };
1297
-
1298
- // src/patch.ts
1299
- var HUNK_HEADER_RE = /^@@ -(\d+)(?:,\d+)? \+\d+(?:,\d+)? @@/;
1300
- var hunkOldStart = (line) => {
1301
- const raw = HUNK_HEADER_RE.exec(line)?.[1];
1302
- return raw !== void 0 ? Number(raw) : null;
1303
- };
1304
- var classifyBodyLine = (line) => {
1305
- if (line.startsWith(" ")) return { kind: "context", text: line.slice(1) };
1306
- if (line.startsWith("-")) return { kind: "removed", text: line.slice(1) };
1307
- if (line.startsWith("+")) return { kind: "added", text: line.slice(1) };
1308
- return null;
1309
- };
1310
- var trimmedMiddle = (body) => {
1311
- const first = body.findIndex((l) => l.kind !== "context");
1312
- if (first === -1) return [];
1313
- const last = body.findLastIndex((l) => l.kind !== "context");
1314
- return body.slice(first, last + 1);
1418
+ var whatsWrong = (state, draftPath, kind) => {
1419
+ switch (state.kind) {
1420
+ case "missing":
1421
+ return `${draftPath} does not exist yet`;
1422
+ case "unreadable":
1423
+ return `${draftPath} could not be read: ${state.error}`;
1424
+ case "invalid":
1425
+ return `${draftPath} does not validate against the ${kind} schema:
1426
+ ${state.errors.map((e) => ` - ${e}`).join("\n")}`;
1427
+ }
1315
1428
  };
1316
- var isContiguousChange = (middle) => {
1317
- if (middle.some((l) => l.kind === "context")) return false;
1318
- const firstAdded = middle.findIndex((l) => l.kind === "added");
1319
- if (firstAdded === -1) return true;
1320
- return middle.slice(0, firstAdded).every((l) => l.kind === "removed") && middle.slice(firstAdded).every((l) => l.kind === "added");
1429
+ var decideGate = (state, nudges, maxNudges, draftPath, kind) => {
1430
+ if (state.kind === "valid") return { kind: "allow" };
1431
+ if (nudges >= maxNudges) return { kind: "allow" };
1432
+ return {
1433
+ kind: "block",
1434
+ reason: [
1435
+ `This review is not complete \u2014 ${whatsWrong(state, draftPath, kind)}`,
1436
+ `The only deliverable is a ${kind} document that validates against the ${kind} schema \u2014 run "code-review print-schema ${kind}" to see the exact shape.`,
1437
+ `Write it to ${draftPath}, then run "code-review validate ${draftPath} --kind ${kind}" until it exits 0 before ending your turn.`
1438
+ ].join("\n")
1439
+ };
1321
1440
  };
1322
- var removedRange = (body, oldStart) => body.reduce(
1323
- (acc, line) => line.kind === "added" ? acc : {
1324
- lineNumber: acc.lineNumber + 1,
1325
- firstRemoved: line.kind === "removed" && acc.firstRemoved === null ? acc.lineNumber : acc.firstRemoved,
1326
- lastRemoved: line.kind === "removed" ? acc.lineNumber : acc.lastRemoved
1327
- },
1328
- { lineNumber: oldStart, firstRemoved: null, lastRemoved: null }
1329
- );
1330
- var drop = (reason) => ({ kind: "drop", reason });
1331
- var lowerPatch = (patch, fileLines) => {
1332
- const rawLines = patch.split("\n");
1333
- const lines = rawLines.length > 0 && rawLines[rawLines.length - 1] === "" ? rawLines.slice(0, -1) : rawLines;
1334
- const headerHits = lines.reduce(
1335
- (acc, line, index) => {
1336
- const oldStart = hunkOldStart(line);
1337
- return oldStart !== null ? [...acc, { index, oldStart }] : acc;
1338
- },
1339
- []
1340
- );
1341
- if (headerHits.length !== 1) {
1342
- return drop(`expected exactly one hunk, got ${String(headerHits.length)}`);
1441
+ var draftState = (draftPath, resolveSchema) => {
1442
+ let raw;
1443
+ try {
1444
+ raw = readFileSync(draftPath, "utf-8");
1445
+ } catch (err) {
1446
+ if (err instanceof Error && err.code === "ENOENT") {
1447
+ return { kind: "missing" };
1448
+ }
1449
+ return { kind: "unreadable", error: err instanceof Error ? err.message : String(err) };
1343
1450
  }
1344
- const hit = headerHits[0];
1345
- if (hit === void 0) return drop("malformed hunk header");
1346
- const bodyRaw = lines.slice(hit.index + 1).filter((line) => !line.startsWith("\\"));
1347
- const classified = bodyRaw.map(classifyBodyLine);
1348
- if (classified.some((line) => line === null)) return drop("malformed hunk body line");
1349
- const body = classified.filter((line) => line !== null);
1350
- const oldSideTexts = body.filter((l) => l.kind !== "added").map((l) => l.text);
1351
- const expected = fileLines.slice(hit.oldStart - 1, hit.oldStart - 1 + oldSideTexts.length);
1352
- const oldSideMatches = expected.length === oldSideTexts.length && expected.every((line, i) => line === oldSideTexts[i]);
1353
- if (!oldSideMatches) {
1354
- return drop(
1355
- `patch context does not match the file at lines ${String(hit.oldStart)}..${String(hit.oldStart + oldSideTexts.length - 1)}`
1356
- );
1451
+ let parsed;
1452
+ try {
1453
+ parsed = JSON.parse(raw);
1454
+ } catch (err) {
1455
+ return {
1456
+ kind: "invalid",
1457
+ errors: [`not valid JSON: ${err instanceof Error ? err.message : String(err)}`]
1458
+ };
1357
1459
  }
1358
- if (!isContiguousChange(trimmedMiddle(body))) {
1359
- return drop("change is not a single contiguous block");
1460
+ let schemaPath;
1461
+ try {
1462
+ schemaPath = resolveSchema(parsed);
1463
+ } catch (err) {
1464
+ return { kind: "invalid", errors: [err instanceof Error ? err.message : String(err)] };
1465
+ }
1466
+ try {
1467
+ const { valid, errors } = validateAgainstSchema(parsed, schemaPath);
1468
+ return valid ? { kind: "valid" } : { kind: "invalid", errors };
1469
+ } catch (err) {
1470
+ return { kind: "invalid", errors: [err instanceof Error ? err.message : String(err)] };
1360
1471
  }
1361
- const removedCount = body.filter((l) => l.kind === "removed").length;
1362
- const addedLines = body.filter((l) => l.kind === "added");
1363
- if (removedCount === 0 && addedLines.length === 0) return drop("hunk contains no changes");
1364
- if (removedCount === 0) return drop("pure insertion can't be expressed as a suggestion");
1365
- const { firstRemoved, lastRemoved } = removedRange(body, hit.oldStart);
1366
- if (firstRemoved === null || lastRemoved === null) return drop("malformed hunk body");
1367
- return {
1368
- kind: "ok",
1369
- startLine: firstRemoved,
1370
- endLine: lastRemoved,
1371
- suggestion: addedLines.map((l) => l.text).join("\n")
1372
- };
1373
1472
  };
1473
+ var readNudges = (counterPath) => {
1474
+ try {
1475
+ const n = Number.parseInt(readFileSync(counterPath, "utf-8").trim(), 10);
1476
+ return Number.isInteger(n) && n >= 0 ? n : 0;
1477
+ } catch {
1478
+ return 0;
1479
+ }
1480
+ };
1481
+ var bumpNudges = (counterPath, current) => {
1482
+ writeFileSync(counterPath, `${String(current + 1)}
1483
+ `);
1484
+ };
1485
+ var shellQuote = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
1486
+ var defaultHookCommand = (draftPath, opts) => [
1487
+ "code-review stop-gate --draft",
1488
+ shellQuote(draftPath),
1489
+ ...opts.kind ? ["--kind", shellQuote(opts.kind)] : [],
1490
+ ...opts.schema ? ["--schema", shellQuote(opts.schema)] : [],
1491
+ ...opts.schemaVersion ? ["--schema-version", shellQuote(opts.schemaVersion)] : [],
1492
+ ...opts.maxNudges ? ["--max-nudges", shellQuote(opts.maxNudges)] : [],
1493
+ ...opts.counter ? ["--counter", shellQuote(opts.counter)] : []
1494
+ ].join(" ");
1495
+ var stopHookSettings = (command) => ({
1496
+ hooks: { Stop: [{ hooks: [{ type: "command", command }] }] }
1497
+ });
1374
1498
 
1375
1499
  // src/index.ts
1376
1500
  var readJSON = (path) => {
@@ -1406,12 +1530,13 @@ var unwrapAdapt = (either) => {
1406
1530
  var bundledPath = (...segments) => resolve$1(import.meta.dirname, "..", ...segments);
1407
1531
  var packageVersion = JSON.parse(readFileSync(bundledPath("package.json"), "utf-8")).version;
1408
1532
  var resolveTemplatePath = (templateArg) => templateArg ? resolve$1(templateArg) : bundledPath("templates", "comment.eta");
1409
- var resolvePricesPath = (pricesArg) => {
1410
- if (pricesArg) return resolve$1(pricesArg);
1533
+ var resolveInlineTemplatePath = (templateArg) => templateArg ? resolve$1(templateArg) : bundledPath("templates", "inline.eta");
1534
+ var resolvePrices = (pricesArg) => {
1535
+ if (pricesArg) return { kind: "provided", path: resolve$1(pricesArg) };
1411
1536
  process.stderr.write(
1412
- "code-review: no --prices given \u2014 using the bundled example prices (all zero); cost figures will be $0\n"
1537
+ "code-review: no --prices given \u2014 cost will be reported as N/A (no price map to recompute from)\n"
1413
1538
  );
1414
- return bundledPath("schema", "prices.example.json");
1539
+ return { kind: "absent", path: bundledPath("schema", "prices.example.json") };
1415
1540
  };
1416
1541
  var TEST_REPORT_DESCRIPTION = 'Path to a JSON test summary: {"passed": number, "failed": number, "total": number, "failures"?: [{"name": string, "message"?: string}]}';
1417
1542
  var renderCmd = defineCommand({
@@ -1459,14 +1584,15 @@ var renderCmd = defineCommand({
1459
1584
  const findings = decode(FindingsCodec.decode(readJSON(args.findings)), "findings");
1460
1585
  const envelope = decode(ResultEnvelopeCodec.decode(readJSON(args.usage)), "envelope");
1461
1586
  const templatePath = resolveTemplatePath(args.template);
1462
- const pricesPath = resolvePricesPath(args.prices);
1463
- const prices = decode(PriceMapCodec.decode(readJSON(pricesPath)), "prices");
1587
+ const priceResolution = resolvePrices(args.prices);
1588
+ const prices = decode(PriceMapCodec.decode(readJSON(priceResolution.path)), "prices");
1464
1589
  const template = readFileSync(templatePath, "utf-8");
1465
1590
  const testReport = args["test-report"] ? decode(TestSummaryCodec.decode(readJSON(args["test-report"])), "test report") : void 0;
1466
1591
  const output = render({
1467
1592
  findings,
1468
1593
  envelope,
1469
1594
  prices,
1595
+ pricesProvided: priceResolution.kind === "provided",
1470
1596
  template,
1471
1597
  reviewedSha: args["reviewed-sha"],
1472
1598
  route: args.route,
@@ -1494,14 +1620,17 @@ var inlineCmd = defineCommand({
1494
1620
  },
1495
1621
  template: {
1496
1622
  type: "string",
1497
- description: "Path to inline comment Eta template (default: built-in format)"
1623
+ description: "Path to inline comment Eta template (default: bundled templates/inline.eta)"
1498
1624
  }
1499
1625
  },
1500
1626
  run: async ({ args }) => {
1501
1627
  const findings = decode(FindingsCodec.decode(readJSON(args.findings)), "findings");
1502
1628
  const diff = readFileSync(resolve$1(args.diff), "utf-8");
1503
- const inlineTemplate = args.template ? readFileSync(resolve$1(args.template), "utf-8") : void 0;
1504
- const { comments, strays } = buildInlineComments(findings.findings, diff, { inlineTemplate });
1629
+ const inlineTemplate = readFileSync(resolveInlineTemplatePath(args.template), "utf-8");
1630
+ const { comments, strays } = buildInlineComments(findings.findings, diff, {
1631
+ inlineTemplate,
1632
+ findings
1633
+ });
1505
1634
  process.stdout.write(
1506
1635
  JSON.stringify({ comments, strays, stray_markdown: renderStraysSection(strays) }, null, 2)
1507
1636
  );
@@ -1531,8 +1660,7 @@ var costCmd = defineCommand({
1531
1660
  process.stdout.write(JSON.stringify(report, null, 2));
1532
1661
  }
1533
1662
  });
1534
- var declaredSchemaVersion = (raw) => typeof raw === "object" && raw !== null && "schema_version" in raw ? typeof raw.schema_version === "string" ? raw.schema_version : void 0 : void 0;
1535
- var derivedSchemaVersion = (kind, raw) => kind === "findings" ? declaredSchemaVersion(raw) : void 0;
1663
+ var derivedSchemaVersion = (kind, raw) => kind === "findings" ? declaredVersion(raw) : void 0;
1536
1664
  var validateCmd = defineCommand({
1537
1665
  meta: {
1538
1666
  name: "validate",
@@ -1684,36 +1812,30 @@ var readFileLines = (path) => {
1684
1812
  return null;
1685
1813
  }
1686
1814
  };
1687
- var lowerFinding = (finding, repoRoot) => {
1688
- if (finding.patch === void 0) return finding;
1689
- const base = withoutPatch(finding);
1815
+ var validateFinding = (finding, repoRoot) => {
1816
+ if (finding.patch === void 0 || finding.patch === null) return finding;
1690
1817
  const lines = readFileLines(resolve$1(repoRoot, finding.path));
1691
1818
  if (lines === null) {
1692
1819
  process.stderr.write(
1693
- `lower-suggestions: ${finding.path}: could not read file at "${repoRoot}" \u2014 dropping patch
1820
+ `validate-patches: ${finding.path}: could not read file at "${repoRoot}" \u2014 dropping patch
1694
1821
  `
1695
1822
  );
1696
- return base;
1823
+ return withoutPatch(finding);
1697
1824
  }
1698
- const result = lowerPatch(finding.patch, lines);
1699
- if (result.kind === "drop") {
1825
+ const result = validatePatch(finding.patch, lines);
1826
+ if ("kind" in result) {
1700
1827
  process.stderr.write(
1701
- `lower-suggestions: ${finding.path}:${String(finding.start_line)}: ${result.reason} \u2014 dropping patch
1828
+ `validate-patches: ${finding.path}:${String(finding.start_line)}: ${result.reason} \u2014 dropping patch
1702
1829
  `
1703
1830
  );
1704
- return base;
1831
+ return withoutPatch(finding);
1705
1832
  }
1706
- return {
1707
- ...base,
1708
- suggestion: result.suggestion,
1709
- start_line: result.startLine,
1710
- end_line: result.endLine
1711
- };
1833
+ return { ...finding, start_line: result.startLine, end_line: result.endLine };
1712
1834
  };
1713
- var lowerSuggestionsCmd = defineCommand({
1835
+ var validatePatchesCmd = defineCommand({
1714
1836
  meta: {
1715
- name: "lower-suggestions",
1716
- description: "Validate each finding's patch against the real PR-head tree and lower it to an exact suggestion + range, or drop it (issue #10)"
1837
+ name: "validate-patches",
1838
+ description: "Validate each finding's patch against the real PR-head tree, aligning the finding's range to it and keeping the patch, or dropping the patch (issue #10)"
1717
1839
  },
1718
1840
  args: {
1719
1841
  findings: {
@@ -1729,11 +1851,11 @@ var lowerSuggestionsCmd = defineCommand({
1729
1851
  run: async ({ args }) => {
1730
1852
  const findings = decode(FindingsCodec.decode(readJSON(args.findings)), "findings");
1731
1853
  const repoRoot = args["repo-root"] ? resolve$1(args["repo-root"]) : process.cwd();
1732
- const lowered = {
1854
+ const validated = {
1733
1855
  ...findings,
1734
- findings: findings.findings.map((f) => lowerFinding(f, repoRoot))
1856
+ findings: findings.findings.map((f) => validateFinding(f, repoRoot))
1735
1857
  };
1736
- process.stdout.write(`${JSON.stringify(lowered, null, 2)}
1858
+ process.stdout.write(`${JSON.stringify(validated, null, 2)}
1737
1859
  `);
1738
1860
  }
1739
1861
  });
@@ -1783,6 +1905,97 @@ var printSchemaCmd = defineCommand({
1783
1905
  `);
1784
1906
  }
1785
1907
  });
1908
+ var MAX_NUDGES_DEFAULT = 5;
1909
+ var drainStdin = () => {
1910
+ if (process.stdin.isTTY) return;
1911
+ try {
1912
+ readFileSync(0);
1913
+ } catch {
1914
+ }
1915
+ };
1916
+ var requireMaxNudges = (raw) => {
1917
+ if (raw === void 0) return MAX_NUDGES_DEFAULT;
1918
+ if (!/^\d+$/.test(raw)) {
1919
+ fail(`--max-nudges must be a non-negative integer; got "${raw}"`);
1920
+ }
1921
+ const n = Number.parseInt(raw, 10);
1922
+ if (n < 1) {
1923
+ fail(`--max-nudges must be >= 1 \u2014 a gate that never blocks must be omitted, not set to ${raw}`);
1924
+ }
1925
+ return n;
1926
+ };
1927
+ var stopGateCmd = defineCommand({
1928
+ meta: {
1929
+ name: "stop-gate",
1930
+ description: "Claude Code Stop-hook gate: refuse to let the agent end its turn until --draft validates against the schema (bounded by --max-nudges). With --print-settings, emit the --settings JSON that wires this as the Stop hook."
1931
+ },
1932
+ args: {
1933
+ draft: {
1934
+ type: "string",
1935
+ description: "Path to the findings document the agent must produce and keep valid",
1936
+ required: true
1937
+ },
1938
+ kind: {
1939
+ type: "string",
1940
+ description: "Schema kind to validate against: findings | triage | prices (default: findings)"
1941
+ },
1942
+ schema: { type: "string", description: "Path to a schema file (wins over --kind)" },
1943
+ "schema-version": {
1944
+ type: "string",
1945
+ description: "Schema major.minor to validate against (default: the draft's declared version)"
1946
+ },
1947
+ "max-nudges": {
1948
+ type: "string",
1949
+ description: `Times to block before relenting so the step fails downstream as before (default: ${String(MAX_NUDGES_DEFAULT)})`
1950
+ },
1951
+ counter: {
1952
+ type: "string",
1953
+ description: "Path for the nudge counter (default: <draft>.nudges)"
1954
+ },
1955
+ "print-settings": {
1956
+ type: "boolean",
1957
+ description: "Print the Stop-hook settings JSON that wires this gate, then exit"
1958
+ }
1959
+ },
1960
+ run: async ({ args }) => {
1961
+ const draftPath = resolve$1(args.draft);
1962
+ if (args["print-settings"]) {
1963
+ const command = defaultHookCommand(draftPath, {
1964
+ kind: args.kind,
1965
+ schema: args.schema,
1966
+ schemaVersion: args["schema-version"],
1967
+ maxNudges: args["max-nudges"],
1968
+ counter: args.counter
1969
+ });
1970
+ process.stdout.write(`${JSON.stringify(stopHookSettings(command))}
1971
+ `);
1972
+ return;
1973
+ }
1974
+ drainStdin();
1975
+ const kind = requireSchemaKind(args.kind || "findings");
1976
+ const maxNudges = requireMaxNudges(args["max-nudges"]);
1977
+ const counterPath = args.counter ? resolve$1(args.counter) : `${draftPath}.nudges`;
1978
+ const state = draftState(
1979
+ draftPath,
1980
+ (parsed) => args.schema ? resolve$1(args.schema) : schemaPathFor(kind, args["schema-version"] || derivedSchemaVersion(kind, parsed))
1981
+ );
1982
+ const nudges = readNudges(counterPath);
1983
+ const decision = decideGate(state, nudges, maxNudges, draftPath, kind);
1984
+ if (decision.kind === "block") {
1985
+ try {
1986
+ bumpNudges(counterPath, nudges);
1987
+ } catch (err) {
1988
+ process.stderr.write(
1989
+ `stop-gate: cannot persist nudge counter at ${counterPath} \u2192 allowing to avoid an unbounded block loop: ${err instanceof Error ? err.message : String(err)}
1990
+ `
1991
+ );
1992
+ return;
1993
+ }
1994
+ process.stdout.write(`${JSON.stringify({ decision: "block", reason: decision.reason })}
1995
+ `);
1996
+ }
1997
+ }
1998
+ });
1786
1999
  var gatherCmd = defineCommand({
1787
2000
  meta: {
1788
2001
  name: "gather",
@@ -1867,7 +2080,7 @@ var postCmd = defineCommand({
1867
2080
  },
1868
2081
  "inline-template": {
1869
2082
  type: "string",
1870
- description: "Path to inline comment Eta template (default: built-in format)"
2083
+ description: "Path to inline comment Eta template (default: bundled templates/inline.eta)"
1871
2084
  },
1872
2085
  route: {
1873
2086
  type: "string",
@@ -1899,15 +2112,17 @@ var postCmd = defineCommand({
1899
2112
  }
1900
2113
  },
1901
2114
  run: async ({ args }) => {
2115
+ const priceResolution = resolvePrices(args.prices);
1902
2116
  await post({
1903
2117
  repo: args.repo,
1904
2118
  headSha: args["head-sha"],
1905
2119
  botLogin: args["bot-login"] || "github-actions[bot]",
1906
2120
  findingsPath: args.findings,
1907
2121
  envelopePath: args.usage,
1908
- pricesPath: resolvePricesPath(args.prices),
2122
+ pricesPath: priceResolution.path,
2123
+ pricesProvided: priceResolution.kind === "provided",
1909
2124
  templatePath: resolveTemplatePath(args.template),
1910
- inlineTemplatePath: args["inline-template"] ? resolve$1(args["inline-template"]) : void 0,
2125
+ inlineTemplatePath: resolveInlineTemplatePath(args["inline-template"]),
1911
2126
  route: args.route,
1912
2127
  headBranch: args["head-branch"],
1913
2128
  effort: args.effort,
@@ -1921,7 +2136,7 @@ var main = defineCommand({
1921
2136
  meta: {
1922
2137
  name: "code-review",
1923
2138
  version: packageVersion,
1924
- description: "Deterministic commenter for agentic PR review \u2014 gather, render, inline, post, adapt, extract, lower-suggestions, cost, and validate findings JSON"
2139
+ description: "Deterministic commenter for agentic PR review \u2014 gather, render, inline, post, adapt, extract, validate-patches, cost, validate, and stop-gate findings JSON"
1925
2140
  },
1926
2141
  subCommands: {
1927
2142
  gather: gatherCmd,
@@ -1932,8 +2147,9 @@ var main = defineCommand({
1932
2147
  validate: validateCmd,
1933
2148
  adapt: adaptCmd,
1934
2149
  extract: extractCmd,
1935
- "lower-suggestions": lowerSuggestionsCmd,
1936
- "print-schema": printSchemaCmd
2150
+ "validate-patches": validatePatchesCmd,
2151
+ "print-schema": printSchemaCmd,
2152
+ "stop-gate": stopGateCmd
1937
2153
  }
1938
2154
  });
1939
2155
  if (!process.env["VITEST"]) {