@attalabs/vinaya 0.22.0 → 0.24.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 (194) hide show
  1. package/README.md +14 -3
  2. package/aeg-root/contracts/archivist-tranche-archivist.md +2 -2
  3. package/aeg-root/contracts/brief-developer.md +1 -0
  4. package/aeg-root/contracts/developer-reviewer.md +3 -3
  5. package/aeg-root/contracts/reviewer-archivist.md +3 -3
  6. package/aeg-root/enforcement.md +26 -20
  7. package/aeg-root/process.md +8 -4
  8. package/aeg-root/roles/archivist.md +2 -2
  9. package/aeg-root/roles/brief-author.md +9 -3
  10. package/aeg-root/roles/developer.md +63 -31
  11. package/aeg-root/roles/planner.md +1 -1
  12. package/aeg-root/roles/principal.md +14 -0
  13. package/aeg-root/roles/reviewer.md +32 -15
  14. package/aeg-root/roles/security.md +15 -5
  15. package/aeg-root/skills/aeg/SKILL.md +9 -5
  16. package/aeg-root/skills/aeg-roles/SKILL.md +2 -2
  17. package/aeg-root/skills/brief-authoring/SKILL.md +39 -27
  18. package/aeg-root/state-machine.md +9 -11
  19. package/aeg-root/templates/brief-template.md +6 -6
  20. package/aeg-root/templates/pr-report-template.md +10 -7
  21. package/aeg-root/tranche-model.md +6 -2
  22. package/dist/checks/bin/check-body-bare-digits.js +559 -140
  23. package/dist/checks/bin/check-branch-topology.js +630 -145
  24. package/dist/checks/bin/check-brief-shape.js +600 -143
  25. package/dist/checks/bin/check-changeset-coverage.js +942 -162
  26. package/dist/checks/bin/check-closes-n.js +630 -145
  27. package/dist/checks/bin/check-coherence.js +641 -148
  28. package/dist/checks/bin/check-dead-branch-push.js +559 -140
  29. package/dist/checks/bin/check-dispatch-readiness.js +639 -146
  30. package/dist/checks/bin/check-doc-coverage-push.js +696 -219
  31. package/dist/checks/bin/check-doc-coverage.js +696 -219
  32. package/dist/checks/bin/check-doctrine-no-procedures.js +4800 -0
  33. package/dist/checks/bin/check-doctrine-portability.js +691 -147
  34. package/dist/checks/bin/check-evidence-fresh.js +1048 -222
  35. package/dist/checks/bin/check-exec-bits.js +4937 -0
  36. package/dist/checks/bin/check-first-push-dispatch.js +630 -145
  37. package/dist/checks/bin/check-issue-assignment.js +630 -145
  38. package/dist/checks/bin/check-main-branch-refusal.js +565 -141
  39. package/dist/checks/bin/check-no-disk-state.js +559 -140
  40. package/dist/checks/bin/check-pr-report-density.js +4467 -0
  41. package/dist/checks/bin/check-quoted-command.js +656 -164
  42. package/dist/checks/bin/check-reader-resolvable-prose.js +652 -163
  43. package/dist/checks/bin/check-registry-gates.js +602 -145
  44. package/dist/checks/bin/check-retired-vocabulary.js +652 -163
  45. package/dist/checks/bin/check-review-gate.js +677 -142
  46. package/dist/checks/bin/check-single-plan-pr.js +559 -140
  47. package/dist/checks/bin/check-test-plan.js +560 -141
  48. package/dist/checks/bin/check-token-collection-wired.js +559 -140
  49. package/dist/checks/bin/check-token-report.js +559 -140
  50. package/dist/checks/bin/check-workspace-escape.js +992 -146
  51. package/dist/index.js +8884 -5627
  52. package/package.json +1 -1
  53. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/actions.ts +173 -0
  54. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/anchored-region.ts +118 -0
  55. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/archive-task.ts +206 -0
  56. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/baseline-capture.ts +65 -0
  57. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/blast-radius-domains.ts +198 -0
  58. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/branch-topology-gate.ts +85 -0
  59. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/brief-render.ts +462 -0
  60. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/brief-validation.ts +912 -0
  61. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/claude-code-transcript.ts +460 -0
  62. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/coherence-checks.ts +806 -0
  63. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/consumer-enumeration.ts +75 -0
  64. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dead-branch-push-audit.ts +55 -0
  65. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dead-branch-push-guard.ts +77 -0
  66. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/derive-section7.ts +66 -0
  67. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/derive-tranche.ts +94 -0
  68. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/diagram-model.ts +372 -0
  69. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/direct-main-push.ts +39 -0
  70. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dispatch-gate.ts +268 -0
  71. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doc-owners.ts +314 -0
  72. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/build-doc-nav.ts +38 -0
  73. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/docs-coherence.ts +136 -0
  74. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/index.ts +29 -0
  75. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/legacy-anchors.ts +48 -0
  76. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/nav-helpers.ts +17 -0
  77. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/node-route.ts +134 -0
  78. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/parse-doc.ts +43 -0
  79. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/published-prose.ts +306 -0
  80. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/surfaced-manifest.ts +89 -0
  81. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/types.ts +33 -0
  82. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-no-procedures.ts +94 -0
  83. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-portability.ts +293 -0
  84. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-source.ts +23 -0
  85. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/ensure-label.ts +28 -0
  86. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/file-classify.ts +51 -0
  87. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/first-push-dispatch-gate.ts +80 -0
  88. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/gate-audience.ts +171 -0
  89. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/index.ts +323 -0
  90. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/issue-assignment.ts +102 -0
  91. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/issue-validation.ts +746 -0
  92. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/leftover-detection.ts +68 -0
  93. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/local-anchor-coverage.ts +245 -0
  94. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/main-branch-refusal.ts +64 -0
  95. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/manifest-validity.ts +81 -0
  96. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/markdown-table.ts +76 -0
  97. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/metering-io-guard.ts +119 -0
  98. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/milestone-validation.ts +240 -0
  99. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/no-disk-state.ts +50 -0
  100. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-ledger.ts +77 -0
  101. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-registry.ts +105 -0
  102. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-token-report.ts +177 -0
  103. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-tranche.ts +183 -0
  104. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/pr-report-density.ts +116 -0
  105. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/pr-tier.ts +81 -0
  106. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/premise-check.ts +160 -0
  107. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/quoted-command.ts +275 -0
  108. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/reader-resolvable-prose.ts +320 -0
  109. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-checks.ts +255 -0
  110. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-parse.ts +164 -0
  111. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-scaffold.ts +216 -0
  112. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/report-tokens.ts +261 -0
  113. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/retired-vocabulary.ts +163 -0
  114. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/review-gate.ts +378 -0
  115. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/review-status.ts +200 -0
  116. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/single-plan-pr.ts +73 -0
  117. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/state-machine-model.ts +269 -0
  118. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/state-source.ts +14 -0
  119. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/status-block.ts +8 -0
  120. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/sum-ledger.ts +26 -0
  121. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/symbol-collisions.ts +78 -0
  122. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-gate.ts +117 -0
  123. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-section.ts +65 -0
  124. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/types.ts +174 -0
  125. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/verdict-extraction.ts +199 -0
  126. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/vocabulary-citation.ts +114 -0
  127. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/waiver-label.ts +52 -0
  128. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/workspace-escape.ts +172 -0
  129. package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
  130. package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
  131. package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
  132. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
  133. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
  134. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  135. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  136. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  137. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  138. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  139. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
  140. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  141. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  142. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
  143. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
  144. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
  145. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
  146. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
  147. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
  148. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
  149. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
  150. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
  151. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
  152. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
  153. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
  154. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
  155. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
  156. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
  157. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
  158. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
  159. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
  160. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
  161. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
  162. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
  163. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
  164. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
  165. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
  166. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
  167. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/[root-of-the-server]__02a-3g8._.js +1 -1
  168. 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
  169. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0053k9k._.js +1 -1
  170. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0112h-k._.js +1 -1
  171. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0o771t1._.js +1 -1
  172. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0puovz5._.js +1 -1
  173. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1hs0dcu._.js +1 -1
  174. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__069-_41._.js → [root-of-the-server]__1pndh3_._.js} +1 -1
  175. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1wc4-ip._.js +1 -1
  176. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_03x_w6q._.js +1 -1
  177. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0gvm3og._.js +1 -1
  178. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0lwxg63._.js +1 -1
  179. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +2 -2
  180. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1n0cnq-._.js +1 -1
  181. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_app_studio_projects_[name]_tranches_[slug]_05ufo8i._.js +3 -3
  182. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
  183. package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
  184. package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
  185. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
  186. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
  187. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{37hnluhzj9bei.js → 0zebsmmk1bvnb.js} +1 -1
  188. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{267o-tsrnuwnv.js → 1uogpj5w2n5ju.js} +1 -1
  189. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1hufpuody6vjv.js → 3m1kgax7j2vgs.js} +1 -1
  190. package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
  191. /package/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/{aeg-core → vinaya/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/aeg-core}/bin/verify-coherence.ts +0 -0
  192. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → QY2GhiI47-765D9DHGy1I}/_buildManifest.js +0 -0
  193. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → QY2GhiI47-765D9DHGy1I}/_clientMiddlewareManifest.js +0 -0
  194. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → QY2GhiI47-765D9DHGy1I}/_ssgManifest.js +0 -0
@@ -1,36 +1,9 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
2
4
 
3
5
  // src/checks/bin/check-evidence-fresh.ts
4
- import { execFileSync as execFileSync2 } from "node:child_process";
5
-
6
- // src/checks/contract.ts
7
- var CHECK_SCHEMA_VERSION = 1;
8
- function emitCheckError(error) {
9
- process.stderr.write(`${JSON.stringify(error)}
10
- `);
11
- }
12
-
13
- // src/lib/numstat.ts
14
- function summariseNumstat(numstat) {
15
- const rows = numstat.split(`
16
- `).filter((l) => l.trim() !== "");
17
- let added = 0;
18
- let deleted = 0;
19
- let binary = 0;
20
- for (const row of rows) {
21
- const [a, d] = row.split("\t");
22
- if (a === "-" || d === "-") {
23
- binary++;
24
- continue;
25
- }
26
- added += Number(a) || 0;
27
- deleted += Number(d) || 0;
28
- }
29
- const files = `${rows.length} file${rows.length === 1 ? "" : "s"} changed`;
30
- const bin = binary > 0 ? `, ${binary} binary` : "";
31
- return `${files}, ${added} insertion${added === 1 ? "" : "s"}(+), ${deleted} deletion${deleted === 1 ? "" : "s"}(-)${bin}`;
32
- }
33
- var EVIDENCE_SUMMARY_PREFIX = "Summary: ";
6
+ import { execFileSync as execFileSync3 } from "node:child_process";
34
7
  // ../../packages/aeg-core/src/gate-audience.ts
35
8
  var GATE_AUDIENCE = {
36
9
  "check-branch-topology": { shippedAs: "branch-topology", ring: 0 },
@@ -239,17 +212,18 @@ function splitIds(raw) {
239
212
  return [];
240
213
  return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0 && !isEmptyMarker(s) && ID_TOKEN.test(s));
241
214
  }
