@attalabs/vinaya 0.16.0 → 0.17.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 (121) hide show
  1. package/README.md +8 -2
  2. package/aeg-root/contracts/planner-brief.md +1 -1
  3. package/aeg-root/enforcement.md +2 -2
  4. package/aeg-root/roles/developer.md +1 -0
  5. package/aeg-root/roles/planner.md +2 -2
  6. package/aeg-root/roles/reviewer.md +1 -1
  7. package/aeg-root/skills/aeg/SKILL.md +1 -1
  8. package/aeg-root/state-machine.md +1 -1
  9. package/aeg-root/templates/pr-report-template.md +2 -0
  10. package/aeg-root/tranche-model.md +1 -1
  11. package/dist/checks/bin/check-body-bare-digits.js +3545 -0
  12. package/dist/checks/bin/check-branch-topology.js +72 -6
  13. package/dist/checks/bin/check-brief-shape.js +72 -6
  14. package/dist/checks/bin/check-closes-n.js +72 -6
  15. package/dist/checks/bin/check-coherence.js +77 -7
  16. package/dist/checks/bin/check-dead-branch-push.js +72 -6
  17. package/dist/checks/bin/check-dispatch-readiness.js +77 -7
  18. package/dist/checks/bin/check-doc-coverage-push.js +77 -7
  19. package/dist/checks/bin/check-doc-coverage.js +80 -11
  20. package/dist/checks/bin/check-evidence-fresh.js +72 -6
  21. package/dist/checks/bin/check-first-push-dispatch.js +77 -7
  22. package/dist/checks/bin/check-issue-assignment.js +72 -6
  23. package/dist/checks/bin/check-no-disk-state.js +72 -6
  24. package/dist/checks/bin/check-reader-resolvable-prose.js +72 -6
  25. package/dist/checks/bin/check-registry-gates.js +72 -6
  26. package/dist/checks/bin/check-review-gate.js +77 -18
  27. package/dist/checks/bin/check-single-plan-pr.js +72 -6
  28. package/dist/checks/bin/check-test-plan.js +72 -6
  29. package/dist/index.js +591 -206
  30. package/package.json +1 -1
  31. package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
  32. package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
  33. package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
  34. package/studio-standalone/apps/vinaya-studio/web/.next/required-server-files.json +1 -1
  35. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
  36. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
  37. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  38. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  39. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  40. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
  41. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  42. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  43. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
  44. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
  45. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
  46. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
  47. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js +1 -1
  48. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
  49. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
  50. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
  51. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js +1 -1
  52. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
  53. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
  54. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
  55. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
  56. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
  57. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
  58. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js +1 -1
  59. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
  60. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
  61. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
  62. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js +1 -1
  63. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
  64. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
  65. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
  66. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js +1 -1
  67. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
  68. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
  69. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
  70. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
  71. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
  72. 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
  73. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__029-1ch._.js +1 -1
  74. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0gw1_g2._.js +1 -1
  75. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0q56ohl._.js +1 -1
  76. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0rr_lq7._.js +1 -1
  77. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__19jyp3m._.js +1 -1
  78. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1_ojzp1._.js +1 -1
  79. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__1qpl4m_._.js → [root-of-the-server]__1ccgndq._.js} +1 -1
  80. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1d_8nm4._.js +1 -1
  81. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1dfzjrd._.js +1 -1
  82. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1zkwtx8._.js +1 -1
  83. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__210ehg9._.js +1 -1
  84. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0-n77dx._.js +1 -1
  85. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_00yjcv1._.js +1 -1
  86. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_01605g7._.js +1 -1
  87. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0o54suy._.js +1 -1
  88. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0rix02i._.js +1 -1
  89. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_1u3abhy._.js → _0x999pb._.js} +2 -2
  90. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_0ndy563._.js → _1-6rzip._.js} +2 -2
  91. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_18-d2be._.js +1 -1
  92. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_087xa25._.js → _1_talrs._.js} +2 -2
  93. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_11vnfnm._.js → _1ar5uqe._.js} +2 -2
  94. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_02drwa_._.js → _1c9b9j-._.js} +2 -2
  95. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1gw6vc9._.js +1 -1
  96. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_app_studio_18kqbvs._.js +1 -1
  97. 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
  98. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
  99. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_animate_components_index_ts_186fje1._.js +1 -1
  100. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_basic_components_index_ts_0xk1vda._.js +1 -1
  101. package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
  102. package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
  103. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
  104. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
  105. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1mexqnauhtbtp.js → 0_jnwbarv-cqk.js} +1 -1
  106. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0ek6qlqhokys0.js → 0l_vubs1ephe5.js} +1 -1
  107. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0ojfbn3yvy-8_.js +1 -0
  108. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1jx5_q1g9l-cm.js → 0phqfqwbnt_ey.js} +1 -1
  109. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0qcu17x3h94a9.js → 17r0szvqeeyhx.js} +1 -1
  110. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0ve1-nz-mu_lr.js → 18x3rbqzeutpz.js} +1 -1
  111. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{22y4d5yrovcv-.js → 1bqpfvromtwn1.js} +1 -1
  112. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{39w6jbw3rleez.js → 24ojyzmt_fkg3.js} +1 -1
  113. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0tmcnl-z8yt2_.js → 30lyvn0yzwdiz.js} +1 -1
  114. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0fm4p9os2lsom.js → 3m0_gk59s-rbx.js} +1 -1
  115. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0ywkq74falx-x.js → 40x7f5hm7a5tk.js} +1 -1
  116. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0-08g_jjalnd9.js → 453zhecht_m1q.js} +1 -1
  117. package/studio-standalone/apps/vinaya-studio/web/server.js +1 -1
  118. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1s9ixp5nhnevp.js +0 -1
  119. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{R7xvB2iuNFp_jxENpL7xv → p5sWGp4ANwORiaX4Kk6U2}/_buildManifest.js +0 -0
  120. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{R7xvB2iuNFp_jxENpL7xv → p5sWGp4ANwORiaX4Kk6U2}/_clientMiddlewareManifest.js +0 -0
  121. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{R7xvB2iuNFp_jxENpL7xv → p5sWGp4ANwORiaX4Kk6U2}/_ssgManifest.js +0 -0
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync20 } from "node:fs";
4
+ import { readFileSync as readFileSync21 } from "node:fs";
5
5
  import { dirname as dirname8, join as join22 } from "node:path";
6
6
  import { fileURLToPath as fileURLToPath2 } from "node:url";
7
7
 
@@ -10,7 +10,10 @@ import { execFileSync as execFileSync4 } from "node:child_process";
10
10
  // ../../packages/aeg-forge-state/src/strip-code.ts
