@attalabs/vinaya 0.9.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +33 -7
  2. package/aeg-root/contracts/planner-brief.md +1 -1
  3. package/aeg-root/enforcement.md +7 -7
  4. package/aeg-root/glossary.md +4 -0
  5. package/aeg-root/roles/archivist.md +2 -2
  6. package/aeg-root/roles/developer.md +29 -5
  7. package/aeg-root/roles/planner.md +3 -3
  8. package/aeg-root/roles/reviewer.md +17 -4
  9. package/aeg-root/roles/security.md +16 -5
  10. package/aeg-root/roles/tranche-archivist.md +1 -1
  11. package/aeg-root/skills/aeg/SKILL.md +1 -1
  12. package/aeg-root/state-machine.md +8 -8
  13. package/aeg-root/templates/pr-report-template.md +8 -4
  14. package/aeg-root/tranche-model.md +34 -12
  15. package/dist/checks/bin/check-branch-topology.js +232 -120
  16. package/dist/checks/bin/check-brief-shape.js +232 -120
  17. package/dist/checks/bin/check-closes-n.js +232 -120
  18. package/dist/checks/bin/check-coherence.js +234 -121
  19. package/dist/checks/bin/check-dead-branch-push.js +232 -120
  20. package/dist/checks/bin/check-dispatch-readiness.js +234 -121
  21. package/dist/checks/bin/check-doc-coverage-push.js +261 -130
  22. package/dist/checks/bin/check-doc-coverage.js +263 -132
  23. package/dist/checks/bin/check-evidence-fresh.js +3196 -0
  24. package/dist/checks/bin/check-first-push-dispatch.js +234 -121
  25. package/dist/checks/bin/check-issue-assignment.js +232 -120
  26. package/dist/checks/bin/check-no-disk-state.js +232 -120
  27. package/dist/checks/bin/check-reader-resolvable-prose.js +232 -120
  28. package/dist/checks/bin/check-registry-gates.js +232 -120
  29. package/dist/checks/bin/check-review-gate.js +237 -123
  30. package/dist/checks/bin/check-single-plan-pr.js +232 -120
  31. package/dist/checks/bin/check-test-plan.js +232 -120
  32. package/dist/index.js +1232 -262
  33. package/package.json +1 -1
  34. package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
  35. package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
  36. package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
  37. package/studio-standalone/apps/vinaya-studio/web/.next/required-server-files.json +3 -1
  38. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
  39. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
  40. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  41. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  42. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  43. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
  44. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  45. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  46. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
  47. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
  48. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
  49. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
  50. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
  51. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
  52. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
  53. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
  54. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js +3 -3
  55. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
  56. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
  57. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
  58. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
  59. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
  60. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
  61. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js +2 -3
  62. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
  63. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
  64. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
  65. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
  66. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
  67. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
  68. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
  69. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
  70. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
  71. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
  72. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
  73. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/[root-of-the-server]__0e95dv5._.js +1 -1
  74. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/1q96_modules_@clerk_nextjs_dist_esm_app-router_client_keyless-creator-reader_0lom2js.js +1 -1
  75. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__029-1ch._.js +1 -1
  76. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__05t6482._.js +1 -1
  77. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0gw1_g2._.js +1 -1
  78. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0mdkphe._.js +3 -0
  79. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0plfbyn._.js +45 -0
  80. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0q56ohl._.js +1 -1
  81. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0rr_lq7._.js +51 -0
  82. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0zfjc2o._.js +1 -1
  83. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__19jyp3m._.js +1 -1
  84. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1_ojzp1._.js +1 -1
  85. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1d_8nm4._.js +1 -1
  86. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__1p19382._.js → [root-of-the-server]__1qpl4m_._.js} +1 -1
  87. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1zkwtx8._.js +1 -1
  88. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__210ehg9._.js +1 -1
  89. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_00yjcv1._.js +1 -1
  90. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0o54suy._.js +1 -1
  91. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0rix02i._.js +1 -1
  92. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_18-d2be._.js +1 -1
  93. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +5 -4
  94. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1gw6vc9._.js +1 -1
  95. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_app_studio_projects_[name]_tranches_[slug]_20yap_f._.js +1 -1
  96. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
  97. package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
  98. package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
  99. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
  100. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
  101. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3antrj5z0uf_c.js → 0ek6qlqhokys0.js} +1 -1
  102. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1khy4oilrab6g.js → 0fm4p9os2lsom.js} +1 -1
  103. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3zyedaek9nvr_.js → 0ywkq74falx-x.js} +1 -1
  104. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1s9ixp5nhnevp.js +1 -0
  105. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/27sqffqm2wbxx.css +1 -0
  106. package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
  107. package/studio-standalone/apps/vinaya-studio/web/server.js +1 -1
  108. package/templates/custom-check.template.ts +5 -0
  109. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1s7kj3s._.js +0 -3
  110. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1tswrm4._.js +0 -9
  111. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_01jl99k._.js +0 -45
  112. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_20c6-3m._.js +0 -45
  113. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0qu0xgvifbacu.css +0 -1
  114. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/2e68sbtdhi5dg.js +0 -1
  115. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{pY8RKWuVAUZOjj2-bAYCY → R7xvB2iuNFp_jxENpL7xv}/_buildManifest.js +0 -0
  116. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{pY8RKWuVAUZOjj2-bAYCY → R7xvB2iuNFp_jxENpL7xv}/_clientMiddlewareManifest.js +0 -0
  117. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{pY8RKWuVAUZOjj2-bAYCY → R7xvB2iuNFp_jxENpL7xv}/_ssgManifest.js +0 -0
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync18 } from "node:fs";
5
- import { dirname as dirname8, join as join20 } from "node:path";
4
+ import { readFileSync as readFileSync20 } from "node:fs";
5
+ import { dirname as dirname8, join as join22 } from "node:path";
6
6
  import { fileURLToPath as fileURLToPath2 } from "node:url";
7
7
 
8
8
  // src/commands/archive.ts