242
- function resolveIds(raw, lastSlug) {
215
+ function resolveIds(raw) {
243
216
  const ids = splitIds(raw);
244
217
  const resolved = [];
218
+ let lastSlug = null;
245
219
  for (const id of ids) {
246
220
  const qualified = id.match(SLUG_QUALIFIED_ID);
247
221
  if (qualified) {
248
- lastSlug.current = qualified[1] ?? null;
222
+ lastSlug = qualified[1] ?? null;
249
223
  resolved.push(id);
250
- } else if (lastSlug.current) {
251
- resolved.push(`${lastSlug.current} ${id}`);
252
- lastSlug.current = null;
224
+ } else if (lastSlug) {
225
+ resolved.push(`${lastSlug} ${id}`);
226
+ lastSlug = null;
253
227
  } else {
254
228
  resolved.push(id);
255
229
  }
@@ -269,12 +243,7 @@ function parseRationaleDeps(body) {
269
243
  const section = extractSection(body);
270
244
  const dependsOn = [];
271
245
  const conflictsWith = [];
272
- let current = null;
273
246
  const assignedFields = new Set;
274
- const lastSlugByField = {
275
- dependsOn: { current: null },
276
- conflictsWith: { current: null }
277
- };
278
247
  const spanPattern = /`([^`]*)`/g;
279
248
  let match = spanPattern.exec(section);
280
249
  while (match !== null) {
@@ -284,13 +253,9 @@ function parseRationaleDeps(body) {
284
253
  const field = labelMatch[1]?.toLowerCase() === "conflicts-with" ? "conflictsWith" : "dependsOn";
285
254
  if (!assignedFields.has(field)) {
286
255
  assignedFields.add(field);
287
- current = field;
288
- const ids = resolveIds(labelMatch[2] ?? "", lastSlugByField[field]);
256
+ const ids = resolveIds(labelMatch[2] ?? "");
289
257
  pushUnique(field === "dependsOn" ? dependsOn : conflictsWith, ids);
290
258
  }
291
- } else if (current) {
292
- const ids = resolveIds(content, lastSlugByField[current]);
293
- pushUnique(current === "dependsOn" ? dependsOn : conflictsWith, ids);
294
259
  }
295
260
  match = spanPattern.exec(section);
296
261
  }
@@ -1755,6 +1720,48 @@ function splitTableRow(row) {
1755
1720
  parts.pop();
1756
1721
  return parts.map((p) => p.split(PLACEHOLDER).join("|"));
1757
1722
  }
1723
+ // ../../packages/aeg-core/src/blast-radius-domains.ts
1724
+ function resolveWorkspaceEntry(entry2, listDirs) {
1725
+ if (entry2.endsWith("/*")) {
1726
+ const baseDir = entry2.slice(0, -2);
1727
+ return listDirs(baseDir).map((name) => `${baseDir}/${name}`);
1728
+ }
1729
+ if (entry2.includes("*"))
1730
+ return [];
1731
+ return [entry2];
1732
+ }
1733
+ // ../../packages/aeg-core/src/consumer-enumeration.ts
1734
+ function deriveWorkspaceMemberDirs(workspaces, listDirs) {
1735
+ const positive = workspaces.filter((entry2) => !entry2.startsWith("!"));
1736
+ const negative = workspaces.filter((entry2) => entry2.startsWith("!")).map((entry2) => entry2.slice(1));
1737
+ const excluded = new Set;
1738
+ for (const entry2 of negative) {
1739
+ for (const member of resolveWorkspaceEntry(entry2, listDirs))
1740
+ excluded.add(member);
1741
+ }
1742
+ const members = new Set;
1743
+ for (const entry2 of positive) {
1744
+ for (const member of resolveWorkspaceEntry(entry2, listDirs)) {
1745
+ if (!excluded.has(member))
1746
+ members.add(member);
1747
+ }
1748
+ }
1749
+ return [...members].sort();
1750
+ }
1751
+ function buildConsumersOf(workspaces, listDirs, readManifest) {
1752
+ const members = deriveWorkspaceMemberDirs(workspaces, listDirs);
1753
+ return (pkg) => {
1754
+ const target = `@attalabs/${pkg}`;
1755
+ return members.filter((dir) => {
1756
+ if (dir === `packages/${pkg}`)
1757
+ return false;
1758
+ const manifest = readManifest(dir);
1759
+ if (!manifest)
1760
+ return false;
1761
+ return target in (manifest.dependencies ?? {}) || target in (manifest.devDependencies ?? {});
1762
+ });
1763
+ };
1764
+ }
1758
1765
  // ../../packages/aeg-core/src/state-machine-model.ts
1759
1766
  var FORGE_FACT_INPUTS = [
1760
1767
  {
@@ -1859,6 +1866,22 @@ function isEmDashOrDash(s) {
1859
1866
  const t = s.trim();
1860
1867
  return t === "—" || t === "-" || t === "–";
1861
1868
  }
1869
+ // ../../packages/aeg-core/src/waiver-label.ts
1870
+ var WAIVER_LABEL = label("waiver-docs");
1871
+ var WAIVER_LABEL_REVIEW = label("waiver-review");
1872
+ var PRINCIPAL_ALLOWLIST = ["daniboomerang"];
1873
+ function isPrincipal(login, principalAllowlist) {
1874
+ if (login === null)
1875
+ return false;
1876
+ const normalized = login.toLowerCase();
1877
+ return principalAllowlist.some((p) => p.toLowerCase() === normalized);
1878
+ }
1879
+ function isWaiverLabelActorVerified(opts) {
1880
+ if (!opts.labels.includes(opts.label))
1881
+ return false;
1882
+ return isPrincipal(opts.labelActor, opts.principalAllowlist);
1883
+ }
1884
+
1862
1885
  // ../../packages/aeg-core/src/parse-token-report.ts
1863
1886
  var TOKEN_REPORT_HEADING = /^\s*(?:#{1,6}\s*Token report\s*|\*\*Token report\*\*)\s*$/i;
1864
1887
  var INLINE_FIELD_LIST = /Phase:\s*([^|]+?)\s*\|\s*Role:\s*([^|]+?)\s*\|\s*Agent\/Model:\s*([^|]+?)\s*\|\s*Tokens in:\s*([^|]+?)\s*\|\s*Tokens out:\s*([^|]+?)\s*\|\s*Cost:\s*([^|]+?)\s*\|\s*Date:\s*(.+?)\s*$/im;
@@ -1913,6 +1936,47 @@ function parseInlineFieldList(section) {
1913
1936
  return null;
1914
1937
  return rowFromCells([m[1], m[2], m[3], m[4], m[5], m[6], m[7]]);
1915
1938
  }
1939
+ // ../../packages/aeg-core/src/verdict-extraction.ts
1940
+ var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
1941
+ function firstThreeLines(comment) {
1942
+ return comment.split(`
1943
+ `).slice(0, 3).join(`
1944
+ `);
1945
+ }
1946
+ function extractHeadSha(comment) {
1947
+ const m = firstThreeLines(comment).match(HEAD_SHA_PATTERN);
1948
+ return m ? m[1].toLowerCase() : null;
1949
+ }
1950
+ function extractVerdict(comments, valuePattern, missingLabel) {
1951
+ const candidates = comments.filter((c) => valuePattern.test(c));
1952
+ if (candidates.length === 0) {
1953
+ return {
1954
+ value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
1955
+ headSha: null,
1956
+ danglingNote: `no ${missingLabel} verdict comment found on this PR`
1957
+ };
1958
+ }
1959
+ const latest = candidates[candidates.length - 1];
1960
+ const m = firstThreeLines(latest).match(valuePattern);
1961
+ if (!m) {
1962
+ return {
1963
+ value: `the most recent ${missingLabel} comment's VERDICT line is not within its first three lines — DANGLING, see below`,
1964
+ headSha: null,
1965
+ danglingNote: `the most recent ${missingLabel} verdict comment carries a VERDICT-shaped line outside the first-three-line read window`
1966
+ };
1967
+ }
1968
+ return {
1969
+ value: m[1].toUpperCase().replace(/[_-]/g, " "),
1970
+ headSha: extractHeadSha(latest),
1971
+ danglingNote: null
1972
+ };
1973
+ }
1974
+ function extractCodeReviewVerdict(comments) {
1975
+ return extractVerdict(comments, /^[ \t]*(?:\*{1,3}|_{1,3})?VERDICT:\s*(APPROVE|REQUEST[ _-]?CHANGES|LGTM)(?![A-Za-z0-9])/im, "code-reviewer");
1976
+ }
1977
+ function extractSecurityReviewVerdict(comments) {
1978
+ return extractVerdict(comments, /^[ \t]*(?:\*{1,3}|_{1,3})?VERDICT:\s*(PASS|FAIL)(?![A-Za-z0-9])/im, "security-review");
1979
+ }
1916
1980
  // ../../packages/aeg-core/src/report-tokens.ts
1917
1981
  var DASH_LOOKALIKES = {
1918
1982
  "-": "‑",
@@ -2077,22 +2141,6 @@ function isCodeFile(p) {
2077
2141
  return false;
2078
2142
  return /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|sql|css)$/.test(p);
2079
2143
  }
2080
- // ../../packages/aeg-core/src/waiver-label.ts
2081
- var WAIVER_LABEL = label("waiver-docs");
2082
- var WAIVER_LABEL_REVIEW = label("waiver-review");
2083
- var PRINCIPAL_ALLOWLIST = ["daniboomerang"];
2084
- function isPrincipal(login, principalAllowlist) {
2085
- if (login === null)
2086
- return false;
2087
- const normalized = login.toLowerCase();
2088
- return principalAllowlist.some((p) => p.toLowerCase() === normalized);
2089
- }
2090
- function isWaiverLabelActorVerified(opts) {
2091
- if (!opts.labels.includes(opts.label))
2092
- return false;
2093
- return isPrincipal(opts.labelActor, opts.principalAllowlist);
2094
- }
2095
-
2096
2144
  // ../../packages/aeg-core/src/doc-owners.ts
2097
2145
  var DOC_OWNERS_PATH = ".vinaya/doc-owners";
2098
2146
  function parseDocOwners(content) {
@@ -2309,6 +2357,32 @@ function parsePremiseBlock(prBody) {
2309
2357
  }
2310
2358
  return assertions;
2311
2359
  }