11
11
  function maskCode(body) {
12
12
  const fill = (line) => " ".repeat(line.length);
13
- return maskIndentedCode(maskFencedCode(body, fill), fill).replace(/(`+)[^\n]*?\1/g, (m) => " ".repeat(m.length));
13
+ const blocksMasked = maskIndentedCode(maskFencedCode(body, fill), fill);
14
+ return blocksMasked.split(`
15
+ `).map((line) => replaceInlineSpans(line, (span) => " ".repeat(span.length))).join(`
16
+ `);
14
17
  }
15
18
  function stripCode(body, options = {}) {
16
19
  const normalised = body.replace(/\r\n?/g, `
@@ -19,7 +22,50 @@ function stripCode(body, options = {}) {
19
22
  const blocksStripped = maskIndentedCode(maskFencedCode(normalised, blank), blank);
20
23
  if (options.inlineSpans === "keep")
21
24
  return blocksStripped;
22
- return blocksStripped.replace(/(`+)[^\n]*?\1/g, "");
25
+ return blocksStripped.split(`
26
+ `).map((line) => replaceInlineSpans(line, () => "")).join(`
27
+ `);
28
+ }
29
+ function replaceInlineSpans(line, replace) {
30
+ let result = "";
31
+ let i = 0;
32
+ const runLengthAt = (at) => {
33
+ let j = at;
34
+ while (j < line.length && line[j] === "`")
35
+ j++;
36
+ return j - at;
37
+ };
38
+ while (i < line.length) {
39
+ if (line[i] !== "`") {
40
+ result += line[i];
41
+ i++;
42
+ continue;
43
+ }
44
+ const openLen = runLengthAt(i);
45
+ const openEnd = i + openLen;
46
+ let j = openEnd;
47
+ let closeEnd = -1;
48
+ while (j < line.length) {
49
+ if (line[j] !== "`") {
50
+ j++;
51
+ continue;
52
+ }
53
+ const runLen = runLengthAt(j);
54
+ if (runLen === openLen) {
55
+ closeEnd = j + runLen;
56
+ break;
57
+ }
58
+ j += runLen;
59
+ }
60
+ if (closeEnd === -1) {
61
+ result += line.slice(i, openEnd);
62
+ i = openEnd;
63
+ } else {
64
+ result += replace(line.slice(i, closeEnd));
65
+ i = closeEnd;
66
+ }
67
+ }
68
+ return result;
23
69
  }
24
70
  var FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})([^\r\n]*)\r?$/;
25
71
  var FENCE_CLOSE = /^ {0,3}(`+|~+)[ \t]*\r?$/;
@@ -53,6 +99,10 @@ function scanFencedCode(body) {
53
99
  });
54
100
  return { lines, unterminated: fenceChar !== null };
55
101
  }
102
+ function hasUnterminatedFence(body) {
103
+ return scanFencedCode(body.replace(/\r\n?/g, `
104
+ `)).unterminated;
105
+ }
56
106
  var LIST_MARKER = /^ {0,3}(?:[-*+]|\d{1,9}[.)])(?:\s|$)/;
57
107
  function indentWidth(line) {
58
108
  let width = 0;
@@ -240,6 +290,42 @@ function matchesLabel(key, name) {
240
290
  function hasLabel(key, names) {
241
291
  return names.some((n) => matchesLabel(key, n));
242
292
  }
293
+
294
+ // ../../packages/aeg-forge-state/src/list-tasks.ts
295
+ var PROJECT_SLUG = /^[a-z0-9][a-z0-9-]*$/i;
296
+ var PROJECT_FIELD = /^\s*(?:\*\*)?Project(?:\(s\))?(?:\*\*)?\s*:\s*(?:\*\*)?\s*(.+)$/im;
297
+ var VALUE_EDGE_MARKUP = /^[`.;\s]+|[`.;\s]+$/g;
298
+ function unwrapValue(raw) {
299
+ return raw.replace(/\*\*/g, "").replace(VALUE_EDGE_MARKUP, "");
300
+ }
301
+ function projectFieldFromBody(body) {
302
+ const m = stripCode(body, { inlineSpans: "keep" }).match(PROJECT_FIELD);
303
+ if (m)
304
+ return parseFieldValue(m[1] ?? "");
305
+ if (hasUnterminatedFence(body)) {
306
+ const raw = body.match(PROJECT_FIELD);
307
+ if (raw)
308
+ return { declared: true, names: [], unparsed: [(raw[1] ?? "").trim()], unreadable: true };
309
+ }
310
+ return { declared: false, names: [], unparsed: [], unreadable: false };
311
+ }
312
+ function parseFieldValue(value) {
313
+ const names = [];
314
+ const unparsed = [];
315
+ for (const raw of value.split(",")) {
316
+ const name = unwrapValue(raw);
317
+ if (name.length === 0)
318
+ continue;
319
+ if (PROJECT_SLUG.test(name))
320
+ names.push(name);
321
+ else
322
+ unparsed.push(raw.trim());
323
+ }
324
+ return { declared: true, names: [...new Set(names)], unparsed, unreadable: false };
325
+ }
326
+ function projectsFromBody(body) {
327
+ return projectFieldFromBody(body).names;
328
+ }
243
329
  // ../../packages/aeg-forge-state/src/github-token.ts
244
330
  import { execFile as execFile2 } from "node:child_process";
245
331
  import { promisify as promisify2 } from "node:util";
@@ -613,9 +699,10 @@ function pointerToPath(p) {
613
699
  }
614
700
  var SEPARATOR = /(?:[ \t]*[—–][ \t]*|[ \t]+-[ \t]+)/.source;
615
701
  // ../../packages/aeg-core/src/pr-tier.ts
702
+ var TIER_FIELD = /(\*\*)?\s*Tier\s*(\*\*)?\s*:\s*(\*\*)?\s*([013])\b/i;
616
703
  function readTierFromPrBody(prBody) {
617
704
  const searchIn = anchoredRegion(prBody, "TIER") ?? prBody;
618
- const m = searchIn.match(/(\*\*)?\s*Tier\s*(\*\*)?\s*:\s*(\*\*)?\s*([013])\b/i);
705
+ const m = searchIn.match(TIER_FIELD);
619
706
  if (!m)
620
707
  return null;
621
708
  const t = Number(m[4]);
@@ -893,6 +980,82 @@ function checkIssueRationale(body) {
893
980
  function isTaskIssueLabelSet(labels) {
894
981
  return hasLabel("tranche", labels);
895
982
  }
983
+ var PATH_TEXT = (body) => stripCode(body, { inlineSpans: "keep" });
984
+ var DOC_EXTENSION_RE = /\.(?:md|mdx|txt)$/i;
985
+ function namesPath(text, path) {
986
+ const escaped = path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
987
+ const occurrences = new RegExp(`(?:^|[^\\w./-])(${escaped}(?:/[\\w.@-]+)*)(?![\\w-])`, "gm");
988
+ for (const m of text.matchAll(occurrences)) {
989
+ if (!DOC_EXTENSION_RE.test(m[1]))
990
+ return true;
991
+ }
992
+ return false;
993
+ }
994
+ var BLAST_RADIUS_ACK_RE = /(?:\*\*)?blast-radius-ack(?:\*\*)?\s*[:—–-]/i;
995
+ function checkBlastRadiusScope(body, _labels, sharedPackages, projectPaths) {
996
+ if (sharedPackages.length === 0)
997
+ return { status: "pass", errors: [] };
998
+ const text = [
999
+ rationaleFieldText(PATH_TEXT(body), "Boundary"),
1000
+ rationaleFieldText(PATH_TEXT(body), "Project\\(s\\)|Project(?:s)?\\s*\\+|blast radius")
1001
+ ].join(`
1002
+ `);
1003
+ const named = sharedPackages.filter((d) => namesPath(text, d));
1004
+ if (named.length === 0)
1005
+ return { status: "pass", errors: [] };
1006
+ const registered = new Set(projectPaths.map((p) => p.name.trim()));
1007
+ const declared = projectsFromBody(body);
1008
+ const projects = declared.filter((p) => registered.has(p));
1009
+ const ownedPaths = projectPaths.filter((p) => projects.includes(p.name)).map((p) => p.path.replace(/\/+$/, ""));
1010
+ const unowned = named.filter((d) => !ownedPaths.some((owned) => d === owned || d.startsWith(`${owned}/`)));
1011
+ if (unowned.length === 0)
1012
+ return { status: "pass", errors: [] };
1013
+ if (projects.length > 1)
1014
+ return { status: "pass", errors: [] };
1015
+ if (BLAST_RADIUS_ACK_RE.test(PATH_TEXT(body)))
1016
+ return { status: "pass", errors: [] };
1017
+ const unregistered = declared.filter((p) => !registered.has(p));
1018
+ return {
1019
+ status: "fail",
1020
+ errors: [
1021
+ `issue-validation blast radius: the rationale names ${unowned.join(", ")} — a shared collision domain no declared project (${projects.join(", ") || "none"}) owns — but declares a single registered project and no \`blast-radius-ack:\` line.${unregistered.length > 0 ? ` Not counted: ${unregistered.join(", ")} — no row in \`.vinaya/projects.md\`, so it adds no review lens.` : ""} Project(s) drives the review fan-out (.vinaya/projects.md); list every consumer in the blast radius, or add \`blast-radius-ack: <why one lens is enough>\`.`
1022
+ ]
1023
+ };
1024
+ }
1025
+ var BRIEF_MARKERS = [
1026
+ { name: "## References", pattern: /(?:^#{1,6}\s*|\*\*)\s*References\b/im },
1027
+ { name: "Technical surface map", pattern: /(?:^#{1,6}\s*|\*\*)\s*Technical surface map\b/im },
1028
+ { name: "Premise", pattern: /(?:^#{1,6}\s*|\*\*)\s*Premise(?:\*\*)?\s*[:—–]/im },
1029
+ { name: "Step 0", pattern: /(?:^#{1,6}\s*|\*\*)\s*Step 0\b/im },
1030
+ { name: "Test Plan", pattern: /(?:^#{1,6}\s*|\*\*)\s*Test Plan\b/im }
1031
+ ];
1032
+ function checkNoBriefContent(body) {
1033
+ const text = stripCode(body);
1034
+ const errors = BRIEF_MARKERS.filter((m) => m.pattern.test(text)).map((m) => `issue-validation brief content: the Issue body carries a brief-shaped "${m.name}" section. Brief-time content (surface pointers, skills-to-read, premise, test plan) belongs in the brief, not the Issue — it goes stale before work starts (aeg-root/skills/brief-authoring/SKILL.md). Move it to the brief.`);
1035
+ return { status: errors.length > 0 ? "fail" : "pass", errors };
1036
+ }
1037
+ function rationaleFieldText(text, labelPattern) {
1038
+ const re = new RegExp(`(?:\\*\\*|^#{1,4}\\s+)\\s*(?:${labelPattern})[^\\n]*\\n?([\\s\\S]*?)(?=\\n\\s*(?:\\*\\*[A-Z]|#{1,4}\\s)|(?![\\s\\S]))`, "im");
1039
+ const m = re.exec(text);
1040
+ return m ? m[0] : "";
1041
+ }
1042
+ var DOC_PATH_RE = /(?:(?:aeg-root|apps|packages|specs|docs|tools|\.claude|\.github)\/[\w./@-]*\.(?:md|mdx)|\.claude\/(?:skills|rules)\/[\w./-]+|\b(?:docs-index|decisions|projects|state-machine|enforcement|process|README|CLAUDE)\.md\b|\b[\w-]+-(?:spec|decisions|backlog)\.md\b)/i;
1043
+ var NO_DOC_SURFACE_RE = /(?:\*\*)?\s*no-doc-surface/i;
1044
+ function checkRationaleNamesDocs(body) {
1045
+ const text = PATH_TEXT(body);
1046
+ const scope = [rationaleFieldText(text, "Docs to keep coherent|§7"), rationaleFieldText(text, "Traps")].join(`
1047
+ `);
1048
+ if (NO_DOC_SURFACE_RE.test(scope))
1049
+ return { status: "pass", errors: [] };
1050
+ if (DOC_PATH_RE.test(scope))
1051
+ return { status: "pass", errors: [] };
1052
+ return {
1053
+ status: "fail",
1054
+ errors: [
1055
+ 'issue-validation docs read: neither "Docs to keep coherent" nor "Traps to avoid" names a concrete doc path (aeg-root/…, .claude/skills/…, apps/*/CLAUDE.md, apps/*/specs/…). Naming one is the artifact of having read the surface being planned — the forge write triggers no skill-check hook, so this field is the only read-obligation signal. For a genuinely doc-less surface, write the explicit `no-doc-surface` sentinel.'
1056
+ ]
1057
+ };
1058
+ }
896
1059
 
897
1060
  // ../../packages/aeg-core/src/coherence-checks.ts
898
1061
  var R1_GRANDFATHERED_ISSUES = new Set([279, 280, 281, 282]);
@@ -1077,6 +1240,7 @@ var CHECKS_WORKFLOW_PATH = ".github/workflows/vinaya-checks.yml";
1077
1240
  var REVIEW_WORKFLOW_PATH = ".github/workflows/vinaya-review.yml";
1078
1241
  var REVIEW_VERDICT_WORKFLOW_PATH = ".github/workflows/vinaya-review-verdict.yml";
1079
1242
  var ARCHIVIST_WORKFLOW_PATH = ".github/workflows/vinaya-archivist.yml";
1243
+ var BODY_CHECKS_WORKFLOW_PATH = ".github/workflows/vinaya-body-checks.yml";
1080
1244
  var MANAGED_NOTE = "Managed by Vinaya — created by `vinaya init`. `vinaya upgrade` regenerates it; `vinaya eject` removes it.";
1081
1245
  function starterConfig() {
1082
1246
  return {
@@ -1103,9 +1267,31 @@ function ownVersion() {
1103
1267
  return pkg.version;
1104
1268
  }
1105
1269
  var SETUP_BUN_SHA = "0c5077e51419868618aeaa5fe8019c62421857d6";
1106
- function vinayaSetupSteps(selfHost) {
1270
+ function vinayaSetupSteps(selfHost, sourceTrust = "pull-request") {
1107
1271
  if (!selfHost)
1108
1272
  return "";
1273
+ if (sourceTrust === "trusted") {
1274
+ return ` # Pinned to a commit, not the mutable \`v2\` tag. A repoint of
1275
+ # \`v2\` would execute new upstream code in every adopter on the next run,
1276
+ # with no diff anywhere to review. Resolved from the \`v2\` tag on
1277
+ # 2026-08-14. Bun's own version still comes from the repo's
1278
+ # \`packageManager\` field, not from this pin.
1279
+ - uses: oven-sh/setup-bun@${SETUP_BUN_SHA}
1280
+ # This repo declares the \`@attalabs/vinaya\` workspace package itself, so
1281
+ # \`npx @attalabs/vinaya\` resolves to that local member instead of the
1282
+ # registry and dies on its unbuilt \`bin\`. Build and run the trusted
1283
+ # default-branch copy directly.
1284
+ #
1285
+ # This workflow executes only a trusted checkout selected by its event:
1286
+ # never the pull request head or merge ref. The review-authority decision
1287
+ # therefore comes from code the pull request cannot alter. \`--ignore-scripts\`
1288
+ # remains defense in depth against lifecycle hooks in that trusted tree.
1289
+ - name: Build the trusted Vinaya CLI
1290
+ run: |
1291
+ bun install --frozen-lockfile --ignore-scripts
1292
+ bun run --cwd ${selfHost.dir} build
1293
+ `;
1294
+ }
1109
1295
  return ` # Pinned to a commit, not the mutable \`v2\` tag. This is the first
1110
1296
  # THIRD-PARTY action this generator writes into an adopter's repository,
1111
1297
  # and it runs in the same job that then builds and executes pull-request
@@ -1209,7 +1395,7 @@ jobs:
1209
1395
  - uses: actions/setup-node@v4
1210
1396
  with:
1211
1397
  node-version: 20
1212
- ${vinayaSetupSteps(selfHost)}${adopterSetupStep(ciSetup)} - name: Run checks
1398
+ ${vinayaSetupSteps(selfHost, "pull-request")}${adopterSetupStep(ciSetup)} - name: Run checks
1213
1399
  env:
1214
1400
  GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
1215
1401
  PR_NUMBER: \${{ github.event.pull_request.number }}
@@ -1243,21 +1429,27 @@ ${vinayaSetupSteps(selfHost)}${adopterSetupStep(ciSetup)} - name: Run check
1243
1429
  } >> "$GITHUB_STEP_SUMMARY"
1244
1430
  `;
1245
1431
  }
1246
- function reviewWorkflow(selfHost, ciSetup) {
1432
+ function reviewWorkflow(selfHost) {
1247
1433
  return `# ${MANAGED_NOTE}
1248
1434
  #
1249
- # The required review gate pull_request events only. The verdict-comment
1435
+ # The required review gate. \`pull_request_target\` is the trust boundary:
1436
+ # GitHub loads this workflow and its default checkout from the repository's
1437
+ # default branch, never from the pull request being judged. The job does not
1438
+ # fetch, check out, install, build, or execute pull-request content.
1439
+ #
1440
+ # The verdict-comment
1250
1441
  # half lives in its own workflow (vinaya-review-verdict.yml): a new PR
1251
- # comment fires a different GitHub event that this pull_request-only
1442
+ # comment fires a different GitHub event that this
1252
1443
  # workflow structurally cannot receive — and keeping the comment path in a
1253
1444
  # separate FILE means this workflow's runs never list permanently-skipped
1254
1445
  # comment jobs on the PR's checks panel. When a clean final verdict lands,
1255
1446
  # the verdict workflow re-runs this one, so the required check below goes
1256
1447
  # green natively with no manual rerun.
1257
1448
  name: Vinaya Review Gate
1449
+ run-name: "Vinaya Review Gate PR #\${{ github.event.pull_request.number }} @ \${{ github.event.pull_request.head.sha }}"
1258
1450
 
1259
1451
  on:
1260
- pull_request:
1452
+ pull_request_target:
1261
1453
  types: [opened, synchronize, reopened, labeled, unlabeled]
1262
1454
 
1263
1455
  # One run per pull request COMMIT, always. Several \`types:\` above can fire in the
@@ -1297,30 +1489,96 @@ jobs:
1297
1489
  steps:
1298
1490
  - uses: actions/checkout@v4
1299
1491
  with:
1300
- # This job executes repo content; don't leave the token in
1301
- # .git/config for scripts to read.
1492
+ # Explicit trusted ref: never use the PR head/merge ref in this job.
1493
+ ref: \${{ github.event.repository.default_branch }}
1302
1494
  persist-credentials: false
1303
1495
  fetch-depth: 0
1304
1496
  - uses: actions/setup-node@v4
1305
1497
  with:
1306
1498
  node-version: 20
1307
- ${vinayaSetupSteps(selfHost)}${adopterSetupStep(ciSetup)} - name: Review gate
1499
+ ${vinayaSetupSteps(selfHost, "trusted")} - name: Review gate
1308
1500
  env:
1309
1501
  GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
1310
1502
  # PR_NUMBER is what makes the review-gate check EVALUATE: without
1311
1503
  # it the adapter reads "no PR yet — local dev" and exits 0, and
1312
1504
  # the gate is green regardless of review state.
1313
1505
  PR_NUMBER: \${{ github.event.pull_request.number }}
1314
- BRANCH: \${{ github.head_ref }}
1315
1506
  run: ${vinayaRun(selfHost, "check review-gate")}
1316
1507
  `;
1317
1508
  }
1318
- function reviewVerdictWorkflow(selfHost, ciSetup) {
1509
+ function bodyChecksWorkflow(selfHost) {
1510
+ return `# ${MANAGED_NOTE}
1511
+ #
1512
+ # Required, PR-content-independent checks that need a trust anchor no pull
1513
+ # request can rewrite. \`pull_request_target\` is the same boundary
1514
+ # \`vinaya-review.yml\` uses: GitHub loads this workflow and its default
1515
+ # checkout from the repository's default branch, never from the pull
1516
+ # request being judged. The job does not fetch, check out, install, build,
1517
+ # or execute pull-request content — every check registered here reads only
1518
+ # live-fetched PR metadata (\`gh pr view\`), never the diff or repo tree, the
1519
+ # same shape \`review-gate\` already requires of its own checks.
1520
+ #
1521
+ # \`body-bare-digits\` is the one check here today: its Changesets-release
1522
+ # exemption live-fetches the PR's real author, keyed on \`PR_NUMBER\` — on a
1523
+ # \`pull_request\` trigger that value is PR-editable (the PR's own workflow
1524
+ # YAML controls it), so an attacker could redirect it to any
1525
+ # already-approved PR by the configured release actor. Found live (round 5,
1526
+ # security review, PR #165), and verified no env-var or git-state signal
1527
+ # inside a \`pull_request\` job closes it — \`pull_request_target\` does,
1528
+ # because the workflow text assigning \`PR_NUMBER\` comes from THIS file on
1529
+ # the default branch, which a pull request cannot edit.
1530
+ name: Vinaya Body Checks
1531
+ run-name: "Vinaya Body Checks PR #\${{ github.event.pull_request.number }} @ \${{ github.event.pull_request.head.sha }}"
1532
+
1533
+ on:
1534
+ pull_request_target:
1535
+ types: [opened, synchronize, reopened, edited]
1536
+
1537
+ # Same collapsing rationale as \`vinaya-review.yml\`'s concurrency group — see
1538
+ # that file's own comment for the two measured failure modes (duplicate
1539
+ # runs from simultaneous event types; an old rerun cancelling the current
1540
+ # commit's run) this group exists to prevent.
1541
+ concurrency:
1542
+ group: vinaya-body-checks-\${{ github.event.pull_request.number || github.ref }}-\${{ github.event.pull_request.head.sha || github.sha }}
1543
+ cancel-in-progress: true
1544
+
1545
+ jobs:
1546
+ vinaya-body-checks:
1547
+ name: vinaya check body-bare-digits
1548
+ runs-on: ubuntu-latest
1549
+ permissions:
1550
+ contents: read
1551
+ pull-requests: read
1552
+ steps:
1553
+ - uses: actions/checkout@v4
1554
+ with:
1555
+ # Explicit trusted ref: never use the PR head/merge ref in this job.
1556
+ ref: \${{ github.event.repository.default_branch }}
1557
+ persist-credentials: false
1558
+ fetch-depth: 0
1559
+ - uses: actions/setup-node@v4
1560
+ with:
1561
+ node-version: 20
1562
+ ${vinayaSetupSteps(selfHost, "trusted")} - name: Body checks
1563
+ env:
1564
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
1565
+ # PR_NUMBER is what makes the Changesets-release exemption
1566
+ # EVALUATE its live gh pr view fetch: without it the check reads
1567
+ # "no PR yet — local dev" and falls through to the ordinary
1568
+ # bare-digit scan.
1569
+ PR_NUMBER: \${{ github.event.pull_request.number }}
1570
+ # PR_BODY is what makes body-bare-digits EVALUATE at all — the bin
1571
+ # reads \`process.env.PR_BODY\` only, never fetches it itself.
1572
+ PR_BODY: \${{ github.event.pull_request.body }}
1573
+ run: ${vinayaRun(selfHost, "check body-bare-digits")}
1574
+ `;
1575
+ }
1576
+ function reviewVerdictWorkflow(selfHost) {
1319
1577
  return `# ${MANAGED_NOTE}
1320
1578
  #
1321
1579
  # The verdict-comment half of the review gate. A reviewer's verdict arrives
1322
1580
  # as a PR comment (\`VERDICT: APPROVE\` / \`VERDICT: PASS\`), which fires
1323
- # GitHub's \`issue_comment\` event — an event the required pull_request
1581
+ # GitHub's \`issue_comment\` event — an event the required PR-event
1324
1582
  # workflow cannot receive. This workflow evaluates the gate on that comment
1325
1583
  # and, when the evaluation is clean, RE-RUNS the required workflow so its
1326
1584
  # check goes green natively with no manual rerun. (Writing check-run
@@ -1328,11 +1586,10 @@ function reviewVerdictWorkflow(selfHost, ciSetup) {
1328
1586
  # restricts check-run updates to the owning workflow — re-running is the
1329
1587
  # supported channel.)
1330
1588
  #
1331
- # Privilege split, deliberate: \`evaluate\` checks out and executes repo
1332
- # content and therefore holds NO write permission; \`retrigger\` holds
1333
- # \`actions: write\` but checks out and executes nothing its only inputs
1334
- # are the evaluator's outputs, resolved via \`gh pr view\` before any repo
1335
- # content ran. A malicious branch can at worst fail its own evaluation.
1589
+ # Privilege split, deliberate: \`evaluate\` executes only the default branch's
1590
+ # trusted gate code and holds NO write permission; \`retrigger\` holds
1591
+ # \`actions: write\` but checks out and executes nothing. Neither job checks
1592
+ # out or executes pull-request content.
1336
1593
  name: Vinaya Review Gate (on verdict)
1337
1594
 
1338
1595
  on:
@@ -1349,38 +1606,36 @@ jobs:
1349
1606
  pull-requests: read
1350
1607
  issues: read
1351
1608
  outputs:
1352
- branch: \${{ steps.pr.outputs.branch }}
1609
+ number: \${{ steps.pr.outputs.number }}
1353
1610
  sha: \${{ steps.pr.outputs.sha }}
1354
1611
  steps:
1355
- # issue_comment payloads carry no PR head SHA/branch — resolve them
1356
- # before checkout, and check out that exact commit (the event's default
1357
- # ref is the repo's default branch, not the PR head).
1612
+ # issue_comment payloads carry no PR head SHA — resolve it for immutable
1613
+ # run identity only. It never selects executable code.
1358
1614
  - name: Resolve PR head
1359
1615
  id: pr
1360
1616
  env:
1361
1617
  GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
1362
1618
  run: |
1363
1619
  NUMBER="\${{ github.event.issue.number }}"
1364
- BRANCH=$(gh pr view "$NUMBER" --repo "\${{ github.repository }}" --json headRefName -q .headRefName)
1365
1620
  SHA=$(gh pr view "$NUMBER" --repo "\${{ github.repository }}" --json headRefOid -q .headRefOid)
1366
1621
  echo "number=$NUMBER" >> "$GITHUB_OUTPUT"
1367
- echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
1368
1622
  echo "sha=$SHA" >> "$GITHUB_OUTPUT"
1369
1623
  - uses: actions/checkout@v4
1370
1624
  with:
1371
- ref: \${{ steps.pr.outputs.sha }}
1625
+ # issue_comment already sources workflows from the default branch;
1626
+ # make the trusted code checkout explicit too.
1627
+ ref: \${{ github.event.repository.default_branch }}
1372
1628
  persist-credentials: false
1373
1629
  fetch-depth: 0
1374
1630
  - uses: actions/setup-node@v4
1375
1631
  with:
1376
1632
  node-version: 20
1377
- ${vinayaSetupSteps(selfHost)}${adopterSetupStep(ciSetup)} - name: Review gate (verdict evaluation)
1633
+ ${vinayaSetupSteps(selfHost, "trusted")} - name: Review gate (verdict evaluation)
1378
1634
  env:
1379
1635
  GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
1380
1636
  # Same wiring as the required workflow: PR_NUMBER is what makes
1381
1637
  # the adapter evaluate instead of no-op'ing as "local dev".
1382
1638
  PR_NUMBER: \${{ steps.pr.outputs.number }}
1383
- BRANCH: \${{ steps.pr.outputs.branch }}
1384
1639
  run: ${vinayaRun(selfHost, "check review-gate")}
1385
1640
 
1386
1641
  # Executes nothing; consumes only the evaluator's outputs.
@@ -1401,16 +1656,11 @@ ${vinayaSetupSteps(selfHost)}${adopterSetupStep(ciSetup)} - name: Review ga
1401
1656
  - name: Re-run the required review gate for this branch
1402
1657
  env:
1403
1658
  GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
1404
- BRANCH: \${{ needs.evaluate.outputs.branch }}
1659
+ PR_NUMBER: \${{ needs.evaluate.outputs.number }}
1405
1660
  HEAD_SHA: \${{ needs.evaluate.outputs.sha }}
1406
1661
  run: |
1407
- # \`--branch ""\` is NOT a no-op: gh drops the filter and lists runs
1408
- # across every branch, so the rerun would land on an unrelated PR's
1409
- # gate. Reachable now that this job runs on a failed evaluation —
1410
- # \`Resolve PR head\` is the only producer of this output, and its
1411
- # failure used to be unreachable from here.
1412
- if [ -z "$BRANCH" ]; then
1413
- echo "No branch resolved (the evaluate job failed before resolving it) - nothing to re-run."
1662
+ if [ -z "$PR_NUMBER" ]; then
1663
+ echo "No PR number resolved (the evaluate job failed before resolving it) - nothing to re-run."
1414
1664
  exit 0
1415
1665
  fi
1416
1666
  # Same guard, same producer: an empty SHA would make the selection
@@ -1420,43 +1670,28 @@ ${vinayaSetupSteps(selfHost)}${adopterSetupStep(ciSetup)} - name: Review ga
1420
1670
  echo "No head SHA resolved (the evaluate job failed before resolving it) - nothing to re-run."
1421
1671
  exit 0
1422
1672
  fi
1423
- # Select by HEAD SHA, never by recency. "Newest completed" looks
1424
- # right and is not: \`--status completed\` excludes a run that is
1425
- # currently re-running but INCLUDES cancelled ones, so a verdict
1426
- # arriving while the current head's run is still in flight can
1427
- # select a stale cancelled sibling, rerun it, and — because the
1428
- # review workflow's concurrency group is keyed per pull request —
1429
- # cancel the live evaluation it was supposed to refresh.
1430
- #
1431
- # The run for this head SHA is the only one whose conclusion gates
1432
- # the merge; runs for earlier commits are scoped to those commits
1433
- # and never gate the current head. Cancelled runs are excluded for
1434
- # the same reason, not a mechanical one — a rerun with nothing else
1435
- # in its group completes normally. A cancellation here means the
1436
- # \`opened\`+\`labeled\` collapse discarded that run in favour of a
1437
- # sibling on the same commit, and it is the survivor whose
1438
- # conclusion the merge box reads.
1439
- # \`--commit\` filters server-side, deliberately. gh's \`--jq\` is a
1440
- # single-expression flag of its own, NOT a passthrough to jq, so jq's
1441
- # variable-binding flag cannot be used with it: pflag swallows that
1442
- # flag as the expression and gh exits 1 on the stray token — which
1443
- # under \`bash -e\` kills the step before the empty-RUN_ID no-op can
1444
- # run, so every verdict reddens and nothing reruns. Filter on the
1445
- # server and keep the jq expression free of variables.
1446
- RUN_ID=$(gh run list --repo "\${{ github.repository }}" \\
1447
- --workflow vinaya-review.yml --branch "$BRANCH" \\
1448
- --commit "$HEAD_SHA" \\
1449
- --json databaseId,event,status,conclusion \\
1450
- --jq '[.[]
1451
- | select(.event=="pull_request")
1452
- | select(.status=="completed")
1453
- | select(.conclusion!="cancelled")]
1454
- | .[0].databaseId // empty')
1673
+ # pull_request_target runs execute at the DEFAULT branch SHA, so a
1674
+ # run's head_sha cannot identify the PR commit. GitHub's nested
1675
+ # pull_requests snapshot is also not immutable: an old rerun can
1676
+ # expose the PR's current head. The required workflow therefore
1677
+ # records PR number + head SHA in run-name at creation, and this
1678
+ # query matches that immutable display_title exactly.
1679
+ set -o pipefail
1680
+ RUN_TITLE="Vinaya Review Gate PR #$PR_NUMBER @ $HEAD_SHA"
1681
+ RUN_ID=$(gh api --paginate \\
1682
+ "repos/\${{ github.repository }}/actions/workflows/vinaya-review.yml/runs?event=pull_request_target&per_page=100" \\
1683
+ | jq -sr --arg title "$RUN_TITLE" '
1684
+ [.[].workflow_runs[]
1685
+ | select(.display_title == $title)
1686
+ | select(.event == "pull_request_target")
1687
+ | select(.status == "completed")
1688
+ | select(.conclusion != "cancelled")
1689
+ | .id][0] // empty')
1455
1690
  if [ -z "$RUN_ID" ]; then
1456
- echo "No completed, non-cancelled pull_request run of vinaya-review.yml for $HEAD_SHA - nothing to re-run."
1691
+ echo "No completed, non-cancelled pull_request_target run of vinaya-review.yml for PR #$PR_NUMBER at $HEAD_SHA - nothing to re-run."
1457
1692
  exit 0
1458
1693
  fi
1459
- echo "Re-running vinaya-review.yml run $RUN_ID for $HEAD_SHA"
1694
+ echo "Re-running vinaya-review.yml run $RUN_ID for PR #$PR_NUMBER at $HEAD_SHA"
1460
1695
  # Two verdict comments landing together run two retriggers in
1461
1696
  # parallel (this workflow has no concurrency group, deliberately —
1462
1697
  # a queued retrigger is a delayed one). Both can select the same
@@ -1697,6 +1932,13 @@ var BRANCH_PROTECTION_NOTE = `Recommended (run yourself — vinaya never applies
1697
1932
  -F required_status_checks.strict=true \\
1698
1933
  -F 'required_status_checks.contexts[]=vinaya-checks' \\
1699
1934
  -F enforce_admins=true -F restrictions=`;
1935
+ var CODEOWNERS_NOTE = `Recommended: require review on vinaya's own workflow files.
1936
+
1937
+ echo '/.github/workflows/** @your-github-login' >> .github/CODEOWNERS
1938
+
1939
+ Then add this flag to the branch-protection command above:
1940
+
1941
+ -F required_pull_request_reviews.require_code_owner_reviews=true`;
1700
1942
  function buildInitOps(ctx) {
1701
1943
  const ops = [];
1702
1944
  const hookMode = 493;
@@ -1709,13 +1951,13 @@ function buildInitOps(ctx) {
1709
1951
  ops.push({
1710
1952
  kind: "create-file",
1711
1953
  path: REVIEW_WORKFLOW_PATH,
1712
- content: reviewWorkflow(ctx.selfHost, ctx.ciSetup),
1954
+ content: reviewWorkflow(ctx.selfHost),
1713
1955
  group: "CI workflows"
1714
1956
  });
1715
1957
  ops.push({
1716
1958
  kind: "create-file",
1717
1959
  path: REVIEW_VERDICT_WORKFLOW_PATH,
1718
- content: reviewVerdictWorkflow(ctx.selfHost, ctx.ciSetup),
1960
+ content: reviewVerdictWorkflow(ctx.selfHost),
1719
1961
  group: "CI workflows"
1720
1962
  });
1721
1963
  ops.push({
@@ -1724,6 +1966,12 @@ function buildInitOps(ctx) {
1724
1966
  content: archivistWorkflow(ctx.selfHost),
1725
1967
  group: "CI workflows"
1726
1968
  });
1969
+ ops.push({
1970
+ kind: "create-file",
1971
+ path: BODY_CHECKS_WORKFLOW_PATH,
1972
+ content: bodyChecksWorkflow(ctx.selfHost),
1973
+ group: "CI workflows"
1974
+ });
1727
1975
  ops.push({
1728
1976
  kind: "managed-block",
1729
1977
  path: `${ctx.hookDir}/pre-commit`,
@@ -1774,6 +2022,7 @@ function buildInitOps(ctx) {
1774
2022
  });
1775
2023
  ops.push(...labelOps());
1776
2024
  ops.push({ kind: "print", message: BRANCH_PROTECTION_NOTE, group: "Branch protection (printed, never applied)" });
2025
+ ops.push({ kind: "print", message: CODEOWNERS_NOTE, group: "Branch protection (printed, never applied)" });
1777
2026
  return ops;
1778
2027
  }
1779
2028
  function buildInitProductOps(name) {
@@ -1923,6 +2172,7 @@ var VinayaConfigSchema = z.object({
1923
2172
  briefSchema: BriefSchemaSchema.optional(),
1924
2173
  managed: ManagedManifestSchema.optional(),
1925
2174
  principals: z.array(z.string()).min(1).optional(),
2175
+ releaseActor: z.string().min(1).optional(),
1926
2176
  ci: z.object({ setup: z.string().min(1) }).optional(),
1927
2177
  blastRadius: z.object({ extraDomains: z.array(z.string()).optional() }).optional()
1928
2178
  });
@@ -1957,6 +2207,9 @@ function globalChecksIgnoredWarning(path) {
1957
2207
  function globalPrincipalsIgnoredWarning(path) {
1958
2208
  return `${path}: "principals" in the global config is ignored — principals may only be declared from a repo-local vinaya.config.json.`;
1959
2209
  }
2210
+ function globalReleaseActorIgnoredWarning(path) {
2211
+ return `${path}: "releaseActor" in the global config is ignored — releaseActor may only be declared from a repo-local vinaya.config.json.`;
2212
+ }
1960
2213
  function stripGlobalOnlyKeys(config, path) {
1961
2214
  if (path !== GLOBAL_CONFIG_PATH)
1962
2215
  return config;
@@ -1969,6 +2222,10 @@ function stripGlobalOnlyKeys(config, path) {
1969
2222
  console.error(`⚠ ${globalPrincipalsIgnoredWarning(path)}`);
1970
2223
  result = { ...result, principals: undefined };
1971
2224
  }
2225
+ if (result.releaseActor) {
2226
+ console.error(`⚠ ${globalReleaseActorIgnoredWarning(path)}`);
2227
+ result = { ...result, releaseActor: undefined };
2228
+ }
1972
2229
  return result;
1973
2230
  }
1974
2231
  function resolvePrincipalAllowlist(config) {
@@ -2016,7 +2273,7 @@ function isMissingFileError(err) {
2016
2273
  return /\b404\b|not found/i.test(haystack);
2017
2274
  }
2018
2275
  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.
2276
+ const warn = (why) => process.stdout.write(`⚠ could not read the trust-anchor config (\`principals\`/\`releaseActor\`) from the default branch (${why}) — falling back to vinaya's built-in defaults.
2020
2277
  `);
2021
2278
  let base64;
2022
2279
  try {
@@ -3244,6 +3501,21 @@ function coreCheckRegistry() {
3244
3501
  BRANCH: { optional: true }
3245
3502
  }
3246
3503
  },
3504
+ {
3505
+ name: "body-bare-digits",
3506
+ run: bin("check-body-bare-digits"),
3507
+ scope: "diff",
3508
+ timeoutMs: 15000,
3509
+ requiresOpenPr: true,
3510
+ ownWorkflow: true,
3511
+ env: {
3512
+ PR_BODY: { optional: true },
3513
+ PR_NUMBER: { optional: true },
3514
+ GITHUB_REPOSITORY: { optional: true },
3515
+ GITHUB_TOKEN: { optional: true },
3516
+ GH_TOKEN: { optional: true }
3517
+ }
3518
+ },
3247
3519
  {
3248
3520
  name: "no-disk-state",
3249
3521
  run: bin("check-no-disk-state"),
@@ -3269,7 +3541,6 @@ function coreCheckRegistry() {
3269
3541
  timeoutMs: 30000,
3270
3542
  ownWorkflow: true,
3271
3543
  env: {
3272
- BRANCH: { optional: true },
3273
3544
  PR_NUMBER: { optional: true },
3274
3545
  GITHUB_REPOSITORY: { optional: true },
3275
3546
  GITHUB_TOKEN: { optional: true },
@@ -3981,12 +4252,12 @@ Test Plan: unit-tests-only
3981
4252
  ## Stop Conditions
3982
4253
  Autonomy: Do not stop to ask clarifying questions — this is a scripted demo; proceed and clean up automatically.
3983
4254
 
3984
- Illustrative Step 0 (never actually run by this demo):
4255
+ Illustrative \`Step 0\` (never actually run by this demo):
3985
4256
  \`\`\`
3986
4257
  git worktree add .worktrees/demo-fixture -b demo/fixture origin/main
3987
4258
  \`\`\`
3988
4259
 
3989
- Closes #0 (fixture value — no real Issue; discarded with the demo branch)
4260
+ Closes \`#0\` (fixture value — no real Issue; discarded with the demo branch)
3990
4261
  `;
3991
4262
  }
3992
4263
  function cleanup(repoRoot, originalBranch, demoBranch, statePath) {
@@ -4524,6 +4795,25 @@ async function diagnoseBranchProtection(deps, owner, repo) {
4524
4795
  }
4525
4796
  return info("branch-protection", "main branch protection could not be determined (no gh auth, no remote, or a permission gap).");
4526
4797
  }
4798
+ function coversWorkflowsDir(pattern) {
4799
+ const bare = pattern.replace(/\*+$/, "").replace(/\/+$/, "");
4800
+ return bare === ".github/workflows" || bare === "/.github/workflows";
4801
+ }
4802
+ function diagnoseCodeowners(repoRoot) {
4803
+ const path = join9(repoRoot, ".github", "CODEOWNERS");
4804
+ if (!existsSync8(path)) {
4805
+ return info("codeowners", "no .github/CODEOWNERS — vinaya's workflow files have no required-review protection; see `vinaya init`'s printed recommendation.");
4806
+ }
4807
+ let body;
4808
+ try {
4809
+ body = readFileSync8(path, "utf-8");
4810
+ } catch (err) {
4811
+ return warn("codeowners", `.github/CODEOWNERS exists but could not be read: ${err.message}`);
4812
+ }
4813
+ const covered = body.split(`
4814
+ `).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).some((line) => coversWorkflowsDir(line.split(/\s+/)[0] ?? ""));
4815
+ return covered ? info("codeowners", ".github/CODEOWNERS covers .github/workflows/**.") : warn("codeowners", ".github/CODEOWNERS exists but has no entry covering .github/workflows/** — workflow file edits can merge unreviewed.");
4816
+ }
4527
4817
  var TEST_INVOCATION_SUBSTRINGS = ["npm test", "npm run test", "bun test", "bunx turbo test"];
4528
4818
  function diagnoseTestCi(repoRoot) {
4529
4819
  let pkg;
@@ -4608,6 +4898,7 @@ async function runDoctor(args, deps) {
4608
4898
  findings.push(...diagnoseGlobalConfigChecks());
4609
4899
  findings.push(...await diagnoseEnvironment(deps, hasDrift));
4610
4900
  findings.push(await diagnoseBranchProtection(deps, repo.owner, repo.repo));
4901
+ findings.push(diagnoseCodeowners(repo.repoRoot));
4611
4902
  findings.push(...diagnoseTestCi(repo.repoRoot));
4612
4903
  const healthy = findings.every((f) => f.severity === "ok" || f.severity === "info");
4613
4904
  if (jsonOutput) {
@@ -4622,8 +4913,9 @@ async function doctorCommand(args) {
4622
4913
  }
4623
4914
 
4624
4915
  // src/commands/doctrine.ts
4625
- import { existsSync as existsSync9, readdirSync as readdirSync4 } from "node:fs";
4916
+ import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync9 } from "node:fs";
4626
4917
  import { dirname as dirname4, join as join10, sep as sep3 } from "node:path";
4918
+ import matter2 from "gray-matter";
4627
4919
  var ENTRY_SEGMENTS = ["skills", "aeg", "SKILL.md"];
4628
4920
  function resolveDoctrineRoot(pkg = packageRoot(import.meta.url)) {
4629
4921
  const candidates = [join10(pkg, "aeg-root")];
@@ -4639,7 +4931,10 @@ function listRoleNames(root) {
4639
4931
  const rolesDir = join10(root, "roles");
4640
4932
  if (!existsSync9(rolesDir))
4641
4933
  return [];
4642
- return readdirSync4(rolesDir).filter((name) => name.endsWith(".md")).map((name) => name.slice(0, -".md".length)).sort();
4934
+ return readdirSync4(rolesDir).filter((name) => name.endsWith(".md")).map((name) => name.slice(0, -".md".length)).filter((roleName) => {
4935
+ const { data } = matter2(readFileSync9(join10(rolesDir, `${roleName}.md`), "utf8"));
4936
+ return data.actor !== "human";
4937
+ }).sort();
4643
4938
  }
4644
4939
  function doctrineCommand(args) {
4645
4940
  const root = resolveDoctrineRoot();
@@ -4676,7 +4971,7 @@ function doctrineCommand(args) {
4676
4971
  }
4677
4972
 
4678
4973
  // src/commands/eject.ts
4679
- import { existsSync as existsSync10, readFileSync as readFileSync9 } from "node:fs";
4974
+ import { existsSync as existsSync10, readFileSync as readFileSync10 } from "node:fs";
4680
4975
  import { join as join11 } from "node:path";
4681
4976
  function realDeps4() {
4682
4977
  return {
@@ -4699,7 +4994,7 @@ function readManifest(repoRoot) {
4699
4994
  return { kind: "none" };
4700
4995
  let raw;
4701
4996
  try {
4702
- raw = JSON.parse(readFileSync9(p, "utf-8"));
4997
+ raw = JSON.parse(readFileSync10(p, "utf-8"));
4703
4998
  } catch (err) {
4704
4999
  return { kind: "orphan", reason: `vinaya.config.json is not valid JSON: ${err.message}` };
4705
5000
  }
@@ -4789,11 +5084,11 @@ async function ejectCommand(args) {
4789
5084
  }
4790
5085
 
4791
5086
  // src/commands/init.ts
4792
- import { existsSync as existsSync12, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "node:fs";
5087
+ import { existsSync as existsSync12, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "node:fs";
4793
5088
  import { join as join13 } from "node:path";
4794
5089
 
4795
5090
  // src/lib/registry-write.ts
4796
- import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync10, writeFileSync as writeFileSync4 } from "node:fs";
5091
+ import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync4 } from "node:fs";
4797
5092
  import { dirname as dirname5, join as join12 } from "node:path";
4798
5093
  var PROJECTS_REGISTRY_PATH = ".vinaya/projects.md";
4799
5094
  function rowLine(name, path, specsPath) {
@@ -4864,7 +5159,7 @@ function planRegistryRow(repoRoot, name, path, specsPath) {
4864
5159
  if (!existsSync11(abs2)) {
4865
5160
  return { action: "create-host", rowLine: line, path: PROJECTS_REGISTRY_PATH };
4866
5161
  }
4867
- const existing = readFileSync10(abs2, "utf-8");
5162
+ const existing = readFileSync11(abs2, "utf-8");
4868
5163
  const already = parseRegistry(existing).some((p) => p.name === name);
4869
5164
  return { action: already ? "skip-present" : "append-row", rowLine: line, path: PROJECTS_REGISTRY_PATH };
4870
5165
  }
@@ -4874,7 +5169,7 @@ function applyRegistryRow(repoRoot, plan, name, path, specsPath) {
4874
5169
  mkdirSync3(dirname5(abs2), { recursive: true });
4875
5170
  writeFileSync4(abs2, freshProjectsRegistry(name, path, specsPath), "utf-8");
4876
5171
  } else if (plan.action === "append-row") {
4877
- const existing = readFileSync10(abs2, "utf-8");
5172
+ const existing = readFileSync11(abs2, "utf-8");
4878
5173
  writeFileSync4(abs2, appendRegistryRow(existing, name, path, specsPath), "utf-8");
4879
5174
  }
4880
5175
  }
@@ -4913,14 +5208,14 @@ function readManifest2(repoRoot) {
4913
5208
  if (!existsSync12(p))
4914
5209
  return null;
4915
5210
  try {
4916
- return VinayaConfigSchema.parse(JSON.parse(readFileSync11(p, "utf-8"))).managed ?? null;
5211
+ return VinayaConfigSchema.parse(JSON.parse(readFileSync12(p, "utf-8"))).managed ?? null;
4917
5212
  } catch {
4918
5213
  return null;
4919
5214
  }
4920
5215
  }
4921
5216
  function writeManifest(repoRoot, manifest) {
4922
5217
  const configAbs = join13(repoRoot, CONFIG_PATH);
4923
- const seed = JSON.parse(readFileSync11(configAbs, "utf-8"));
5218
+ const seed = JSON.parse(readFileSync12(configAbs, "utf-8"));
4924
5219
  writeFileSync5(configAbs, `${JSON.stringify({ ...seed, managed: manifest }, null, 2)}
4925
5220
  `, "utf-8");
4926
5221
  }
@@ -5082,11 +5377,12 @@ async function initProductCommand(args) {
5082
5377
  }
5083
5378
 
5084
5379
  // src/commands/issue.ts
5085
- import { execFileSync as execFileSync9 } from "node:child_process";
5380
+ import { execFileSync as execFileSync10 } from "node:child_process";
5086
5381
 
5087
5382
  // src/lib/forge-write.ts
5088
- import { mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
5089
- import { readFileSync as readFileSync12 } from "node:fs";
5383
+ import { execFileSync as execFileSync9 } from "node:child_process";
5384
+ import { existsSync as existsSync13, mkdtempSync, readdirSync as readdirSync5, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
5385
+ import { readFileSync as readFileSync13 } from "node:fs";
5090
5386
  import { tmpdir } from "node:os";
5091
5387
  import { join as join14 } from "node:path";
5092
5388
  class ForgeArgError extends Error {
@@ -5098,13 +5394,13 @@ function locateBody(args) {
5098
5394
  const p = args[i + 1];
5099
5395
  if (!p)
5100
5396
  throw new ForgeArgError("`--body-file` was given with no path.");
5101
- return { body: readFileSync12(p, "utf8"), source: { kind: "file", argIndex: i + 1, inlineForm: false } };
5397
+ return { body: readFileSync13(p, "utf8"), source: { kind: "file", argIndex: i + 1, inlineForm: false } };
5102
5398
  }
5103
5399
  if (a.startsWith("--body-file=")) {
5104
5400
  const p = a.slice("--body-file=".length);
5105
5401
  if (!p)
5106
5402
  throw new ForgeArgError("`--body-file=` was given with no path.");
5107
- return { body: readFileSync12(p, "utf8"), source: { kind: "file", argIndex: i, inlineForm: true } };
5403
+ return { body: readFileSync13(p, "utf8"), source: { kind: "file", argIndex: i, inlineForm: true } };
5108
5404
  }
5109
5405
  if (a === "--body" || a === "-b") {
5110
5406
  const v = args[i + 1];
@@ -5288,6 +5584,86 @@ function validateForgeWrite(input) {
5288
5584
  }
5289
5585
  return errors;
5290
5586
  }
5587
+ function git2(args) {
5588
+ try {
5589
+ return execFileSync9("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
5590
+ } catch {
5591
+ return "";
5592
+ }
5593
+ }
5594
+ function repoRoot() {
5595
+ return git2(["rev-parse", "--show-toplevel"]);
5596
+ }
5597
+ function readWorkspaceGlobs(root) {
5598
+ const fromPackageJson = () => {
5599
+ try {
5600
+ const pkg = JSON.parse(readFileSync13(join14(root, "package.json"), "utf8"));
5601
+ return Array.isArray(pkg.workspaces) ? pkg.workspaces.filter((w) => typeof w === "string") : [];
5602
+ } catch {
5603
+ return [];
5604
+ }
5605
+ };
5606
+ const fromPnpmWorkspaceYaml = () => {
5607
+ try {
5608
+ return parsePnpmWorkspaceYaml(readFileSync13(join14(root, "pnpm-workspace.yaml"), "utf8"));
5609
+ } catch {
5610
+ return [];
5611
+ }
5612
+ };
5613
+ return [...fromPackageJson(), ...fromPnpmWorkspaceYaml()];
5614
+ }
5615
+ function listWorkspaceChildDirs(dir, root) {
5616
+ try {
5617
+ return readdirSync5(join14(root, dir), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
5618
+ } catch {
5619
+ return [];
5620
+ }
5621
+ }
5622
+ function readConfigExtraDomains(root) {
5623
+ try {
5624
+ const parsed = VinayaConfigSchema.safeParse(JSON.parse(readFileSync13(join14(root, "vinaya.config.json"), "utf8")));
5625
+ return parsed.success ? parsed.data.blastRadius?.extraDomains ?? [] : [];
5626
+ } catch {
5627
+ return [];
5628
+ }
5629
+ }
5630
+ function readSharedPackages(root = repoRoot()) {
5631
+ if (!root)
5632
+ return [];
5633
+ const derived = deriveWorkspacePackageDomains(readWorkspaceGlobs(root), (dir) => listWorkspaceChildDirs(dir, root));
5634
+ const defaults = deriveBuiltinCrossCuttingDefaults((p) => existsSync13(join14(root, p)));
5635
+ const configExtra = readConfigExtraDomains(root);
5636
+ return [...new Set([...derived, ...defaults, ...configExtra])];
5637
+ }
5638
+ function readProjectPaths(root = repoRoot()) {
5639
+ if (!root)
5640
+ return [];
5641
+ try {
5642
+ return parseRegistry(readFileSync13(join14(root, ".vinaya/projects.md"), "utf8"));
5643
+ } catch {
5644
+ return [];
5645
+ }
5646
+ }
5647
+ var CHECK_ISSUE_CONTENT = "issue-content";
5648
+ var ISSUE_CONTENT_RECOVERY = {
5649
+ blastRadius: "Add a second registered `Project(s)` this task also touches, or a `blast-radius-ack: <why one lens is enough>` line, then re-run `{cmd}`.",
5650
+ noBriefContent: "Move the brief-shaped section named above out of the Issue body and into the brief — the Issue carries the Planner's durable rationale, not the brief's just-in-time surface — then re-run `{cmd}`.",
5651
+ rationaleNamesDocs: 'Name a concrete doc/skill path (e.g. `aeg-root/…`, `.claude/skills/…/SKILL.md`) in "Docs to keep coherent" or "Traps", or write the `no-doc-surface` sentinel if the surface genuinely has none, then re-run `{cmd}`.'
5652
+ };
5653
+ function validateIssueContent(input) {
5654
+ const findings = [
5655
+ [checkBlastRadiusScope(input.body, input.labels, input.sharedPackages, input.projectPaths).errors, "blastRadius"],
5656
+ [checkNoBriefContent(input.body).errors, "noBriefContent"],
5657
+ [checkRationaleNamesDocs(input.body).errors, "rationaleNamesDocs"]
5658
+ ];
5659
+ const errors = [];
5660
+ for (const [messages, kind] of findings) {
5661
+ const recovery = ISSUE_CONTENT_RECOVERY[kind].replace("{cmd}", input.retryCommand);
5662
+ for (const message of messages)
5663
+ errors.push(makeCheckError(CHECK_ISSUE_CONTENT, message, recovery));
5664
+ }
5665
+ return errors;
5666
+ }
5291
5667
 
5292
5668
  // src/commands/issue.ts
5293
5669
  var RETRY_CREATE = "vinaya issue create --validate-only …";
@@ -5305,7 +5681,7 @@ function locateBodyOrRefuse(ghArgs, retryCommand) {
5305
5681
  function fetchForgeLabels(issueRef) {
5306
5682
  let out;
5307
5683
  try {
5308
- out = execFileSync9("gh", ["issue", "view", issueRef, "--json", "labels"], {
5684
+ out = execFileSync10("gh", ["issue", "view", issueRef, "--json", "labels"], {
5309
5685
  encoding: "utf8",
5310
5686
  stdio: ["ignore", "pipe", "pipe"]
5311
5687
  });
@@ -5333,7 +5709,7 @@ function reportPass(json, command) {
5333
5709
  function runGhWrite(ghCmd, ghArgs, bodyResult, json) {
5334
5710
  const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
5335
5711
  try {
5336
- const out = execFileSync9("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
5712
+ const out = execFileSync10("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
5337
5713
  const url = out.trim();
5338
5714
  if (json)
5339
5715
  printJson({ validated: true, written: true, url });
@@ -5344,22 +5720,31 @@ function runGhWrite(ghCmd, ghArgs, bodyResult, json) {
5344
5720
  cleanup2();
5345
5721
  }
5346
5722
  }
5347
- function validateTaskIssue(body, title, retryCommand) {
5723
+ function validateTaskIssue(body, title, labels, retryCommand) {
5348
5724
  if (body === null) {
5349
5725
  refuse([
5350
5726
  makeCheckError("forge-args", "A task Issue (a `vinaya/tranche:*` label) requires a `--body-file <path>` so the rationale gate can validate it.", `Add \`--body-file <path>\`, then re-run \`${retryCommand}\`.`)
5351
5727
  ]);
5352
5728
  }
5353
5729
  const sections = resolveSections("issue", retryCommand);
5354
- const errors = validateForgeWrite({
5730
+ const schemaErrors = validateForgeWrite({
5355
5731
  body,
5356
5732
  title,
5357
5733
  sections,
5358
5734
  changedFiles: [],
5359
5735
  retryCommand
5360
5736
  });
5361
- if (errors.length > 0)
5362
- refuse(errors);
5737
+ if (schemaErrors.length > 0)
5738
+ refuse(schemaErrors);
5739
+ const contentErrors = validateIssueContent({
5740
+ body,
5741
+ labels,
5742
+ sharedPackages: readSharedPackages(),
5743
+ projectPaths: readProjectPaths(),
5744
+ retryCommand
5745
+ });
5746
+ if (contentErrors.length > 0)
5747
+ refuse(contentErrors);
5363
5748
  }
5364
5749
  function issueCreateCommand(args) {
5365
5750
  const json = args.includes("--json");
@@ -5370,7 +5755,7 @@ function issueCreateCommand(args) {
5370
5755
  const title = extractTitle(ghArgs);
5371
5756
  const labels = extractLabels(ghArgs);
5372
5757
  if (isTaskIssueLabelSet(labels)) {
5373
- validateTaskIssue(body, title, RETRY_CREATE);
5758
+ validateTaskIssue(body, title, labels, RETRY_CREATE);
5374
5759
  }
5375
5760
  if (validateOnly) {
5376
5761
  reportPass(json, "issue create");
@@ -5394,7 +5779,7 @@ function issueEditCommand(args) {
5394
5779
  const title = extractTitle(ghArgs);
5395
5780
  const labels = [...new Set([...fetchForgeLabels(issueRef), ...extractLabels(ghArgs)])];
5396
5781
  if (isTaskIssueLabelSet(labels)) {
5397
- validateTaskIssue(body, title, RETRY_EDIT);
5782
+ validateTaskIssue(body, title, labels, RETRY_EDIT);
5398
5783
  }
5399
5784
  if (validateOnly) {
5400
5785
  reportPass(json, "issue edit");
@@ -5404,7 +5789,7 @@ function issueEditCommand(args) {
5404
5789
  }
5405
5790
 
5406
5791
  // src/commands/new-check.ts
5407
- import { chmodSync as chmodSync2, existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "node:fs";
5792
+ import { chmodSync as chmodSync2, existsSync as existsSync14, mkdirSync as mkdirSync4, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "node:fs";
5408
5793
  import { join as join15 } from "node:path";
5409
5794
  var TEMPLATE_PATH = join15(packageRoot(import.meta.url), "templates", "custom-check.template.ts");
5410
5795
  var CHECKS_DIR = join15("scripts", "vinaya-checks");
@@ -5425,15 +5810,15 @@ function newCheckCommand(args) {
5425
5810
  process.exit(2);
5426
5811
  }
5427
5812
  const scriptsDir = join15(process.cwd(), CHECKS_DIR);
5428
- if (!existsSync13(scriptsDir))
5813
+ if (!existsSync14(scriptsDir))
5429
5814
  mkdirSync4(scriptsDir, { recursive: true });
5430
5815
  const fileStem = name.slice(name.indexOf("/") + 1);
5431
5816
  const targetPath = join15(scriptsDir, `${fileStem}.ts`);
5432
- if (existsSync13(targetPath)) {
5817
+ if (existsSync14(targetPath)) {
5433
5818
  console.error(`Error: ${targetPath} already exists.`);
5434
5819
  process.exit(1);
5435
5820
  }
5436
- const template = readFileSync13(TEMPLATE_PATH, "utf-8");
5821
+ const template = readFileSync14(TEMPLATE_PATH, "utf-8");
5437
5822
  const contents = template.split("{{CHECK_NAME}}").join(name);
5438
5823
  writeFileSync7(targetPath, contents, "utf-8");
5439
5824
  chmodSync2(targetPath, 493);
@@ -5447,21 +5832,21 @@ ${registration}
5447
5832
  }
5448
5833
 
5449
5834
  // src/commands/pr.ts
5450
- import { execFileSync as execFileSync10 } from "node:child_process";
5835
+ import { execFileSync as execFileSync11 } from "node:child_process";
5451
5836
  var RETRY_CREATE2 = "vinaya pr create --validate-only …";
5452
5837
  var RETRY_EDIT2 = "vinaya pr edit <n> --validate-only …";
5453
- function git2(args) {
5838
+ function git3(args) {
5454
5839
  try {
5455
- return execFileSync10("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
5840
+ return execFileSync11("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
5456
5841
  } catch {
5457
5842
  return "";
5458
5843
  }
5459
5844
  }
5460
5845
  function localChangedFiles() {
5461
5846
  const base = process.env.BASE_SHA || "origin/main";
5462
- let out = git2(["diff", "--name-only", `${base}...HEAD`]);
5847
+ let out = git3(["diff", "--name-only", `${base}...HEAD`]);
5463
5848
  if (!out)
5464
- out = git2(["diff", "--name-only", "main...HEAD"]);
5849
+ out = git3(["diff", "--name-only", "main...HEAD"]);
5465
5850
  return out.split(`
5466
5851
  `).map((s) => s.trim()).filter(Boolean);
5467
5852
  }
@@ -5486,7 +5871,7 @@ function reportPass2(json, command) {
5486
5871
  function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
5487
5872
  const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
5488
5873
  try {
5489
- const out = execFileSync10("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
5874
+ const out = execFileSync11("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
5490
5875
  const url = out.trim();
5491
5876
  if (json)
5492
5877
  printJson({ validated: true, written: true, url });
@@ -5500,7 +5885,7 @@ function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
5500
5885
  function fetchPrForgeContext(prRef) {
5501
5886
  let viewOut;
5502
5887
  try {
5503
- viewOut = execFileSync10("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
5888
+ viewOut = execFileSync11("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
5504
5889
  encoding: "utf8",
5505
5890
  stdio: ["ignore", "pipe", "pipe"]
5506
5891
  });
@@ -5539,8 +5924,8 @@ function prCreateCommand(args) {
5539
5924
  }
5540
5925
  const sections = resolveSections("pr", RETRY_CREATE2);
5541
5926
  const changedFiles2 = localChangedFiles();
5542
- const headCommit = git2(["rev-parse", "--verify", "--quiet", "HEAD"]);
5543
- const branch = headCommit === "" ? "" : git2(["symbolic-ref", "--quiet", "--short", "HEAD"]);
5927
+ const headCommit = git3(["rev-parse", "--verify", "--quiet", "HEAD"]);
5928
+ const branch = headCommit === "" ? "" : git3(["symbolic-ref", "--quiet", "--short", "HEAD"]);
5544
5929
  const errors = validateForgeWrite({
5545
5930
  body,
5546
5931
  title,
@@ -5602,14 +5987,14 @@ function prEditCommand(args) {
5602
5987
  }
5603
5988
 
5604
5989
  // 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";
5990
+ import { execFileSync as execFileSync12, spawnSync as spawnSync2 } from "node:child_process";
5991
+ import { existsSync as existsSync15, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
5607
5992
  import { join as join16 } from "node:path";
5608
5993
  var EVIDENCE_START = "<!-- AEG:EVIDENCE:START -->";
5609
5994
  var EVIDENCE_END = "<!-- AEG:EVIDENCE:END -->";
5610
- function git3(args) {
5995
+ function git4(args) {
5611
5996
  try {
5612
- return execFileSync11("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
5997
+ return execFileSync12("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
5613
5998
  } catch {
5614
5999
  return "";
5615
6000
  }
@@ -5627,7 +6012,7 @@ class GitCommandError extends Error {
5627
6012
  }
5628
6013
  function gitStrict(args) {
5629
6014
  try {
5630
- return execFileSync11("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
6015
+ return execFileSync12("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
5631
6016
  } catch (err) {
5632
6017
  const stderr = err.stderr;
5633
6018
  throw new GitCommandError(args, String(stderr ?? err.message).trim() || "non-zero exit");
@@ -5648,7 +6033,7 @@ function resolveMergeBase(head) {
5648
6033
  const primary = process.env.BASE_SHA || "origin/main";
5649
6034
  const tried = primary === "main" ? [primary] : [primary, "main"];
5650
6035
  for (const ref of tried) {
5651
- const base = git3(["merge-base", ref, head]);
6036
+ const base = git4(["merge-base", ref, head]);
5652
6037
  if (base)
5653
6038
  return base;
5654
6039
  }
@@ -5663,7 +6048,7 @@ function computeGroupA() {
5663
6048
  function resolveSelfEntry() {
5664
6049
  const root = packageRoot(import.meta.url);
5665
6050
  const dist = join16(root, "dist", "index.js");
5666
- return existsSync14(dist) ? dist : join16(root, "src", "index.ts");
6051
+ return existsSync15(dist) ? dist : join16(root, "src", "index.ts");
5667
6052
  }
5668
6053
  var FAILING_STATUSES = new Set(["fail", "error", "timeout"]);
5669
6054
  function anyGateFailed(outcomes) {
@@ -5768,7 +6153,7 @@ async function prReportCommand(args) {
5768
6153
  throw err;
5769
6154
  }
5770
6155
  if (writePath) {
5771
- const existing = existsSync14(writePath) ? readFileSync14(writePath, "utf8") : "";
6156
+ const existing = existsSync15(writePath) ? readFileSync15(writePath, "utf8") : "";
5772
6157
  const updated = replaceEvidenceBlock(existing, result.blockInner);
5773
6158
  writeFileSync8(writePath, updated);
5774
6159
  process.stdout.write(`Wrote AEG:EVIDENCE block to ${writePath}
@@ -5781,12 +6166,12 @@ async function prReportCommand(args) {
5781
6166
  }
5782
6167
 
5783
6168
  // src/commands/quickstart.ts
5784
- import { execFileSync as execFileSync12 } from "node:child_process";
5785
- import { existsSync as existsSync16, readFileSync as readFileSync16 } from "node:fs";
6169
+ import { execFileSync as execFileSync13 } from "node:child_process";
6170
+ import { existsSync as existsSync17, readFileSync as readFileSync17 } from "node:fs";
5786
6171
  import { join as join18 } from "node:path";
5787
6172
 
5788
6173
  // src/lib/doc-owners-write.ts
5789
- import { existsSync as existsSync15, mkdirSync as mkdirSync5, readFileSync as readFileSync15, writeFileSync as writeFileSync9 } from "node:fs";
6174
+ import { existsSync as existsSync16, mkdirSync as mkdirSync5, readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "node:fs";
5790
6175
  import { dirname as dirname6, join as join17 } from "node:path";
5791
6176
  function bindingLine(glob, pointer) {
5792
6177
  return `${glob} ${pointer}`;
@@ -5802,23 +6187,23 @@ function appendDocOwnersBinding(content, glob, pointer) {
5802
6187
  return `${content}${sep4}${bindingLine(glob, pointer)}
5803
6188
  `;
5804
6189
  }
5805
- function planDocOwnersBinding(repoRoot, glob, pointer) {
5806
- const abs2 = join17(repoRoot, DOC_OWNERS_PATH);
6190
+ function planDocOwnersBinding(repoRoot2, glob, pointer) {
6191
+ const abs2 = join17(repoRoot2, DOC_OWNERS_PATH);
5807
6192
  const line = bindingLine(glob, pointer);
5808
- if (!existsSync15(abs2)) {
6193
+ if (!existsSync16(abs2)) {
5809
6194
  return { action: "create-host", line, path: DOC_OWNERS_PATH };
5810
6195
  }
5811
- const existing = readFileSync15(abs2, "utf-8");
6196
+ const existing = readFileSync16(abs2, "utf-8");
5812
6197
  const already = parseDocOwners(existing).bindings.some((b) => b.glob === glob);
5813
6198
  return { action: already ? "skip-present" : "append-row", line, path: DOC_OWNERS_PATH };
5814
6199
  }
5815
- function applyDocOwnersBinding(repoRoot, plan, glob, pointer) {
5816
- const abs2 = join17(repoRoot, DOC_OWNERS_PATH);
6200
+ function applyDocOwnersBinding(repoRoot2, plan, glob, pointer) {
6201
+ const abs2 = join17(repoRoot2, DOC_OWNERS_PATH);
5817
6202
  if (plan.action === "create-host") {
5818
6203
  mkdirSync5(dirname6(abs2), { recursive: true });
5819
6204
  writeFileSync9(abs2, freshDocOwners(glob, pointer), "utf-8");
5820
6205
  } else if (plan.action === "append-row") {
5821
- const existing = readFileSync15(abs2, "utf-8");
6206
+ const existing = readFileSync16(abs2, "utf-8");
5822
6207
  writeFileSync9(abs2, appendDocOwnersBinding(existing, glob, pointer), "utf-8");
5823
6208
  }
5824
6209
  }
@@ -5875,13 +6260,13 @@ var GLOB_INJECTION_RE = /[\s\r\n]/;
5875
6260
  var POINTER_INJECTION_RE = /[\r\n]/;
5876
6261
  var NAME_INJECTION_RE = /[\s|\r\n]/;
5877
6262
  var PATH_INJECTION_RE2 = /[|\r\n]/;
5878
- function pointerIsMissingLocalFile(repoRoot, pointer) {
6263
+ function pointerIsMissingLocalFile(repoRoot2, pointer) {
5879
6264
  if (/^https?:\/\//i.test(pointer))
5880
6265
  return false;
5881
6266
  const path = pointer.split("#")[0] ?? pointer;
5882
- return !existsSync16(join18(repoRoot, path));
6267
+ return !existsSync17(join18(repoRoot2, path));
5883
6268
  }
5884
- async function bindDocOwnerLoop(deps, repoRoot) {
6269
+ async function bindDocOwnerLoop(deps, repoRoot2) {
5885
6270
  let any = false;
5886
6271
  for (;; ) {
5887
6272
  if (any && !await deps.confirm("Bind another doc→code pair?", false))
@@ -5896,11 +6281,11 @@ async function bindDocOwnerLoop(deps, repoRoot) {
5896
6281
  }
5897
6282
  if (GLOB_INJECTION_RE.test(glob) || POINTER_INJECTION_RE.test(pointer)) {
5898
6283
  console.error(red("Error: glob must not contain whitespace, and the pointer must not contain a newline."));
5899
- } else if (pointerIsMissingLocalFile(repoRoot, pointer)) {
6284
+ } else if (pointerIsMissingLocalFile(repoRoot2, pointer)) {
5900
6285
  console.error(red(`✗ "${pointer}" does not exist in this repo (and isn't a URL).`));
5901
6286
  } else {
5902
- const plan = planDocOwnersBinding(repoRoot, glob, pointer);
5903
- applyDocOwnersBinding(repoRoot, plan, glob, pointer);
6287
+ const plan = planDocOwnersBinding(repoRoot2, glob, pointer);
6288
+ applyDocOwnersBinding(repoRoot2, plan, glob, pointer);
5904
6289
  process.stdout.write(`${green(renderDocOwnersBindingDiffLine(plan))}
5905
6290
  `);
5906
6291
  any = true;
@@ -5946,7 +6331,7 @@ async function registerProjectLoop(deps) {
5946
6331
  }
5947
6332
  }
5948
6333
  function readPackageVersion() {
5949
- const pkg = JSON.parse(readFileSync16(join18(packageRoot(import.meta.url), "package.json"), "utf-8"));
6334
+ const pkg = JSON.parse(readFileSync17(join18(packageRoot(import.meta.url), "package.json"), "utf-8"));
5950
6335
  return pkg.version;
5951
6336
  }
5952
6337
  function realDeps6() {
@@ -5976,8 +6361,8 @@ function realDeps6() {
5976
6361
  closeStdin
5977
6362
  };
5978
6363
  }
5979
- function execGit(repoRoot, args) {
5980
- return execFileSync12("git", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
6364
+ function execGit(repoRoot2, args) {
6365
+ return execFileSync13("git", args, { cwd: repoRoot2, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
5981
6366
  }
5982
6367
  function errorDetail(err) {
5983
6368
  const stderr = err.stderr;
@@ -5985,31 +6370,31 @@ function errorDetail(err) {
5985
6370
  return stderr.toString().trim();
5986
6371
  return err instanceof Error ? err.message : String(err);
5987
6372
  }
5988
- function gitStatusShort(repoRoot) {
5989
- return execGit(repoRoot, ["status", "--short"]);
6373
+ function gitStatusShort(repoRoot2) {
6374
+ return execGit(repoRoot2, ["status", "--short"]);
5990
6375
  }
5991
- function commitInstall(repoRoot) {
5992
- execGit(repoRoot, ["add", "-A"]);
5993
- const staged = execGit(repoRoot, ["diff", "--cached", "--stat"]);
6376
+ function commitInstall(repoRoot2) {
6377
+ execGit(repoRoot2, ["add", "-A"]);
6378
+ const staged = execGit(repoRoot2, ["diff", "--cached", "--stat"]);
5994
6379
  if (!staged)
5995
6380
  return { committed: false, message: "Nothing to commit — working tree already clean." };
5996
6381
  try {
5997
- execGit(repoRoot, ["commit", "-m", "Chore: install Vinaya"]);
6382
+ execGit(repoRoot2, ["commit", "-m", "Chore: install Vinaya"]);
5998
6383
  return { committed: true, message: "✓ Committed `Chore: install Vinaya`." };
5999
6384
  } catch (err) {
6000
6385
  return { committed: false, message: `git commit failed: ${errorDetail(err)}` };
6001
6386
  }
6002
6387
  }
6003
- function pushInstall(repoRoot) {
6388
+ function pushInstall(repoRoot2) {
6004
6389
  try {
6005
- const branch = execGit(repoRoot, ["symbolic-ref", "--short", "HEAD"]);
6390
+ const branch = execGit(repoRoot2, ["symbolic-ref", "--short", "HEAD"]);
6006
6391
  let hasUpstream = true;
6007
6392
  try {
6008
- execGit(repoRoot, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
6393
+ execGit(repoRoot2, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
6009
6394
  } catch {
6010
6395
  hasUpstream = false;
6011
6396
  }
6012
- execGit(repoRoot, hasUpstream ? ["push"] : ["push", "-u", "origin", branch]);
6397
+ execGit(repoRoot2, hasUpstream ? ["push"] : ["push", "-u", "origin", branch]);
6013
6398
  return { pushed: true, message: "✓ Pushed." };
6014
6399
  } catch (err) {
6015
6400
  return { pushed: false, message: `git push failed: ${errorDetail(err)}` };
@@ -6094,8 +6479,8 @@ async function quickstartCommand(args) {
6094
6479
  }
6095
6480
 
6096
6481
  // 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";
6482
+ import { execFileSync as execFileSync14 } from "node:child_process";
6483
+ import { readFileSync as readFileSync18, rmSync as rmSync4, writeFileSync as writeFileSync10 } from "node:fs";
6099
6484
  import { tmpdir as tmpdir2 } from "node:os";
6100
6485
  import { join as join19 } from "node:path";
6101
6486
  class FindingsParseError extends Error {
@@ -6258,7 +6643,7 @@ function readFindingsFile(path, allowedSeverities) {
6258
6643
  return [];
6259
6644
  let content;
6260
6645
  try {
6261
- content = readFileSync17(path, "utf8");
6646
+ content = readFileSync18(path, "utf8");
6262
6647
  } catch {
6263
6648
  refuseCmd(`Could not read findings file at ${path}.`, "Check the path and re-run.");
6264
6649
  }
@@ -6289,7 +6674,7 @@ function normalizeSecurityVerdict(raw) {
6289
6674
  }
6290
6675
  function gh(args) {
6291
6676
  try {
6292
- return execFileSync13("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
6677
+ return execFileSync14("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
6293
6678
  } catch (err) {
6294
6679
  const stderr = err.stderr;
6295
6680
  throw new Error(String(stderr ?? err.message).trim() || "gh command failed");
@@ -6374,7 +6759,7 @@ async function reviewPostCommand(args) {
6374
6759
  let secretsEvidence = null;
6375
6760
  if (secretsEvidenceFile) {
6376
6761
  try {
6377
- secretsEvidence = readFileSync17(secretsEvidenceFile, "utf8");
6762
+ secretsEvidence = readFileSync18(secretsEvidenceFile, "utf8");
6378
6763
  } catch {
6379
6764
  refuseCmd(`Could not read secrets evidence file at ${secretsEvidenceFile}.`, "Check the path and re-run.");
6380
6765
  }
@@ -6415,7 +6800,7 @@ Self-verification: clean — re-parsed VERDICT is bound to head ${headSha}.
6415
6800
 
6416
6801
  // src/commands/studio.ts
6417
6802
  import { execFile as execFile5, spawn as spawn2 } from "node:child_process";
6418
- import { existsSync as existsSync17, readFileSync as readFileSync18, renameSync } from "node:fs";
6803
+ import { existsSync as existsSync18, readFileSync as readFileSync19, renameSync } from "node:fs";
6419
6804
  import net from "node:net";
6420
6805
  import { dirname as dirname7, join as join20 } from "node:path";
6421
6806
  import { promisify as promisify5 } from "node:util";
@@ -6435,15 +6820,15 @@ function resolveStudioTarget(cwd, moduleUrl = import.meta.url) {
6435
6820
  for (;; ) {
6436
6821
  const webDir = join20(dir, "apps", "vinaya-studio", "web");
6437
6822
  const pkgPath = join20(webDir, "package.json");
6438
- if (existsSync17(pkgPath)) {
6823
+ if (existsSync18(pkgPath)) {
6439
6824
  try {
6440
- const pkg = JSON.parse(readFileSync18(pkgPath, "utf-8"));
6825
+ const pkg = JSON.parse(readFileSync19(pkgPath, "utf-8"));
6441
6826
  if (pkg.name === "@atta/vinaya-studio-web") {
6442
6827
  return { kind: "workspace", webDir };
6443
6828
  }
6444
6829
  } catch {}
6445
6830
  }
6446
- if (existsSync17(join20(dir, ".git")))
6831
+ if (existsSync18(join20(dir, ".git")))
6447
6832
  break;
6448
6833
  const parent = dirname7(dir);
6449
6834
  if (parent === dir)
@@ -6451,7 +6836,7 @@ function resolveStudioTarget(cwd, moduleUrl = import.meta.url) {
6451
6836
  dir = parent;
6452
6837
  }
6453
6838
  const standaloneWebDir = join20(packageRoot(moduleUrl), "studio-standalone", "apps", "vinaya-studio", "web");
6454
- if (existsSync17(join20(standaloneWebDir, "server.js"))) {
6839
+ if (existsSync18(join20(standaloneWebDir, "server.js"))) {
6455
6840
  return { kind: "package", packageDir: standaloneWebDir };
6456
6841
  }
6457
6842
  return { kind: "missing" };
@@ -6473,7 +6858,7 @@ function isPortFree(port) {
6473
6858
  function ensureStudioNodeModules(bundleRoot) {
6474
6859
  const real = join20(bundleRoot, "node_modules");
6475
6860
  const packed = join20(bundleRoot, STUDIO_NODE_MODULES_PACKED_DIRNAME);
6476
- if (!existsSync17(real) && existsSync17(packed)) {
6861
+ if (!existsSync18(real) && existsSync18(packed)) {
6477
6862
  renameSync(packed, real);
6478
6863
  }
6479
6864
  }
@@ -6509,7 +6894,7 @@ async function runStudio(cwd, args, moduleUrl = import.meta.url) {
6509
6894
  }
6510
6895
 
6511
6896
  // src/commands/upgrade.ts
6512
- import { existsSync as existsSync18, readFileSync as readFileSync19, rmSync as rmSync5, writeFileSync as writeFileSync11 } from "node:fs";
6897
+ import { existsSync as existsSync19, readFileSync as readFileSync20, rmSync as rmSync5, writeFileSync as writeFileSync11 } from "node:fs";
6513
6898
  import { join as join21 } from "node:path";
6514
6899
  function realDeps7() {
6515
6900
  return {
@@ -6527,13 +6912,13 @@ function realDeps7() {
6527
6912
  function flags2(args) {
6528
6913
  return { dryRun: args.includes("--dry-run"), yes: args.includes("--yes") };
6529
6914
  }
6530
- function readManifest3(repoRoot) {
6531
- const p = join21(repoRoot, CONFIG_PATH);
6532
- if (!existsSync18(p))
6915
+ function readManifest3(repoRoot2) {
6916
+ const p = join21(repoRoot2, CONFIG_PATH);
6917
+ if (!existsSync19(p))
6533
6918
  return { kind: "missing" };
6534
6919
  let raw;
6535
6920
  try {
6536
- raw = JSON.parse(readFileSync19(p, "utf-8"));
6921
+ raw = JSON.parse(readFileSync20(p, "utf-8"));
6537
6922
  } catch (err) {
6538
6923
  return { kind: "invalid", error: `invalid JSON: ${err.message}` };
6539
6924
  }
@@ -6548,31 +6933,31 @@ function readManifest3(repoRoot) {
6548
6933
  return { kind: "not-initialized" };
6549
6934
  return { kind: "ok", manifest: parsed.data.managed };
6550
6935
  }
6551
- function writeManifestVersion(repoRoot, manifest) {
6552
- const configAbs = join21(repoRoot, CONFIG_PATH);
6553
- const seed = JSON.parse(readFileSync19(configAbs, "utf-8"));
6936
+ function writeManifestVersion(repoRoot2, manifest) {
6937
+ const configAbs = join21(repoRoot2, CONFIG_PATH);
6938
+ const seed = JSON.parse(readFileSync20(configAbs, "utf-8"));
6554
6939
  const updated = { ...manifest, version: MANAGED_MANIFEST_VERSION };
6555
6940
  writeFileSync11(configAbs, `${JSON.stringify({ ...seed, managed: updated }, null, 2)}
6556
6941
  `, "utf-8");
6557
6942
  }
6558
- function stripFor(repoRoot, path, marker, comment) {
6559
- const abs2 = resolveManagedBlockPath(repoRoot, path);
6560
- if (!existsSync18(abs2))
6943
+ function stripFor(repoRoot2, path, marker, comment) {
6944
+ const abs2 = resolveManagedBlockPath(repoRoot2, path);
6945
+ if (!existsSync19(abs2))
6561
6946
  return { path, marker, comment, present: false, removesHost: false };
6562
- const stripped = stripBlockFromContent(readFileSync19(abs2, "utf-8"), marker, comment);
6947
+ const stripped = stripBlockFromContent(readFileSync20(abs2, "utf-8"), marker, comment);
6563
6948
  if (stripped === null)
6564
6949
  return { path, marker, comment, present: false, removesHost: false };
6565
6950
  return { path, marker, comment, present: true, removesHost: blockStripLeavesEmpty(stripped) };
6566
6951
  }
6567
- function planHookRouting(repoRoot, manifest, recorded, hooksPathValue) {
6952
+ function planHookRouting(repoRoot2, manifest, recorded, hooksPathValue) {
6568
6953
  const none = { target: recorded, strips: [], arm: false, migratesManifest: false, blockedReason: null };
6569
6954
  if (recorded === ".husky")
6570
6955
  return none;
6571
6956
  if (recorded === TRACKED_HOOK_DIR) {
6572
- const strips = manifest.blocks.filter((b) => b.path.startsWith(`${TRACKED_HOOK_DIR}/`)).map((b) => stripFor(repoRoot, `.git/hooks/${b.marker}`, b.marker, b.comment)).filter((s) => s.present);
6957
+ const strips = manifest.blocks.filter((b) => b.path.startsWith(`${TRACKED_HOOK_DIR}/`)).map((b) => stripFor(repoRoot2, `.git/hooks/${b.marker}`, b.marker, b.comment)).filter((s) => s.present);
6573
6958
  if (hooksPathValue === TRACKED_HOOK_DIR)
6574
6959
  return { ...none, strips };
6575
- const foreign = foreignRawHooks(repoRoot);
6960
+ const foreign = foreignRawHooks(repoRoot2);
6576
6961
  if (foreign.length > 0) {
6577
6962
  return {
6578
6963
  ...none,
@@ -6585,17 +6970,17 @@ function planHookRouting(repoRoot, manifest, recorded, hooksPathValue) {
6585
6970
  const legacy = manifest.blocks.filter((b) => b.path.startsWith(".git/hooks/"));
6586
6971
  const blocked = [];
6587
6972
  for (const b of legacy) {
6588
- const abs2 = resolveManagedBlockPath(repoRoot, b.path);
6589
- if (!existsSync18(abs2))
6973
+ const abs2 = resolveManagedBlockPath(repoRoot2, b.path);
6974
+ if (!existsSync19(abs2))
6590
6975
  continue;
6591
- const stripped = stripBlockFromContent(readFileSync19(abs2, "utf-8"), b.marker, b.comment);
6976
+ const stripped = stripBlockFromContent(readFileSync20(abs2, "utf-8"), b.marker, b.comment);
6592
6977
  if (stripped === null)
6593
6978
  blocked.push(`${b.path} exists without vinaya's managed block`);
6594
6979
  else if (!blockStripLeavesEmpty(stripped))
6595
6980
  blocked.push(`${b.path} carries your own lines alongside the managed block`);
6596
6981
  }
6597
6982
  const legacyNames = new Set(legacy.map((b) => b.path.slice(".git/hooks/".length)));
6598
- for (const f of activeRawHooks(repoRoot)) {
6983
+ for (const f of activeRawHooks(repoRoot2)) {
6599
6984
  if (!legacyNames.has(f))
6600
6985
  blocked.push(`.git/hooks/${f} is an active raw hook vinaya does not manage`);
6601
6986
  }
@@ -6603,7 +6988,7 @@ function planHookRouting(repoRoot, manifest, recorded, hooksPathValue) {
6603
6988
  return { ...none, blockedReason: blocked.join("; ") };
6604
6989
  return {
6605
6990
  target: TRACKED_HOOK_DIR,
6606
- strips: legacy.map((b) => stripFor(repoRoot, b.path, b.marker, b.comment)).filter((s) => s.present),
6991
+ strips: legacy.map((b) => stripFor(repoRoot2, b.path, b.marker, b.comment)).filter((s) => s.present),
6607
6992
  arm: hooksPathValue !== TRACKED_HOOK_DIR,
6608
6993
  migratesManifest: true,
6609
6994
  blockedReason: null
@@ -6615,7 +7000,7 @@ function translateHookPaths(manifest) {
6615
7000
  blocks: manifest.blocks.map((b) => b.path.startsWith(".git/hooks/") ? { ...b, path: `${TRACKED_HOOK_DIR}/${b.path.slice(".git/hooks/".length)}` } : b)
6616
7001
  };
6617
7002
  }
6618
- function planUpgrade(ops, repoRoot, manifest, routing) {
7003
+ function planUpgrade(ops, repoRoot2, manifest, routing) {
6619
7004
  const entries = [];
6620
7005
  let hasChanges = routing.arm || routing.migratesManifest || routing.strips.some((s) => s.present);
6621
7006
  const ownedFiles = new Set(manifest.files);
@@ -6623,8 +7008,8 @@ function planUpgrade(ops, repoRoot, manifest, routing) {
6623
7008
  const ownedBlocks = new Set(manifest.blocks.map((b) => blockKey(b.path, b.marker)));
6624
7009
  for (const op of ops) {
6625
7010
  if (op.kind === "create-file") {
6626
- const abs2 = join21(repoRoot, op.path);
6627
- const exists = existsSync18(abs2);
7011
+ const abs2 = join21(repoRoot2, op.path);
7012
+ const exists = existsSync19(abs2);
6628
7013
  const owned = ownedFiles.has(op.path);
6629
7014
  let action;
6630
7015
  if (op.path === CONFIG_PATH || op.path === DOC_OWNERS_PATH) {
@@ -6634,7 +7019,7 @@ function planUpgrade(ops, repoRoot, manifest, routing) {
6634
7019
  } else if (!exists) {
6635
7020
  action = "recreate";
6636
7021
  hasChanges = true;
6637
- } else if (readFileSync19(abs2, "utf-8") !== op.content) {
7022
+ } else if (readFileSync20(abs2, "utf-8") !== op.content) {
6638
7023
  action = "regenerate";
6639
7024
  hasChanges = true;
6640
7025
  } else {
@@ -6642,16 +7027,16 @@ function planUpgrade(ops, repoRoot, manifest, routing) {
6642
7027
  }
6643
7028
  entries.push({ kind: "create-file", op, action });
6644
7029
  } else if (op.kind === "managed-block") {
6645
- const abs2 = resolveManagedBlockPath(repoRoot, op.path);
7030
+ const abs2 = resolveManagedBlockPath(repoRoot2, op.path);
6646
7031
  const owned = ownedBlocks.has(blockKey(op.path, op.marker));
6647
7032
  let action;
6648
7033
  if (!owned) {
6649
7034
  action = "not-installed";
6650
- } else if (!existsSync18(abs2)) {
7035
+ } else if (!existsSync19(abs2)) {
6651
7036
  action = "recreate-host";
6652
7037
  hasChanges = true;
6653
7038
  } else {
6654
- const content = readFileSync19(abs2, "utf-8");
7039
+ const content = readFileSync20(abs2, "utf-8");
6655
7040
  const { begin, end } = markerLines(op.marker, op.comment);
6656
7041
  if (!(content.includes(begin) && content.includes(end))) {
6657
7042
  action = "recreate-append";
@@ -6749,39 +7134,39 @@ function renderUpgradeDiff(plan) {
6749
7134
  return lines.join(`
6750
7135
  `);
6751
7136
  }
6752
- function regenerateBlock(repoRoot, op) {
6753
- const abs2 = resolveManagedBlockPath(repoRoot, op.path);
6754
- const content = readFileSync19(abs2, "utf-8");
7137
+ function regenerateBlock(repoRoot2, op) {
7138
+ const abs2 = resolveManagedBlockPath(repoRoot2, op.path);
7139
+ const content = readFileSync20(abs2, "utf-8");
6755
7140
  const stripped = stripBlockFromContent(content, op.marker, op.comment);
6756
7141
  if (stripped !== null) {
6757
7142
  writeFileSync11(abs2, stripped.endsWith(`
6758
7143
  `) ? stripped : `${stripped}
6759
7144
  `, "utf-8");
6760
7145
  }
6761
- appendBlock(repoRoot, op);
7146
+ appendBlock(repoRoot2, op);
6762
7147
  }
6763
- function applyUpgrade(plan, repoRoot) {
7148
+ function applyUpgrade(plan, repoRoot2) {
6764
7149
  for (const e of plan.entries) {
6765
7150
  if (e.kind === "create-file") {
6766
7151
  if (e.action === "regenerate" || e.action === "recreate") {
6767
- writeFileWithDirs(join21(repoRoot, e.op.path), e.op.content, e.op.mode);
7152
+ writeFileWithDirs(join21(repoRoot2, e.op.path), e.op.content, e.op.mode);
6768
7153
  }
6769
7154
  } else {
6770
7155
  if (e.action === "recreate-host")
6771
- createHost(repoRoot, e.op);
7156
+ createHost(repoRoot2, e.op);
6772
7157
  else if (e.action === "recreate-append")
6773
- appendBlock(repoRoot, e.op);
7158
+ appendBlock(repoRoot2, e.op);
6774
7159
  else if (e.action === "regenerate-block")
6775
- regenerateBlock(repoRoot, e.op);
7160
+ regenerateBlock(repoRoot2, e.op);
6776
7161
  }
6777
7162
  }
6778
7163
  for (const s of plan.routing.strips) {
6779
7164
  if (!s.present)
6780
7165
  continue;
6781
- const abs2 = resolveManagedBlockPath(repoRoot, s.path);
6782
- if (!existsSync18(abs2))
7166
+ const abs2 = resolveManagedBlockPath(repoRoot2, s.path);
7167
+ if (!existsSync19(abs2))
6783
7168
  continue;
6784
- const stripped = stripBlockFromContent(readFileSync19(abs2, "utf-8"), s.marker, s.comment);
7169
+ const stripped = stripBlockFromContent(readFileSync20(abs2, "utf-8"), s.marker, s.comment);
6785
7170
  if (stripped === null)
6786
7171
  continue;
6787
7172
  if (blockStripLeavesEmpty(stripped))
@@ -6869,12 +7254,12 @@ async function upgradeCommand(args) {
6869
7254
  }
6870
7255
 
6871
7256
  // src/commands/waiver.ts
6872
- import { execFileSync as execFileSync14 } from "node:child_process";
7257
+ import { execFileSync as execFileSync15 } from "node:child_process";
6873
7258
  function labelFor(kind) {
6874
7259
  return kind === "docs" ? WAIVER_LABEL : WAIVER_LABEL_REVIEW;
6875
7260
  }
6876
7261
  function gh2(args) {
6877
- return execFileSync14("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], env: process.env }).trim();
7262
+ return execFileSync15("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], env: process.env }).trim();
6878
7263
  }
6879
7264
  function detectPrNumber() {
6880
7265
  try {
@@ -6974,8 +7359,8 @@ Reason: ${reason}`];
6974
7359
  }
6975
7360
  process.stdout.write(`Applying \`${label2}\` to PR #${prNumber} under your authenticated \`gh\` identity — vinaya never fabricates an actor.
6976
7361
  `);
6977
- execFileSync14("gh", editArgs, { stdio: "inherit", env: process.env });
6978
- execFileSync14("gh", commentArgs, { stdio: "inherit", env: process.env });
7362
+ execFileSync15("gh", editArgs, { stdio: "inherit", env: process.env });
7363
+ execFileSync15("gh", commentArgs, { stdio: "inherit", env: process.env });
6979
7364
  process.stdout.write(`
6980
7365
  ✓ Label applied and reason posted as a PR comment.
6981
7366
  `);
@@ -7324,7 +7709,7 @@ function printHelp() {
7324
7709
  // src/index.ts
7325
7710
  var PACKAGE_ROOT = join22(dirname8(fileURLToPath2(import.meta.url)), "..");
7326
7711
  function readVersion2() {
7327
- const pkg = JSON.parse(readFileSync20(join22(PACKAGE_ROOT, "package.json"), "utf-8"));
7712
+ const pkg = JSON.parse(readFileSync21(join22(PACKAGE_ROOT, "package.json"), "utf-8"));
7328
7713
  return pkg.version;
7329
7714
  }
7330
7715
  var [, , command, ...args] = process.argv;