9
- import { execFileSync as execFileSync3 } from "node:child_process";
10
- // ../../packages/aeg-core/src/anchored-region.ts
9
+ import { execFileSync as execFileSync4 } from "node:child_process";
10
+ // ../../packages/aeg-forge-state/src/strip-code.ts
11
11
  function maskCode(body) {
12
12
  const fill = (line) => " ".repeat(line.length);
13
13
  return maskIndentedCode(maskFencedCode(body, fill), fill).replace(/(`+)[^\n]*?\1/g, (m) => " ".repeat(m.length));
@@ -24,29 +24,34 @@ function stripCode(body, options = {}) {
24
24
  var FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})([^\r\n]*)\r?$/;
25
25
  var FENCE_CLOSE = /^ {0,3}(`+|~+)[ \t]*\r?$/;
26
26
  function maskFencedCode(body, fill) {
27
+ const { lines } = scanFencedCode(body);
28
+ return lines.map(({ line, inCode }) => inCode ? fill(line) : line).join(`
29
+ `);
30
+ }
31
+ function scanFencedCode(body) {
27
32
  let fenceChar = null;
28
33
  let fenceLen = 0;
29
- return body.split(`
34
+ const lines = body.split(`
30
35
  `).map((line) => {
31
36
  if (fenceChar === null) {
32
37
  const open = line.match(FENCE_OPEN);
33
38
  if (!open)
34
- return line;
39
+ return { line, inCode: false };
35
40
  const marker = open[1];
36
41
  if (marker[0] === "`" && open[2].includes("`"))
37
- return line;
42
+ return { line, inCode: false };
38
43
  fenceChar = marker[0];
39
44
  fenceLen = marker.length;
40
- return fill(line);
45
+ return { line, inCode: true };
41
46
  }
42
47
  const close = line.match(FENCE_CLOSE);
43
48
  if (close && close[1][0] === fenceChar && close[1].length >= fenceLen) {
44
49
  fenceChar = null;
45
50
  fenceLen = 0;
46
51
  }
47
- return fill(line);
48
- }).join(`
49
- `);
52
+ return { line, inCode: true };
53
+ });
54
+ return { lines, unterminated: fenceChar !== null };
50
55
  }
51
56
  var LIST_MARKER = /^ {0,3}(?:[-*+]|\d{1,9}[.)])(?:\s|$)/;
52
57
  function indentWidth(line) {
@@ -89,88 +94,7 @@ function maskIndentedCode(body, fill) {
89
94
  return out.join(`
90
95
  `);
91
96
  }
92
- function anchoredRegion(body, field) {
93
- const masked = maskCode(body);
94
- const start = new RegExp(`<!--\\s*AEG:${field}:START\\s*-->`).exec(masked);
95
- if (!start)
96
- return null;
97
- const afterStart = start.index + start[0].length;
98
- const end = new RegExp(`<!--\\s*AEG:${field}:END\\s*-->`).exec(masked.slice(afterStart));
99
- if (!end)
100
- return null;
101
- return body.slice(afterStart, afterStart + end.index);
102
- }
103
- // ../../packages/aeg-core/src/parse-registry.ts
104
- function parseRegistry(md) {
105
- const rows = readMarkdownTable(md, /^##\s+Registry\b/i);
106
- const projects = [];
107
- for (const row of rows) {
108
- if (row.length < 4)
109
- continue;
110
- const name = stripBackticks(row[0] ?? "").trim();
111
- const path = stripBackticks(row[1] ?? "").trim();
112
- const specsPath = stripBackticks(row[2] ?? "").trim();
113
- const stateCell = (row[3] ?? "").trim();
114
- if (!name || !path || !specsPath)
115
- continue;
116
- projects.push({
117
- name,
118
- path,
119
- specsPath,
120
- statePath: parseStateCell(stateCell)
121
- });
122
- }
123
- return projects;
124
- }
125
- function parseStateCell(cell) {
126
- if (!cell)
127
- return null;
128
- const stripped = stripBackticks(cell).trim();
129
- if (/^[\w./@-]+\/?$/.test(stripped) && cell.includes("`")) {
130
- return stripped;
131
- }
132
- return null;
133
- }
134
- function stripBackticks(s) {
135
- return s.replace(/`/g, "");
136
- }
137
- function readMarkdownTable(md, headingMatcher) {
138
- const lines = md.split(/\r?\n/);
139
- let i = 0;
140
- for (;i < lines.length; i++) {
141
- if (headingMatcher.test(lines[i] ?? "")) {
142
- i++;
143
- break;
144
- }
145
- }
146
- if (i >= lines.length)
147
- return [];
148
- for (;i < lines.length; i++) {
149
- if ((lines[i] ?? "").trim().startsWith("|"))
150
- break;
151
- }
152
- if (i + 1 >= lines.length)
153
- return [];
154
- i += 2;
155
- const out = [];
156
- for (;i < lines.length; i++) {
157
- const trimmed = (lines[i] ?? "").trim();
158
- if (!trimmed.startsWith("|"))
159
- break;
160
- out.push(splitTableRow(trimmed));
161
- }
162
- return out;
163
- }
164
- function splitTableRow(row) {
165
- const PLACEHOLDER = "\x00PIPE\x00";
166
- const escaped = row.replace(/\\\|/g, PLACEHOLDER);
167
- const parts = escaped.split("|").map((s) => s.trim());
168
- if (parts.length && parts[0] === "")
169
- parts.shift();
170
- if (parts.length && parts[parts.length - 1] === "")
171
- parts.pop();
172
- return parts.map((p) => p.split(PLACEHOLDER).join("|"));
173
- }
97
+
174
98
  // ../../packages/aeg-forge-state/src/parse-rationale-deps.ts
175
99
  var SECTION_HEADER = /\*\*Dependency rationale\*\*/i;
176
100
  // ../../packages/aeg-forge-state/src/gh.ts
@@ -378,6 +302,175 @@ function parseOwnerRepo(value) {
378
302
  return null;
379
303
  return { owner: match[1], repo: match[2] };
380
304
  }
305
+ // ../../packages/aeg-core/src/anchored-region.ts
306
+ function anchoredRegionBounds(body, field) {
307
+ const masked = maskCode(body);
308
+ const start = new RegExp(`<!--\\s*AEG:${field}:START\\s*-->`).exec(masked);
309
+ if (!start)
310
+ return null;
311
+ const innerStart = start.index + start[0].length;
312
+ const end = new RegExp(`<!--\\s*AEG:${field}:END\\s*-->`).exec(masked.slice(innerStart));
313
+ if (!end)
314
+ return null;
315
+ const innerEnd = innerStart + end.index;
316
+ return { outerStart: start.index, outerEnd: innerEnd + end[0].length, innerStart, innerEnd };
317
+ }
318
+ function anchoredRegion(body, field) {
319
+ const bounds = anchoredRegionBounds(body, field);
320
+ if (!bounds)
321
+ return null;
322
+ return body.slice(bounds.innerStart, bounds.innerEnd);
323
+ }
324
+ // ../../packages/aeg-core/src/parse-registry.ts
325
+ function parseRegistry(md) {
326
+ const rows = readMarkdownTable(md, /^##\s+Registry\b/i);
327
+ const projects = [];
328
+ for (const row of rows) {
329
+ if (row.length < 4)
330
+ continue;
331
+ const name = stripBackticks(row[0] ?? "").trim();
332
+ const path = stripBackticks(row[1] ?? "").trim();
333
+ const specsPath = stripBackticks(row[2] ?? "").trim();
334
+ const stateCell = (row[3] ?? "").trim();
335
+ if (!name || !path || !specsPath)
336
+ continue;
337
+ projects.push({
338
+ name,
339
+ path,
340
+ specsPath,
341
+ statePath: parseStateCell(stateCell)
342
+ });
343
+ }
344
+ return projects;
345
+ }
346
+ function parseStateCell(cell) {
347
+ if (!cell)
348
+ return null;
349
+ const stripped = stripBackticks(cell).trim();
350
+ if (/^[\w./@-]+\/?$/.test(stripped) && cell.includes("`")) {
351
+ return stripped;
352
+ }
353
+ return null;
354
+ }
355
+ function stripBackticks(s) {
356
+ return s.replace(/`/g, "");
357
+ }
358
+ function readMarkdownTable(md, headingMatcher) {
359
+ const lines = md.split(/\r?\n/);
360
+ let i = 0;
361
+ for (;i < lines.length; i++) {
362
+ if (headingMatcher.test(lines[i] ?? "")) {
363
+ i++;
364
+ break;
365
+ }
366
+ }
367
+ if (i >= lines.length)
368
+ return [];
369
+ for (;i < lines.length; i++) {
370
+ if ((lines[i] ?? "").trim().startsWith("|"))
371
+ break;
372
+ }
373
+ if (i + 1 >= lines.length)
374
+ return [];
375
+ i += 2;
376
+ const out = [];
377
+ for (;i < lines.length; i++) {
378
+ const trimmed = (lines[i] ?? "").trim();
379
+ if (!trimmed.startsWith("|"))
380
+ break;
381
+ out.push(splitTableRow(trimmed));
382
+ }
383
+ return out;
384
+ }
385
+ function splitTableRow(row) {
386
+ const PLACEHOLDER = "\x00PIPE\x00";
387
+ const escaped = row.replace(/\\\|/g, PLACEHOLDER);
388
+ const parts = escaped.split("|").map((s) => s.trim());
389
+ if (parts.length && parts[0] === "")
390
+ parts.shift();
391
+ if (parts.length && parts[parts.length - 1] === "")
392
+ parts.pop();
393
+ return parts.map((p) => p.split(PLACEHOLDER).join("|"));
394
+ }
395
+ // ../../packages/aeg-core/src/blast-radius-domains.ts
396
+ function resolveWorkspaceEntry(entry2, listDirs) {
397
+ if (entry2.endsWith("/*")) {
398
+ const baseDir = entry2.slice(0, -2);
399
+ return listDirs(baseDir).map((name) => `${baseDir}/${name}`);
400
+ }
401
+ if (entry2.includes("*"))
402
+ return [];
403
+ return [entry2];
404
+ }
405
+ function deriveWorkspacePackageDomains(workspaces, listDirs) {
406
+ const positive = workspaces.filter((entry2) => !entry2.startsWith("!"));
407
+ const negative = workspaces.filter((entry2) => entry2.startsWith("!")).map((entry2) => entry2.slice(1));
408
+ const excluded = new Set;
409
+ for (const entry2 of negative) {
410
+ for (const member of resolveWorkspaceEntry(entry2, listDirs))
411
+ excluded.add(member);
412
+ }
413
+ const domains = new Set;
414
+ for (const entry2 of positive) {
415
+ for (const member of resolveWorkspaceEntry(entry2, listDirs)) {
416
+ if (excluded.has(member))
417
+ continue;
418
+ const segments = member.split("/");
419
+ if (segments[0] !== "packages" || segments.length < 2)
420
+ continue;
421
+ domains.add(`packages/${segments[1]}`);
422
+ }
423
+ }
424
+ return [...domains].sort();
425
+ }
426
+ function parsePnpmWorkspaceYaml(content) {
427
+ const unquote = (s) => {
428
+ const t = s.trim();
429
+ if (t.length >= 2 && (t[0] === "'" && t.at(-1) === "'" || t[0] === '"' && t.at(-1) === '"')) {
430
+ return t.slice(1, -1);
431
+ }
432
+ return t;
433
+ };
434
+ const inlineMatch = content.match(/^packages:\s*\[(.*)\]\s*$/m);
435
+ if (inlineMatch) {
436
+ return (inlineMatch[1] ?? "").split(",").map(unquote).filter((s) => s.length > 0);
437
+ }
438
+ const entries = [];
439
+ let inBlock = false;
440
+ for (const rawLine of content.split(`
441
+ `)) {
442
+ const line = rawLine.replace(/#.*$/, "");
443
+ if (/^packages:\s*$/.test(line)) {
444
+ inBlock = true;
445
+ continue;
446
+ }
447
+ if (!inBlock)
448
+ continue;
449
+ const itemMatch = line.match(/^\s+-\s*(.+?)\s*$/);
450
+ if (itemMatch?.[1]) {
451
+ entries.push(unquote(itemMatch[1]));
452
+ continue;
453
+ }
454
+ if (line.trim().length === 0)
455
+ continue;
456
+ break;
457
+ }
458
+ return entries;
459
+ }
460
+ var CROSS_CUTTING_CANDIDATES = [
461
+ "bun.lock",
462
+ "package-lock.json",
463
+ "pnpm-lock.yaml",
464
+ "yarn.lock",
465
+ "turbo.json",
466
+ "biome.json",
467
+ "tsconfig.json",
468
+ ".github/workflows",
469
+ ".husky"
470
+ ];
471
+ function deriveBuiltinCrossCuttingDefaults(exists) {
472
+ return CROSS_CUTTING_CANDIDATES.filter((path) => exists(path));
473
+ }
381
474
  // ../../packages/aeg-core/src/state-machine-model.ts
382
475
  var FORGE_FACT_INPUTS = [
383
476
  {
@@ -443,12 +536,30 @@ var DERIVED_STATUSES = [
443
536
  "incoherent"
444
537
  ];
445
538
  var DERIVABLE_STATUSES = DERIVED_STATUSES.filter((s) => s !== "backlog");
539
+ // ../../packages/aeg-core/src/file-classify.ts
540
+ function isCodeFile(p) {
541
+ if (p.endsWith(".md"))
542
+ return false;
543
+ return /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|sql|css)$/.test(p);
544
+ }
446
545
  // ../../packages/aeg-core/src/waiver-label.ts
447
546
  var WAIVER_LABEL = label("waiver-docs");
448
547
  var WAIVER_LABEL_REVIEW = label("waiver-review");
548
+ var PRINCIPAL_ALLOWLIST = ["daniboomerang"];
549
+ function isPrincipal(login, principalAllowlist) {
550
+ if (login === null)
551
+ return false;
552
+ const normalized = login.toLowerCase();
553
+ return principalAllowlist.some((p) => p.toLowerCase() === normalized);
554
+ }
449
555
 
450
556
  // ../../packages/aeg-core/src/doc-owners.ts
451
557
  var DOC_OWNERS_PATH = ".vinaya/doc-owners";
558
+ function classifyDocOwnersManifest(content) {
559
+ if (content === null)
560
+ return "absent";
561
+ return content.trim() === "" ? "empty" : "present";
562
+ }
452
563
  function parseDocOwners(content) {
453
564
  const bindings = [];
454
565
  const errors = [];
@@ -493,6 +604,13 @@ function globToRegex(pat) {
493
604
  re += "$";
494
605
  return new RegExp(re);
495
606
  }
607
+ function isUrlPointer(p) {
608
+ return /^https?:\/\//.test(p);
609
+ }
610
+ function pointerToPath(p) {
611
+ const idx = p.indexOf("#");
612
+ return idx === -1 ? p : p.slice(0, idx);
613
+ }
496
614
  var SEPARATOR = /(?:[ \t]*[—–][ \t]*|[ \t]+-[ \t]+)/.source;
497
615
  // ../../packages/aeg-core/src/pr-tier.ts
498
616
  function readTierFromPrBody(prBody) {
@@ -779,15 +897,25 @@ function isTaskIssueLabelSet(labels) {
779
897
  // ../../packages/aeg-core/src/coherence-checks.ts
780
898
  var R1_GRANDFATHERED_ISSUES = new Set([279, 280, 281, 282]);
781
899
  // ../../packages/aeg-core/src/verdict-extraction.ts
900
+ var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
901
+ function extractHeadSha(comment) {
902
+ const m = comment.match(HEAD_SHA_PATTERN);
903
+ return m ? m[1].toLowerCase() : null;
904
+ }
782
905
  function extractVerdict(comments, valuePattern, missingLabel) {
783
906
  const clearHits = comments.filter((c) => valuePattern.test(c));
784
907
  if (clearHits.length > 0) {
785
908
  const latest = clearHits[clearHits.length - 1];
786
909
  const m = latest.match(valuePattern);
787
- return { value: m[1].toUpperCase().replace(/[_-]/g, " "), danglingNote: null };
910
+ return {
911
+ value: m[1].toUpperCase().replace(/[_-]/g, " "),
912
+ headSha: extractHeadSha(latest),
913
+ danglingNote: null
914
+ };
788
915
  }
789
916
  return {
790
917
  value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
918
+ headSha: null,
791
919
  danglingNote: `no ${missingLabel} verdict comment found on this PR`
792
920
  };
793
921
  }
@@ -970,6 +1098,10 @@ function starterConfig() {
970
1098
  }
971
1099
  };
972
1100
  }
1101
+ function ownVersion() {
1102
+ const pkg = JSON.parse(readFileSync(join2(packageRoot(import.meta.url), "package.json"), "utf-8"));
1103
+ return pkg.version;
1104
+ }
973
1105
  var SETUP_BUN_SHA = "0c5077e51419868618aeaa5fe8019c62421857d6";
974
1106
  function vinayaSetupSteps(selfHost) {
975
1107
  if (!selfHost)
@@ -1001,7 +1133,7 @@ function vinayaSetupSteps(selfHost) {
1001
1133
  `;
1002
1134
  }
1003
1135
  function vinayaRun(selfHost, args) {
1004
- return selfHost ? `node ${selfHost.bin} ${args}` : `npx --yes @attalabs/vinaya ${args}`;
1136
+ return selfHost ? `node ${selfHost.bin} ${args}` : `npx --yes @attalabs/vinaya@${ownVersion()} ${args}`;
1005
1137
  }
1006
1138
  function adopterSetupStep(ciSetup) {
1007
1139
  if (!ciSetup)
@@ -1427,10 +1559,6 @@ ${vinayaSetupSteps(selfHost)} - name: Run vinaya audit --only=direct-push
1427
1559
  }
1428
1560
  var HOOK_PREAMBLE = `#!/usr/bin/env sh
1429
1561
  `;
1430
- function ownVersion() {
1431
- const pkg = JSON.parse(readFileSync(join2(packageRoot(import.meta.url), "package.json"), "utf-8"));
1432
- return pkg.version;
1433
- }
1434
1562
  function hookRun(selfHost, args) {
1435
1563
  if (!selfHost)
1436
1564
  return `npx --yes @attalabs/vinaya@${ownVersion()} ${args} || exit 1`;
@@ -1522,6 +1650,14 @@ function starterDocOwners() {
1522
1650
  # This file absent, OR no glob matches any changed code file → silent no-op.
1523
1651
  # The gate has no opinion until you teach it one.
1524
1652
  #
1653
+ # \`vinaya doctor\` (never \`vinaya check\`) separately reports — it does not
1654
+ # fail the check — any binding whose code glob matches zero tracked files
1655
+ # anywhere in the repo, or whose in-repo pointer doesn't exist on disk. This
1656
+ # catches what the dormancy rule above structurally cannot: a glob matching
1657
+ # nothing trivially satisfies "no changed file matched" on every diff,
1658
+ # forever, so a binding naming deleted or renamed code reads as healthy
1659
+ # indefinitely unless something walks the whole repo, not just one diff.
1660
+ #
1525
1661
  # ── No-doc allow-list ───────────────────────────────────────────────────────
1526
1662
  # Surfaces that legitimately need no bound doc are listed below. Format:
1527
1663
  # # no-doc: <glob> — <reason>
@@ -1654,11 +1790,12 @@ function buildInitProductOps(name) {
1654
1790
  }
1655
1791
 
1656
1792
  // src/lib/ops.ts
1657
- import { execFileSync as execFileSync2 } from "node:child_process";
1793
+ import { execFileSync as execFileSync3 } from "node:child_process";
1658
1794
  import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
1659
1795
  import { dirname as dirname3, isAbsolute, join as join4, resolve, sep } from "node:path";
1660
1796
 
1661
1797
  // src/lib/config.ts
1798
+ import { execFileSync as execFileSync2 } from "node:child_process";
1662
1799
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
1663
1800
  import { homedir } from "node:os";
1664
1801
  import { dirname as dirname2, join as join3 } from "node:path";
@@ -1786,7 +1923,8 @@ var VinayaConfigSchema = z.object({
1786
1923
  briefSchema: BriefSchemaSchema.optional(),
1787
1924
  managed: ManagedManifestSchema.optional(),
1788
1925
  principals: z.array(z.string()).min(1).optional(),
1789
- ci: z.object({ setup: z.string().min(1) }).optional()
1926
+ ci: z.object({ setup: z.string().min(1) }).optional(),
1927
+ blastRadius: z.object({ extraDomains: z.array(z.string()).optional() }).optional()
1790
1928
  });
1791
1929
  var GLOBAL_VINAYA_HOME = join3(homedir(), ".vinaya");
1792
1930
  var GLOBAL_CONFIG_PATH = join3(GLOBAL_VINAYA_HOME, "config.json");
@@ -1833,6 +1971,75 @@ function stripGlobalOnlyKeys(config, path) {
1833
1971
  }
1834
1972
  return result;
1835
1973
  }
1974
+ function resolvePrincipalAllowlist(config) {
1975
+ return config?.principals ?? PRINCIPAL_ALLOWLIST;
1976
+ }
1977
+ function trustAnchorRepo() {
1978
+ const wellFormed = (slug) => /^[^/\s]+\/[^/\s]+$/.test(slug) ? slug : null;
1979
+ const fromRunner = process.env.GITHUB_REPOSITORY?.trim();
1980
+ if (fromRunner) {
1981
+ const validated = wellFormed(fromRunner);
1982
+ if (validated)
1983
+ return validated;
1984
+ }
1985
+ try {
1986
+ const url = execFileSync2("git", ["remote", "get-url", "origin"], {
1987
+ encoding: "utf-8",
1988
+ stdio: ["ignore", "pipe", "pipe"]
1989
+ }).trim();
1990
+ const m = url.match(/github\.com[:/]([^/]+)\/(.+?)(?:\.git)?\/?$/);
1991
+ return m?.[1] && m[2] ? wellFormed(`${m[1]}/${m[2]}`) : null;
1992
+ } catch {
1993
+ return null;
1994
+ }
1995
+ }
1996
+ function ghFetchTrustAnchorConfig() {
1997
+ const repo = trustAnchorRepo();
1998
+ if (!repo)
1999
+ throw new Error("could not resolve repo identity for the trust-anchor read");
2000
+ return execFileSync2("gh", ["api", `repos/${repo}/contents/${LOCAL_CONFIG_FILENAME}`, "--jq", ".content"], {
2001
+ encoding: "utf-8",
2002
+ stdio: ["ignore", "pipe", "pipe"]
2003
+ });
2004
+ }
2005
+ function firstLine(err) {
2006
+ return err?.message?.split(`
2007
+ `)[0] ?? "unknown error";
2008
+ }
2009
+ function isMissingFileError(err) {
2010
+ const stderr = err?.stderr;
2011
+ const haystack = [
2012
+ err?.message ?? "",
2013
+ typeof stderr === "string" ? stderr : stderr?.toString() ?? ""
2014
+ ].join(`
2015
+ `);
2016
+ return /\b404\b|not found/i.test(haystack);
2017
+ }
2018
+ function loadTrustAnchorConfig(fetcher = ghFetchTrustAnchorConfig) {
2019
+ const warn = (why) => process.stdout.write(`⚠ could not read \`principals\` from the default branch (${why}) — falling back to vinaya's built-in principal allowlist.
2020
+ `);
2021
+ let base64;
2022
+ try {
2023
+ base64 = fetcher().trim();
2024
+ } catch (err) {
2025
+ if (!isMissingFileError(err))
2026
+ warn(firstLine(err));
2027
+ return null;
2028
+ }
2029
+ if (!base64)
2030
+ return null;
2031
+ try {
2032
+ const raw = Buffer.from(base64, "base64").toString("utf-8");
2033
+ const parsed = VinayaConfigSchema.safeParse(JSON.parse(raw));
2034
+ if (parsed.success)
2035
+ return parsed.data;
2036
+ warn("it did not satisfy the config schema");
2037
+ return null;
2038
+ } catch {
2039
+ warn("it was not readable as JSON");
2040
+ return null;
2041
+ }
2042
+ }
1836
2043
  function readRepoCiSetup(repoRoot) {
1837
2044
  const p = join3(repoRoot, "vinaya.config.json");
1838
2045
  if (!existsSync2(p))
@@ -1883,7 +2090,7 @@ function resolveManagedBlockPath(repoRoot, opPath) {
1883
2090
  if (!opPath.startsWith(".git/"))
1884
2091
  return join4(repoRoot, opPath);
1885
2092
  try {
1886
- const commonDir = execFileSync2("git", ["-C", repoRoot, "rev-parse", "--git-common-dir"], {
2093
+ const commonDir = execFileSync3("git", ["-C", repoRoot, "rev-parse", "--git-common-dir"], {
1887
2094
  encoding: "utf8"
1888
2095
  }).trim();
1889
2096
  const gitDir = isAbsolute(commonDir) ? commonDir : join4(repoRoot, commonDir);
@@ -2449,7 +2656,7 @@ function realDeps() {
2449
2656
  return { detectRepo: detectGitRepo };
2450
2657
  }
2451
2658
  function sh(args, input) {
2452
- return execFileSync3(args[0], args.slice(1), { encoding: "utf8", input }).trim();
2659
+ return execFileSync4(args[0], args.slice(1), { encoding: "utf8", input }).trim();
2453
2660
  }
2454
2661
  function shJson(args) {
2455
2662
  return JSON.parse(sh(args));
@@ -2625,7 +2832,7 @@ async function archiveTrancheCommand(args) {
2625
2832
  }
2626
2833
 
2627
2834
  // src/commands/audit.ts
2628
- import { execFileSync as execFileSync4 } from "node:child_process";
2835
+ import { execFileSync as execFileSync5 } from "node:child_process";
2629
2836
 
2630
2837
  // src/lib/envelope.ts
2631
2838
  var ENVELOPE_SCHEMA_VERSION = 1;
@@ -2653,7 +2860,7 @@ var DIRECT_PUSH_MARKER = `<!-- ${DIRECT_PUSH_LABEL} -->`;
2653
2860
  var DIRECT_PUSH_LABEL_DESCRIPTION = "A commit landed on main with no associated merged PR (ring-2 detection)";
2654
2861
  function shSoft(args, input) {
2655
2862
  try {
2656
- return execFileSync4(args[0], args.slice(1), {
2863
+ return execFileSync5(args[0], args.slice(1), {
2657
2864
  encoding: "utf8",
2658
2865
  input,
2659
2866
  timeout: COMMAND_TIMEOUT_MS
@@ -2677,7 +2884,7 @@ function shSoftJson(args) {
2677
2884
  }
2678
2885
  }
2679
2886
  function sh2(args, input) {
2680
- return execFileSync4(args[0], args.slice(1), { encoding: "utf8", input }).trim();
2887
+ return execFileSync5(args[0], args.slice(1), { encoding: "utf8", input }).trim();
2681
2888
  }
2682
2889
  function listTaskBranches() {
2683
2890
  const out = shSoft(["git", "ls-remote", "--heads", "origin", "task/*"]);
@@ -2927,7 +3134,7 @@ async function auditCommand(args) {
2927
3134
  }
2928
3135
 
2929
3136
  // src/commands/check.ts
2930
- import { execFileSync as execFileSync5 } from "node:child_process";
3137
+ import { execFileSync as execFileSync6 } from "node:child_process";
2931
3138
 
2932
3139
  // src/checks/contract.ts
2933
3140
  var CHECK_SCHEMA_VERSION = 1;
@@ -3132,17 +3339,31 @@ function coreCheckRegistry() {
3132
3339
  GITHUB_TOKEN: { optional: true },
3133
3340
  GH_TOKEN: { optional: true }
3134
3341
  }
3135
- }
3136
- ];
3137
- }
3138
-
3139
- // src/checks/resolver.ts
3140
- var NAMESPACE_SEGMENT = /^[a-z0-9][a-z0-9-]*$/;
3141
- var RESERVED_PREFIX = "vinaya";
3142
- function isValidNamespacedKey(key) {
3143
- const parts = key.split("/");
3144
- if (parts.length !== 2)
3145
- return false;
3342
+ },
3343
+ {
3344
+ name: "evidence-fresh",
3345
+ run: bin("check-evidence-fresh"),
3346
+ scope: "diff",
3347
+ timeoutMs: 15000,
3348
+ requiresOpenPr: true,
3349
+ env: {
3350
+ PR_NUMBER: { optional: true },
3351
+ PR_BODY: { optional: true },
3352
+ BASE_SHA: { optional: true },
3353
+ GITHUB_TOKEN: { optional: true },
3354
+ GH_TOKEN: { optional: true }
3355
+ }
3356
+ }
3357
+ ];
3358
+ }
3359
+
3360
+ // src/checks/resolver.ts
3361
+ var NAMESPACE_SEGMENT = /^[a-z0-9][a-z0-9-]*$/;
3362
+ var RESERVED_PREFIX = "vinaya";
3363
+ function isValidNamespacedKey(key) {
3364
+ const parts = key.split("/");
3365
+ if (parts.length !== 2)
3366
+ return false;
3146
3367
  const [prefix, rest] = parts;
3147
3368
  if (!prefix || !rest)
3148
3369
  return false;
@@ -3177,11 +3398,16 @@ function resolveChecks(core, configChecks) {
3177
3398
  }
3178
3399
  return { resolved, failures };
3179
3400
  }
3180
- function overriddenNextMinorWarning(name) {
3181
- return `check "${name}" shares its name with a core check — it will replace the core check starting next minor (currently it runs alongside it under the flat registry).`;
3401
+ var MAX_KEY_DISPLAY_LENGTH = 80;
3402
+ function sanitizeKeyForDisplay(key) {
3403
+ const stripped = key.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ");
3404
+ return stripped.length > MAX_KEY_DISPLAY_LENGTH ? `${stripped.slice(0, MAX_KEY_DISPLAY_LENGTH)}…` : stripped;
3405
+ }
3406
+ function overriddenReplacesCoreDiagnostic(name) {
3407
+ return `check "${sanitizeKeyForDisplay(name)}" shares its name with a core check — it REPLACES that core check. The core check does not run.`;
3182
3408
  }
3183
- function bareKeyNextMinorWarning(key) {
3184
- return `check "${key}" has no namespace and matches no core check — it will be rejected starting next minor. Rename it to "<yourname>/x".`;
3409
+ function bareKeyRejectedDiagnostic(key) {
3410
+ return `check "${sanitizeKeyForDisplay(key)}" has no namespace and matches no core check — it is REJECTED, and \`vinaya check\` refuses the whole run rather than executing a partial ruleset. Rename it to "<yourname>/<id>", both segments matching [a-z0-9][a-z0-9-]* — prefixing alone is not enough when the bare name itself breaks that grammar ("my_check", "QALint" need a real rename, not just a prefix).`;
3185
3411
  }
3186
3412
 
3187
3413
  // src/checks/runner.ts
@@ -3445,7 +3671,7 @@ async function runChecks(specs, opts) {
3445
3671
  // src/commands/check.ts
3446
3672
  function git(args) {
3447
3673
  try {
3448
- return execFileSync5("git", args, { encoding: "utf8" }).trim();
3674
+ return execFileSync6("git", args, { encoding: "utf8" }).trim();
3449
3675
  } catch {
3450
3676
  return "";
3451
3677
  }
@@ -3466,26 +3692,6 @@ function parseParallel(args) {
3466
3692
  }
3467
3693
  return;
3468
3694
  }
3469
- function configErrorOutcome(path, error) {
3470
- const finding = {
3471
- schema: 1,
3472
- check: "config",
3473
- severity: "error",
3474
- message: `${path}: invalid \`checks\` registration — ${error}`,
3475
- agent_recovery_prompt: `Fix the invalid key/value named above in ${path}, then re-run \`vinaya check\`.`
3476
- };
3477
- return { name: "config", status: "error", exitCode: null, errors: [finding], durationMs: 0 };
3478
- }
3479
- function customSpecsFromConfig() {
3480
- const result = loadConfigChecked();
3481
- if (!result.ok)
3482
- return { specs: [], errorOutcome: configErrorOutcome(result.path, result.error), checks: undefined };
3483
- const checks = result.config?.checks;
3484
- if (!checks)
3485
- return { specs: [], errorOutcome: null, checks: undefined };
3486
- const specs = Object.entries(checks).map(([name, entry2]) => ({ name, ...entry2 }));
3487
- return { specs, errorOutcome: null, checks };
3488
- }
3489
3695
  function classifyEnvValue(value) {
3490
3696
  if (value === true)
3491
3697
  return "passthrough";
@@ -3539,27 +3745,46 @@ function renderPlanTable(result) {
3539
3745
  return table.map((row) => row.map((cell, col) => (cell ?? "").padEnd(widths[col] ?? 0)).join(" ")).join(`
3540
3746
  `);
3541
3747
  }
3542
- function resolveForPlan(configResult) {
3543
- if (configResult.ok) {
3544
- return resolveChecks(coreCheckRegistry(), configResult.config?.checks);
3748
+ function duplicateIdFailures(resolved) {
3749
+ const seen = new Set;
3750
+ const reported = new Set;
3751
+ const failures = [];
3752
+ for (const entry2 of resolved) {
3753
+ if (!seen.has(entry2.name)) {
3754
+ seen.add(entry2.name);
3755
+ continue;
3756
+ }
3757
+ if (reported.has(entry2.name))
3758
+ continue;
3759
+ reported.add(entry2.name);
3760
+ failures.push({ key: entry2.name, reason: "duplicate check id — two resolved entries claim the same name" });
3545
3761
  }
3546
- const core = resolveChecks(coreCheckRegistry(), undefined);
3547
- return {
3548
- resolved: core.resolved,
3762
+ return failures;
3763
+ }
3764
+ var FAIL_CLOSED_RECOVERY = "Fix or remove the rejected `checks` entries named above in vinaya.config.json, then re-run `vinaya check`. Until every entry resolves, NO check runs — vinaya refuses the whole run rather than executing a partial ruleset. `vinaya check --plan` prints the same resolution, and `vinaya doctor` carries the permanent diagnostic for each rejected entry.";
3765
+ function refusal(message) {
3766
+ return { schema: 1, check: "config", severity: "error", message, agent_recovery_prompt: FAIL_CLOSED_RECOVERY };
3767
+ }
3768
+ function resolveForRun(configResult) {
3769
+ const base = configResult.ok ? resolveChecks(coreCheckRegistry(), configResult.config?.checks) : {
3770
+ resolved: resolveChecks(coreCheckRegistry(), undefined).resolved,
3549
3771
  failures: [{ key: configResult.path, reason: `invalid \`checks\` registration — ${configResult.error}` }]
3550
3772
  };
3773
+ const duplicates = duplicateIdFailures(base.resolved);
3774
+ const refusals = [
3775
+ ...base.failures.map((failure) => configResult.ok ? refusal(bareKeyRejectedDiagnostic(failure.key)) : refusal(`${failure.key}: ${failure.reason} — refusing to run any check.`)),
3776
+ ...duplicates.map((failure) => refusal(`${failure.key}: ${failure.reason} — refusing to run any check.`))
3777
+ ];
3778
+ return { result: { resolved: base.resolved, failures: [...base.failures, ...duplicates] }, refusals };
3551
3779
  }
3552
- function printClassificationWarnings(configChecks) {
3553
- const classification = resolveChecks(coreCheckRegistry(), configChecks);
3554
- for (const entry2 of classification.resolved) {
3555
- if (entry2.state === "overridden")
3556
- process.stdout.write(`${overriddenNextMinorWarning(entry2.name)}
3557
- `);
3558
- }
3559
- for (const failure of classification.failures) {
3560
- process.stdout.write(`${bareKeyNextMinorWarning(failure.key)}
3561
- `);
3562
- }
3780
+ function substitutionNotices(resolved) {
3781
+ return resolved.filter((entry2) => entry2.state === "overridden").map((entry2) => ({
3782
+ schema: 1,
3783
+ check: "config",
3784
+ severity: "warning",
3785
+ message: overriddenReplacesCoreDiagnostic(entry2.name),
3786
+ agent_recovery_prompt: `If replacing the core check "${entry2.name}" was intended, nothing to do — this is a notice, not a failure. If it was not, remove or rename that \`checks\` entry in vinaya.config.json so the core check runs again.`
3787
+ }));
3563
3788
  }
3564
3789
  async function checkCommand(args) {
3565
3790
  const jsonOutput = args.includes("--json");
@@ -3570,8 +3795,8 @@ async function checkCommand(args) {
3570
3795
  const planRequested = args.includes("--plan");
3571
3796
  const positional = args.filter((a) => !a.startsWith("--"));
3572
3797
  const requestedName = positional[0];
3798
+ const { result, refusals } = resolveForRun(loadConfigChecked());
3573
3799
  if (planRequested) {
3574
- const result = resolveForPlan(loadConfigChecked());
3575
3800
  if (jsonOutput) {
3576
3801
  process.stdout.write(`${JSON.stringify(renderPlanJson(result), null, 2)}
3577
3802
  `);
@@ -3579,18 +3804,50 @@ async function checkCommand(args) {
3579
3804
  process.stdout.write(`${renderPlanTable(result)}
3580
3805
  `);
3581
3806
  }
3582
- process.exit(result.failures.length > 0 ? 1 : 0);
3807
+ process.exitCode = result.failures.length > 0 ? 1 : 0;
3808
+ return;
3583
3809
  }
3584
3810
  if (!allRequested && !requestedName) {
3585
3811
  console.error("Usage: vinaya check <name> | --all | --plan [--json] [--diff-only] [--local] [--parallel[=n]]");
3586
- process.exit(2);
3812
+ process.exitCode = 2;
3813
+ return;
3814
+ }
3815
+ if (refusals.length > 0) {
3816
+ const outcome = {
3817
+ name: "config",
3818
+ status: "error",
3819
+ exitCode: null,
3820
+ errors: refusals,
3821
+ durationMs: 0
3822
+ };
3823
+ for (const e of refusals)
3824
+ emitCheckError(e);
3825
+ if (jsonOutput) {
3826
+ printJson({ checks: [outcome] });
3827
+ } else {
3828
+ process.stdout.write(`✗ ${outcome.name}: refused — no checks ran
3829
+ `);
3830
+ for (const e of refusals)
3831
+ process.stdout.write(` ${e.severity}: ${e.message}
3832
+ `);
3833
+ }
3834
+ process.exitCode = 1;
3835
+ return;
3836
+ }
3837
+ const allSpecs = result.resolved.map((entry2) => entry2.spec);
3838
+ const notices = substitutionNotices(result.resolved);
3839
+ for (const notice of notices)
3840
+ emitCheckError(notice);
3841
+ if (!jsonOutput) {
3842
+ for (const notice of notices)
3843
+ process.stdout.write(`⚠ ${notice.message}
3844
+ `);
3587
3845
  }
3588
- const { specs: customSpecs, errorOutcome, checks: configChecks } = customSpecsFromConfig();
3589
- const allSpecs = [...coreCheckRegistry(), ...customSpecs];
3590
3846
  const specsToRun = allRequested ? allSpecs.filter(runsUnderAll) : allSpecs.filter((s) => s.name === requestedName);
3591
3847
  if (!allRequested && specsToRun.length === 0) {
3592
3848
  console.error(`Unknown check: ${requestedName}`);
3593
- process.exit(2);
3849
+ process.exitCode = 2;
3850
+ return;
3594
3851
  }
3595
3852
  const changed = diffOnly ? changedFiles() : null;
3596
3853
  const outcomes = specsToRun.length > 0 ? await runChecks(specsToRun, {
@@ -3600,15 +3857,14 @@ async function checkCommand(args) {
3600
3857
  defaultTimeoutMs: 30000,
3601
3858
  localOnly
3602
3859
  }) : [];
3603
- const allOutcomes = errorOutcome ? [...outcomes, errorOutcome] : outcomes;
3604
- for (const o of allOutcomes) {
3860
+ for (const o of outcomes) {
3605
3861
  for (const e of o.errors)
3606
3862
  emitCheckError(e);
3607
3863
  }
3608
3864
  if (jsonOutput) {
3609
- printJson({ checks: allOutcomes });
3865
+ printJson({ checks: outcomes });
3610
3866
  } else {
3611
- for (const o of allOutcomes) {
3867
+ for (const o of outcomes) {
3612
3868
  const symbol = o.status === "pass" ? "✓" : o.status === "skipped" ? "·" : "✗";
3613
3869
  process.stdout.write(`${symbol} ${o.name}: ${o.status} (${Math.round(o.durationMs)}ms)
3614
3870
  `);
@@ -3616,21 +3872,20 @@ async function checkCommand(args) {
3616
3872
  process.stdout.write(` ${e.severity}: ${e.message}
3617
3873
  `);
3618
3874
  }
3619
- printClassificationWarnings(configChecks);
3620
3875
  }
3621
- const failed = allOutcomes.some((o) => o.status === "fail" || o.status === "error" || o.status === "timeout");
3622
- process.exit(failed ? 1 : 0);
3876
+ const failed = outcomes.some((o) => o.status === "fail" || o.status === "error" || o.status === "timeout");
3877
+ process.exitCode = failed ? 1 : 0;
3623
3878
  }
3624
3879
 
3625
3880
  // src/commands/demo.ts
3626
- import { execFileSync as execFileSync6, spawnSync } from "node:child_process";
3881
+ import { execFileSync as execFileSync7, spawnSync } from "node:child_process";
3627
3882
  import { existsSync as existsSync6, readFileSync as readFileSync5, rmSync as rmSync2, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
3628
3883
  import { isAbsolute as isAbsolute2, join as join7 } from "node:path";
3629
3884
  var DEMO_BRANCH_PREFIX = "vinaya/demo-break-";
3630
3885
  var FIXTURE_PATH = ".vinaya-demo-brief.md";
3631
3886
  var STATE_FILENAME = "vinaya-demo-state.json";
3632
3887
  function gitCapture(repoRoot, args) {
3633
- return execFileSync6("git", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
3888
+ return execFileSync7("git", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
3634
3889
  }
3635
3890
  function currentBranchName(repoRoot) {
3636
3891
  try {
@@ -3641,7 +3896,7 @@ function currentBranchName(repoRoot) {
3641
3896
  }
3642
3897
  function branchExists(repoRoot, name) {
3643
3898
  try {
3644
- execFileSync6("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${name}`], {
3899
+ execFileSync7("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${name}`], {
3645
3900
  cwd: repoRoot,
3646
3901
  stdio: ["ignore", "pipe", "pipe"]
3647
3902
  });
@@ -3843,7 +4098,8 @@ async function demoBreakCommand(args) {
3843
4098
  }
3844
4099
 
3845
4100
  // src/commands/doctor.ts
3846
- import { existsSync as existsSync8, readFileSync as readFileSync8, statSync as statSync2 } from "node:fs";
4101
+ import { execFileSync as execFileSync8 } from "node:child_process";
4102
+ import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync2 } from "node:fs";
3847
4103
  import { join as join9 } from "node:path";
3848
4104
 
3849
4105
  // src/lib/self-host.ts
@@ -4124,13 +4380,94 @@ function diagnoseCustomChecks(repoRoot, config) {
4124
4380
  }
4125
4381
  return findings;
4126
4382
  }
4383
+ function listTrackedFiles(repoRoot) {
4384
+ try {
4385
+ return execFileSync8("git", ["ls-files"], { cwd: repoRoot, encoding: "utf-8" }).split(`
4386
+ `).map((s) => s.trim()).filter(Boolean);
4387
+ } catch {
4388
+ return [];
4389
+ }
4390
+ }
4391
+ function diagnoseDocOwnersHealth(repoRoot) {
4392
+ const path = join9(repoRoot, DOC_OWNERS_PATH);
4393
+ const content = existsSync8(path) ? readFileSync8(path, "utf-8") : null;
4394
+ const state = classifyDocOwnersManifest(content);
4395
+ if (state === "absent" || state === "empty")
4396
+ return [];
4397
+ const { bindings } = parseDocOwners(content);
4398
+ if (bindings.length === 0)
4399
+ return [];
4400
+ const codeFiles = listTrackedFiles(repoRoot).filter(isCodeFile);
4401
+ const findings = [];
4402
+ for (const b of bindings) {
4403
+ let flagged = false;
4404
+ const re = globToRegex(b.glob);
4405
+ if (!codeFiles.some((f) => re.test(f))) {
4406
+ flagged = true;
4407
+ findings.push(warn("doc-owners", `${DOC_OWNERS_PATH}:${b.lineNum} binds glob '${b.glob}', which matches none of the ${codeFiles.length} ` + "tracked code file(s) in this repo — the code it names may have been deleted, renamed, or never " + "existed. Repoint the binding to the code's new location, or remove it."));
4408
+ }
4409
+ if (!isUrlPointer(b.pointer)) {
4410
+ const pointerPath = join9(repoRoot, pointerToPath(b.pointer));
4411
+ if (!existsSync8(pointerPath)) {
4412
+ flagged = true;
4413
+ findings.push(warn("doc-owners", `${DOC_OWNERS_PATH}:${b.lineNum} points to ${b.pointer}, which does not exist on disk. Repoint the ` + "binding, or add the missing doc."));
4414
+ }
4415
+ }
4416
+ if (!flagged) {
4417
+ findings.push(ok("doc-owners", `${DOC_OWNERS_PATH}:${b.lineNum} — '${b.glob}' → ${b.pointer} is live.`));
4418
+ }
4419
+ }
4420
+ return findings;
4421
+ }
4422
+ var LEGACY_AEG_PACKAGES_PATH = ".aeg/packages";
4423
+ function readWorkspacesForDoctor(repoRoot) {
4424
+ const fromPackageJson = () => {
4425
+ try {
4426
+ const pkg = JSON.parse(readFileSync8(join9(repoRoot, "package.json"), "utf-8"));
4427
+ return Array.isArray(pkg.workspaces) ? pkg.workspaces.filter((w) => typeof w === "string") : [];
4428
+ } catch {
4429
+ return [];
4430
+ }
4431
+ };
4432
+ const fromPnpmWorkspaceYaml = () => {
4433
+ try {
4434
+ return parsePnpmWorkspaceYaml(readFileSync8(join9(repoRoot, "pnpm-workspace.yaml"), "utf-8"));
4435
+ } catch {
4436
+ return [];
4437
+ }
4438
+ };
4439
+ return [...fromPackageJson(), ...fromPnpmWorkspaceYaml()];
4440
+ }
4441
+ function listChildDirsForDoctor(dir, repoRoot) {
4442
+ try {
4443
+ return readdirSync3(join9(repoRoot, dir), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
4444
+ } catch {
4445
+ return [];
4446
+ }
4447
+ }
4448
+ function diagnoseBlastRadiusDeprecation(repoRoot, config) {
4449
+ const derived = deriveWorkspacePackageDomains(readWorkspacesForDoctor(repoRoot), (dir) => listChildDirsForDoctor(dir, repoRoot));
4450
+ const defaults = deriveBuiltinCrossCuttingDefaults((p) => existsSync8(join9(repoRoot, p)));
4451
+ const covered = new Set([...derived, ...defaults]);
4452
+ const configExtra = new Set(config?.blastRadius?.extraDomains ?? []);
4453
+ const legacyPath = join9(repoRoot, LEGACY_AEG_PACKAGES_PATH);
4454
+ if (!existsSync8(legacyPath)) {
4455
+ return [
4456
+ info("blast-radius", `checkBlastRadiusScope is active via live derivation (${derived.length} packages/* domain(s)) + built-in defaults (${defaults.length} present) — no ${LEGACY_AEG_PACKAGES_PATH}, and the check is not dormant.`)
4457
+ ];
4458
+ }
4459
+ const legacyEntries = readFileSync8(legacyPath, "utf-8").split(`
4460
+ `).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
4461
+ const uncovered = legacyEntries.filter((e) => !covered.has(e) && !configExtra.has(e));
4462
+ const migrationNote = uncovered.length === 0 ? "every entry is already covered by live derivation, the built-in defaults, or vinaya.config.json blastRadius.extraDomains — the file can be deleted." : `migrate ${uncovered.length} entr${uncovered.length === 1 ? "y" : "ies"} not yet covered before deleting it: ${uncovered.join(", ")}.`;
4463
+ return [
4464
+ warn("blast-radius", `${LEGACY_AEG_PACKAGES_PATH} is deprecated — checkBlastRadiusScope now derives packages/* domains live and ships built-in cross-cutting defaults. Declare anything beyond those in vinaya.config.json's blastRadius.extraDomains instead. ${migrationNote}`)
4465
+ ];
4466
+ }
4127
4467
  function diagnoseEnvDeclarations(repoRoot, config) {
4128
- const customSpecs = Object.entries(config?.checks ?? {}).map(([name, entry2]) => ({
4129
- name,
4130
- ...entry2,
4131
- run: join9(repoRoot, entry2.run)
4132
- }));
4133
- const missing = checksMissingEnvDeclaration([...coreCheckRegistry(), ...customSpecs]);
4468
+ const resolved = resolveChecks(coreCheckRegistry(), config?.checks).resolved;
4469
+ const specs = resolved.map((entry2) => entry2.source === "config" ? { ...entry2.spec, run: join9(repoRoot, entry2.spec.run) } : entry2.spec);
4470
+ const missing = checksMissingEnvDeclaration(specs);
4134
4471
  const findings = missing.map((name) => info("env", envDeclarationWarning(name)));
4135
4472
  for (const message of lintEnvDeclarations(config?.checks)) {
4136
4473
  findings.push(warn("env", message));
@@ -4142,10 +4479,10 @@ function diagnoseCheckClassification(config) {
4142
4479
  const findings = [];
4143
4480
  for (const entry2 of classification.resolved) {
4144
4481
  if (entry2.state === "overridden")
4145
- findings.push(warn("checks", overriddenNextMinorWarning(entry2.name)));
4482
+ findings.push(warn("checks", overriddenReplacesCoreDiagnostic(entry2.name)));
4146
4483
  }
4147
4484
  for (const failure of classification.failures) {
4148
- findings.push(warn("checks", bareKeyNextMinorWarning(failure.key)));
4485
+ findings.push(error("checks", bareKeyRejectedDiagnostic(failure.key)));
4149
4486
  }
4150
4487
  return findings;
4151
4488
  }
@@ -4187,6 +4524,29 @@ async function diagnoseBranchProtection(deps, owner, repo) {
4187
4524
  }
4188
4525
  return info("branch-protection", "main branch protection could not be determined (no gh auth, no remote, or a permission gap).");
4189
4526
  }
4527
+ var TEST_INVOCATION_SUBSTRINGS = ["npm test", "npm run test", "bun test", "bunx turbo test"];
4528
+ function diagnoseTestCi(repoRoot) {
4529
+ let pkg;
4530
+ try {
4531
+ pkg = JSON.parse(readFileSync8(join9(repoRoot, "package.json"), "utf-8"));
4532
+ } catch {
4533
+ return [];
4534
+ }
4535
+ const testScript = pkg?.scripts?.test;
4536
+ if (typeof testScript !== "string" || testScript.trim() === "")
4537
+ return [];
4538
+ const workflowsDir = join9(repoRoot, ".github/workflows");
4539
+ const files = existsSync8(workflowsDir) ? readdirSync3(workflowsDir).filter((name) => statSync2(join9(workflowsDir, name)).isFile()) : [];
4540
+ const invoked = files.some((name) => {
4541
+ const content = readFileSync8(join9(workflowsDir, name), "utf-8");
4542
+ return TEST_INVOCATION_SUBSTRINGS.some((s) => content.includes(s));
4543
+ });
4544
+ if (invoked)
4545
+ return [];
4546
+ return [
4547
+ warn("test-ci", "no workflow under .github/workflows/ appears to invoke this repo's `package.json` test script — " + "vinaya requires a Test Plan on every PR and enforces it as a blocking gate, but nothing here verifies that tests actually run.")
4548
+ ];
4549
+ }
4190
4550
  function symbolFor(severity) {
4191
4551
  switch (severity) {
4192
4552
  case "ok":
@@ -4240,12 +4600,15 @@ async function runDoctor(args, deps) {
4240
4600
  hasDrift = install.hasDrift;
4241
4601
  findings.push(...await diagnoseHookRouting(repo.repoRoot, hookDir, deps.readHooksPath));
4242
4602
  findings.push(...diagnoseCustomChecks(repo.repoRoot, configRead.config));
4603
+ findings.push(...diagnoseDocOwnersHealth(repo.repoRoot));
4243
4604
  }
4605
+ findings.push(...diagnoseBlastRadiusDeprecation(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
4244
4606
  findings.push(...diagnoseEnvDeclarations(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
4245
4607
  findings.push(...diagnoseCheckClassification(configRead.kind === "ok" ? configRead.config : null));
4246
4608
  findings.push(...diagnoseGlobalConfigChecks());
4247
4609
  findings.push(...await diagnoseEnvironment(deps, hasDrift));
4248
4610
  findings.push(await diagnoseBranchProtection(deps, repo.owner, repo.repo));
4611
+ findings.push(...diagnoseTestCi(repo.repoRoot));
4249
4612
  const healthy = findings.every((f) => f.severity === "ok" || f.severity === "info");
4250
4613
  if (jsonOutput) {
4251
4614
  printJson({ healthy, findings });
@@ -4259,7 +4622,7 @@ async function doctorCommand(args) {
4259
4622
  }
4260
4623
 
4261
4624
  // src/commands/doctrine.ts
4262
- import { existsSync as existsSync9 } from "node:fs";
4625
+ import { existsSync as existsSync9, readdirSync as readdirSync4 } from "node:fs";
4263
4626
  import { dirname as dirname4, join as join10, sep as sep3 } from "node:path";
4264
4627
  var ENTRY_SEGMENTS = ["skills", "aeg", "SKILL.md"];
4265
4628
  function resolveDoctrineRoot(pkg = packageRoot(import.meta.url)) {
@@ -4272,6 +4635,12 @@ function resolveDoctrineRoot(pkg = packageRoot(import.meta.url)) {
4272
4635
  }
4273
4636
  return null;
4274
4637
  }
4638
+ function listRoleNames(root) {
4639
+ const rolesDir = join10(root, "roles");
4640
+ if (!existsSync9(rolesDir))
4641
+ return [];
4642
+ return readdirSync4(rolesDir).filter((name) => name.endsWith(".md")).map((name) => name.slice(0, -".md".length)).sort();
4643
+ }
4275
4644
  function doctrineCommand(args) {
4276
4645
  const root = resolveDoctrineRoot();
4277
4646
  if (root === null) {
@@ -4279,6 +4648,24 @@ function doctrineCommand(args) {
4279
4648
  `);
4280
4649
  process.exit(1);
4281
4650
  }
4651
+ const roleFlagIndex = args.indexOf("--role");
4652
+ if (roleFlagIndex !== -1) {
4653
+ const roleName = args[roleFlagIndex + 1];
4654
+ const validRoleNames = listRoleNames(root);
4655
+ if (roleName === undefined || roleName.startsWith("--") || !validRoleNames.includes(roleName)) {
4656
+ process.stderr.write(`vinaya doctrine --role: '${roleName ?? ""}' is not a known role. ` + `Valid role names: ${validRoleNames.join(", ")}
4657
+ `);
4658
+ process.exit(1);
4659
+ }
4660
+ const entry3 = join10(root, "roles", `${roleName}.md`);
4661
+ if (args.includes("--json")) {
4662
+ printJson({ root, entry: entry3 });
4663
+ return;
4664
+ }
4665
+ process.stdout.write(`${entry3}
4666
+ `);
4667
+ return;
4668
+ }
4282
4669
  const entry2 = join10(root, ...ENTRY_SEGMENTS);
4283
4670
  if (args.includes("--json")) {
4284
4671
  printJson({ root, entry: entry2 });
@@ -4695,7 +5082,7 @@ async function initProductCommand(args) {
4695
5082
  }
4696
5083
 
4697
5084
  // src/commands/issue.ts
4698
- import { execFileSync as execFileSync7 } from "node:child_process";
5085
+ import { execFileSync as execFileSync9 } from "node:child_process";
4699
5086
 
4700
5087
  // src/lib/forge-write.ts
4701
5088
  import { mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
@@ -4918,7 +5305,7 @@ function locateBodyOrRefuse(ghArgs, retryCommand) {
4918
5305
  function fetchForgeLabels(issueRef) {
4919
5306
  let out;
4920
5307
  try {
4921
- out = execFileSync7("gh", ["issue", "view", issueRef, "--json", "labels"], {
5308
+ out = execFileSync9("gh", ["issue", "view", issueRef, "--json", "labels"], {
4922
5309
  encoding: "utf8",
4923
5310
  stdio: ["ignore", "pipe", "pipe"]
4924
5311
  });
@@ -4946,7 +5333,7 @@ function reportPass(json, command) {
4946
5333
  function runGhWrite(ghCmd, ghArgs, bodyResult, json) {
4947
5334
  const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
4948
5335
  try {
4949
- const out = execFileSync7("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
5336
+ const out = execFileSync9("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
4950
5337
  const url = out.trim();
4951
5338
  if (json)
4952
5339
  printJson({ validated: true, written: true, url });
@@ -5021,17 +5408,27 @@ import { chmodSync as chmodSync2, existsSync as existsSync13, mkdirSync as mkdir
5021
5408
  import { join as join15 } from "node:path";
5022
5409
  var TEMPLATE_PATH = join15(packageRoot(import.meta.url), "templates", "custom-check.template.ts");
5023
5410
  var CHECKS_DIR = join15("scripts", "vinaya-checks");
5024
- var VALID_NAME = /^[a-z0-9][a-z0-9-]*$/;
5411
+ var USAGE = "Usage: vinaya new check <yourname>/<id> (both segments: lowercase letters, digits, hyphens; e.g. myteam/vocab-check)";
5025
5412
  function newCheckCommand(args) {
5026
5413
  const name = args[0];
5027
- if (!name || !VALID_NAME.test(name)) {
5028
- console.error("Usage: vinaya new check <name> (name: lowercase letters, digits, hyphens)");
5414
+ if (!name) {
5415
+ console.error(USAGE);
5416
+ process.exit(2);
5417
+ }
5418
+ if (coreCheckRegistry().some((spec) => spec.name === name)) {
5419
+ console.error(`Refusing: "${name}" is a core check id. Registering it under \`checks\` REPLACES that core check — the core one stops running — so a scaffolded stub is never the right shape for it. Pick a namespaced name, or write the override entry by hand if you really mean to replace the core check.`);
5420
+ process.exit(2);
5421
+ }
5422
+ if (!isValidNamespacedKey(name)) {
5423
+ console.error(`Refusing: "${name}" is not a valid check id. ${USAGE.slice("Usage: ".length)}
5424
+ \`vinaya check\` refuses the entire run over a key it cannot resolve, so a bare, un-namespaced name would brick every check invocation in this repo.`);
5029
5425
  process.exit(2);
5030
5426
  }
5031
5427
  const scriptsDir = join15(process.cwd(), CHECKS_DIR);
5032
5428
  if (!existsSync13(scriptsDir))
5033
5429
  mkdirSync4(scriptsDir, { recursive: true });
5034
- const targetPath = join15(scriptsDir, `${name}.ts`);
5430
+ const fileStem = name.slice(name.indexOf("/") + 1);
5431
+ const targetPath = join15(scriptsDir, `${fileStem}.ts`);
5035
5432
  if (existsSync13(targetPath)) {
5036
5433
  console.error(`Error: ${targetPath} already exists.`);
5037
5434
  process.exit(1);
@@ -5040,7 +5437,7 @@ function newCheckCommand(args) {
5040
5437
  const contents = template.split("{{CHECK_NAME}}").join(name);
5041
5438
  writeFileSync7(targetPath, contents, "utf-8");
5042
5439
  chmodSync2(targetPath, 493);
5043
- const relPath = join15(CHECKS_DIR, `${name}.ts`);
5440
+ const relPath = join15(CHECKS_DIR, `${fileStem}.ts`);
5044
5441
  const registration = JSON.stringify({ checks: { [name]: { run: `./${relPath}`, scope: "diff" } } }, null, 2);
5045
5442
  process.stdout.write(`Created ${relPath}
5046
5443
 
@@ -5050,12 +5447,12 @@ ${registration}
5050
5447
  }
5051
5448
 
5052
5449
  // src/commands/pr.ts
5053
- import { execFileSync as execFileSync8 } from "node:child_process";
5450
+ import { execFileSync as execFileSync10 } from "node:child_process";
5054
5451
  var RETRY_CREATE2 = "vinaya pr create --validate-only …";
5055
5452
  var RETRY_EDIT2 = "vinaya pr edit <n> --validate-only …";
5056
5453
  function git2(args) {
5057
5454
  try {
5058
- return execFileSync8("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
5455
+ return execFileSync10("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
5059
5456
  } catch {
5060
5457
  return "";
5061
5458
  }
@@ -5089,7 +5486,7 @@ function reportPass2(json, command) {
5089
5486
  function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
5090
5487
  const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
5091
5488
  try {
5092
- const out = execFileSync8("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
5489
+ const out = execFileSync10("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
5093
5490
  const url = out.trim();
5094
5491
  if (json)
5095
5492
  printJson({ validated: true, written: true, url });
@@ -5103,7 +5500,7 @@ function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
5103
5500
  function fetchPrForgeContext(prRef) {
5104
5501
  let viewOut;
5105
5502
  try {
5106
- viewOut = execFileSync8("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
5503
+ viewOut = execFileSync10("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
5107
5504
  encoding: "utf8",
5108
5505
  stdio: ["ignore", "pipe", "pipe"]
5109
5506
  });
@@ -5204,14 +5601,193 @@ function prEditCommand(args) {
5204
5601
  runGhWrite2(["pr", "edit", prRef], ghArgs, bodyResult, json);
5205
5602
  }
5206
5603
 
5604
+ // src/commands/pr-report.ts
5605
+ import { execFileSync as execFileSync11, spawnSync as spawnSync2 } from "node:child_process";
5606
+ import { existsSync as existsSync14, readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "node:fs";
5607
+ import { join as join16 } from "node:path";
5608
+ var EVIDENCE_START = "<!-- AEG:EVIDENCE:START -->";
5609
+ var EVIDENCE_END = "<!-- AEG:EVIDENCE:END -->";
5610
+ function git3(args) {
5611
+ try {
5612
+ return execFileSync11("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
5613
+ } catch {
5614
+ return "";
5615
+ }
5616
+ }
5617
+
5618
+ class GitCommandError extends Error {
5619
+ args;
5620
+ cause;
5621
+ constructor(args, cause) {
5622
+ super(`\`git ${args.join(" ")}\` failed: ${cause}`);
5623
+ this.args = args;
5624
+ this.cause = cause;
5625
+ this.name = "GitCommandError";
5626
+ }
5627
+ }
5628
+ function gitStrict(args) {
5629
+ try {
5630
+ return execFileSync11("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
5631
+ } catch (err) {
5632
+ const stderr = err.stderr;
5633
+ throw new GitCommandError(args, String(stderr ?? err.message).trim() || "non-zero exit");
5634
+ }
5635
+ }
5636
+
5637
+ class UnresolvableMergeBaseError extends Error {
5638
+ head;
5639
+ triedRefs;
5640
+ constructor(head, triedRefs) {
5641
+ super(`could not resolve a merge-base for HEAD (${head}) against any of: ${triedRefs.join(", ")}. ` + `This repo's default branch may not be named "main", or "origin" may not be configured — set BASE_SHA to override.`);
5642
+ this.head = head;
5643
+ this.triedRefs = triedRefs;
5644
+ this.name = "UnresolvableMergeBaseError";
5645
+ }
5646
+ }
5647
+ function resolveMergeBase(head) {
5648
+ const primary = process.env.BASE_SHA || "origin/main";
5649
+ const tried = primary === "main" ? [primary] : [primary, "main"];
5650
+ for (const ref of tried) {
5651
+ const base = git3(["merge-base", ref, head]);
5652
+ if (base)
5653
+ return base;
5654
+ }
5655
+ throw new UnresolvableMergeBaseError(head, tried);
5656
+ }
5657
+ function computeGroupA() {
5658
+ const head = gitStrict(["rev-parse", "HEAD"]);
5659
+ const base = resolveMergeBase(head);
5660
+ const numstat = gitStrict(["diff", `${base}...${head}`, "--numstat"]);
5661
+ return { head, base, numstat };
5662
+ }
5663
+ function resolveSelfEntry() {
5664
+ const root = packageRoot(import.meta.url);
5665
+ const dist = join16(root, "dist", "index.js");
5666
+ return existsSync14(dist) ? dist : join16(root, "src", "index.ts");
5667
+ }
5668
+ var FAILING_STATUSES = new Set(["fail", "error", "timeout"]);
5669
+ function anyGateFailed(outcomes) {
5670
+ return outcomes.some((o) => FAILING_STATUSES.has(o.status));
5671
+ }
5672
+ function runRealGates() {
5673
+ const entry2 = resolveSelfEntry();
5674
+ const proc = spawnSync2(process.execPath, [entry2, "check", "--all", "--diff-only", "--json"], {
5675
+ cwd: process.cwd(),
5676
+ encoding: "utf8",
5677
+ stdio: ["ignore", "pipe", "ignore"],
5678
+ maxBuffer: 32 * 1024 * 1024
5679
+ });
5680
+ const stdout = proc.stdout ?? "";
5681
+ try {
5682
+ const parsed = JSON.parse(stdout);
5683
+ const outcomes = parsed.data.checks;
5684
+ return { outcomes, failed: anyGateFailed(outcomes) };
5685
+ } catch {
5686
+ return {
5687
+ outcomes: [
5688
+ {
5689
+ name: "check",
5690
+ status: "error",
5691
+ errors: [
5692
+ { severity: "error", message: "`vinaya check --all --diff-only --json` did not produce parseable output." }
5693
+ ]
5694
+ }
5695
+ ],
5696
+ failed: true
5697
+ };
5698
+ }
5699
+ }
5700
+ function renderGroupA(groupA) {
5701
+ return [
5702
+ "### Group A — recomputable",
5703
+ "",
5704
+ `\`git diff ${groupA.base}...${groupA.head} --numstat\``,
5705
+ "",
5706
+ "```",
5707
+ groupA.numstat,
5708
+ "```"
5709
+ ].join(`
5710
+ `);
5711
+ }
5712
+ function renderGroupB(outcomes) {
5713
+ const sorted = [...outcomes].sort((a, b) => a.name.localeCompare(b.name));
5714
+ const lines = sorted.flatMap((o) => {
5715
+ const rows = [`${o.name}: ${o.status}`];
5716
+ for (const e of o.errors)
5717
+ rows.push(` ${e.severity}: ${e.message}`);
5718
+ return rows;
5719
+ });
5720
+ return ["### Group B — attested", "", "`vinaya check --all --diff-only`", "", "```", lines.join(`
5721
+ `), "```"].join(`
5722
+ `);
5723
+ }
5724
+ function buildBlockInner(groupA, gateOutcomes) {
5725
+ return [`Head: ${groupA.head}`, "", renderGroupA(groupA), "", renderGroupB(gateOutcomes)].join(`
5726
+ `);
5727
+ }
5728
+ function replaceEvidenceBlock(body, blockInner) {
5729
+ const full = `${EVIDENCE_START}
5730
+ ${blockInner}
5731
+ ${EVIDENCE_END}`;
5732
+ const bounds = anchoredRegionBounds(body, "EVIDENCE");
5733
+ if (bounds) {
5734
+ return `${body.slice(0, bounds.outerStart)}${full}${body.slice(bounds.outerEnd)}`;
5735
+ }
5736
+ const sep4 = body.length === 0 || body.endsWith(`
5737
+ `) ? "" : `
5738
+ `;
5739
+ return `${body}${sep4}
5740
+ ${full}
5741
+ `;
5742
+ }
5743
+ async function buildReport(opts = {}) {
5744
+ const groupA = opts.groupA ?? computeGroupA();
5745
+ const gateRunner = opts.gateRunner ?? runRealGates;
5746
+ const gateResult = await gateRunner();
5747
+ const blockInner = buildBlockInner(groupA, gateResult.outcomes);
5748
+ const block = `${EVIDENCE_START}
5749
+ ${blockInner}
5750
+ ${EVIDENCE_END}`;
5751
+ return { block, blockInner, gatesFailed: gateResult.failed, gateOutcomes: gateResult.outcomes };
5752
+ }
5753
+ async function prReportCommand(args) {
5754
+ const writeIdx = args.indexOf("--write");
5755
+ const writePath = writeIdx !== -1 ? args[writeIdx + 1] : undefined;
5756
+ if (writeIdx !== -1 && !writePath) {
5757
+ console.error("Usage: vinaya pr report [--write <body-file>]");
5758
+ process.exit(2);
5759
+ }
5760
+ let result;
5761
+ try {
5762
+ result = await buildReport();
5763
+ } catch (err) {
5764
+ if (err instanceof UnresolvableMergeBaseError || err instanceof GitCommandError) {
5765
+ console.error(`vinaya pr report: refused — ${err.message}`);
5766
+ process.exit(1);
5767
+ }
5768
+ throw err;
5769
+ }
5770
+ if (writePath) {
5771
+ const existing = existsSync14(writePath) ? readFileSync14(writePath, "utf8") : "";
5772
+ const updated = replaceEvidenceBlock(existing, result.blockInner);
5773
+ writeFileSync8(writePath, updated);
5774
+ process.stdout.write(`Wrote AEG:EVIDENCE block to ${writePath}
5775
+ `);
5776
+ } else {
5777
+ process.stdout.write(`${result.block}
5778
+ `);
5779
+ }
5780
+ process.exit(result.gatesFailed ? 1 : 0);
5781
+ }
5782
+
5207
5783
  // src/commands/quickstart.ts
5208
- import { execFileSync as execFileSync9 } from "node:child_process";
5209
- import { existsSync as existsSync15, readFileSync as readFileSync15 } from "node:fs";
5210
- import { join as join17 } from "node:path";
5784
+ import { execFileSync as execFileSync12 } from "node:child_process";
5785
+ import { existsSync as existsSync16, readFileSync as readFileSync16 } from "node:fs";
5786
+ import { join as join18 } from "node:path";
5211
5787
 
5212
5788
  // src/lib/doc-owners-write.ts
5213
- import { existsSync as existsSync14, mkdirSync as mkdirSync5, readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "node:fs";
5214
- import { dirname as dirname6, join as join16 } from "node:path";
5789
+ import { existsSync as existsSync15, mkdirSync as mkdirSync5, readFileSync as readFileSync15, writeFileSync as writeFileSync9 } from "node:fs";
5790
+ import { dirname as dirname6, join as join17 } from "node:path";
5215
5791
  function bindingLine(glob, pointer) {
5216
5792
  return `${glob} ${pointer}`;
5217
5793
  }
@@ -5227,23 +5803,23 @@ function appendDocOwnersBinding(content, glob, pointer) {
5227
5803
  `;
5228
5804
  }
5229
5805
  function planDocOwnersBinding(repoRoot, glob, pointer) {
5230
- const abs2 = join16(repoRoot, DOC_OWNERS_PATH);
5806
+ const abs2 = join17(repoRoot, DOC_OWNERS_PATH);
5231
5807
  const line = bindingLine(glob, pointer);
5232
- if (!existsSync14(abs2)) {
5808
+ if (!existsSync15(abs2)) {
5233
5809
  return { action: "create-host", line, path: DOC_OWNERS_PATH };
5234
5810
  }
5235
- const existing = readFileSync14(abs2, "utf-8");
5811
+ const existing = readFileSync15(abs2, "utf-8");
5236
5812
  const already = parseDocOwners(existing).bindings.some((b) => b.glob === glob);
5237
5813
  return { action: already ? "skip-present" : "append-row", line, path: DOC_OWNERS_PATH };
5238
5814
  }
5239
5815
  function applyDocOwnersBinding(repoRoot, plan, glob, pointer) {
5240
- const abs2 = join16(repoRoot, DOC_OWNERS_PATH);
5816
+ const abs2 = join17(repoRoot, DOC_OWNERS_PATH);
5241
5817
  if (plan.action === "create-host") {
5242
5818
  mkdirSync5(dirname6(abs2), { recursive: true });
5243
- writeFileSync8(abs2, freshDocOwners(glob, pointer), "utf-8");
5819
+ writeFileSync9(abs2, freshDocOwners(glob, pointer), "utf-8");
5244
5820
  } else if (plan.action === "append-row") {
5245
- const existing = readFileSync14(abs2, "utf-8");
5246
- writeFileSync8(abs2, appendDocOwnersBinding(existing, glob, pointer), "utf-8");
5821
+ const existing = readFileSync15(abs2, "utf-8");
5822
+ writeFileSync9(abs2, appendDocOwnersBinding(existing, glob, pointer), "utf-8");
5247
5823
  }
5248
5824
  }
5249
5825
  function renderDocOwnersBindingDiffLine(plan) {
@@ -5303,7 +5879,7 @@ function pointerIsMissingLocalFile(repoRoot, pointer) {
5303
5879
  if (/^https?:\/\//i.test(pointer))
5304
5880
  return false;
5305
5881
  const path = pointer.split("#")[0] ?? pointer;
5306
- return !existsSync15(join17(repoRoot, path));
5882
+ return !existsSync16(join18(repoRoot, path));
5307
5883
  }
5308
5884
  async function bindDocOwnerLoop(deps, repoRoot) {
5309
5885
  let any = false;
@@ -5370,7 +5946,7 @@ async function registerProjectLoop(deps) {
5370
5946
  }
5371
5947
  }
5372
5948
  function readPackageVersion() {
5373
- const pkg = JSON.parse(readFileSync15(join17(packageRoot(import.meta.url), "package.json"), "utf-8"));
5949
+ const pkg = JSON.parse(readFileSync16(join18(packageRoot(import.meta.url), "package.json"), "utf-8"));
5374
5950
  return pkg.version;
5375
5951
  }
5376
5952
  function realDeps6() {
@@ -5401,7 +5977,7 @@ function realDeps6() {
5401
5977
  };
5402
5978
  }
5403
5979
  function execGit(repoRoot, args) {
5404
- return execFileSync9("git", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
5980
+ return execFileSync12("git", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
5405
5981
  }
5406
5982
  function errorDetail(err) {
5407
5983
  const stderr = err.stderr;
@@ -5517,11 +6093,331 @@ async function quickstartCommand(args) {
5517
6093
  process.exit(await runQuickstart(args, realDeps6()));
5518
6094
  }
5519
6095
 
6096
+ // src/commands/review-post.ts
6097
+ import { execFileSync as execFileSync13 } from "node:child_process";
6098
+ import { readFileSync as readFileSync17, rmSync as rmSync4, writeFileSync as writeFileSync10 } from "node:fs";
6099
+ import { tmpdir as tmpdir2 } from "node:os";
6100
+ import { join as join19 } from "node:path";
6101
+ class FindingsParseError extends Error {
6102
+ }
6103
+ var CODE_REVIEW_SEVERITIES = ["BLOCKER", "MAJOR", "MINOR"];
6104
+ var SECURITY_SEVERITIES = ["CRITICAL", "HIGH", "MEDIUM", "LOW"];
6105
+ function parseFindingsFile(content, allowedSeverities) {
6106
+ const lines = content.split(`
6107
+ `).map((l) => l.trim()).filter((l) => l.length > 0);
6108
+ return lines.map((line, idx) => {
6109
+ const parts = line.split("|");
6110
+ if (parts.length !== 3) {
6111
+ throw new FindingsParseError(`findings file line ${idx + 1}: expected exactly 3 \`|\`-delimited fields (SEVERITY|file:line|description), found ${parts.length}: ${line}`);
6112
+ }
6113
+ const severity = parts[0].trim().toUpperCase();
6114
+ const location = parts[1].trim();
6115
+ const description = parts[2].trim();
6116
+ if (!allowedSeverities.includes(severity)) {
6117
+ throw new FindingsParseError(`findings file line ${idx + 1}: severity "${parts[0]}" is not one of ${allowedSeverities.join("|")}: ${line}`);
6118
+ }
6119
+ if (!location || !description) {
6120
+ throw new FindingsParseError(`findings file line ${idx + 1}: file:line and description must both be non-empty: ${line}`);
6121
+ }
6122
+ return { severity, location, description };
6123
+ });
6124
+ }
6125
+ function sortBySeverity(findings, order) {
6126
+ return findings.map((f, i) => ({ f, i })).sort((a, b) => order.indexOf(a.f.severity) - order.indexOf(b.f.severity) || a.i - b.i).map(({ f }) => f);
6127
+ }
6128
+ function renderFindingsSection(findings) {
6129
+ if (findings.length === 0)
6130
+ return "None.";
6131
+ return findings.map((f, i) => `${i + 1}. [${f.severity}] ${f.location} — ${f.description}`).join(`
6132
+ `);
6133
+ }
6134
+ function renderTokensLine(role, roleLabel, input) {
6135
+ return `Tokens: ${input.taskId}: ${role} — ${roleLabel} — ${input.model} — ${input.tokensIn}/${input.tokensOut}/${input.cost}`;
6136
+ }
6137
+ var CODE_REVIEW_VERDICT_TEXT = {
6138
+ APPROVE: "APPROVE",
6139
+ REQUEST_CHANGES: "REQUEST CHANGES"
6140
+ };
6141
+ function renderCodeReviewComment(input) {
6142
+ const sorted = sortBySeverity(input.findings, CODE_REVIEW_SEVERITIES);
6143
+ return [
6144
+ `VERDICT: ${CODE_REVIEW_VERDICT_TEXT[input.verdict]}`,
6145
+ "",
6146
+ `Judged head: ${input.headSha}`,
6147
+ "",
6148
+ `BRIEF CONFORMANCE: ${input.briefConformance}`,
6149
+ `SPEC CONFORMANCE: ${input.specConformance}`,
6150
+ "",
6151
+ "FINDINGS (ordered by severity):",
6152
+ renderFindingsSection(sorted),
6153
+ "",
6154
+ `SCOPE: ${input.scope}`,
6155
+ `TESTS: ${input.tests}`,
6156
+ `DOCS: ${input.docs}`,
6157
+ "",
6158
+ renderTokensLine("review", "Reviewer", input)
6159
+ ].join(`
6160
+ `);
6161
+ }
6162
+ function isNoneFoundClaim(value) {
6163
+ return value.trim().toLowerCase().replace(/[-_]+/g, " ").replace(/\s+/g, " ") === "none found";
6164
+ }
6165
+ function renderSecurityComment(input) {
6166
+ const sorted = sortBySeverity(input.findings, SECURITY_SEVERITIES);
6167
+ const lines = [
6168
+ `VERDICT: ${input.verdict}`,
6169
+ "",
6170
+ `Judged head: ${input.headSha}`,
6171
+ "",
6172
+ "FINDINGS (ordered by severity):",
6173
+ renderFindingsSection(sorted),
6174
+ "",
6175
+ `CONFIG SCAN: ${input.configScan}`,
6176
+ ""
6177
+ ];
6178
+ if (input.secretsEvidence !== null) {
6179
+ lines.push("```", input.secretsEvidence, "```", "");
6180
+ }
6181
+ lines.push(`SECRETS: ${input.secrets}`, "", renderTokensLine("security", "Security", input));
6182
+ return lines.join(`
6183
+ `);
6184
+ }
6185
+ function isBoundToHead(extraction, headSha) {
6186
+ if (!extraction.headSha)
6187
+ return false;
6188
+ return headSha.toLowerCase().startsWith(extraction.headSha.toLowerCase());
6189
+ }
6190
+ function checkExtraction(extraction, expectedValue, headSha) {
6191
+ if (extraction.danglingNote) {
6192
+ return {
6193
+ ok: false,
6194
+ reason: `no clean VERDICT was found on re-fetch (${extraction.danglingNote}) — the posted comment does not match the gate's line-anchored \`VERDICT:\` pattern.`
6195
+ };
6196
+ }
6197
+ if (extraction.value !== expectedValue) {
6198
+ return {
6199
+ ok: false,
6200
+ reason: `re-extraction found VERDICT "${extraction.value}", expected "${expectedValue}" — the posted comment's VERDICT line does not match what this command rendered.`
6201
+ };
6202
+ }
6203
+ if (!extraction.headSha) {
6204
+ return { ok: false, reason: "the winning VERDICT comment carries no `Judged head:` line on re-fetch." };
6205
+ }
6206
+ if (!isBoundToHead(extraction, headSha)) {
6207
+ return {
6208
+ ok: false,
6209
+ reason: `re-extraction found \`Judged head: ${extraction.headSha}\`, which does not cover the resolved head ${headSha}.`
6210
+ };
6211
+ }
6212
+ return { ok: true, reason: "clean" };
6213
+ }
6214
+ function principalBodies(comments, principalAllowlist) {
6215
+ return comments.filter((c) => isPrincipal(c.author, principalAllowlist)).map((c) => c.body);
6216
+ }
6217
+ function verifyPostedCodeReview(comments, verdict, headSha, principalAllowlist) {
6218
+ return checkExtraction(extractCodeReviewVerdict(principalBodies(comments, principalAllowlist)), CODE_REVIEW_VERDICT_TEXT[verdict], headSha);
6219
+ }
6220
+ function verifyPostedSecurity(comments, verdict, headSha, principalAllowlist) {
6221
+ return checkExtraction(extractSecurityReviewVerdict(principalBodies(comments, principalAllowlist)), verdict, headSha);
6222
+ }
6223
+ function parseFlags(args) {
6224
+ const map = new Map;
6225
+ for (let i = 0;i < args.length; i++) {
6226
+ const a = args[i];
6227
+ if (a.startsWith("--")) {
6228
+ const value = args[i + 1];
6229
+ if (value === undefined || value.startsWith("--")) {
6230
+ map.set(a, "");
6231
+ continue;
6232
+ }
6233
+ map.set(a, value);
6234
+ i++;
6235
+ }
6236
+ }
6237
+ return map;
6238
+ }
6239
+ function refuseCmd(message, recovery) {
6240
+ refuse([makeCheckError("review-post", message, recovery)]);
6241
+ }
6242
+ function requireFlag(flags2, name) {
6243
+ const v = flags2.get(name);
6244
+ if (v === undefined || v === "") {
6245
+ refuseCmd(`Missing required \`${name}\` flag.`, `Pass \`${name} <value>\`, then re-run \`vinaya review post ...\`.`);
6246
+ }
6247
+ return v;
6248
+ }
6249
+ function requireTokenField(flags2, name) {
6250
+ const v = requireFlag(flags2, name);
6251
+ if (v !== "-" && !/^\d+$/.test(v)) {
6252
+ refuseCmd(`\`${name} ${v}\` is neither a non-negative integer nor \`-\` (unknown).`, `Pass a whole number or \`-\` for ${name}.`);
6253
+ }
6254
+ return v;
6255
+ }
6256
+ function readFindingsFile(path, allowedSeverities) {
6257
+ if (!path)
6258
+ return [];
6259
+ let content;
6260
+ try {
6261
+ content = readFileSync17(path, "utf8");
6262
+ } catch {
6263
+ refuseCmd(`Could not read findings file at ${path}.`, "Check the path and re-run.");
6264
+ }
6265
+ try {
6266
+ return parseFindingsFile(content, allowedSeverities);
6267
+ } catch (err) {
6268
+ if (err instanceof FindingsParseError) {
6269
+ refuseCmd(err.message, "Fix the malformed line in the findings file, then re-run.");
6270
+ }
6271
+ throw err;
6272
+ }
6273
+ }
6274
+ function normalizeCodeReviewVerdict(raw) {
6275
+ const v = raw.trim().toUpperCase().replace(/[-\s]+/g, "_");
6276
+ if (v === "APPROVE")
6277
+ return "APPROVE";
6278
+ if (v === "REQUEST_CHANGES")
6279
+ return "REQUEST_CHANGES";
6280
+ refuseCmd(`\`--verdict ${raw}\` is not APPROVE or REQUEST_CHANGES.`, "Pass `--verdict APPROVE` or `--verdict REQUEST_CHANGES`.");
6281
+ }
6282
+ function normalizeSecurityVerdict(raw) {
6283
+ const v = raw.trim().toUpperCase();
6284
+ if (v === "PASS")
6285
+ return "PASS";
6286
+ if (v === "FAIL")
6287
+ return "FAIL";
6288
+ refuseCmd(`\`--verdict ${raw}\` is not PASS or FAIL.`, "Pass `--verdict PASS` or `--verdict FAIL`.");
6289
+ }
6290
+ function gh(args) {
6291
+ try {
6292
+ return execFileSync13("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
6293
+ } catch (err) {
6294
+ const stderr = err.stderr;
6295
+ throw new Error(String(stderr ?? err.message).trim() || "gh command failed");
6296
+ }
6297
+ }
6298
+ function resolveHeadSha(pr) {
6299
+ let out;
6300
+ try {
6301
+ out = gh(["pr", "view", pr, "--json", "headRefOid", "-q", ".headRefOid"]);
6302
+ } catch (err) {
6303
+ refuseCmd(`Could not resolve PR ${pr}'s head via \`gh pr view --json headRefOid\`: ${err instanceof Error ? err.message : String(err)}`, "Confirm `gh auth status` passes and the PR number is correct, then re-run.");
6304
+ }
6305
+ if (!out) {
6306
+ refuseCmd(`\`gh pr view ${pr} --json headRefOid\` returned no head sha.`, "Confirm PR exists, then re-run.");
6307
+ }
6308
+ return out;
6309
+ }
6310
+ function postComment(pr, body) {
6311
+ const tmp = join19(tmpdir2(), `vinaya-review-post-${process.pid}-${Date.now()}.md`);
6312
+ writeFileSync10(tmp, body);
6313
+ try {
6314
+ return gh(["pr", "comment", pr, "--body-file", tmp]).trim();
6315
+ } catch (err) {
6316
+ refuseCmd(`Rendered comment failed to post via \`gh pr comment\`: ${err instanceof Error ? err.message : String(err)}`, "Check `gh auth status`/network, then re-run — nothing was posted.");
6317
+ } finally {
6318
+ rmSync4(tmp, { force: true });
6319
+ }
6320
+ }
6321
+ function fetchComments(pr) {
6322
+ const out = gh(["pr", "view", pr, "--json", "comments"]);
6323
+ const parsed = JSON.parse(out);
6324
+ return parsed.comments.map((c) => ({ body: c.body, author: c.author?.login ?? null }));
6325
+ }
6326
+ async function reviewPostCommand(args) {
6327
+ const json = args.includes("--json");
6328
+ const flags2 = parseFlags(args.filter((a) => a !== "--json"));
6329
+ const role = flags2.get("--role");
6330
+ if (role !== "code-reviewer" && role !== "security") {
6331
+ refuseCmd(`\`--role ${role ?? "(missing)"}\` is not \`code-reviewer\` or \`security\`.`, "Pass `--role code-reviewer` or `--role security`.");
6332
+ }
6333
+ const pr = requireFlag(flags2, "--pr");
6334
+ const taskId = requireFlag(flags2, "--task-id");
6335
+ const model = requireFlag(flags2, "--model");
6336
+ const tokensIn = requireTokenField(flags2, "--tokens-in");
6337
+ const tokensOut = requireTokenField(flags2, "--tokens-out");
6338
+ const cost = requireFlag(flags2, "--cost");
6339
+ const tokens = { taskId, model, tokensIn, tokensOut, cost };
6340
+ const headSha = resolveHeadSha(pr);
6341
+ const principalAllowlist = resolvePrincipalAllowlist(loadTrustAnchorConfig());
6342
+ let body;
6343
+ let verify;
6344
+ if (role === "code-reviewer") {
6345
+ const verdict = normalizeCodeReviewVerdict(requireFlag(flags2, "--verdict"));
6346
+ const findings = readFindingsFile(flags2.get("--findings-file"), CODE_REVIEW_SEVERITIES);
6347
+ if (findings.some((f) => f.severity === "BLOCKER") && verdict === "APPROVE") {
6348
+ refuseCmd("Findings include a BLOCKER but `--verdict APPROVE` — reviewer.md requires REQUEST CHANGES whenever any BLOCKER finding exists.", "Pass `--verdict REQUEST_CHANGES`, or fix the finding severity if BLOCKER was a miscategorization.");
6349
+ }
6350
+ const input = {
6351
+ ...tokens,
6352
+ headSha,
6353
+ verdict,
6354
+ briefConformance: requireFlag(flags2, "--brief-conformance"),
6355
+ specConformance: requireFlag(flags2, "--spec-conformance"),
6356
+ findings,
6357
+ scope: requireFlag(flags2, "--scope"),
6358
+ tests: requireFlag(flags2, "--tests"),
6359
+ docs: requireFlag(flags2, "--docs")
6360
+ };
6361
+ body = renderCodeReviewComment(input);
6362
+ verify = (comments2) => verifyPostedCodeReview(comments2, verdict, headSha, principalAllowlist);
6363
+ } else {
6364
+ const verdict = normalizeSecurityVerdict(requireFlag(flags2, "--verdict"));
6365
+ const findings = readFindingsFile(flags2.get("--findings-file"), SECURITY_SEVERITIES);
6366
+ if (findings.some((f) => f.severity === "CRITICAL" || f.severity === "HIGH") && verdict === "PASS") {
6367
+ refuseCmd("Findings include a CRITICAL/HIGH finding but `--verdict PASS` — security.md requires FAIL whenever any CRITICAL or HIGH finding exists.", "Pass `--verdict FAIL`, or fix the finding severity if it was a miscategorization.");
6368
+ }
6369
+ const secrets = requireFlag(flags2, "--secrets");
6370
+ const secretsEvidenceFile = flags2.get("--secrets-evidence-file");
6371
+ if (isNoneFoundClaim(secrets) && !secretsEvidenceFile) {
6372
+ refuseCmd('`--secrets` normalizes to "none found" but no `--secrets-evidence-file` was given — security.md: "SECRETS: none found" with no scan output pasted is an unbacked self-attestation.', "Pass `--secrets-evidence-file <path>` containing the actual scanner output, or change `--secrets` to describe what was found instead.");
6373
+ }
6374
+ let secretsEvidence = null;
6375
+ if (secretsEvidenceFile) {
6376
+ try {
6377
+ secretsEvidence = readFileSync17(secretsEvidenceFile, "utf8");
6378
+ } catch {
6379
+ refuseCmd(`Could not read secrets evidence file at ${secretsEvidenceFile}.`, "Check the path and re-run.");
6380
+ }
6381
+ }
6382
+ const input = {
6383
+ ...tokens,
6384
+ headSha,
6385
+ verdict,
6386
+ findings,
6387
+ configScan: requireFlag(flags2, "--config-scan"),
6388
+ secrets,
6389
+ secretsEvidence
6390
+ };
6391
+ body = renderSecurityComment(input);
6392
+ verify = (comments2) => verifyPostedSecurity(comments2, verdict, headSha, principalAllowlist);
6393
+ }
6394
+ const url = postComment(pr, body);
6395
+ let comments;
6396
+ try {
6397
+ comments = fetchComments(pr);
6398
+ } catch (err) {
6399
+ refuseCmd(`Posted the comment (${url}) but could not re-fetch PR ${pr}'s comments to self-verify: ${err instanceof Error ? err.message : String(err)}`, "Check `gh auth status`/network and manually confirm the posted comment parses cleanly — this command could not verify it.");
6400
+ }
6401
+ const result = verify(comments);
6402
+ if (!result.ok) {
6403
+ refuseCmd(`Posted comment ${url}, but self-verification FAILED on re-parse: ${result.reason}`, "The posted comment does not re-parse clean through the same extractCodeReviewVerdict/extractSecurityReviewVerdict functions the merge gate calls. Do not treat the post as valid — inspect the comment and this command for drift, fix, and re-run.");
6404
+ }
6405
+ if (json) {
6406
+ printJson({ posted: true, url, role, headSha, selfVerified: true });
6407
+ } else {
6408
+ process.stdout.write(`${body}
6409
+
6410
+ Posted: ${url}
6411
+ Self-verification: clean — re-parsed VERDICT is bound to head ${headSha}.
6412
+ `);
6413
+ }
6414
+ }
6415
+
5520
6416
  // src/commands/studio.ts
5521
6417
  import { execFile as execFile5, spawn as spawn2 } from "node:child_process";
5522
- import { existsSync as existsSync16, readFileSync as readFileSync16, renameSync } from "node:fs";
6418
+ import { existsSync as existsSync17, readFileSync as readFileSync18, renameSync } from "node:fs";
5523
6419
  import net from "node:net";
5524
- import { dirname as dirname7, join as join18 } from "node:path";
6420
+ import { dirname as dirname7, join as join20 } from "node:path";
5525
6421
  import { promisify as promisify5 } from "node:util";
5526
6422
 
5527
6423
  // src/lib/studio-bundle.ts
@@ -5537,25 +6433,25 @@ var FALLBACK_PORT = 3108;
5537
6433
  function resolveStudioTarget(cwd, moduleUrl = import.meta.url) {
5538
6434
  let dir = cwd;
5539
6435
  for (;; ) {
5540
- const webDir = join18(dir, "apps", "vinaya-studio", "web");
5541
- const pkgPath = join18(webDir, "package.json");
5542
- if (existsSync16(pkgPath)) {
6436
+ const webDir = join20(dir, "apps", "vinaya-studio", "web");
6437
+ const pkgPath = join20(webDir, "package.json");
6438
+ if (existsSync17(pkgPath)) {
5543
6439
  try {
5544
- const pkg = JSON.parse(readFileSync16(pkgPath, "utf-8"));
6440
+ const pkg = JSON.parse(readFileSync18(pkgPath, "utf-8"));
5545
6441
  if (pkg.name === "@atta/vinaya-studio-web") {
5546
6442
  return { kind: "workspace", webDir };
5547
6443
  }
5548
6444
  } catch {}
5549
6445
  }
5550
- if (existsSync16(join18(dir, ".git")))
6446
+ if (existsSync17(join20(dir, ".git")))
5551
6447
  break;
5552
6448
  const parent = dirname7(dir);
5553
6449
  if (parent === dir)
5554
6450
  break;
5555
6451
  dir = parent;
5556
6452
  }
5557
- const standaloneWebDir = join18(packageRoot(moduleUrl), "studio-standalone", "apps", "vinaya-studio", "web");
5558
- if (existsSync16(join18(standaloneWebDir, "server.js"))) {
6453
+ const standaloneWebDir = join20(packageRoot(moduleUrl), "studio-standalone", "apps", "vinaya-studio", "web");
6454
+ if (existsSync17(join20(standaloneWebDir, "server.js"))) {
5559
6455
  return { kind: "package", packageDir: standaloneWebDir };
5560
6456
  }
5561
6457
  return { kind: "missing" };
@@ -5575,9 +6471,9 @@ function isPortFree(port) {
5575
6471
  });
5576
6472
  }
5577
6473
  function ensureStudioNodeModules(bundleRoot) {
5578
- const real = join18(bundleRoot, "node_modules");
5579
- const packed = join18(bundleRoot, STUDIO_NODE_MODULES_PACKED_DIRNAME);
5580
- if (!existsSync16(real) && existsSync16(packed)) {
6474
+ const real = join20(bundleRoot, "node_modules");
6475
+ const packed = join20(bundleRoot, STUDIO_NODE_MODULES_PACKED_DIRNAME);
6476
+ if (!existsSync17(real) && existsSync17(packed)) {
5581
6477
  renameSync(packed, real);
5582
6478
  }
5583
6479
  }
@@ -5603,8 +6499,8 @@ async function runStudio(cwd, args, moduleUrl = import.meta.url) {
5603
6499
  case "workspace":
5604
6500
  return spawnDev(target.webDir, args);
5605
6501
  case "package": {
5606
- const bundleRoot = join18(target.packageDir, "..", "..", "..");
5607
- return spawnStandalone(cwd, join18(target.packageDir, "server.js"), bundleRoot);
6502
+ const bundleRoot = join20(target.packageDir, "..", "..", "..");
6503
+ return spawnStandalone(cwd, join20(target.packageDir, "server.js"), bundleRoot);
5608
6504
  }
5609
6505
  case "missing":
5610
6506
  console.error("Vinaya Studio isn't available in this install — no published @attalabs/vinaya build bundles the Studio app yet. Inside a checkout that contains Studio's source (apps/vinaya-studio/web), `vinaya studio` runs it directly.");
@@ -5613,8 +6509,8 @@ async function runStudio(cwd, args, moduleUrl = import.meta.url) {
5613
6509
  }
5614
6510
 
5615
6511
  // src/commands/upgrade.ts
5616
- import { existsSync as existsSync17, readFileSync as readFileSync17, rmSync as rmSync4, writeFileSync as writeFileSync9 } from "node:fs";
5617
- import { join as join19 } from "node:path";
6512
+ import { existsSync as existsSync18, readFileSync as readFileSync19, rmSync as rmSync5, writeFileSync as writeFileSync11 } from "node:fs";
6513
+ import { join as join21 } from "node:path";
5618
6514
  function realDeps7() {
5619
6515
  return {
5620
6516
  detectRepo: detectGitRepo,
@@ -5632,12 +6528,12 @@ function flags2(args) {
5632
6528
  return { dryRun: args.includes("--dry-run"), yes: args.includes("--yes") };
5633
6529
  }
5634
6530
  function readManifest3(repoRoot) {
5635
- const p = join19(repoRoot, CONFIG_PATH);
5636
- if (!existsSync17(p))
6531
+ const p = join21(repoRoot, CONFIG_PATH);
6532
+ if (!existsSync18(p))
5637
6533
  return { kind: "missing" };
5638
6534
  let raw;
5639
6535
  try {
5640
- raw = JSON.parse(readFileSync17(p, "utf-8"));
6536
+ raw = JSON.parse(readFileSync19(p, "utf-8"));
5641
6537
  } catch (err) {
5642
6538
  return { kind: "invalid", error: `invalid JSON: ${err.message}` };
5643
6539
  }
@@ -5653,17 +6549,17 @@ function readManifest3(repoRoot) {
5653
6549
  return { kind: "ok", manifest: parsed.data.managed };
5654
6550
  }
5655
6551
  function writeManifestVersion(repoRoot, manifest) {
5656
- const configAbs = join19(repoRoot, CONFIG_PATH);
5657
- const seed = JSON.parse(readFileSync17(configAbs, "utf-8"));
6552
+ const configAbs = join21(repoRoot, CONFIG_PATH);
6553
+ const seed = JSON.parse(readFileSync19(configAbs, "utf-8"));
5658
6554
  const updated = { ...manifest, version: MANAGED_MANIFEST_VERSION };
5659
- writeFileSync9(configAbs, `${JSON.stringify({ ...seed, managed: updated }, null, 2)}
6555
+ writeFileSync11(configAbs, `${JSON.stringify({ ...seed, managed: updated }, null, 2)}
5660
6556
  `, "utf-8");
5661
6557
  }
5662
6558
  function stripFor(repoRoot, path, marker, comment) {
5663
6559
  const abs2 = resolveManagedBlockPath(repoRoot, path);
5664
- if (!existsSync17(abs2))
6560
+ if (!existsSync18(abs2))
5665
6561
  return { path, marker, comment, present: false, removesHost: false };
5666
- const stripped = stripBlockFromContent(readFileSync17(abs2, "utf-8"), marker, comment);
6562
+ const stripped = stripBlockFromContent(readFileSync19(abs2, "utf-8"), marker, comment);
5667
6563
  if (stripped === null)
5668
6564
  return { path, marker, comment, present: false, removesHost: false };
5669
6565
  return { path, marker, comment, present: true, removesHost: blockStripLeavesEmpty(stripped) };
@@ -5690,9 +6586,9 @@ function planHookRouting(repoRoot, manifest, recorded, hooksPathValue) {
5690
6586
  const blocked = [];
5691
6587
  for (const b of legacy) {
5692
6588
  const abs2 = resolveManagedBlockPath(repoRoot, b.path);
5693
- if (!existsSync17(abs2))
6589
+ if (!existsSync18(abs2))
5694
6590
  continue;
5695
- const stripped = stripBlockFromContent(readFileSync17(abs2, "utf-8"), b.marker, b.comment);
6591
+ const stripped = stripBlockFromContent(readFileSync19(abs2, "utf-8"), b.marker, b.comment);
5696
6592
  if (stripped === null)
5697
6593
  blocked.push(`${b.path} exists without vinaya's managed block`);
5698
6594
  else if (!blockStripLeavesEmpty(stripped))
@@ -5727,8 +6623,8 @@ function planUpgrade(ops, repoRoot, manifest, routing) {
5727
6623
  const ownedBlocks = new Set(manifest.blocks.map((b) => blockKey(b.path, b.marker)));
5728
6624
  for (const op of ops) {
5729
6625
  if (op.kind === "create-file") {
5730
- const abs2 = join19(repoRoot, op.path);
5731
- const exists = existsSync17(abs2);
6626
+ const abs2 = join21(repoRoot, op.path);
6627
+ const exists = existsSync18(abs2);
5732
6628
  const owned = ownedFiles.has(op.path);
5733
6629
  let action;
5734
6630
  if (op.path === CONFIG_PATH || op.path === DOC_OWNERS_PATH) {
@@ -5738,7 +6634,7 @@ function planUpgrade(ops, repoRoot, manifest, routing) {
5738
6634
  } else if (!exists) {
5739
6635
  action = "recreate";
5740
6636
  hasChanges = true;
5741
- } else if (readFileSync17(abs2, "utf-8") !== op.content) {
6637
+ } else if (readFileSync19(abs2, "utf-8") !== op.content) {
5742
6638
  action = "regenerate";
5743
6639
  hasChanges = true;
5744
6640
  } else {
@@ -5751,11 +6647,11 @@ function planUpgrade(ops, repoRoot, manifest, routing) {
5751
6647
  let action;
5752
6648
  if (!owned) {
5753
6649
  action = "not-installed";
5754
- } else if (!existsSync17(abs2)) {
6650
+ } else if (!existsSync18(abs2)) {
5755
6651
  action = "recreate-host";
5756
6652
  hasChanges = true;
5757
6653
  } else {
5758
- const content = readFileSync17(abs2, "utf-8");
6654
+ const content = readFileSync19(abs2, "utf-8");
5759
6655
  const { begin, end } = markerLines(op.marker, op.comment);
5760
6656
  if (!(content.includes(begin) && content.includes(end))) {
5761
6657
  action = "recreate-append";
@@ -5855,10 +6751,10 @@ function renderUpgradeDiff(plan) {
5855
6751
  }
5856
6752
  function regenerateBlock(repoRoot, op) {
5857
6753
  const abs2 = resolveManagedBlockPath(repoRoot, op.path);
5858
- const content = readFileSync17(abs2, "utf-8");
6754
+ const content = readFileSync19(abs2, "utf-8");
5859
6755
  const stripped = stripBlockFromContent(content, op.marker, op.comment);
5860
6756
  if (stripped !== null) {
5861
- writeFileSync9(abs2, stripped.endsWith(`
6757
+ writeFileSync11(abs2, stripped.endsWith(`
5862
6758
  `) ? stripped : `${stripped}
5863
6759
  `, "utf-8");
5864
6760
  }
@@ -5868,7 +6764,7 @@ function applyUpgrade(plan, repoRoot) {
5868
6764
  for (const e of plan.entries) {
5869
6765
  if (e.kind === "create-file") {
5870
6766
  if (e.action === "regenerate" || e.action === "recreate") {
5871
- writeFileWithDirs(join19(repoRoot, e.op.path), e.op.content, e.op.mode);
6767
+ writeFileWithDirs(join21(repoRoot, e.op.path), e.op.content, e.op.mode);
5872
6768
  }
5873
6769
  } else {
5874
6770
  if (e.action === "recreate-host")
@@ -5883,15 +6779,15 @@ function applyUpgrade(plan, repoRoot) {
5883
6779
  if (!s.present)
5884
6780
  continue;
5885
6781
  const abs2 = resolveManagedBlockPath(repoRoot, s.path);
5886
- if (!existsSync17(abs2))
6782
+ if (!existsSync18(abs2))
5887
6783
  continue;
5888
- const stripped = stripBlockFromContent(readFileSync17(abs2, "utf-8"), s.marker, s.comment);
6784
+ const stripped = stripBlockFromContent(readFileSync19(abs2, "utf-8"), s.marker, s.comment);
5889
6785
  if (stripped === null)
5890
6786
  continue;
5891
6787
  if (blockStripLeavesEmpty(stripped))
5892
- rmSync4(abs2, { force: true });
6788
+ rmSync5(abs2, { force: true });
5893
6789
  else
5894
- writeFileSync9(abs2, stripped.endsWith(`
6790
+ writeFileSync11(abs2, stripped.endsWith(`
5895
6791
  `) ? stripped : `${stripped}
5896
6792
  `, "utf-8");
5897
6793
  }
@@ -5973,16 +6869,16 @@ async function upgradeCommand(args) {
5973
6869
  }
5974
6870
 
5975
6871
  // src/commands/waiver.ts
5976
- import { execFileSync as execFileSync10 } from "node:child_process";
6872
+ import { execFileSync as execFileSync14 } from "node:child_process";
5977
6873
  function labelFor(kind) {
5978
6874
  return kind === "docs" ? WAIVER_LABEL : WAIVER_LABEL_REVIEW;
5979
6875
  }
5980
- function gh(args) {
5981
- return execFileSync10("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], env: process.env }).trim();
6876
+ function gh2(args) {
6877
+ return execFileSync14("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], env: process.env }).trim();
5982
6878
  }
5983
6879
  function detectPrNumber() {
5984
6880
  try {
5985
- const out = gh(["pr", "view", "--json", "number", "-q", ".number"]);
6881
+ const out = gh2(["pr", "view", "--json", "number", "-q", ".number"]);
5986
6882
  return /^\d+$/.test(out) ? out : null;
5987
6883
  } catch {
5988
6884
  return null;
@@ -6078,8 +6974,8 @@ Reason: ${reason}`];
6078
6974
  }
6079
6975
  process.stdout.write(`Applying \`${label2}\` to PR #${prNumber} under your authenticated \`gh\` identity — vinaya never fabricates an actor.
6080
6976
  `);
6081
- execFileSync10("gh", editArgs, { stdio: "inherit", env: process.env });
6082
- execFileSync10("gh", commentArgs, { stdio: "inherit", env: process.env });
6977
+ execFileSync14("gh", editArgs, { stdio: "inherit", env: process.env });
6978
+ execFileSync14("gh", commentArgs, { stdio: "inherit", env: process.env });
6083
6979
  process.stdout.write(`
6084
6980
  ✓ Label applied and reason posted as a PR comment.
6085
6981
  `);
@@ -6149,7 +7045,8 @@ var COMMANDS = [
6149
7045
  ],
6150
7046
  details: [
6151
7047
  "Each spawned check's child process sees only a fixed baseline (`PATH`, `LANG`, `HOME`, `HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY`, `TMPDIR`) plus whatever its `CheckSpec['env']` declaration explicitly forwards — never the full parent environment. A required (`true`) or unsatisfied `anyOf` declaration missing from the caller's environment synthesizes a `CheckError` before the check ever spawns. Declare `env` (a core check's own registration, or `vinaya.config.json`'s `checks.<name>.env` for a custom one) for any check that reads `process.env`/`Bun.env`/`Deno.env` directly — `vinaya doctor` carries the permanent diagnostic for one that doesn't.",
6152
- "`--plan` composes with `--json`. It requires zero env vars and never prints an env value only how each one resolves (passthrough, optional, literal, or anyOf). A `FAIL_CLOSED` entry (a bare key with no namespace matching no core check) always renders inline rather than being dropped, and exits non-zero. `--plan` previews what the next minor's execution will enforce `vinaya check` itself still runs the flat, unvalidated registry this release.",
7048
+ "The resolved registry IS what runs. A `checks` key that exactly matches a core check id REPLACES that core check (the core one does not run); any other key must be namespaced `<yourname>/<id>`. Anything the resolver cannot classify — a malformed entry, a bare un-namespaced key matching no core id, a duplicate id makes `vinaya check` refuse the ENTIRE run: exit 1, nothing executes, never a partial ruleset and never a core-only fallback. `vinaya doctor` carries the permanent diagnostic for each rejected entry, so a refused config is still diagnosable.",
7049
+ "`--plan` composes with `--json`. It requires zero env vars and never prints an env value — only how each one resolves (passthrough, optional, literal, or anyOf). A `FAIL_CLOSED` entry always renders inline rather than being dropped, and exits non-zero. `--plan` and execution read the same resolution, so what the plan prints is what runs.",
6153
7050
  "`--local` exists because a `requiresOpenPr` check (the core `closes-n`/`test-plan`, or a custom check declaring the same field) can only evaluate for real once a pull request exists — the generated `pre-commit`/`pre-push` hooks pass it so the first commit on a fresh task branch is never asked to satisfy a PR-body field before a PR can possibly exist. CI's `vinaya-checks.yml` omits it, so these checks always run for real once a PR is open."
6154
7051
  ],
6155
7052
  status: "shipped"
@@ -6157,6 +7054,9 @@ var COMMANDS = [
6157
7054
  {
6158
7055
  name: "new check",
6159
7056
  description: "Scaffold a custom check into ./scripts/vinaya-checks/",
7057
+ details: [
7058
+ "Takes the REGISTRATION KEY, not a bare name: `vinaya new check <yourname>/<id>` writes `./scripts/vinaya-checks/<id>.ts` and prints the namespaced `checks` entry to paste. It refuses a bare, un-namespaced name — `vinaya check` refuses its entire run over a key it cannot resolve, so scaffolding one would brick every check invocation in the repo — and refuses a core check id, since registering one REPLACES that core gate and a scaffolded stub is never what an adopter means by that."
7059
+ ],
6160
7060
  status: "shipped"
6161
7061
  },
6162
7062
  {
@@ -6191,6 +7091,21 @@ var COMMANDS = [
6191
7091
  ],
6192
7092
  status: "shipped"
6193
7093
  },
7094
+ {
7095
+ name: "pr report",
7096
+ description: "Emit the AEG:EVIDENCE block — a PR body's factual claims, from commands, never typed",
7097
+ flags: [
7098
+ {
7099
+ flag: "--write",
7100
+ description: "Path to the PR body file; replaces the content between the AEG:EVIDENCE anchors in place"
7101
+ }
7102
+ ],
7103
+ details: [
7104
+ "Two groups: Group A (recomputable) is the head sha and a width-invariant `git diff --numstat` against the merge-base with `BASE_SHA` (else `origin/main`, then `main`) — `check-evidence-fresh` recomputes and byte-compares this exactly. Group B (attested) is the result of `vinaya check --all --diff-only`, this CLI's own portable gate suite — `check-evidence-fresh` can only check it for staleness (the block's recorded head still matches the PR's real head), never re-run it.",
7105
+ "With no `--write`, prints the block to stdout instead of writing a file. Exits non-zero whenever the gate run failed, whether or not `--write` was given — the block records a failing result rather than hiding one, and a non-zero exit stops a scripted `--write && open-pr` from carrying a failing suite onto the forge."
7106
+ ],
7107
+ status: "shipped"
7108
+ },
6194
7109
  {
6195
7110
  name: "issue create",
6196
7111
  description: "Open an issue after full brief-schema validation",
@@ -6220,12 +7135,55 @@ var COMMANDS = [
6220
7135
  ],
6221
7136
  status: "shipped"
6222
7137
  },
7138
+ {
7139
+ name: "review post",
7140
+ description: "Render, post, and self-verify a code-reviewer or security-review verdict comment on a PR",
7141
+ flags: [
7142
+ { flag: "--role", description: "`code-reviewer` or `security` — selects which role template is rendered" },
7143
+ {
7144
+ flag: "--pr",
7145
+ description: "Target PR number — its real head is resolved via `gh pr view`, never caller-supplied"
7146
+ },
7147
+ {
7148
+ flag: "--verdict",
7149
+ description: "code-reviewer: `APPROVE` | `REQUEST_CHANGES`. security: `PASS` | `FAIL`"
7150
+ },
7151
+ {
7152
+ flag: "--findings-file",
7153
+ description: "One finding per line: `SEVERITY|file:line|description` (`|`-delimited). Omit for zero findings."
7154
+ },
7155
+ { flag: "--brief-conformance", description: "code-reviewer only: the BRIEF CONFORMANCE line" },
7156
+ { flag: "--spec-conformance", description: "code-reviewer only: the SPEC CONFORMANCE line" },
7157
+ { flag: "--scope", description: "code-reviewer only: the SCOPE line" },
7158
+ { flag: "--tests", description: "code-reviewer only: the TESTS line" },
7159
+ { flag: "--docs", description: "code-reviewer only: the DOCS line" },
7160
+ { flag: "--config-scan", description: "security only: the CONFIG SCAN line" },
7161
+ { flag: "--secrets", description: "security only: the SECRETS line" },
7162
+ {
7163
+ flag: "--secrets-evidence-file",
7164
+ description: 'security only: required whenever `--secrets` claims "none found" — the pasted scanner output backing that claim'
7165
+ },
7166
+ { flag: "--task-id", description: "the closing `Tokens:` line's task id" },
7167
+ { flag: "--model", description: "the closing `Tokens:` line's model name" },
7168
+ { flag: "--tokens-in", description: "a non-negative integer, or `-` if unknown" },
7169
+ { flag: "--tokens-out", description: "a non-negative integer, or `-` if unknown" },
7170
+ { flag: "--cost", description: "free text, or `-` if unknown" },
7171
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
7172
+ ],
7173
+ details: [
7174
+ "Every structural line (`VERDICT:`, `Judged head:`) is rendered from this command's own validated enum/sha inputs — never from a caller-supplied string — so a Reviewer's free-typed prose can no longer produce a shape the merge gate's line-anchored regex fails to see.",
7175
+ "Refuses a contradictory verdict before posting anything: a BLOCKER finding with `--verdict APPROVE`, or a CRITICAL/HIGH finding with `--verdict PASS`, is rejected outright, mirroring each role doc's own consistency rule.",
7176
+ "After posting, re-fetches the PR's comments and runs them through the exact `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` functions `checkReviewGate` calls — the same functions, not a second implementation — and exits non-zero naming precisely what failed to re-parse if the post does not come back clean and bound to the resolved head. There is no `--skip-verify` escape."
7177
+ ],
7178
+ status: "shipped"
7179
+ },
6223
7180
  {
6224
7181
  name: "doctor",
6225
7182
  description: "Diagnose hook, workflow, and config health — report only, never mutates",
6226
7183
  flags: [{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }],
6227
7184
  details: [
6228
- 'Carries the same env-declaration diagnostic `vinaya check` warns with — permanently, at `info` severity, not just ahead of the spawn-default flip — plus a `warn`-severity lint over suspicious `env` literal forms (a stray `"true"`/`"false"` string, or a high-entropy literal that reads like a leaked secret committed to config).'
7185
+ 'Carries the same env-declaration diagnostic `vinaya check` warns with — permanently, at `info` severity, not just ahead of the spawn-default flip — plus a `warn`-severity lint over suspicious `env` literal forms (a stray `"true"`/`"false"` string, or a high-entropy literal that reads like a leaked secret committed to config).',
7186
+ "Also carries the two permanent `checks`-classification diagnostics: a config key that REPLACES a core check (`warn`), and a bare un-namespaced key that is REJECTED (`error`, naming the rename requirement). These are the reason a config `vinaya check` now refuses outright is still diagnosable — the refusal runs nothing, so `doctor` is the surface that explains why."
6229
7187
  ],
6230
7188
  status: "shipped"
6231
7189
  },
@@ -6364,9 +7322,9 @@ function printHelp() {
6364
7322
  }
6365
7323
 
6366
7324
  // src/index.ts
6367
- var PACKAGE_ROOT = join20(dirname8(fileURLToPath2(import.meta.url)), "..");
7325
+ var PACKAGE_ROOT = join22(dirname8(fileURLToPath2(import.meta.url)), "..");
6368
7326
  function readVersion2() {
6369
- const pkg = JSON.parse(readFileSync18(join20(PACKAGE_ROOT, "package.json"), "utf-8"));
7327
+ const pkg = JSON.parse(readFileSync20(join22(PACKAGE_ROOT, "package.json"), "utf-8"));
6370
7328
  return pkg.version;
6371
7329
  }
6372
7330
  var [, , command, ...args] = process.argv;
@@ -6449,8 +7407,10 @@ try {
6449
7407
  prCreateCommand(rest);
6450
7408
  } else if (subcommand === "edit") {
6451
7409
  prEditCommand(rest);
7410
+ } else if (subcommand === "report") {
7411
+ await prReportCommand(rest);
6452
7412
  } else {
6453
- console.error(`Unknown 'pr' subcommand: ${subcommand ?? "(none)"} (expected 'create' or 'edit')`);
7413
+ console.error(`Unknown 'pr' subcommand: ${subcommand ?? "(none)"} (expected 'create', 'edit', or 'report')`);
6454
7414
  process.exit(2);
6455
7415
  }
6456
7416
  break;
@@ -6467,6 +7427,16 @@ try {
6467
7427
  }
6468
7428
  break;
6469
7429
  }
7430
+ case "review": {
7431
+ const [subcommand, ...rest] = args;
7432
+ if (subcommand === "post") {
7433
+ await reviewPostCommand(rest);
7434
+ } else {
7435
+ console.error(`Unknown 'review' subcommand: ${subcommand ?? "(none)"} (expected 'post')`);
7436
+ process.exit(2);
7437
+ }
7438
+ break;
7439
+ }
6470
7440
  case "demo": {
6471
7441
  const [subcommand, ...rest] = args;
6472
7442
  if (subcommand === "break") {