2360
+ function premiseBlockText(prBody) {
2361
+ const searchIn = anchoredRegion(prBody, "PREMISE") ?? prBody;
2362
+ const lines = searchIn.split(/\r?\n/);
2363
+ let startIdx = -1;
2364
+ let endIdxExclusive = lines.length;
2365
+ for (let i = 0;i < lines.length; i++) {
2366
+ const line = lines[i].trim();
2367
+ if (startIdx === -1) {
2368
+ if (isPremiseHeader(line))
2369
+ startIdx = i;
2370
+ continue;
2371
+ }
2372
+ if (line === "") {
2373
+ endIdxExclusive = i;
2374
+ break;
2375
+ }
2376
+ if (!PREMISE_LINE.test(line)) {
2377
+ endIdxExclusive = i;
2378
+ break;
2379
+ }
2380
+ }
2381
+ if (startIdx === -1)
2382
+ return null;
2383
+ return lines.slice(startIdx, endIdxExclusive).join(`
2384
+ `);
2385
+ }
2312
2386
  function checkPremises(assertions, fileReader) {
2313
2387
  const failures = [];
2314
2388
  for (const a of assertions) {
@@ -2395,15 +2469,18 @@ function testPlanRegion(prBody) {
2395
2469
  return located.found ? located.section : prBody;
2396
2470
  }
2397
2471
  function checkTestPlan(prBody) {
2398
- const region = testPlanRegion(prBody);
2472
+ const located = locateTestPlanSection(prBody);
2473
+ const region = located.found ? located.section : prBody;
2399
2474
  if (TEST_PLAN_UNIT_TESTS_ONLY_RE.test(region))
2400
2475
  return { status: "pass", errors: [] };
2401
2476
  if (/\*\*\[(?:agent|principal)\]\*\*/.test(region))
2402
2477
  return { status: "pass", errors: [] };
2478
+ if (located.found && extractFencedBlocks(region).length > 0)
2479
+ return { status: "pass", errors: [] };
2403
2480
  return {
2404
2481
  status: "fail",
2405
2482
  errors: [
2406
- "brief-validation Test Plan: no Test Plan section found — expected `Test Plan: unit-tests-only`, or at least one `**[agent]**`/`**[principal]**`-tagged checklist item."
2483
+ "brief-validation Test Plan: no Test Plan section found — expected `Test Plan: unit-tests-only`, a fenced `[agent]` command list, or at least one `**[agent]**`/`**[principal]**`-tagged checklist item."
2407
2484
  ]
2408
2485
  };
2409
2486
  }
@@ -2526,8 +2603,162 @@ function isBriefShaped(prBody) {
2526
2603
  const stripped = stripCode(prBody);
2527
2604
  return BRIEF_SHAPE_MARKERS.filter((check) => check(stripped).status === "pass").length >= 2;
2528
2605
  }
2606
+ var COMMAND_WORDS = ["export", "bun", "gh", "git", "grep", "sed", "cat", "diff", "vinaya"];
2607
+ var BRIEF_RULES_SINCE_PR = 394;
2608
+ var AGENT_BOXES_REFUSED_SINCE_PR = 396;
2609
+ var AGENT_BOX_LINE_RE = /^-\s*\[[ xX]\]\s*\*{2}\[agent\]\*{2}/im;
2610
+ function checkNoAgentBoxes(prBody) {
2611
+ const region = testPlanRegion(prBody);
2612
+ if (!AGENT_BOX_LINE_RE.test(region))
2613
+ return { status: "pass", errors: [] };
2614
+ return {
2615
+ status: "fail",
2616
+ errors: [
2617
+ "brief-validation no agent boxes: the Test Plan carries a checkbox `[agent]` item — an agent never ticks a box or edits a PR body (Principal ruling, 2026-09-03, after PR #395). Render the `[agent]` half as a fenced command list instead; `vinaya pr report` runs each command from the PR head and writes its actual output into the AEG:EVIDENCE block. `[principal]` checkboxes are unaffected."
2618
+ ]
2619
+ };
2620
+ }
2621
+ function rolloutThresholdFor(message) {
2622
+ if (message.startsWith("brief-validation unpinned code claim:") || message.startsWith("brief-validation commands carry output:") || message.startsWith("brief-validation consumer tests:") || message.startsWith("brief-validation defeat cases:")) {
2623
+ return BRIEF_RULES_SINCE_PR;
2624
+ }
2625
+ if (message.startsWith("brief-validation no agent boxes:"))
2626
+ return AGENT_BOXES_REFUSED_SINCE_PR;
2627
+ return null;
2628
+ }
2629
+ function partitionBriefErrorsByRollout(errors, prNumber) {
2630
+ const blocking = [];
2631
+ const info = [];
2632
+ for (const e of errors) {
2633
+ const threshold = rolloutThresholdFor(e);
2634
+ const grandfathered = threshold !== null && prNumber !== null && prNumber < threshold;
2635
+ (grandfathered ? info : blocking).push(e);
2636
+ }
2637
+ return { blocking, info };
2638
+ }
2639
+ function extractFencedBlocks(text) {
2640
+ const re = /^[ \t]*(`{3,}|~{3,})([^\n]*)\n([\s\S]*?)^[ \t]*\1[ \t]*$/gm;
2641
+ const blocks = [];
2642
+ let m = re.exec(text);
2643
+ while (m !== null) {
2644
+ const lang = m[2].trim().toLowerCase().split(/\s+/)[0] ?? "";
2645
+ blocks.push({ start: m.index, end: m.index + m[0].length, lang, content: m[3] });
2646
+ m = re.exec(text);
2647
+ }
2648
+ return blocks;
2649
+ }
2650
+ function firstNonBlankLine(text) {
2651
+ const line = text.split(`
2652
+ `).find((l) => l.trim().length > 0);
2653
+ return line?.trim() ?? "";
2654
+ }
2655
+ var FILE_LINE_RE = /\b[\w./-]+\.(?:tsx?|jsx?|mjs|cjs|md|mdx|json|ya?ml|sh|py|toml):\d+\b/g;
2656
+ function checkNoUnpinnedCodeClaims(prBody) {
2657
+ const premiseText = premiseBlockText(prBody);
2658
+ const withoutPremise = premiseText ? prBody.replace(premiseText, "") : prBody;
2659
+ const scanned = stripCode(withoutPremise, { inlineSpans: "keep" });
2660
+ const matches = scanned.match(FILE_LINE_RE) ?? [];
2661
+ if (matches.length === 0)
2662
+ return { status: "pass", errors: [] };
2663
+ return {
2664
+ status: "fail",
2665
+ errors: matches.map((m) => `brief-validation unpinned code claim: "${m}" states a code fact by file-and-line reference outside a \`Premise:\` pin and outside a fenced code block — pin it in \`Premise:\`, or show the executed command and its output instead.`)
2666
+ };
2667
+ }
2668
+ function extractNumberedSection(prBody, num) {
2669
+ const startRe = new RegExp(`^#{1,4}\\s*(?:\\*\\*)?${num}[a-z]?\\.\\s`, "im");
2670
+ const start = startRe.exec(prBody);
2671
+ if (!start)
2672
+ return null;
2673
+ const afterStart = start.index + start[0].length;
2674
+ const rest = prBody.slice(afterStart);
2675
+ const next = /^#{1,4}\s/m.exec(rest);
2676
+ return next ? rest.slice(0, next.index) : rest;
2677
+ }
2678
+ function commandWordOf(line) {
2679
+ return line.trim().split(/\s+/)[0] ?? "";
2680
+ }
2681
+ function isCommandBlock(content) {
2682
+ return COMMAND_WORDS.includes(commandWordOf(firstNonBlankLine(content)));
2683
+ }
2684
+ function isStep0Block(content) {
2685
+ return firstNonBlankLine(content).startsWith("git worktree add");
2686
+ }
2687
+ function checkCommandsCarryOutput(prBody) {
2688
+ const errors = [];
2689
+ for (const [label2, num] of [
2690
+ ["5", 5],
2691
+ ["6", 6]
2692
+ ]) {
2693
+ const section = extractNumberedSection(prBody, num);
2694
+ if (!section)
2695
+ continue;
2696
+ const blocks = extractFencedBlocks(section);
2697
+ for (let i = 0;i < blocks.length; i++) {
2698
+ const block = blocks[i];
2699
+ if (isStep0Block(block.content) || !isCommandBlock(block.content))
2700
+ continue;
2701
+ const next = blocks[i + 1];
2702
+ if (!next || isCommandBlock(next.content)) {
2703
+ errors.push(`brief-validation commands carry output: §${label2} has a command block ("${firstNonBlankLine(block.content)}") not immediately followed by a non-command output block — the very next fenced block must hold that command's actual output, not another command.`);
2704
+ }
2705
+ }
2706
+ }
2707
+ return errors.length === 0 ? { status: "pass", errors: [] } : { status: "fail", errors };
2708
+ }
2709
+ var CONSUMER_TESTS_SENTINEL_RE = /consumer-tests\s*:\s*none\s*[-—–]\s*\S/i;
2710
+ function packagesNamedIn(text) {
2711
+ const re = /\bpackages\/([A-Za-z0-9_-]+)\//g;
2712
+ const pkgs = new Set;
2713
+ let m = re.exec(text);
2714
+ while (m !== null) {
2715
+ pkgs.add(m[1]);
2716
+ m = re.exec(text);
2717
+ }
2718
+ return [...pkgs];
2719
+ }
2720
+ function hasTestPathForConsumer(text, consumerDir) {
2721
+ const escaped = consumerDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2722
+ const re = new RegExp(`${escaped}\\/[\\w./-]*\\.test\\.[A-Za-z0-9]+`);
2723
+ return re.test(text);
2724
+ }
2725
+ function checkConsumerTests(prBody, consumersOf) {
2726
+ const section4 = extractNumberedSection(prBody, 4);
2727
+ if (!section4)
2728
+ return { status: "pass", errors: [] };
2729
+ if (CONSUMER_TESTS_SENTINEL_RE.test(section4))
2730
+ return { status: "pass", errors: [] };
2731
+ const errors = [];
2732
+ for (const pkg of packagesNamedIn(section4)) {
2733
+ for (const consumerDir of consumersOf(pkg)) {
2734
+ if (hasTestPathForConsumer(section4, consumerDir))
2735
+ continue;
2736
+ errors.push(`brief-validation consumer tests: §4 names a path under packages/${pkg}/, and ${consumerDir} depends on @attalabs/${pkg}, but no test path under ${consumerDir} is named in §4 — name one, or add \`consumer-tests: none — <reason>\`.`);
2737
+ }
2738
+ }
2739
+ return errors.length === 0 ? { status: "pass", errors: [] } : { status: "fail", errors };
2740
+ }
2741
+ var CHECK_NAME_RE = /\bcheck-[a-z0-9-]+(?:\.ts)?\b/;
2742
+ var FORGE_WRITE_COMMAND_RE = /\bgh\s+(?:pr|issue)\s+(?:create|merge|close|comment|edit|review)\b|\bgit\s+push\b|\bvinaya\s+pr\s+(?:create|report\s+--write)\b/i;
2743
+ var DEFEAT_CASES_RE = /defeat cases\s*:/i;
2744
+ function checkDefeatCases(prBody) {
2745
+ const section4 = extractNumberedSection(prBody, 4);
2746
+ if (!section4)
2747
+ return { status: "pass", errors: [] };
2748
+ if (!CHECK_NAME_RE.test(section4) && !FORGE_WRITE_COMMAND_RE.test(section4))
2749
+ return { status: "pass", errors: [] };
2750
+ const section6 = extractNumberedSection(prBody, 6) ?? "";
2751
+ if (DEFEAT_CASES_RE.test(section6))
2752
+ return { status: "pass", errors: [] };
2753
+ return {
2754
+ status: "fail",
2755
+ errors: [
2756
+ "brief-validation defeat cases: §4 names a check or a forge-writing command, but §6 carries no `Defeat cases:` line — name the inputs that would defeat this check, or that this command must not accidentally trigger on."
2757
+ ]
2758
+ };
2759
+ }
2529
2760
  function checkBriefSections(prBody, readTier, options = {}) {
2530
- const { requireClosesN = true } = options;
2761
+ const { requireClosesN = true, consumersOf = () => [] } = options;
2531
2762
  const results = [
2532
2763
  checkTierField(prBody, readTier),
2533
2764
  checkTestPlan(prBody),
@@ -2540,63 +2771,272 @@ function checkBriefSections(prBody, readTier, options = {}) {
2540
2771
  checkAutonomyClause(prBody),
2541
2772
  checkProjectField(prBody),
2542
2773
  checkForField(prBody),
2774
+ checkNoUnpinnedCodeClaims(prBody),
2775
+ checkNoAgentBoxes(prBody),
2776
+ checkCommandsCarryOutput(prBody),
2777
+ checkConsumerTests(prBody, consumersOf),
2778
+ checkDefeatCases(prBody),
2543
2779
  ...requireClosesN ? [checkClosesN(prBody)] : []
2544
2780
  ];
2545
2781
  return { errors: results.flatMap((r) => r.errors) };
2546
2782
  }
2547
- // ../../packages/aeg-core/src/issue-validation.ts
2548
- function hasRationaleField(body, labelPattern) {
2549
- const re = new RegExp(`(?:\\*\\*|^#{1,4}\\s+)\\s*(?:${labelPattern})`, "im");
2550
- return re.test(body);
2551
- }
2552
- var RATIONALE_FIELDS = [
2553
- { name: "Boundary", pattern: "Boundary" },
2554
- { name: "Sizing", pattern: "Sizing" },
2555
- { name: "Project(s) + blast radius", pattern: "Project\\(s\\)|Project(?:s)?\\s*\\+|blast radius" },
2556
- { name: "Dependency rationale", pattern: "Dependency rationale|Depends[- ]on" },
2557
- { name: "Traps to avoid", pattern: "Traps" },
2558
- { name: "Suggested agent-class", pattern: "(?:Suggested\\s+)?agent-class" },
2559
- { name: "Stop-and-escalate", pattern: "Stop-and-escalate" },
2560
- { name: "Docs to keep coherent", pattern: "Docs to keep coherent|§7" }
2783
+ // ../../packages/aeg-core/src/doctrine-portability.ts
2784
+ var DEFAULT_SHIPS_PREFIX = "aeg-root/";
2785
+ var STATIC_PORTABLE_PREFIXES = [
2786
+ "roles/",
2787
+ "contracts/",
2788
+ "skills/",
2789
+ ".github/",
2790
+ ".vinaya/",
2791
+ ".claude/",
2792
+ ".git/",
2793
+ ".husky/"
2561
2794
  ];
2562
- var DEPENDENCY_RATIONALE_FIELD_NAME = "Dependency rationale";
2563
- function checkIssueRationale(body) {
2564
- const errors = [];
2565
- for (const f of RATIONALE_FIELDS) {
2566
- if (!hasRationaleField(body, f.pattern)) {
2567
- errors.push(`issue-validation ${f.name}: rationale field not found in the Issue body — every task Issue carries the full Planner's rationale (aeg-root/contracts/planner-brief.md).`);
2568
- continue;
2569
- }
2570
- if (f.name === DEPENDENCY_RATIONALE_FIELD_NAME && !SECTION_HEADER.test(body)) {
2571
- errors.push(`issue-validation ${f.name}: rationale field found, but not in the form amend-deps requires — ` + "write `**Dependency rationale** — …`, not `**Dependency rationale:** …`. " + "`amendRationaleDeps` (the only sanctioned way to edit Depends-on/Conflicts-with) locates this " + "section by the exact anchor `**Dependency rationale**`; a colon inside the bold breaks that match " + "and the Issue becomes unamendable.");
2572
- }
2795
+ var EXEMPT_LITERALS = new Set([
2796
+ "path/inside/the/shipped/diff.ts",
2797
+ "path/inside/the/surface.ts",
2798
+ "apps/x/specs/..."
2799
+ ]);
2800
+ var CITED_PATH_PATTERN = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]*)+$/;
2801
+ var NON_PATH_TOP_SEGMENTS = new Set(["origin", "refs", "HEAD", "vinaya", "fix"]);
2802
+ var VENDOR_NAME_SOURCE = "\\bclaude code\\b|\\bclaude\\b|\\banthropic\\b|\\bchatgpt\\b|\\bopenai\\b|\\bgpt\\b|\\bgemini\\b|\\bcodex\\b|\\bgrok\\b|\\bdeepseek\\b|\\bopus\\b|\\bsonnet\\b|\\bhaiku\\b";
2803
+ var VENDOR_EXAMPLE_START = /<!--\s*AEG:VENDOR-EXAMPLE:START\s*-->/;
2804
+ var VENDOR_EXAMPLE_END = /<!--\s*AEG:VENDOR-EXAMPLE:END\s*-->/;
2805
+ function maskVendorExampleRegion(masked) {
2806
+ const start = VENDOR_EXAMPLE_START.exec(masked);
2807
+ if (!start)
2808
+ return masked;
2809
+ const afterStart = start.index + start[0].length;
2810
+ const end = VENDOR_EXAMPLE_END.exec(masked.slice(afterStart));
2811
+ if (!end)
2812
+ return masked;
2813
+ const regionEnd = afterStart + end.index + end[0].length;
2814
+ const region = masked.slice(start.index, regionEnd);
2815
+ const blanked = region.replace(/[^\n]/g, " ");
2816
+ return masked.slice(0, start.index) + blanked + masked.slice(regionEnd);
2817
+ }
2818
+ function extractVendorMentions(content) {
2819
+ const scoped = maskVendorExampleRegion(maskCode(content));
2820
+ const found = [];
2821
+ const pattern = new RegExp(VENDOR_NAME_SOURCE, "gi");
2822
+ let match = pattern.exec(scoped);
2823
+ while (match !== null) {
2824
+ found.push({ name: match[0], index: match.index });
2825
+ match = pattern.exec(scoped);
2573
2826
  }
2574
- return { status: errors.length > 0 ? "fail" : "pass", errors };
2575
- }
2576
- function isTaskIssueLabelSet(labels) {
2577
- return hasLabel("tranche", labels);
2827
+ return found;
2578
2828
  }
2579
- var TYPE_LABEL_IDS = LABELS.filter((l) => l.category === "type").map((l) => l.id);
2580
- function isControlCodePoint(codePoint) {
2581
- return codePoint <= 31 || codePoint >= 127 && codePoint <= 159;
2829
+ function extractCitedPaths(content) {
2830
+ const found = [];
2831
+ const spanPattern = /`([^`\n]+)`/g;
2832
+ let match = spanPattern.exec(content);
2833
+ while (match !== null) {
2834
+ const cited = match[1] ?? "";
2835
+ const topSegment = cited.slice(0, cited.indexOf("/"));
2836
+ if (CITED_PATH_PATTERN.test(cited) && !NON_PATH_TOP_SEGMENTS.has(topSegment)) {
2837
+ found.push({ cited, index: match.index });
2838
+ }
2839
+ match = spanPattern.exec(content);
2840
+ }
2841
+ return found;
2582
2842
  }
2583
- function isDisplayControlCodePoint(codePoint) {
2584
- return codePoint >= 8203 && codePoint <= 8207 || codePoint >= 8234 && codePoint <= 8238 || codePoint >= 8294 && codePoint <= 8297 || codePoint === 65279;
2843
+ function lineAtIndex(content, index) {
2844
+ let line = 1;
2845
+ for (let i = 0;i < index; i++) {
2846
+ if (content.charCodeAt(i) === 10)
2847
+ line++;
2848
+ }
2849
+ return line;
2585
2850
  }
2586
- var MESSAGE_VALUE_COUNT_MAX = 5;
2587
- function residueForMessage(values) {
2588
- const shown = values.slice(0, MESSAGE_VALUE_COUNT_MAX).map((v) => `\`${forMessage(v)}\``);
2589
- const rest = values.length - shown.length;
2590
- return rest > 0 ? `${shown.join(", ")} and ${rest} more` : shown.join(", ");
2851
+ function isPortable(cited, shipsPrefix) {
2852
+ if (cited.startsWith(shipsPrefix))
2853
+ return true;
2854
+ return STATIC_PORTABLE_PREFIXES.some((prefix) => cited.startsWith(prefix));
2591
2855
  }
2592
- var MESSAGE_VALUE_MAX = 64;
2593
- function forMessage(value) {
2594
- const kept = [];
2595
- for (const ch of value.replace(/\s+/g, " ")) {
2596
- const codePoint = ch.codePointAt(0) ?? 0;
2597
- if (isControlCodePoint(codePoint) || isDisplayControlCodePoint(codePoint))
2598
- continue;
2599
- kept.push(ch);
2856
+ function checkDoctrinePortability(files, shipsPrefix = DEFAULT_SHIPS_PREFIX) {
2857
+ const findings = [];
2858
+ for (const file of files) {
2859
+ if (!file.path.startsWith(shipsPrefix))
2860
+ continue;
2861
+ for (const { cited, index } of extractCitedPaths(file.content)) {
2862
+ if (EXEMPT_LITERALS.has(cited))
2863
+ continue;
2864
+ if (isPortable(cited, shipsPrefix))
2865
+ continue;
2866
+ findings.push({
2867
+ file: file.path,
2868
+ line: lineAtIndex(file.content, index),
2869
+ cited,
2870
+ message: `cites "${cited}", a path that only exists in the authoring repository — not portable doctrine`,
2871
+ kind: "path"
2872
+ });
2873
+ }
2874
+ for (const { name, index } of extractVendorMentions(file.content)) {
2875
+ findings.push({
2876
+ file: file.path,
2877
+ line: lineAtIndex(file.content, index),
2878
+ cited: name,
2879
+ message: `names "${name}" outside the one fenced vendor-example home (an <!-- AEG:VENDOR-EXAMPLE:START --> … <!-- AEG:VENDOR-EXAMPLE:END --> pair) — portable doctrine refers to a host generically everywhere else`,
2880
+ kind: "vendor-name"
2881
+ });
2882
+ }
2883
+ }
2884
+ return findings;
2885
+ }
2886
+
2887
+ // ../../packages/aeg-core/src/doctrine-no-procedures.ts
2888
+ function vendorExampleSpan(content) {
2889
+ const start = VENDOR_EXAMPLE_START.exec(content);
2890
+ if (!start)
2891
+ return null;
2892
+ const afterStart = start.index + start[0].length;
2893
+ const end = VENDOR_EXAMPLE_END.exec(content.slice(afterStart));
2894
+ if (!end)
2895
+ return null;
2896
+ return { start: start.index, end: afterStart + end.index + end[0].length };
2897
+ }
2898
+ function isCommandLine(line) {
2899
+ const word = line.trim().split(/\s+/)[0] ?? "";
2900
+ return COMMAND_WORDS.includes(word);
2901
+ }
2902
+ var SHELL_LANGS = new Set(["", "sh", "bash", "shell", "console"]);
2903
+ function procedureLineNumberAt(content, index) {
2904
+ return content.slice(0, index).split(`
2905
+ `).length;
2906
+ }
2907
+ function isTemplatePath(path) {
2908
+ return /(^|\/)templates\//.test(path);
2909
+ }
2910
+ function checkDoctrineNoProcedures(files) {
2911
+ const findings = [];
2912
+ for (const file of files) {
2913
+ if (isTemplatePath(file.path))
2914
+ continue;
2915
+ const span = vendorExampleSpan(file.content);
2916
+ for (const block of extractFencedBlocks(file.content)) {
2917
+ if (span && block.start >= span.start && block.end <= span.end)
2918
+ continue;
2919
+ if (!SHELL_LANGS.has(block.lang))
2920
+ continue;
2921
+ const commandLineCount = block.content.split(`
2922
+ `).filter(isCommandLine).length;
2923
+ if (commandLineCount >= 2) {
2924
+ findings.push({
2925
+ file: file.path,
2926
+ line: procedureLineNumberAt(file.content, block.start),
2927
+ message: `this fenced block has ${commandLineCount} shell-command lines — this sequence is a \`vinaya\` command, name it (or move it inside the \`AEG:VENDOR-EXAMPLE\` anchor / a \`templates/\` file).`
2928
+ });
2929
+ }
2930
+ }
2931
+ }
2932
+ return findings;
2933
+ }
2934
+ // ../../packages/aeg-core/src/pr-report-density.ts
2935
+ function headingSectionBody(prBody, heading) {
2936
+ const headingRe = new RegExp(`^##\\s+${heading}\\s*$`, "im");
2937
+ const m = headingRe.exec(prBody);
2938
+ if (!m)
2939
+ return null;
2940
+ const start = m.index + m[0].length;
2941
+ const rest = prBody.slice(start);
2942
+ const nextHeading = /^#{1,2}\s/m.exec(rest);
2943
+ return nextHeading ? rest.slice(0, nextHeading.index) : rest;
2944
+ }
2945
+ function stripAnchorBlocks(sectionText) {
2946
+ let result = sectionText;
2947
+ for (const field of ["CLOSES", "PROJECT", "TIER", "PREMISE", "TEST-PLAN", "EVIDENCE"]) {
2948
+ for (;; ) {
2949
+ const bounds = anchoredRegionBounds(result, field);
2950
+ if (!bounds)
2951
+ break;
2952
+ result = result.slice(0, bounds.outerStart) + result.slice(bounds.outerEnd);
2953
+ }
2954
+ }
2955
+ return result;
2956
+ }
2957
+ function paragraphCount(sectionText) {
2958
+ const stripped = stripCode(stripAnchorBlocks(sectionText)).trim();
2959
+ if (stripped.length === 0)
2960
+ return 0;
2961
+ return stripped.split(/\n[ \t]*\n/).map((block) => block.trim()).filter((block) => block.length > 0).length;
2962
+ }
2963
+ function checkSectionDensity(prBody, heading) {
2964
+ const body = headingSectionBody(prBody, heading);
2965
+ if (body === null)
2966
+ return { status: "pass", errors: [] };
2967
+ const count = paragraphCount(body);
2968
+ if (count <= 1)
2969
+ return { status: "pass", errors: [] };
2970
+ return {
2971
+ status: "fail",
2972
+ errors: [
2973
+ `pr-report-density ${heading}: "## ${heading}" holds ${count} paragraphs — the canonical PR-report form (aeg-root/roles/developer.md § PR body) requires exactly one. Collapse it to one paragraph; move the rest into a section of your own below the canonical four ("Add anything you want beneath the four sections", developer.md), a commit message, or the changeset — never into the AEG:EVIDENCE block, which is emitted only, never hand-typed.`
2974
+ ]
2975
+ };
2976
+ }
2977
+ function checkSummaryDensity(prBody) {
2978
+ return checkSectionDensity(prBody, "Summary");
2979
+ }
2980
+ function checkScopeDensity(prBody) {
2981
+ return checkSectionDensity(prBody, "Scope");
2982
+ }
2983
+ function checkPrReportDensity(prBody) {
2984
+ const results = [checkSummaryDensity(prBody), checkScopeDensity(prBody)];
2985
+ return { errors: results.flatMap((r) => r.errors) };
2986
+ }
2987
+ // ../../packages/aeg-core/src/issue-validation.ts
2988
+ function hasRationaleField(body, labelPattern) {
2989
+ const re = new RegExp(`(?:\\*\\*|^#{1,4}\\s+)\\s*(?:${labelPattern})`, "im");
2990
+ return re.test(body);
2991
+ }
2992
+ var RATIONALE_FIELDS = [
2993
+ { name: "Boundary", pattern: "Boundary" },
2994
+ { name: "Sizing", pattern: "Sizing" },
2995
+ { name: "Project(s) + blast radius", pattern: "Project\\(s\\)|Project(?:s)?\\s*\\+|blast radius" },
2996
+ { name: "Dependency rationale", pattern: "Dependency rationale|Depends[- ]on" },
2997
+ { name: "Traps to avoid", pattern: "Traps" },
2998
+ { name: "Suggested agent-class", pattern: "(?:Suggested\\s+)?agent-class" },
2999
+ { name: "Stop-and-escalate", pattern: "Stop-and-escalate" },
3000
+ { name: "Docs to keep coherent", pattern: "Docs to keep coherent|§7" }
3001
+ ];
3002
+ var DEPENDENCY_RATIONALE_FIELD_NAME = "Dependency rationale";
3003
+ function checkIssueRationale(body) {
3004
+ const errors = [];
3005
+ for (const f of RATIONALE_FIELDS) {
3006
+ if (!hasRationaleField(body, f.pattern)) {
3007
+ errors.push(`issue-validation ${f.name}: rationale field not found in the Issue body — every task Issue carries the full Planner's rationale (aeg-root/contracts/planner-brief.md).`);
3008
+ continue;
3009
+ }
3010
+ if (f.name === DEPENDENCY_RATIONALE_FIELD_NAME && !SECTION_HEADER.test(body)) {
3011
+ errors.push(`issue-validation ${f.name}: rationale field found, but not in the form amend-deps requires — ` + "write `**Dependency rationale** — …`, not `**Dependency rationale:** …`. " + "`amendRationaleDeps` (the only sanctioned way to edit Depends-on/Conflicts-with) locates this " + "section by the exact anchor `**Dependency rationale**`; a colon inside the bold breaks that match " + "and the Issue becomes unamendable.");
3012
+ }
3013
+ }
3014
+ return { status: errors.length > 0 ? "fail" : "pass", errors };
3015
+ }
3016
+ function isTaskIssueLabelSet(labels) {
3017
+ return hasLabel("tranche", labels);
3018
+ }
3019
+ var TYPE_LABEL_IDS = LABELS.filter((l) => l.category === "type").map((l) => l.id);
3020
+ function isControlCodePoint(codePoint) {
3021
+ return codePoint <= 31 || codePoint >= 127 && codePoint <= 159;
3022
+ }
3023
+ function isDisplayControlCodePoint(codePoint) {
3024
+ return codePoint >= 8203 && codePoint <= 8207 || codePoint >= 8234 && codePoint <= 8238 || codePoint >= 8294 && codePoint <= 8297 || codePoint === 65279;
3025
+ }
3026
+ var MESSAGE_VALUE_COUNT_MAX = 5;
3027
+ function residueForMessage(values) {
3028
+ const shown = values.slice(0, MESSAGE_VALUE_COUNT_MAX).map((v) => `\`${forMessage(v)}\``);
3029
+ const rest = values.length - shown.length;
3030
+ return rest > 0 ? `${shown.join(", ")} and ${rest} more` : shown.join(", ");
3031
+ }
3032
+ var MESSAGE_VALUE_MAX = 64;
3033
+ function forMessage(value) {
3034
+ const kept = [];
3035
+ for (const ch of value.replace(/\s+/g, " ")) {
3036
+ const codePoint = ch.codePointAt(0) ?? 0;
3037
+ if (isControlCodePoint(codePoint) || isDisplayControlCodePoint(codePoint))
3038
+ continue;
3039
+ kept.push(ch);
2600
3040
  }
2601
3041
  const cleaned = kept.join("").trim();
2602
3042
  if (cleaned.length === 0)
@@ -2795,6 +3235,17 @@ function checkD1(entries, issueToEntry, taskToEntry) {
2795
3235
  const depEntry = resolveDepEntry(dep, e.trancheSlug, issueToEntry, taskToEntry);
2796
3236
  if (!depEntry)
2797
3237
  continue;
3238
+ const sameTask = depEntry.trancheSlug === e.trancheSlug && depEntry.task.id === e.task.id;
3239
+ const sameIssue = depEntry.task.issue !== null && e.task.issue !== null && depEntry.task.issue === e.task.issue;
3240
+ if (sameTask || sameIssue) {
3241
+ failures.push({
3242
+ issue: e.task.issue,
3243
+ tranche: e.trancheSlug,
3244
+ task: e.task.id,
3245
+ reason: `INTERNAL: parsed a self-dependency (depends-on ${dep}) — this is a parser bug in parseRationaleDeps, not a real dependency. Please report it upstream.`
3246
+ });
3247
+ continue;
3248
+ }
2798
3249
  const depFacts = depEntry.facts;
2799
3250
  const depClosed = depFacts?.issueState === "closed";
2800
3251
  if (!depClosed) {
@@ -2938,41 +3389,24 @@ function checkClosesN2(branch, prBody, trancheFiles, taskIssueRefs) {
2938
3389
  }
2939
3390
  return { ok: true, expectedIssue };
2940
3391
  }
2941
- // ../../packages/aeg-core/src/verdict-extraction.ts
2942
- var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
2943
- function extractHeadSha(comment) {
2944
- const m = comment.match(HEAD_SHA_PATTERN);
2945
- return m ? m[1].toLowerCase() : null;
2946
- }
2947
- function extractVerdict(comments, valuePattern, missingLabel) {
2948
- const clearHits = comments.filter((c) => valuePattern.test(c));
2949
- if (clearHits.length > 0) {
2950
- const latest = clearHits[clearHits.length - 1];
2951
- const m = latest.match(valuePattern);
2952
- return {
2953
- value: m[1].toUpperCase().replace(/[_-]/g, " "),
2954
- headSha: extractHeadSha(latest),
2955
- danglingNote: null
2956
- };
2957
- }
2958
- return {
2959
- value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
2960
- headSha: null,
2961
- danglingNote: `no ${missingLabel} verdict comment found on this PR`
2962
- };
2963
- }
2964
- function extractCodeReviewVerdict(comments) {
2965
- return extractVerdict(comments, /^[ \t]*(?:\*{1,3}|_{1,3})?VERDICT:\s*(APPROVE|REQUEST[ _-]?CHANGES|LGTM)(?![A-Za-z0-9])/im, "code-reviewer");
2966
- }
2967
- function extractSecurityReviewVerdict(comments) {
2968
- return extractVerdict(comments, /^[ \t]*(?:\*{1,3}|_{1,3})?VERDICT:\s*(PASS|FAIL)(?![A-Za-z0-9])/im, "security-review");
2969
- }
2970
3392
  // ../../packages/aeg-core/src/review-gate.ts
2971
3393
  function isBoundToHead(extraction, headSha) {
2972
3394
  if (!extraction.headSha)
2973
3395
  return false;
2974
3396
  return headSha.toLowerCase().startsWith(extraction.headSha.toLowerCase());
2975
3397
  }
3398
+ function isBoundByPatchIdentity(extraction, headSha, patchIdOf) {
3399
+ if (patchIdOf === undefined || !extraction.headSha)
3400
+ return false;
3401
+ const judged = patchIdOf(extraction.headSha);
3402
+ const current = patchIdOf(headSha);
3403
+ if (judged === null || current === null)
3404
+ return false;
3405
+ return judged === current;
3406
+ }
3407
+ function isBoundToPatch(extraction, headSha, patchIdOf) {
3408
+ return isBoundToHead(extraction, headSha) || isBoundByPatchIdentity(extraction, headSha, patchIdOf);
3409
+ }
2976
3410
  function checkReviewGate(input) {
2977
3411
  const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
2978
3412
  const waived = isWaiverLabelActorVerified({
@@ -2988,6 +3422,8 @@ function checkReviewGate(input) {
2988
3422
  waived: true
2989
3423
  };
2990
3424
  }
3425
+ const reportedMechanicalChecks = input.mechanicalChecks.filter((c) => c.bucket !== "skipping" && c.bucket !== "neutral");
3426
+ const mechanicalChecksClean = reportedMechanicalChecks.length > 0 && reportedMechanicalChecks.every((c) => c.bucket === "pass");
2991
3427
  const verified = input.comments.filter((c) => isPrincipal(c.author, principalAllowlist));
2992
3428
  const ignoredCount = input.comments.filter((c) => !isPrincipal(c.author, principalAllowlist) && c.body.includes("VERDICT")).length;
2993
3429
  const verifiedBodies = verified.map((c) => c.body);
@@ -2995,12 +3431,12 @@ function checkReviewGate(input) {
2995
3431
  const security = extractSecurityReviewVerdict(verifiedBodies);
2996
3432
  const codeReviewClean = codeReview.value === "APPROVE";
2997
3433
  const securityClean = security.value === "PASS";
2998
- const codeReviewBound = isBoundToHead(codeReview, input.headSha);
2999
- const securityBound = isBoundToHead(security, input.headSha);
3000
- if (codeReviewClean && codeReviewBound && securityClean && securityBound) {
3434
+ const codeReviewBound = isBoundToPatch(codeReview, input.headSha, input.patchIdOf);
3435
+ const securityBound = isBoundToPatch(security, input.headSha, input.patchIdOf);
3436
+ if (codeReviewClean && codeReviewBound && securityClean && securityBound && mechanicalChecksClean) {
3001
3437
  return {
3002
3438
  verdict: "pass",
3003
- reason: `code-reviewer verdict is a clean APPROVE and security-review verdict is a clean PASS, both covering head ${input.headSha}.`,
3439
+ reason: `code-reviewer verdict is a clean APPROVE and security-review verdict is a clean PASS, both covering head ${input.headSha}, and every reported mechanical check is green.`,
3004
3440
  waived: false
3005
3441
  };
3006
3442
  }
@@ -3015,6 +3451,9 @@ function checkReviewGate(input) {
3015
3451
  } else if (!securityBound) {
3016
3452
  problems.push(`the newest security-review verdict covers ${security.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
3017
3453
  }
3454
+ if (!mechanicalChecksClean) {
3455
+ problems.push(reportedMechanicalChecks.length === 0 ? "no mechanical checks have reported for this head yet" : `mechanical check(s) not green: ${reportedMechanicalChecks.filter((c) => c.bucket !== "pass").map((c) => `${c.name} (${c.bucket})`).join(", ")}`);
3456
+ }
3018
3457
  const ignoredNote = ignoredCount > 0 ? ` ${ignoredCount} verdict-shaped comment(s) from authors outside the principal allowlist were ignored.` : "";
3019
3458
  return {
3020
3459
  verdict: "fail",
@@ -3294,72 +3733,6 @@ function compareToBaseline(current, baseline) {
3294
3733
  perTool
3295
3734
  };
3296
3735
  }
3297
- // ../../packages/aeg-core/src/doctrine-portability.ts
3298
- var DEFAULT_SHIPS_PREFIX = "aeg-root/";
3299
- var STATIC_PORTABLE_PREFIXES = [
3300
- "roles/",
3301
- "contracts/",
3302
- "skills/",
3303
- ".github/",
3304
- ".vinaya/",
3305
- ".claude/",
3306
- ".git/",
3307
- ".husky/"
3308
- ];
3309
- var EXEMPT_LITERALS = new Set([
3310
- "path/inside/the/shipped/diff.ts",
3311
- "path/inside/the/surface.ts",
3312
- "apps/x/specs/..."
3313
- ]);
3314
- var CITED_PATH_PATTERN = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]*)+$/;
3315
- var NON_PATH_TOP_SEGMENTS = new Set(["origin", "refs", "HEAD", "vinaya", "fix"]);
3316
- function extractCitedPaths(content) {
3317
- const found = [];
3318
- const spanPattern = /`([^`\n]+)`/g;
3319
- let match = spanPattern.exec(content);
3320
- while (match !== null) {
3321
- const cited = match[1] ?? "";
3322
- const topSegment = cited.slice(0, cited.indexOf("/"));
3323
- if (CITED_PATH_PATTERN.test(cited) && !NON_PATH_TOP_SEGMENTS.has(topSegment)) {
3324
- found.push({ cited, index: match.index });
3325
- }
3326
- match = spanPattern.exec(content);
3327
- }
3328
- return found;
3329
- }
3330
- function lineAtIndex(content, index) {
3331
- let line = 1;
3332
- for (let i = 0;i < index; i++) {
3333
- if (content.charCodeAt(i) === 10)
3334
- line++;
3335
- }
3336
- return line;
3337
- }
3338
- function isPortable(cited, shipsPrefix) {
3339
- if (cited.startsWith(shipsPrefix))
3340
- return true;
3341
- return STATIC_PORTABLE_PREFIXES.some((prefix) => cited.startsWith(prefix));
3342
- }
3343
- function checkDoctrinePortability(files, shipsPrefix = DEFAULT_SHIPS_PREFIX) {
3344
- const findings = [];
3345
- for (const file of files) {
3346
- if (!file.path.startsWith(shipsPrefix))
3347
- continue;
3348
- for (const { cited, index } of extractCitedPaths(file.content)) {
3349
- if (EXEMPT_LITERALS.has(cited))
3350
- continue;
3351
- if (isPortable(cited, shipsPrefix))
3352
- continue;
3353
- findings.push({
3354
- file: file.path,
3355
- line: lineAtIndex(file.content, index),
3356
- cited,
3357
- message: `cites "${cited}", a path that only exists in the authoring repository — not portable doctrine`
3358
- });
3359
- }
3360
- }
3361
- return findings;
3362
- }
3363
3736
  // ../../packages/aeg-core/src/reader-resolvable-prose.ts
3364
3737
  var SHIPS_PREFIX = "aeg-root/";
3365
3738
  function shipsArchivePrefix(shipsPrefix) {
@@ -3695,6 +4068,12 @@ function scanRetiredVocabulary(files) {
3695
4068
  function isHandClosedByRecognizedPrincipal(dep, principalAllowlist) {
3696
4069
  return dep.issueState === "closed" && dep.stateReason === "completed" && isPrincipal(dep.closedByActor ?? null, principalAllowlist);
3697
4070
  }
4071
+ function isSelfDependency(dep, task, issue) {
4072
+ if (dep.issue !== null && issue !== null && dep.issue === issue.number)
4073
+ return true;
4074
+ const bare = dep.id.trim().replace(/^#/, "");
4075
+ return bare.toLowerCase() === task.id.trim().toLowerCase();
4076
+ }
3698
4077
  function checkDispatchReadiness(input) {
3699
4078
  const { trancheSlug, task } = input;
3700
4079
  const taskLabel = `task ${task.id} (tranche ${trancheSlug})`;
@@ -3709,6 +4088,11 @@ function checkDispatchReadiness(input) {
3709
4088
  blockers.push(`dispatch-gate rationale: Issue #${input.issue.number} for ${taskLabel} fails the rationale gate (checkIssueRationale) — the Planner must complete the eight-field rationale before this task is dispatchable.`);
3710
4089
  }
3711
4090
  for (const dep of input.dependsOn) {
4091
+ if (isSelfDependency(dep, task, input.issue)) {
4092
+ const issueStr = input.issue !== null ? `#${input.issue.number}` : "?";
4093
+ blockers.push(`dispatch-gate INTERNAL: parsed a self-dependency for task ${task.id} (${issueStr}) — this is a parser bug in parseRationaleDeps, not a real dependency. Please report it upstream. Re-run once the rationale is corrected or the fix ships.`);
4094
+ continue;
4095
+ }
3712
4096
  if (dep.resolved === false) {
3713
4097
  blockers.push(`dispatch-gate depends-on: ${taskLabel} depends on "${dep.id}", which is UNRESOLVABLE — the resolver could not find a matching tranche/task/Issue for this edge (not a claim about merge status). Not dispatchable until the edge is corrected.`);
3714
4098
  continue;
@@ -3773,8 +4157,12 @@ function isNewDiskStateFile(path, status) {
3773
4157
  return ANY_DEPTH_TRANCHES_MD.test(path) || TOKENS_FILE.test(path);
3774
4158
  }
3775
4159
  // ../../packages/aeg-core/src/main-branch-refusal.ts
4160
+ function pushesAnyBranchRef(pushRefs) {
4161
+ return pushRefs.split(`
4162
+ `).map((line) => line.trim()).filter((line) => line.length > 0).some((line) => line.split(/\s+/)[2]?.startsWith("refs/heads/") === true);
4163
+ }
3776
4164
  function checkMainBranchRefusal(facts) {
3777
- const { currentSymbolicBranch, defaultBranch } = facts;
4165
+ const { currentSymbolicBranch, defaultBranch, pushRefs = null } = facts;
3778
4166
  if (currentSymbolicBranch === null)
3779
4167
  return null;
3780
4168
  if (defaultBranch === null) {
@@ -3786,6 +4174,8 @@ function checkMainBranchRefusal(facts) {
3786
4174
  };
3787
4175
  }
3788
4176
  if (currentSymbolicBranch === defaultBranch) {
4177
+ if (pushRefs !== null && !pushesAnyBranchRef(pushRefs))
4178
+ return null;
3789
4179
  return {
3790
4180
  reason: "on-default-branch",
3791
4181
  severity: "error",
@@ -3948,27 +4338,27 @@ function evaluateTestPlanGate(body, branch) {
3948
4338
  };
3949
4339
  }
3950
4340
  const checkboxLines = section.split(`
3951
- `).map((line) => line.trimStart()).filter((line) => /^[-*]\s+\[[ xX]\]/.test(line));
4341
+ `).map((line) => line.trimStart()).filter((line) => /^[-*]\s+\[[ xX]\]\s*\*{0,2}\[principal\]\*{0,2}/i.test(line));
3952
4342
  if (checkboxLines.length === 0) {
3953
4343
  return {
3954
4344
  verdict: "pass",
3955
4345
  messages: [
3956
- "Test Plan section found but has no checkbox items.",
4346
+ "Test Plan section found but has no `[principal]` checkbox items.",
3957
4347
  "PASS (advisory) — the section may be empty or expressed in prose; verification is the Principal's call."
3958
4348
  ]
3959
4349
  };
3960
4350
  }
3961
4351
  const unchecked = checkboxLines.filter((line) => /^[-*]\s+\[\s\]/.test(line));
3962
4352
  const checked = checkboxLines.filter((line) => /^[-*]\s+\[[xX]\]/.test(line));
3963
- const messages = [`Test Plan items: ${checked.length} ticked, ${unchecked.length} unticked.`];
4353
+ const messages = [`Test Plan [principal] items: ${checked.length} ticked, ${unchecked.length} unticked.`];
3964
4354
  if (unchecked.length > 0) {
3965
- messages.push("", "FAIL — the following Test Plan items are unticked:");
4355
+ messages.push("", "FAIL — the following [principal] Test Plan items are unticked:");
3966
4356
  for (const line of unchecked)
3967
4357
  messages.push(` ${line}`);
3968
- messages.push("", "Per aeg-root/roles/developer.md (Verification), a PR is not mergeable while any Test Plan box is unticked.", "- [agent] items: the Developer-agent posts the actual command output as evidence and ticks the box.", "- [principal] items: the Principal runs the item in a real signed-in browser and ticks the box.", "Note: editing the PR body does not retrigger most workflows. If your PR body changes do not surface here, push an empty commit to re-run.");
4358
+ messages.push("", "Per aeg-root/roles/developer.md (Verification), a PR is not mergeable while any [principal] Test Plan box is unticked.", "The Principal runs the item in a real signed-in browser and ticks the box.", "Note: editing the PR body does not retrigger most workflows. If your PR body changes do not surface here, push an empty commit to re-run.");
3969
4359
  return { verdict: "fail", messages };
3970
4360
  }
3971
- messages.push("All Test Plan items ticked — runtime verification complete.", "PASS.");
4361
+ messages.push("All [principal] Test Plan items ticked — runtime verification complete.", "PASS.");
3972
4362
  return { verdict: "pass", messages };
3973
4363
  }
3974
4364
  // ../../packages/aeg-core/src/workspace-escape.ts
@@ -4048,6 +4438,38 @@ function findWorkspaceEscapes(files, knownPaths, workspaceDirs = DEFAULT_WORKSPA
4048
4438
  }
4049
4439
  return findings;
4050
4440
  }
4441
+ // src/checks/contract.ts
4442
+ var CHECK_SCHEMA_VERSION = 1;
4443
+ function emitCheckError(error) {
4444
+ process.stderr.write(`${JSON.stringify(error)}
4445
+ `);
4446
+ }
4447
+
4448
+ // src/checks/runner.ts
4449
+ var activeKillers = new Set;
4450
+
4451
+ // src/lib/numstat.ts
4452
+ function summariseNumstat(numstat) {
4453
+ const rows = numstat.split(`
4454
+ `).filter((l) => l.trim() !== "");
4455
+ let added = 0;
4456
+ let deleted = 0;
4457
+ let binary = 0;
4458
+ for (const row of rows) {
4459
+ const [a, d] = row.split("\t");
4460
+ if (a === "-" || d === "-") {
4461
+ binary++;
4462
+ continue;
4463
+ }
4464
+ added += Number(a) || 0;
4465
+ deleted += Number(d) || 0;
4466
+ }
4467
+ const files = `${rows.length} file${rows.length === 1 ? "" : "s"} changed`;
4468
+ const bin = binary > 0 ? `, ${binary} binary` : "";
4469
+ return `${files}, ${added} insertion${added === 1 ? "" : "s"}(+), ${deleted} deletion${deleted === 1 ? "" : "s"}(-)${bin}`;
4470
+ }
4471
+ var EVIDENCE_SUMMARY_PREFIX = "Summary: ";
4472
+
4051
4473
  // src/checks/scan-context.ts
4052
4474
  var ZERO_WIDTH = /[\u200B-\u200D\u2060\uFEFF]/g;
4053
4475
  var HTML_ENTITIES = {
@@ -4101,10 +4523,386 @@ function summaryLineIndex(maskedRegion) {
4101
4523
  return index === -1 ? null : index;
4102
4524
  }
4103
4525
 
4526
+ // src/lib/package-root.ts
4527
+ import { existsSync } from "node:fs";
4528
+ import { dirname, join } from "node:path";
4529
+ import { fileURLToPath } from "node:url";
4530
+ function packageRoot(moduleUrl) {
4531
+ let dir = dirname(fileURLToPath(moduleUrl));
4532
+ while (!existsSync(join(dir, "package.json"))) {
4533
+ if (existsSync(join(dir, ".git")))
4534
+ break;
4535
+ const parent = dirname(dir);
4536
+ if (parent === dir)
4537
+ break;
4538
+ dir = parent;
4539
+ }
4540
+ return dir;
4541
+ }
4542
+
4543
+ // src/lib/config.ts
4544
+ import { execFileSync as execFileSync2 } from "node:child_process";
4545
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, realpathSync, writeFileSync } from "node:fs";
4546
+ import { homedir } from "node:os";
4547
+ import { dirname as dirname2, join as join2, resolve } from "node:path";
4548
+ import { z } from "zod";
4549
+
4550
+ // src/lib/agent-vendors.ts
4551
+ var AGENT_VENDORS = ["skills", "claude", "gemini"];
4552
+
4553
+ // src/lib/config.ts
4554
+ var EnvEntrySchema = z.union([
4555
+ z.literal(true),
4556
+ z.object({ optional: z.literal(true) }),
4557
+ z.object({ anyOf: z.array(z.string()).min(2) }),
4558
+ z.string()
4559
+ ]);
4560
+ var CheckEntrySchema = z.object({
4561
+ run: z.string(),
4562
+ scope: z.enum(["diff", "full"]),
4563
+ include: z.array(z.string()).optional(),
4564
+ args: z.array(z.string()).optional(),
4565
+ timeoutMs: z.number().optional(),
4566
+ env: z.record(z.string(), EnvEntrySchema).optional(),
4567
+ requiresOpenPr: z.boolean().optional(),
4568
+ ownWorkflow: z.boolean().optional()
4569
+ }).superRefine((entry2, ctx) => {
4570
+ if (!entry2.env)
4571
+ return;
4572
+ for (const [key, value] of Object.entries(entry2.env)) {
4573
+ if (typeof value !== "object" || value === null || !("anyOf" in value))
4574
+ continue;
4575
+ const members = value.anyOf;
4576
+ if (new Set(members).size !== members.length) {
4577
+ ctx.addIssue({
4578
+ code: z.ZodIssueCode.custom,
4579
+ message: `env.${key}.anyOf has duplicate members`,
4580
+ path: ["env", key, "anyOf"]
4581
+ });
4582
+ }
4583
+ if (!members.includes(key)) {
4584
+ ctx.addIssue({
4585
+ code: z.ZodIssueCode.custom,
4586
+ message: `env.${key}.anyOf must include "${key}" itself as a member`,
4587
+ path: ["env", key, "anyOf"]
4588
+ });
4589
+ }
4590
+ }
4591
+ });
4592
+ var RoleEntrySchema = z.object({
4593
+ contract: z.string().refine((p) => p.includes("/"), {
4594
+ message: 'must be a path (contain at least one "/"), not a bare filename'
4595
+ })
4596
+ });
4597
+ var BRIEF_BUILTINS = [
4598
+ "tier",
4599
+ "testPlan",
4600
+ "testPlanExclusivity",
4601
+ "principalPlaceholder",
4602
+ "surfaceMap",
4603
+ "docUpdateList",
4604
+ "worktreeStep0",
4605
+ "stopConditions",
4606
+ "autonomyClause",
4607
+ "project",
4608
+ "for",
4609
+ "closesN",
4610
+ "premiseCoverage",
4611
+ "issueRationale",
4612
+ "milestoneShape"
4613
+ ];
4614
+ var BriefSectionSchema = z.union([
4615
+ z.object({ builtin: z.enum(BRIEF_BUILTINS) }),
4616
+ z.object({ heading: z.string().min(1), name: z.string().optional() }),
4617
+ z.object({ field: z.string().min(1), name: z.string().optional() }),
4618
+ z.object({ phrase: z.string().min(1), name: z.string().optional() })
4619
+ ]);
4620
+ var BriefSchemaSchema = z.object({
4621
+ pr: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
4622
+ issue: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
4623
+ milestone: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
4624
+ ack: z.array(z.enum(BRIEF_BUILTINS)).optional()
4625
+ });
4626
+ function isSafeRepoRelPath(p) {
4627
+ if (p.length === 0)
4628
+ return false;
4629
+ if (p.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(p))
4630
+ return false;
4631
+ return p.split(/[\\/]/).every((seg) => seg !== ".." && seg !== "");
4632
+ }
4633
+ var SafeRepoRelPath = z.string().refine(isSafeRepoRelPath, {
4634
+ message: "must be a repo-root-relative path with no `..` segment or absolute root"
4635
+ });
4636
+ var CANONICAL_HOOK_BLOCK_PREFIXES = [".git/", ".husky/", ".vinaya/hooks/", ".claude/hooks/"];
4637
+ function isCanonicalHookBlockPath(p) {
4638
+ return CANONICAL_HOOK_BLOCK_PREFIXES.some((prefix) => p.startsWith(prefix) && p.length > prefix.length);
4639
+ }
4640
+ var ManagedHookBlockPath = SafeRepoRelPath.refine(isCanonicalHookBlockPath, {
4641
+ message: `must start with one of ${CANONICAL_HOOK_BLOCK_PREFIXES.join(", ")} (byte-exact, case-sensitive)`
4642
+ });
4643
+ var ManagedBlockRecordSchema = z.object({
4644
+ path: ManagedHookBlockPath,
4645
+ marker: z.string(),
4646
+ comment: z.enum(["hash", "html"])
4647
+ });
4648
+ var ManagedManifestSchema = z.object({
4649
+ version: z.number().int().positive(),
4650
+ files: z.array(SafeRepoRelPath),
4651
+ blocks: z.array(ManagedBlockRecordSchema),
4652
+ labels: z.array(z.string()),
4653
+ agents: z.array(z.enum(AGENT_VENDORS)).optional()
4654
+ });
4655
+ var ProjectEntrySchema = z.object({
4656
+ name: z.string().min(1),
4657
+ description: z.string().min(1).optional(),
4658
+ path: z.string().min(1).optional()
4659
+ });
4660
+ function parseTokensCollectDeclaration(value) {
4661
+ const m = value.trim().match(/^(\S+)\s+(\S+)$/);
4662
+ if (!m)
4663
+ return null;
4664
+ const [, interpreter, script] = m;
4665
+ if (!isSafeRepoRelPath(script))
4666
+ return null;
4667
+ return { interpreter, script };
4668
+ }
4669
+ var VinayaConfigSchema = z.object({
4670
+ rings: z.object({
4671
+ ring1_forgeWriteInterception: z.boolean(),
4672
+ ring2_asyncAudits: z.boolean()
4673
+ }).optional(),
4674
+ checks: z.record(z.string(), CheckEntrySchema).optional(),
4675
+ roles: z.record(z.string(), RoleEntrySchema).optional(),
4676
+ briefSchema: BriefSchemaSchema.optional(),
4677
+ managed: ManagedManifestSchema.optional(),
4678
+ principals: z.array(z.string()).min(1).optional(),
4679
+ releaseActor: z.string().min(1).optional(),
4680
+ ci: z.object({ setup: z.string().min(1) }).optional(),
4681
+ tokens: z.object({
4682
+ collect: z.string().min(1).refine((v) => parseTokensCollectDeclaration(v) !== null, {
4683
+ message: 'tokens.collect must be exactly "<interpreter> <repo-relative-script-path>" — two whitespace-separated tokens, no flags, no shell syntax, no quoting'
4684
+ })
4685
+ }).optional(),
4686
+ blastRadius: z.object({ extraDomains: z.array(z.string()).optional() }).optional(),
4687
+ proseGates: z.object({
4688
+ doctrineRoot: z.string().min(1).optional(),
4689
+ readerFacingPrefix: z.string().min(1).optional(),
4690
+ readerFacingSuffix: z.string().min(1).optional(),
4691
+ legacySlugDir: z.string().min(1).optional()
4692
+ }).optional(),
4693
+ projects: z.array(ProjectEntrySchema).optional()
4694
+ });
4695
+ var GLOBAL_VINAYA_HOME = join2(homedir(), ".vinaya");
4696
+ var GLOBAL_CONFIG_PATH = join2(GLOBAL_VINAYA_HOME, "config.json");
4697
+ var LOCAL_CONFIG_FILENAME = "vinaya.config.json";
4698
+ function findLocalConfig() {
4699
+ let dir = process.cwd();
4700
+ while (true) {
4701
+ const candidate = join2(dir, LOCAL_CONFIG_FILENAME);
4702
+ if (existsSync2(candidate))
4703
+ return candidate;
4704
+ if (existsSync2(join2(dir, ".git")))
4705
+ return null;
4706
+ const parent = dirname2(dir);
4707
+ if (parent === dir)
4708
+ return null;
4709
+ dir = parent;
4710
+ }
4711
+ }
4712
+ function configPath() {
4713
+ const local = findLocalConfig();
4714
+ if (local)
4715
+ return local;
4716
+ if (existsSync2(GLOBAL_CONFIG_PATH))
4717
+ return GLOBAL_CONFIG_PATH;
4718
+ return null;
4719
+ }
4720
+ function globalChecksIgnoredWarning(path) {
4721
+ return `${path}: "checks" registration in the global config is ignored — checks may only be registered from a repo-local vinaya.config.json.`;
4722
+ }
4723
+ function globalPrincipalsIgnoredWarning(path) {
4724
+ return `${path}: "principals" in the global config is ignored — principals may only be declared from a repo-local vinaya.config.json.`;
4725
+ }
4726
+ function globalRolesIgnoredWarning(path) {
4727
+ return `${path}: "roles" registration in the global config is ignored — roles may only be registered from a repo-local vinaya.config.json.`;
4728
+ }
4729
+ function globalReleaseActorIgnoredWarning(path) {
4730
+ return `${path}: "releaseActor" in the global config is ignored — releaseActor may only be declared from a repo-local vinaya.config.json.`;
4731
+ }
4732
+ function globalTokensCollectIgnoredWarning(path) {
4733
+ return `${path}: "tokens" in the global config is ignored — tokens.collect may only be declared from a repo-local vinaya.config.json.`;
4734
+ }
4735
+ function stripGlobalOnlyKeys(config, path) {
4736
+ if (path !== GLOBAL_CONFIG_PATH)
4737
+ return config;
4738
+ let result = config;
4739
+ if (result.checks && Object.keys(result.checks).length > 0) {
4740
+ console.error(`⚠ ${globalChecksIgnoredWarning(path)}`);
4741
+ result = { ...result, checks: undefined };
4742
+ }
4743
+ if (result.roles && Object.keys(result.roles).length > 0) {
4744
+ console.error(`⚠ ${globalRolesIgnoredWarning(path)}`);
4745
+ result = { ...result, roles: undefined };
4746
+ }
4747
+ if (result.principals && result.principals.length > 0) {
4748
+ console.error(`⚠ ${globalPrincipalsIgnoredWarning(path)}`);
4749
+ result = { ...result, principals: undefined };
4750
+ }
4751
+ if (result.releaseActor) {
4752
+ console.error(`⚠ ${globalReleaseActorIgnoredWarning(path)}`);
4753
+ result = { ...result, releaseActor: undefined };
4754
+ }
4755
+ if (result.tokens) {
4756
+ console.error(`⚠ ${globalTokensCollectIgnoredWarning(path)}`);
4757
+ result = { ...result, tokens: undefined };
4758
+ }
4759
+ return result;
4760
+ }
4761
+ function resolvePrincipalAllowlist(config) {
4762
+ return config?.principals ?? PRINCIPAL_ALLOWLIST;
4763
+ }
4764
+ function resolveReleaseActor(config) {
4765
+ return config?.releaseActor ?? DEFAULT_RELEASE_ACTOR;
4766
+ }
4767
+ function trustAnchorRepo() {
4768
+ const wellFormed = (slug) => /^[^/\s]+\/[^/\s]+$/.test(slug) ? slug : null;
4769
+ const fromRunner = process.env.GITHUB_REPOSITORY?.trim();
4770
+ if (fromRunner) {
4771
+ const validated = wellFormed(fromRunner);
4772
+ if (validated)
4773
+ return validated;
4774
+ }
4775
+ try {
4776
+ const url = execFileSync2("git", ["remote", "get-url", "origin"], {
4777
+ encoding: "utf-8",
4778
+ stdio: ["ignore", "pipe", "pipe"]
4779
+ }).trim();
4780
+ const m = url.match(/github\.com[:/]([^/]+)\/(.+?)(?:\.git)?\/?$/);
4781
+ return m?.[1] && m[2] ? wellFormed(`${m[1]}/${m[2]}`) : null;
4782
+ } catch {
4783
+ return null;
4784
+ }
4785
+ }
4786
+ function ghFetchTrustAnchorConfig() {
4787
+ const repo = trustAnchorRepo();
4788
+ if (!repo)
4789
+ throw new Error("could not resolve repo identity for the trust-anchor read");
4790
+ return execFileSync2("gh", ["api", `repos/${repo}/contents/${LOCAL_CONFIG_FILENAME}`, "--jq", ".content"], {
4791
+ encoding: "utf-8",
4792
+ stdio: ["ignore", "pipe", "pipe"]
4793
+ });
4794
+ }
4795
+ function firstLine(err) {
4796
+ return err?.message?.split(`
4797
+ `)[0] ?? "unknown error";
4798
+ }
4799
+ function isMissingFileError(err) {
4800
+ const stderr = err?.stderr;
4801
+ const haystack = [
4802
+ err?.message ?? "",
4803
+ typeof stderr === "string" ? stderr : stderr?.toString() ?? ""
4804
+ ].join(`
4805
+ `);
4806
+ return /\b404\b|not found/i.test(haystack);
4807
+ }
4808
+ function loadTrustAnchorConfig(fetcher = ghFetchTrustAnchorConfig) {
4809
+ 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.
4810
+ `);
4811
+ let base64;
4812
+ try {
4813
+ base64 = fetcher().trim();
4814
+ } catch (err) {
4815
+ if (!isMissingFileError(err))
4816
+ warn(firstLine(err));
4817
+ return null;
4818
+ }
4819
+ if (!base64)
4820
+ return null;
4821
+ try {
4822
+ const raw = Buffer.from(base64, "base64").toString("utf-8");
4823
+ const parsed = VinayaConfigSchema.safeParse(JSON.parse(raw));
4824
+ if (parsed.success)
4825
+ return parsed.data;
4826
+ warn("it did not satisfy the config schema");
4827
+ return null;
4828
+ } catch {
4829
+ warn("it was not readable as JSON");
4830
+ return null;
4831
+ }
4832
+ }
4833
+ function loadConfig() {
4834
+ const path = configPath();
4835
+ if (!path)
4836
+ return null;
4837
+ try {
4838
+ const raw = JSON.parse(readFileSync2(path, "utf-8"));
4839
+ return stripGlobalOnlyKeys(VinayaConfigSchema.parse(raw), path);
4840
+ } catch {
4841
+ return null;
4842
+ }
4843
+ }
4844
+ var TOKENS_COLLECT_TRUST_PATH = join2(GLOBAL_VINAYA_HOME, "tokens-collect-trust.json");
4845
+
4846
+ // src/commands/tokens.ts
4847
+ var USAGE = [
4848
+ 'Usage: vinaya tokens --phase "<task-id>: develop" --role Developer',
4849
+ " [--model <id>] [--transcript <path>] [--in <tokens-in> --out <tokens-out>]",
4850
+ "",
4851
+ " --transcript Read this transcript directly. Supported primary route — use it whenever you know",
4852
+ " which transcript is yours, and always in a repo with no track-transcript.sh hook.",
4853
+ " (omitted) Resolve via the Stop-hook pointer file, if this repo installs that hook.",
4854
+ " --in/--out Manual entry: the exact token figures, for a host whose usage arrives by some other",
4855
+ " means than a Claude Code transcript. Both required together; skips transcript",
4856
+ " resolution entirely.",
4857
+ "",
4858
+ "Usage: vinaya tokens --trust-collect",
4859
+ " Approves this repo's declared tokens.collect command, pinned to the script's exact current",
4860
+ " content, on this machine. Run this once before tokens.collect will ever execute — a changed",
4861
+ " interpreter, script path, or script CONTENT needs its own fresh approval. Does not take",
4862
+ " --phase/--role and never emits a Tokens: line."
4863
+ ].join(`
4864
+ `);
4865
+
4866
+ // src/commands/pr-report.ts
4867
+ var FAILING_STATUSES = new Set(["fail", "error", "timeout"]);
4868
+ function extractAgentCommandLines(prBody) {
4869
+ const located = locateTestPlanSection(prBody);
4870
+ if (!located.found)
4871
+ return [];
4872
+ if (/unit-tests-only/i.test(located.section))
4873
+ return [];
4874
+ const blocks = extractFencedBlocks(located.section);
4875
+ const first = blocks[0];
4876
+ if (!first)
4877
+ return [];
4878
+ return first.content.split(`
4879
+ `).map((l) => l.trim()).filter((l) => l.length > 0);
4880
+ }
4881
+ function agentCommandText(line) {
4882
+ const idx = line.indexOf("→");
4883
+ return (idx === -1 ? line : line.slice(0, idx)).trim();
4884
+ }
4885
+ var TOKEN_ROW_REMEDY = [
4886
+ "Nothing here established that this host cannot meter — only that this session resolved no",
4887
+ "transcript of its own. Report real figures by naming your own transcript:",
4888
+ "",
4889
+ " vinaya pr report --write <body-file> --transcript <path>",
4890
+ "",
4891
+ "or, on a host whose usage arrives by some other means, format figures you already hold with:",
4892
+ "",
4893
+ ' vinaya tokens --phase "<task-id>: develop" --role Developer --in <tokens-in> --out <tokens-out>',
4894
+ "",
4895
+ "and transcribe them into the `## Token report` table. That command prints a `Tokens:` line, not",
4896
+ "a table row — the table takes | Phase | Role | Agent/Model | Tokens in | Tokens out | Cost | Date |,",
4897
+ "and a line inside the block that does not start with `|` truncates the table for every row after it."
4898
+ ].join(`
4899
+ `);
4900
+ var USAGE2 = "Usage: vinaya pr report [--write <body-file> | --push <pr>] [--phase <phase>] [--role <role>] " + "[--model <id>] [--transcript <path>]";
4901
+
4104
4902
  // src/checks/evidence-fresh-logic.ts
4105
4903
  var HEAD_LINE = /^Head:\s*([0-9a-f]{7,40})\s*$/m;
4106
4904
  var FENCE = /```[^\n]*\n?([\s\S]*?)```/g;
4107
- function compareEvidenceBlock(resolved, resolvedHead, actualNumstat) {
4905
+ function compareEvidenceBlock(resolved, resolvedHead, actualNumstat, expectedGroupCCommandLines) {
4108
4906
  const region = resolved.region;
4109
4907
  const headMatch = region.match(HEAD_LINE);
4110
4908
  const fences = [...region.matchAll(FENCE)].map((m) => (m[1] ?? "").trim());
@@ -4142,6 +4940,33 @@ function compareEvidenceBlock(resolved, resolvedHead, actualNumstat) {
4142
4940
  ` block: ${JSON.stringify(storedSummary)}`,
4143
4941
  ` actual: ${JSON.stringify(expectedSummary)}`
4144
4942
  ].join(`
4943
+ `));
4944
+ }
4945
+ }
4946
+ const hasGroupCSection = /^### Group C — Test Plan commands/m.test(resolved.maskedRegion);
4947
+ if (expectedGroupCCommandLines !== undefined && hasGroupCSection) {
4948
+ const HEADING_PREFIX = /^####\s+C\d+:/;
4949
+ const COMMAND_HEADING_LINE = /^####\s+C\d+:\s+`(.*)`\s*$/;
4950
+ const maskedLines = resolved.maskedRegion.split(`
4951
+ `);
4952
+ const rawLines = region.split(`
4953
+ `);
4954
+ const storedCommands = [];
4955
+ for (let i = 0;i < maskedLines.length; i++) {
4956
+ if (!HEADING_PREFIX.test(maskedLines[i]))
4957
+ continue;
4958
+ const match = COMMAND_HEADING_LINE.exec(rawLines[i]);
4959
+ if (match)
4960
+ storedCommands.push(match[1]);
4961
+ }
4962
+ const expected = expectedGroupCCommandLines;
4963
+ const mismatch = storedCommands.length !== expected.length || storedCommands.some((c, i) => c !== expected[i]);
4964
+ if (mismatch) {
4965
+ errors.push([
4966
+ "evidence-fresh: Group C's command headings do not match the PR body's own §9 Test Plan list.",
4967
+ ` block: ${JSON.stringify(storedCommands)}`,
4968
+ ` expected: ${JSON.stringify(expected)}`
4969
+ ].join(`
4145
4970
  `));
4146
4971
  }
4147
4972
  }
@@ -4152,7 +4977,7 @@ function compareEvidenceBlock(resolved, resolvedHead, actualNumstat) {
4152
4977
  var CHECK_NAME = "evidence-fresh";
4153
4978
  function git(args) {
4154
4979
  try {
4155
- return execFileSync2("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
4980
+ return execFileSync3("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
4156
4981
  } catch {
4157
4982
  return "";
4158
4983
  }
@@ -4171,7 +4996,7 @@ class GitCommandError extends Error {
4171
4996
  }
4172
4997
  function gitStrict(args) {
4173
4998
  try {
4174
- return execFileSync2("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
4999
+ return execFileSync3("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
4175
5000
  } catch (err) {
4176
5001
  const stderr = err.stderr;
4177
5002
  throw new GitCommandError(args, String(stderr ?? err.message).trim() || "non-zero exit");
@@ -4189,7 +5014,7 @@ function resolveMergeBase(head) {
4189
5014
  }
4190
5015
  function fetchHeadSha(prNumber) {
4191
5016
  try {
4192
- const out = execFileSync2("gh", ["pr", "view", String(prNumber), "--json", "headRefOid", "-q", ".headRefOid"], {
5017
+ const out = execFileSync3("gh", ["pr", "view", String(prNumber), "--json", "headRefOid", "-q", ".headRefOid"], {
4193
5018
  encoding: "utf8",
4194
5019
  stdio: ["ignore", "pipe", "pipe"]
4195
5020
  }).trim();
@@ -4247,7 +5072,8 @@ function main() {
4247
5072
  });
4248
5073
  process.exit(1);
4249
5074
  }
4250
- const result = compareEvidenceBlock(resolved, resolvedHead, actualNumstat);
5075
+ const expectedGroupCCommandLines = extractAgentCommandLines(body).map(agentCommandText);
5076
+ const result = compareEvidenceBlock(resolved, resolvedHead, actualNumstat, expectedGroupCCommandLines);
4251
5077
  if (result.status === "fail") {
4252
5078
  for (const message of result.errors) {
4253
5079
  emitCheckError({