@attalabs/vinaya 0.20.1 → 0.22.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 (152) hide show
  1. package/README.md +76 -1
  2. package/aeg-root/enforcement.md +15 -7
  3. package/aeg-root/milestone-model.md +3 -1
  4. package/aeg-root/roles/architect.md +2 -2
  5. package/aeg-root/roles/archivist.md +2 -2
  6. package/aeg-root/roles/developer.md +3 -2
  7. package/aeg-root/roles/planner.md +2 -2
  8. package/aeg-root/roles/principal.md +1 -0
  9. package/aeg-root/roles/tranche-archivist.md +2 -2
  10. package/aeg-root/templates/issue-rationale-template.md +4 -4
  11. package/aeg-root/templates/pr-report-template.md +2 -0
  12. package/aeg-root/tranche-model.md +4 -4
  13. package/dist/checks/bin/check-body-bare-digits.js +392 -10
  14. package/dist/checks/bin/check-branch-topology.js +382 -2
  15. package/dist/checks/bin/check-brief-shape.js +357 -2
  16. package/dist/checks/bin/check-changeset-coverage.js +4262 -0
  17. package/dist/checks/bin/check-closes-n.js +382 -2
  18. package/dist/checks/bin/check-coherence.js +416 -8
  19. package/dist/checks/bin/check-dead-branch-push.js +357 -2
  20. package/dist/checks/bin/check-dispatch-readiness.js +492 -25
  21. package/dist/checks/bin/check-doc-coverage-push.js +392 -9
  22. package/dist/checks/bin/check-doc-coverage.js +392 -9
  23. package/dist/checks/bin/check-doctrine-portability.js +391 -8
  24. package/dist/checks/bin/check-evidence-fresh.js +357 -2
  25. package/dist/checks/bin/check-first-push-dispatch.js +481 -17
  26. package/dist/checks/bin/check-issue-assignment.js +382 -2
  27. package/dist/checks/bin/check-main-branch-refusal.js +357 -2
  28. package/dist/checks/bin/check-no-disk-state.js +357 -2
  29. package/dist/checks/bin/check-quoted-command.js +4514 -0
  30. package/dist/checks/bin/check-reader-resolvable-prose.js +430 -21
  31. package/dist/checks/bin/check-registry-gates.js +410 -7
  32. package/dist/checks/bin/check-retired-vocabulary.js +422 -13
  33. package/dist/checks/bin/check-review-gate.js +389 -6
  34. package/dist/checks/bin/check-single-plan-pr.js +357 -2
  35. package/dist/checks/bin/check-test-plan.js +357 -2
  36. package/dist/checks/bin/check-token-collection-wired.js +4081 -0
  37. package/dist/checks/bin/check-token-report.js +4096 -0
  38. package/dist/checks/bin/check-workspace-escape.js +359 -4
  39. package/dist/index.js +1607 -641
  40. package/package.json +1 -1
  41. package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
  42. package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
  43. package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
  44. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
  45. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
  46. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  47. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  48. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  49. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  50. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  51. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
  52. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js +2 -2
  53. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  54. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  55. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
  56. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
  57. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js +2 -2
  58. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
  59. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
  60. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
  61. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js +2 -2
  62. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
  63. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
  64. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
  65. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js +3 -3
  66. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
  67. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
  68. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
  69. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js +3 -3
  70. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
  71. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
  72. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
  73. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js +3 -3
  74. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
  75. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
  76. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
  77. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js +3 -3
  78. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
  79. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
  80. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
  81. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js +3 -3
  82. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
  83. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
  84. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
  85. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js +2 -2
  86. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
  87. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
  88. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/[root-of-the-server]__02a-3g8._.js +1 -1
  89. 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
  90. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0053k9k._.js +1 -1
  91. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0112h-k._.js +1 -1
  92. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__02c71kv._.js +1 -1
  93. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__040smoa._.js +1 -1
  94. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__16cuydh._.js → [root-of-the-server]__069-_41._.js} +1 -1
  95. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0cxtq46._.js +25 -18
  96. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0o771t1._.js +1 -1
  97. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0puovz5._.js +1 -1
  98. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__0ykun-j._.js → [root-of-the-server]__0z6dav-._.js} +3 -3
  99. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__18kqy4q._.js +1 -1
  100. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__19jyp3m._.js +1 -1
  101. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1hs0dcu._.js +1 -1
  102. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1wc4-ip._.js +1 -1
  103. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1widio3._.js +1 -1
  104. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1yoa8tx._.js +3 -0
  105. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_03x_w6q._.js +1 -1
  106. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0gvm3og._.js +1 -1
  107. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0lwxg63._.js +1 -1
  108. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0o54suy._.js +1 -1
  109. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0wxycau._.js +1 -1
  110. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0x999pb._.js +1 -1
  111. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1-6rzip._.js +1 -1
  112. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +2 -2
  113. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_0zys35s._.js → _1mqrlbl._.js} +2 -2
  114. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_1c9b9j-._.js → _1mrtj0f._.js} +2 -2
  115. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1n0cnq-._.js +1 -1
  116. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{apps_vinaya-studio_web_src_app_studio_18kqbvs._.js → apps_vinaya-studio_web_src_app_studio_06it6s-._.js} +2 -2
  117. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_app_studio_projects_[name]_tranches_[slug]_05ufo8i._.js +1 -1
  118. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
  119. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_animate_components_index_ts_186fje1._.js +1 -1
  120. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_brutal_components_index_ts_0n_lcc4._.js +1 -1
  121. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_retro_components_index_ts_0wvpm_z._.js +1 -1
  122. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_retro_components_index_ts_1_cfi48._.js +1 -1
  123. package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
  124. package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
  125. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
  126. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
  127. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/00pi8vhg8w6gi.js +1 -0
  128. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/03q0q9n6-xah8.js +1 -0
  129. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0vc2u252q5q6o.js +1 -0
  130. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/13g16cu00g570.js +1 -0
  131. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1lk7h-0buf4ab.js → 1hufpuody6vjv.js} +1 -1
  132. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1sfxfunirkcr6.js +1 -0
  133. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{29-foak16tjfu.js → 206ajg8f1a-_h.js} +1 -1
  134. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0ea8-8nopz29h.js → 267o-tsrnuwnv.js} +1 -1
  135. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0msr2a1i-x-br.js → 279qq4twinc97.js} +1 -1
  136. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{112g08xpx3kro.js → 2ka8dg687auhj.js} +1 -1
  137. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0up5x3jg-cwov.js → 2rn89uwtf2lwc.js} +1 -1
  138. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{2twnwe77d4n-m.js → 37hnluhzj9bei.js} +4 -4
  139. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/3qc67qhcvbg0o.css +1 -0
  140. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{30ojnsx3p4lp4.js → 3zugx9esh2x51.js} +1 -1
  141. package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
  142. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1rku4gz._.js +0 -3
  143. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0_jnwbarv-cqk.js +0 -1
  144. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0la0hhmjprdxm.css +0 -1
  145. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/25j-y57ng11hl.js +0 -1
  146. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/30lyvn0yzwdiz.js +0 -1
  147. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/35u8inxa732is.js +0 -1
  148. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/3r5efmleneaco.js +0 -1
  149. /package/studio-standalone/_node_modules/@attalabs/{aeg-core → vinaya/studio-standalone/_node_modules/@attalabs/aeg-core}/bin/verify-coherence.ts +0 -0
  150. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{b2VfZK2AItZwlghK_pUN0 → Fmyc42qzLwFcWWHLLNy-G}/_buildManifest.js +0 -0
  151. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{b2VfZK2AItZwlghK_pUN0 → Fmyc42qzLwFcWWHLLNy-G}/_clientMiddlewareManifest.js +0 -0
  152. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{b2VfZK2AItZwlghK_pUN0 → Fmyc42qzLwFcWWHLLNy-G}/_ssgManifest.js +0 -0
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { dirname as dirname9, join as join27 } from "node:path";
6
6
  import { fileURLToPath as fileURLToPath2 } from "node:url";
7
7
 
8
8
  // src/commands/archive.ts
9
- import { execFileSync as execFileSync4 } from "node:child_process";
9
+ import { execFileSync as execFileSync5 } 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);
@@ -164,6 +164,16 @@ function maskIndentedCode(body, fill) {
164
164
 
165
165
  // ../../packages/aeg-forge-state/src/parse-rationale-deps.ts
166
166
  var SECTION_HEADER = /\*\*Dependency rationale\*\*/i;
167
+ // ../../packages/aeg-forge-state/src/check-milestone-attachment.ts
168
+ function checkMilestoneAttachment(labeledIssueNumbers, attachedIssueNumbers) {
169
+ const attachedSet = new Set(attachedIssueNumbers);
170
+ const labeledSet = new Set(labeledIssueNumbers);
171
+ const unattached = labeledIssueNumbers.filter((n) => !attachedSet.has(n));
172
+ const foreign = attachedIssueNumbers.filter((n) => !labeledSet.has(n));
173
+ if (unattached.length === 0 && foreign.length === 0)
174
+ return { status: "clean" };
175
+ return { status: "mismatch", unattached, foreign };
176
+ }
167
177
  // ../../packages/aeg-forge-state/src/gh.ts
168
178
  import { execFile, execFileSync } from "node:child_process";
169
179
  import { promisify } from "node:util";
@@ -172,6 +182,12 @@ var systemEnv = {
172
182
  ...process.env,
173
183
  PATH: [process.env.PATH, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"].filter(Boolean).join(":")
174
184
  };
185
+ function run(args) {
186
+ return execFileSync("gh", args, { encoding: "utf8", env: systemEnv });
187
+ }
188
+ function ghApiGet(path) {
189
+ return JSON.parse(run(["api", path]));
190
+ }
175
191
 
176
192
  // ../../packages/aeg-forge-state/src/labels.ts
177
193
  var LABELS = [
@@ -435,6 +451,47 @@ function parseFieldValue(value) {
435
451
  function projectsFromBody(body) {
436
452
  return projectFieldFromBody(body).names;
437
453
  }
454
+
455
+ // ../../packages/aeg-forge-state/src/fetch-milestone.ts
456
+ var INTENTS_HEADING = /^#{1,6}\s*Tranche intents\s*$/im;
457
+ var NEXT_HEADING = /^#{1,6}\s+\S/m;
458
+ var INTENT_BULLET = /^-\s+([a-z0-9][a-z0-9-]*)\s*:\s*(.+)$/i;
459
+ function intentGoalForSlug(description, slug) {
460
+ const text = stripCode(description, { inlineSpans: "keep" });
461
+ const start = text.match(INTENTS_HEADING);
462
+ if (!start || start.index === undefined)
463
+ return "";
464
+ const rest = text.slice(start.index + start[0].length);
465
+ const next = rest.match(NEXT_HEADING);
466
+ const section = rest.slice(0, next && next.index !== undefined ? next.index : rest.length);
467
+ for (const line of section.split(`
468
+ `)) {
469
+ const trimmed = line.trim();
470
+ if (trimmed.length === 0)
471
+ continue;
472
+ const m = trimmed.match(INTENT_BULLET);
473
+ if (m && (m[1] ?? "").toLowerCase() === slug.toLowerCase())
474
+ return (m[2] ?? "").trim();
475
+ }
476
+ return "";
477
+ }
478
+ function matchesLegacyMilestone(milestones, slug) {
479
+ return milestones.find((m) => m.title === slug) ?? null;
480
+ }
481
+ function resolveMilestoneAttachTarget(milestones, slug) {
482
+ const legacy = matchesLegacyMilestone(milestones, slug);
483
+ if (legacy?.state === "open")
484
+ return { number: legacy.number, title: legacy.title };
485
+ const intentMatch = milestones.find((m) => m.state === "open" && intentGoalForSlug(m.description ?? "", slug) !== "");
486
+ return intentMatch ? { number: intentMatch.number, title: intentMatch.title } : null;
487
+ }
488
+ function findMilestoneAttachTargetForSlug(owner, repo, slug) {
489
+ const milestones = ghApiGet(`repos/${owner}/${repo}/milestones?state=all&per_page=100`);
490
+ return resolveMilestoneAttachTarget(milestones, slug);
491
+ }
492
+ function hasExplicitMilestoneFlag(args) {
493
+ return args.some((a) => a === "--milestone" || a === "-m" || a.startsWith("--milestone="));
494
+ }
438
495
  // ../../packages/aeg-forge-state/src/github-token.ts
439
496
  import { execFile as execFile2 } from "node:child_process";
440
497
  import { promisify as promisify2 } from "node:util";
@@ -732,14 +789,65 @@ var DERIVED_STATUSES = [
732
789
  ];
733
790
  var DERIVABLE_STATUSES = DERIVED_STATUSES.filter((s) => s !== "backlog");
734
791
  // ../../packages/aeg-core/src/report-tokens.ts
735
- function formatTokensLine(input) {
736
- const model = input.modelOverride ?? input.summary?.model ?? "—";
737
- if (!input.summary) {
738
- return `Tokens: ${input.phase} — ${input.role} — ${model} — —`;
792
+ function stripNewlines(value) {
793
+ return value.replace(/\r?\n/g, " ");
794
+ }
795
+ function sanitizeForTableCell(value) {
796
+ return stripNewlines(value).replace(/\|/g, "\\|");
797
+ }
798
+ var DASH_LOOKALIKES = {
799
+ "-": "‑",
800
+ "–": "‒",
801
+ "—": "―"
802
+ };
803
+ var DASH_CHARS = new Set(Object.keys(DASH_LOOKALIKES));
804
+ var PIPE_LOOKALIKE = "|";
805
+ function sanitizeForTokensLine(value) {
806
+ const stripped = stripNewlines(value);
807
+ let result = "";
808
+ for (let i = 0;i < stripped.length; i++) {
809
+ const ch = stripped[i];
810
+ if (ch === "|") {
811
+ result += PIPE_LOOKALIKE;
812
+ continue;
813
+ }
814
+ if (DASH_CHARS.has(ch)) {
815
+ const leftReachable = i === 0 || /\s/.test(stripped[i - 1]);
816
+ const rightReachable = i === stripped.length - 1 || /\s/.test(stripped[i + 1]);
817
+ if (leftReachable && rightReachable) {
818
+ result += DASH_LOOKALIKES[ch];
819
+ continue;
820
+ }
821
+ }
822
+ result += ch;
739
823
  }
824
+ return result;
825
+ }
826
+ function renderCells(input) {
827
+ const model = input.modelOverride ?? input.summary?.model ?? null;
828
+ if (!input.summary)
829
+ return { model, tokensIn: "—", tokensOut: "—" };
740
830
  const { inputTokens, cacheCreationInputTokens, cacheReadInputTokens, outputTokens } = input.summary.components;
741
831
  const tokensIn = inputTokens + cacheCreationInputTokens + cacheReadInputTokens;
742
- return `Tokens: ${input.phase} ${input.role} ${model} ${tokensIn}/${outputTokens}/—`;
832
+ return { model, tokensIn: String(tokensIn), tokensOut: String(outputTokens) };
833
+ }
834
+ function formatTokensLine(input) {
835
+ const { model, tokensIn, tokensOut } = renderCells(input);
836
+ const phase = sanitizeForTokensLine(input.phase);
837
+ const role = sanitizeForTokensLine(input.role);
838
+ const safeModel = model === null ? "—" : sanitizeForTokensLine(model);
839
+ if (!input.summary) {
840
+ return `Tokens: ${phase} — ${role} — ${safeModel} — —`;
841
+ }
842
+ return `Tokens: ${phase} — ${role} — ${safeModel} — ${tokensIn}/${tokensOut}/—`;
843
+ }
844
+ function formatTokenReportRow(input) {
845
+ const { model, tokensIn, tokensOut } = renderCells(input);
846
+ const phase = sanitizeForTableCell(input.phase);
847
+ const role = sanitizeForTableCell(input.role);
848
+ const safeModel = model === null ? "—" : sanitizeForTableCell(model);
849
+ const date = sanitizeForTableCell(input.date);
850
+ return `| ${phase} | ${role} | ${safeModel} | ${tokensIn} | ${tokensOut} | — | ${date} |`;
743
851
  }
744
852
  function formatBreakdown(summary) {
745
853
  const { inputTokens, cacheCreationInputTokens, cacheReadInputTokens, outputTokens } = summary.components;
@@ -754,6 +862,7 @@ function formatBreakdown(summary) {
754
862
  `);
755
863
  }
756
864
  // ../../packages/aeg-core/src/claude-code-transcript.ts
865
+ import { createHash } from "node:crypto";
757
866
  function summarizeTranscript(jsonl) {
758
867
  const seen = new Set;
759
868
  const components = {
@@ -802,48 +911,79 @@ function numberOr(value, fallback) {
802
911
  function sanitizeKey(value) {
803
912
  return value.replace(/[^A-Za-z0-9]+/g, "-");
804
913
  }
914
+ function collisionResistantKey(value) {
915
+ const digest = createHash("sha256").update(value).digest("hex");
916
+ return `${sanitizeKey(value)}-${digest}`;
917
+ }
805
918
  function transcriptPointerPath(projectDir, tmpDir) {
919
+ return `${tmpDir}/claude-transcript-${collisionResistantKey(projectDir)}.txt`;
920
+ }
921
+ function legacyTranscriptPointerPath(projectDir, tmpDir) {
806
922
  return `${tmpDir}/claude-transcript-${sanitizeKey(projectDir)}.txt`;
807
923
  }
808
924
  function resolvePointer(explicitTranscriptPath, deps) {
809
925
  if (explicitTranscriptPath)
810
- return { path: explicitTranscriptPath };
926
+ return { path: explicitTranscriptPath, corroborated: true };
811
927
  const projectDir = deps.env.CLAUDE_PROJECT_DIR ?? deps.cwd;
812
928
  const tmpDir = deps.env.TMPDIR ?? "/tmp";
813
- const pointerPath = transcriptPointerPath(projectDir, tmpDir);
814
- if (!deps.exists(pointerPath)) {
929
+ const primaryPointerPath = transcriptPointerPath(projectDir, tmpDir);
930
+ const legacyPointerPath = legacyTranscriptPointerPath(projectDir, tmpDir);
931
+ const currentSessionId = deps.env.CLAUDE_CODE_SESSION_ID;
932
+ const pointerPath = deps.exists(primaryPointerPath) ? primaryPointerPath : deps.exists(legacyPointerPath) ? legacyPointerPath : undefined;
933
+ if (!pointerPath) {
815
934
  return {
816
- error: `No transcript pointer at ${pointerPath} and no --transcript given. ` + "Either this repo installs no track-transcript.sh Stop hook (lacking one is not a defect — " + "name the transcript directly instead), or no session has completed a turn yet."
935
+ pointerExisted: false,
936
+ corroborated: false,
937
+ error: `No transcript pointer at ${primaryPointerPath} (nor its pre-migration name ${legacyPointerPath}) ` + "and no --transcript given. Either this repo installs no track-transcript.sh Stop hook (lacking one " + "is not a defect — name the transcript directly instead), or no session has completed a turn yet."
817
938
  };
818
939
  }
819
940
  let contents;
820
941
  try {
821
- contents = deps.readFile(pointerPath).trim();
942
+ contents = deps.readFile(pointerPath).replace(/\r?\n+$/, "");
822
943
  } catch (err) {
823
- return { error: `Transcript pointer at ${pointerPath} could not be read: ${err.message}` };
944
+ return {
945
+ pointerExisted: true,
946
+ oursByLocation: true,
947
+ error: `Transcript pointer at ${pointerPath} could not be read: ${err.message}. ` + "Remove that file to clear this — the Stop hook rewrites it on the next turn."
948
+ };
824
949
  }
825
950
  const [pointerSessionId, transcriptPath] = contents.split("\t");
826
951
  if (!transcriptPath) {
827
- return { error: `Transcript pointer file ${pointerPath} is malformed: "${contents}"` };
952
+ return {
953
+ pointerExisted: true,
954
+ oursByLocation: true,
955
+ error: `Transcript pointer file ${pointerPath} is malformed: "${contents.slice(0, 120)}${contents.length > 120 ? "…" : ""}". ` + "Remove that file to clear this — the Stop hook rewrites it on the next turn."
956
+ };
828
957
  }
829
- const currentSessionId = deps.env.CLAUDE_CODE_SESSION_ID;
830
958
  if (currentSessionId && pointerSessionId && currentSessionId !== pointerSessionId) {
831
959
  return {
960
+ pointerExisted: true,
961
+ corroborated: false,
832
962
  error: `Transcript pointer at ${pointerPath} is stale: written for session ${pointerSessionId}, ` + `but this session is ${currentSessionId}. Name your own transcript with --transcript instead of ` + "reporting another session's figures as yours."
833
963
  };
834
964
  }
835
- return { path: transcriptPath };
965
+ return { path: transcriptPath, corroborated: Boolean(currentSessionId && pointerSessionId) };
836
966
  }
837
967
  function resolveMeteringCapability(deps, explicitTranscriptPath) {
838
968
  const resolved = resolvePointer(explicitTranscriptPath, deps);
839
969
  if ("error" in resolved) {
840
- return { capable: false, reason: "no-transcript-resolved", detail: resolved.error };
970
+ const ours = resolved.pointerExisted && (resolved.corroborated || resolved.oursByLocation);
971
+ if (ours)
972
+ return { capable: false, reason: "pointer-unusable", detail: resolved.error };
973
+ return {
974
+ capable: false,
975
+ reason: "no-transcript-resolved",
976
+ detail: resolved.pointerExisted ? `A transcript pointer exists but belongs to another session, so this session has no wiring of its own to use. (Underlying condition, for diagnosis only: ${resolved.error})` : resolved.error
977
+ };
841
978
  }
979
+ const downstream = (r, detail) => resolved.corroborated ? { reason: r, detail } : {
980
+ reason: "no-transcript-resolved",
981
+ detail: `A transcript pointer was found but could not be shown to belong to this session, so it is not treated as this session's wiring. (Underlying condition, for diagnosis only: ${detail})`
982
+ };
842
983
  if (!deps.exists(resolved.path)) {
843
984
  return {
844
985
  capable: false,
845
- reason: "transcript-unreadable",
846
- detail: `Resolved transcript path ${resolved.path} does not exist.`
986
+ ...downstream("transcript-unreadable", `Resolved transcript path ${resolved.path} is not readable as a regular file owned by this user.`)
847
987
  };
848
988
  }
849
989
  let jsonl;
@@ -852,20 +992,70 @@ function resolveMeteringCapability(deps, explicitTranscriptPath) {
852
992
  } catch (err) {
853
993
  return {
854
994
  capable: false,
855
- reason: "transcript-unreadable",
856
- detail: `Transcript at ${resolved.path} could not be read: ${err.message}`
995
+ ...downstream("transcript-unreadable", `Transcript at ${resolved.path} could not be read: ${err.message}`)
857
996
  };
858
997
  }
859
998
  const summary = summarizeTranscript(jsonl);
860
999
  if (summary.messageCount === 0) {
861
1000
  return {
862
1001
  capable: false,
863
- reason: "transcript-empty",
864
- detail: `Transcript at ${resolved.path} yielded zero assistant messages with usage data — ` + "it's empty, unparseable, or not yet flushed to disk."
1002
+ ...downstream("transcript-empty", `Transcript at ${resolved.path} yielded zero assistant messages with usage data — ` + "it's empty, unparseable, or not yet flushed to disk.")
865
1003
  };
866
1004
  }
867
1005
  return { capable: true, transcriptPath: resolved.path, summary };
868
1006
  }
1007
+ // ../../packages/aeg-core/src/metering-io-guard.ts
1008
+ import { closeSync, constants as fsConstants, fstatSync, openSync, readFileSync } from "node:fs";
1009
+ var GUARD_OPEN_FLAGS = fsConstants.O_RDONLY | fsConstants.O_NONBLOCK | fsConstants.O_NOFOLLOW;
1010
+ function isTrustedMeteringStat(stat, ownUid) {
1011
+ if (!stat.isFile())
1012
+ return false;
1013
+ if (typeof ownUid === "number" && stat.uid !== ownUid)
1014
+ return false;
1015
+ return true;
1016
+ }
1017
+ function ownUid() {
1018
+ return typeof process.getuid === "function" ? process.getuid() : undefined;
1019
+ }
1020
+ function guardedOpen(path) {
1021
+ try {
1022
+ return openSync(path, GUARD_OPEN_FLAGS);
1023
+ } catch {
1024
+ return;
1025
+ }
1026
+ }
1027
+ function hardenedExists(path) {
1028
+ const fd = guardedOpen(path);
1029
+ if (fd === undefined)
1030
+ return false;
1031
+ try {
1032
+ return isTrustedMeteringStat(fstatSync(fd), ownUid());
1033
+ } finally {
1034
+ closeSync(fd);
1035
+ }
1036
+ }
1037
+ function hardenedReadFile(path) {
1038
+ const fd = guardedOpen(path);
1039
+ if (fd === undefined) {
1040
+ throw new Error(`refusing to read ${path}: not openable as a plain, non-symlinked file`);
1041
+ }
1042
+ try {
1043
+ if (!isTrustedMeteringStat(fstatSync(fd), ownUid())) {
1044
+ throw new Error(`refusing to read ${path}: not a regular file owned by this user`);
1045
+ }
1046
+ return readFileSync(fd, "utf8");
1047
+ } finally {
1048
+ closeSync(fd);
1049
+ }
1050
+ }
1051
+ function hardenedMeteringDeps() {
1052
+ return {
1053
+ env: process.env,
1054
+ cwd: process.cwd(),
1055
+ exists: hardenedExists,
1056
+ readFile: hardenedReadFile
1057
+ };
1058
+ }
869
1059
  // ../../packages/aeg-core/src/file-classify.ts
870
1060
  function isCodeFile(p) {
871
1061
  if (p.endsWith(".md"))
@@ -1453,6 +1643,8 @@ var EXEMPT_LITERALS = new Set([
1453
1643
  var NON_PATH_TOP_SEGMENTS = new Set(["origin", "refs", "HEAD", "vinaya", "fix"]);
1454
1644
  // ../../packages/aeg-core/src/reader-resolvable-prose.ts
1455
1645
  var SWEPT_CLASSES = new Set(["ships", "reader-facing"]);
1646
+ // ../../packages/aeg-core/src/quoted-command.ts
1647
+ var QUOTED_COMMAND_SWEPT_CLASSES = new Set(["ships", "reader-facing"]);
1456
1648
  // ../../packages/aeg-core/src/retired-vocabulary.ts
1457
1649
  var RETIRED_PATTERNS = [
1458
1650
  String.raw`\bD-[0-9]{3}\b`,
@@ -1545,15 +1737,15 @@ function releaseFieldFromText(text) {
1545
1737
  const raw = unwrapValue2(m[1] ?? "");
1546
1738
  return { declared: true, value: RELEASE_VALUE.test(raw) ? raw : null };
1547
1739
  }
1548
- var INTENTS_HEADING = /^#{1,6}\s*Tranche intents\s*$/im;
1549
- var NEXT_HEADING = /^#{1,6}\s+\S/m;
1550
- var INTENT_BULLET = /^-\s+([a-z0-9][a-z0-9-]*)\s*:\s*(.+)$/i;
1740
+ var INTENTS_HEADING2 = /^#{1,6}\s*Tranche intents\s*$/im;
1741
+ var NEXT_HEADING2 = /^#{1,6}\s+\S/m;
1742
+ var INTENT_BULLET2 = /^-\s+([a-z0-9][a-z0-9-]*)\s*:\s*(.+)$/i;
1551
1743
  function intentsBlock(text) {
1552
- const start = text.match(INTENTS_HEADING);
1744
+ const start = text.match(INTENTS_HEADING2);
1553
1745
  if (!start || start.index === undefined)
1554
1746
  return null;
1555
1747
  const rest = text.slice(start.index + start[0].length);
1556
- const next = rest.match(NEXT_HEADING);
1748
+ const next = rest.match(NEXT_HEADING2);
1557
1749
  const sectionEnd = next && next.index !== undefined ? next.index : rest.length;
1558
1750
  return { start: start.index, end: start.index + start[0].length + sectionEnd, section: rest.slice(0, sectionEnd) };
1559
1751
  }
@@ -1570,7 +1762,7 @@ function parseIntents(text) {
1570
1762
  const trimmed = line.trim();
1571
1763
  if (trimmed.length === 0)
1572
1764
  continue;
1573
- const m = trimmed.match(INTENT_BULLET);
1765
+ const m = trimmed.match(INTENT_BULLET2);
1574
1766
  if (!m)
1575
1767
  return { intents: [], malformed: true };
1576
1768
  intents.push({ slug: (m[1] ?? "").toLowerCase(), goal: (m[2] ?? "").trim() });
@@ -1628,16 +1820,16 @@ function checkAdoptable(facts) {
1628
1820
  }
1629
1821
  // src/lib/detect.ts
1630
1822
  import { execFile as execFile4 } from "node:child_process";
1631
- import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync6, statSync } from "node:fs";
1823
+ import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync } from "node:fs";
1632
1824
  import { join as join7 } from "node:path";
1633
1825
  import { promisify as promisify4 } from "node:util";
1634
1826
 
1635
1827
  // src/lib/artifacts.ts
1636
- import { readFileSync as readFileSync3 } from "node:fs";
1828
+ import { readFileSync as readFileSync4 } from "node:fs";
1637
1829
  import { join as join4 } from "node:path";
1638
1830
 
1639
1831
  // src/commands/doctrine.ts
1640
- import { existsSync as existsSync2, readdirSync, readFileSync } from "node:fs";
1832
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "node:fs";
1641
1833
  import { dirname as dirname2, join as join2, sep } from "node:path";
1642
1834
  import matter2 from "gray-matter";
1643
1835
 
@@ -1675,17 +1867,20 @@ function resolveDoctrineRoot(pkg = packageRoot(import.meta.url)) {
1675
1867
  if (!pkg.split(sep).includes("node_modules"))
1676
1868
  candidates.push(join2(dirname2(dirname2(pkg)), "aeg-root"));
1677
1869
  for (const root of candidates) {
1678
- if (existsSync2(join2(root, ...ENTRY_SEGMENTS)))
1870
+ if (hasDoctrineEntry(root))
1679
1871
  return root;
1680
1872
  }
1681
1873
  return null;
1682
1874
  }
1875
+ function hasDoctrineEntry(root) {
1876
+ return existsSync2(join2(root, ...ENTRY_SEGMENTS));
1877
+ }
1683
1878
  function listRoleNames(root) {
1684
1879
  const rolesDir = join2(root, "roles");
1685
1880
  if (!existsSync2(rolesDir))
1686
1881
  return [];
1687
1882
  return readdirSync(rolesDir).filter((name) => name.endsWith(".md")).map((name) => name.slice(0, -".md".length)).filter((roleName) => {
1688
- const { data } = matter2(readFileSync(join2(rolesDir, `${roleName}.md`), "utf8"));
1883
+ const { data } = matter2(readFileSync2(join2(rolesDir, `${roleName}.md`), "utf8"));
1689
1884
  return data.actor !== "human";
1690
1885
  }).sort();
1691
1886
  }
@@ -1724,7 +1919,7 @@ function doctrineCommand(args) {
1724
1919
  }
1725
1920
 
1726
1921
  // src/lib/agents-skills-emitter.ts
1727
- import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "node:fs";
1922
+ import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "node:fs";
1728
1923
  import { join as join3 } from "node:path";
1729
1924
  import matter3 from "gray-matter";
1730
1925
  var AGENTS_SKILLS_GROUP = "Agent skills (.agents/skills/)";
@@ -1736,7 +1931,7 @@ function discoverRoleNames(doctrineRoot) {
1736
1931
  if (!existsSync3(rolesDir))
1737
1932
  return [];
1738
1933
  return readdirSync2(rolesDir).filter((name) => name.endsWith(".md")).map((name) => name.slice(0, -".md".length)).filter((roleName) => {
1739
- const { data } = matter3(readFileSync2(join3(rolesDir, `${roleName}.md`), "utf8"));
1934
+ const { data } = matter3(readFileSync3(join3(rolesDir, `${roleName}.md`), "utf8"));
1740
1935
  return data.actor !== "human";
1741
1936
  }).sort();
1742
1937
  }
@@ -1808,9 +2003,11 @@ process.stdin.on("end", () => {
1808
2003
  return
1809
2004
  }
1810
2005
  if (!hook.transcript_path) return
2006
+ const crypto = require("crypto")
1811
2007
  const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd()
1812
2008
  const tmpDir = process.env.TMPDIR || "/tmp"
1813
- const key = projectDir.replace(/[^A-Za-z0-9]+/g, "-")
2009
+ const digest = crypto.createHash("sha256").update(projectDir).digest("hex")
2010
+ const key = projectDir.replace(/[^A-Za-z0-9]+/g, "-") + "-" + digest
1814
2011
  const pointerPath = tmpDir + "/claude-transcript-" + key + ".txt"
1815
2012
  const content = (hook.session_id || "") + "\\t" + hook.transcript_path + "\\n"
1816
2013
  const scratchPath = pointerPath + "." + process.pid + "." + Math.random().toString(36).slice(2) + ".tmp"
@@ -1927,7 +2124,7 @@ function starterConfig() {
1927
2124
  };
1928
2125
  }
1929
2126
  function ownVersion() {
1930
- const pkg = JSON.parse(readFileSync3(join4(packageRoot(import.meta.url), "package.json"), "utf-8"));
2127
+ const pkg = JSON.parse(readFileSync4(join4(packageRoot(import.meta.url), "package.json"), "utf-8"));
1931
2128
  return pkg.version;
1932
2129
  }
1933
2130
  var SETUP_BUN_SHA = "0c5077e51419868618aeaa5fe8019c62421857d6";
@@ -2837,14 +3034,15 @@ function buildInitOps(ctx) {
2837
3034
 
2838
3035
  // src/lib/ops.ts
2839
3036
  import { execFileSync as execFileSync3 } from "node:child_process";
2840
- import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
2841
- import { dirname as dirname4, isAbsolute, join as join6, resolve, sep as sep2 } from "node:path";
3037
+ import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
3038
+ import { dirname as dirname4, isAbsolute, join as join6, resolve as resolve2, sep as sep2 } from "node:path";
2842
3039
 
2843
3040
  // src/lib/config.ts
2844
3041
  import { execFileSync as execFileSync2 } from "node:child_process";
2845
- import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync4, writeFileSync } from "node:fs";
3042
+ import { createHash as createHash2 } from "node:crypto";
3043
+ import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync5, realpathSync, writeFileSync } from "node:fs";
2846
3044
  import { homedir } from "node:os";
2847
- import { dirname as dirname3, join as join5 } from "node:path";
3045
+ import { dirname as dirname3, join as join5, resolve } from "node:path";
2848
3046
  import { z } from "zod";
2849
3047
 
2850
3048
  // src/lib/agent-vendors.ts
@@ -2995,6 +3193,20 @@ function resolveAgentVendors(manifest) {
2995
3193
  return new Set(AGENT_VENDORS);
2996
3194
  return new Set(manifest.agents);
2997
3195
  }
3196
+ var ProjectEntrySchema = z.object({
3197
+ name: z.string().min(1),
3198
+ description: z.string().min(1).optional(),
3199
+ path: z.string().min(1).optional()
3200
+ });
3201
+ function parseTokensCollectDeclaration(value) {
3202
+ const m = value.trim().match(/^(\S+)\s+(\S+)$/);
3203
+ if (!m)
3204
+ return null;
3205
+ const [, interpreter, script] = m;
3206
+ if (!isSafeRepoRelPath(script))
3207
+ return null;
3208
+ return { interpreter, script };
3209
+ }
2998
3210
  var VinayaConfigSchema = z.object({
2999
3211
  rings: z.object({
3000
3212
  ring1_forgeWriteInterception: z.boolean(),
@@ -3007,13 +3219,19 @@ var VinayaConfigSchema = z.object({
3007
3219
  principals: z.array(z.string()).min(1).optional(),
3008
3220
  releaseActor: z.string().min(1).optional(),
3009
3221
  ci: z.object({ setup: z.string().min(1) }).optional(),
3222
+ tokens: z.object({
3223
+ collect: z.string().min(1).refine((v) => parseTokensCollectDeclaration(v) !== null, {
3224
+ message: 'tokens.collect must be exactly "<interpreter> <repo-relative-script-path>" — two whitespace-separated tokens, no flags, no shell syntax, no quoting'
3225
+ })
3226
+ }).optional(),
3010
3227
  blastRadius: z.object({ extraDomains: z.array(z.string()).optional() }).optional(),
3011
3228
  proseGates: z.object({
3012
3229
  doctrineRoot: z.string().min(1).optional(),
3013
3230
  readerFacingPrefix: z.string().min(1).optional(),
3014
3231
  readerFacingSuffix: z.string().min(1).optional(),
3015
3232
  legacySlugDir: z.string().min(1).optional()
3016
- }).optional()
3233
+ }).optional(),
3234
+ projects: z.array(ProjectEntrySchema).optional()
3017
3235
  });
3018
3236
  var GLOBAL_VINAYA_HOME = join5(homedir(), ".vinaya");
3019
3237
  var GLOBAL_CONFIG_PATH = join5(GLOBAL_VINAYA_HOME, "config.json");
@@ -3052,6 +3270,9 @@ function globalRolesIgnoredWarning(path) {
3052
3270
  function globalReleaseActorIgnoredWarning(path) {
3053
3271
  return `${path}: "releaseActor" in the global config is ignored — releaseActor may only be declared from a repo-local vinaya.config.json.`;
3054
3272
  }
3273
+ function globalTokensCollectIgnoredWarning(path) {
3274
+ return `${path}: "tokens" in the global config is ignored — tokens.collect may only be declared from a repo-local vinaya.config.json.`;
3275
+ }
3055
3276
  function stripGlobalOnlyKeys(config, path) {
3056
3277
  if (path !== GLOBAL_CONFIG_PATH)
3057
3278
  return config;
@@ -3072,6 +3293,10 @@ function stripGlobalOnlyKeys(config, path) {
3072
3293
  console.error(`⚠ ${globalReleaseActorIgnoredWarning(path)}`);
3073
3294
  result = { ...result, releaseActor: undefined };
3074
3295
  }
3296
+ if (result.tokens) {
3297
+ console.error(`⚠ ${globalTokensCollectIgnoredWarning(path)}`);
3298
+ result = { ...result, tokens: undefined };
3299
+ }
3075
3300
  return result;
3076
3301
  }
3077
3302
  function resolvePrincipalAllowlist(config) {
@@ -3148,7 +3373,7 @@ function loadConfig() {
3148
3373
  if (!path)
3149
3374
  return null;
3150
3375
  try {
3151
- const raw = JSON.parse(readFileSync4(path, "utf-8"));
3376
+ const raw = JSON.parse(readFileSync5(path, "utf-8"));
3152
3377
  return stripGlobalOnlyKeys(VinayaConfigSchema.parse(raw), path);
3153
3378
  } catch {
3154
3379
  return null;
@@ -3159,7 +3384,7 @@ function readRepoCiSetup(repoRoot) {
3159
3384
  if (!existsSync4(p))
3160
3385
  return null;
3161
3386
  try {
3162
- return VinayaConfigSchema.parse(JSON.parse(readFileSync4(p, "utf-8"))).ci?.setup ?? null;
3387
+ return VinayaConfigSchema.parse(JSON.parse(readFileSync5(p, "utf-8"))).ci?.setup ?? null;
3163
3388
  } catch {
3164
3389
  return null;
3165
3390
  }
@@ -3170,7 +3395,7 @@ function loadConfigChecked() {
3170
3395
  return { ok: true, config: null };
3171
3396
  let raw;
3172
3397
  try {
3173
- raw = JSON.parse(readFileSync4(path, "utf-8"));
3398
+ raw = JSON.parse(readFileSync5(path, "utf-8"));
3174
3399
  } catch (err) {
3175
3400
  return { ok: false, path, error: `invalid JSON: ${err.message}` };
3176
3401
  }
@@ -3181,6 +3406,69 @@ function loadConfigChecked() {
3181
3406
  }
3182
3407
  return { ok: true, config: stripGlobalOnlyKeys(parsed.data, path) };
3183
3408
  }
3409
+ var TOKENS_COLLECT_TRUST_PATH = join5(GLOBAL_VINAYA_HOME, "tokens-collect-trust.json");
3410
+ function readTokensCollectTrustStore(storePath) {
3411
+ try {
3412
+ if (!existsSync4(storePath))
3413
+ return {};
3414
+ const raw = JSON.parse(readFileSync5(storePath, "utf-8"));
3415
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
3416
+ } catch {
3417
+ return {};
3418
+ }
3419
+ }
3420
+ function writeTokensCollectTrustStore(storePath, store) {
3421
+ const dir = dirname3(storePath);
3422
+ if (!existsSync4(dir))
3423
+ mkdirSync(dir, { recursive: true });
3424
+ writeFileSync(storePath, JSON.stringify(store, null, 2), "utf-8");
3425
+ }
3426
+ var GIT_IDENTITY_TIMEOUT_MS = 5000;
3427
+ function gitCommonDir(cwd = process.cwd()) {
3428
+ try {
3429
+ const raw = execFileSync2("git", ["rev-parse", "--git-common-dir"], {
3430
+ cwd,
3431
+ encoding: "utf-8",
3432
+ stdio: ["ignore", "pipe", "pipe"],
3433
+ timeout: GIT_IDENTITY_TIMEOUT_MS
3434
+ }).trim();
3435
+ if (!raw)
3436
+ return null;
3437
+ return realpathSync(resolve(cwd, raw));
3438
+ } catch {
3439
+ return null;
3440
+ }
3441
+ }
3442
+ function repoLocalConfigDir() {
3443
+ const local = findLocalConfig();
3444
+ return local ? dirname3(local) : null;
3445
+ }
3446
+ function gitBlobHash(absolutePath, cwd) {
3447
+ try {
3448
+ const raw = execFileSync2("git", ["hash-object", absolutePath], {
3449
+ cwd,
3450
+ encoding: "utf-8",
3451
+ stdio: ["ignore", "pipe", "pipe"],
3452
+ timeout: GIT_IDENTITY_TIMEOUT_MS
3453
+ }).trim();
3454
+ return raw || null;
3455
+ } catch {
3456
+ return null;
3457
+ }
3458
+ }
3459
+ function tokensCollectTrustKey(repoGitCommonDir, interpreter, script) {
3460
+ return createHash2("sha256").update(JSON.stringify([repoGitCommonDir, interpreter, script])).digest("hex");
3461
+ }
3462
+ function getTokensCollectTrust(repoGitCommonDir, interpreter, script, storePath = TOKENS_COLLECT_TRUST_PATH) {
3463
+ const key = tokensCollectTrustKey(repoGitCommonDir, interpreter, script);
3464
+ return readTokensCollectTrustStore(storePath)[key] ?? null;
3465
+ }
3466
+ function trustTokensCollectCommand(repoGitCommonDir, interpreter, script, scriptBlobHash, storePath = TOKENS_COLLECT_TRUST_PATH) {
3467
+ const store = readTokensCollectTrustStore(storePath);
3468
+ const key = tokensCollectTrustKey(repoGitCommonDir, interpreter, script);
3469
+ store[key] = { interpreter, script, scriptBlobHash, trustedAt: new Date().toISOString() };
3470
+ writeTokensCollectTrustStore(storePath, store);
3471
+ }
3184
3472
 
3185
3473
  // src/lib/ops.ts
3186
3474
  var MARKER_NS = "vinaya:managed";
@@ -3204,9 +3492,9 @@ var GIT_DIR_PREFIX = ".git/";
3204
3492
  function resolveManagedBlockPath(repoRoot, opPath) {
3205
3493
  if (!opPath.startsWith(GIT_DIR_PREFIX))
3206
3494
  return join6(repoRoot, opPath);
3207
- return join6(gitCommonDir(repoRoot), opPath.slice(GIT_DIR_PREFIX.length));
3495
+ return join6(gitCommonDir2(repoRoot), opPath.slice(GIT_DIR_PREFIX.length));
3208
3496
  }
3209
- function gitCommonDir(repoRoot) {
3497
+ function gitCommonDir2(repoRoot) {
3210
3498
  try {
3211
3499
  const commonDir = execFileSync3("git", ["-C", repoRoot, "rev-parse", "--git-common-dir"], {
3212
3500
  encoding: "utf8"
@@ -3219,8 +3507,8 @@ function gitCommonDir(repoRoot) {
3219
3507
  }
3220
3508
  }
3221
3509
  function containedAbs(repoRoot, relPath) {
3222
- const root = resolve(repoRoot);
3223
- const target = resolve(root, relPath);
3510
+ const root = resolve2(repoRoot);
3511
+ const target = resolve2(root, relPath);
3224
3512
  if (target === root)
3225
3513
  return null;
3226
3514
  return target === root || target.startsWith(root + sep2) ? target : null;
@@ -3228,9 +3516,9 @@ function containedAbs(repoRoot, relPath) {
3228
3516
  function containedManagedBlockAbs(repoRoot, relPath) {
3229
3517
  if (!relPath.startsWith(GIT_DIR_PREFIX))
3230
3518
  return containedAbs(repoRoot, relPath);
3231
- const commonDir = gitCommonDir(repoRoot);
3232
- const hooksRoot = resolve(join6(commonDir, "hooks"));
3233
- const target = resolve(join6(commonDir, relPath.slice(GIT_DIR_PREFIX.length)));
3519
+ const commonDir = gitCommonDir2(repoRoot);
3520
+ const hooksRoot = resolve2(join6(commonDir, "hooks"));
3521
+ const target = resolve2(join6(commonDir, relPath.slice(GIT_DIR_PREFIX.length)));
3234
3522
  if (target === hooksRoot)
3235
3523
  return null;
3236
3524
  return target.startsWith(hooksRoot + sep2) ? target : null;
@@ -3239,7 +3527,7 @@ function fileContains(repoRoot, relPath, needle) {
3239
3527
  const p = resolveManagedBlockPath(repoRoot, relPath);
3240
3528
  if (!existsSync5(p))
3241
3529
  return false;
3242
- return readFileSync5(p, "utf-8").includes(needle);
3530
+ return readFileSync6(p, "utf-8").includes(needle);
3243
3531
  }
3244
3532
  function planInstall(ops, repoRoot, ownedFiles = new Set) {
3245
3533
  const entries = [];
@@ -3349,7 +3637,7 @@ function writeFileWithDirs(target, content, mode) {
3349
3637
  }
3350
3638
  function appendBlock(repoRoot, op) {
3351
3639
  const target = resolveManagedBlockPath(repoRoot, op.path);
3352
- const existing = readFileSync5(target, "utf-8");
3640
+ const existing = readFileSync6(target, "utf-8");
3353
3641
  const sep3 = existing.endsWith(`
3354
3642
  `) ? `
3355
3643
  ` : `
@@ -3471,7 +3759,7 @@ function planEject(manifest, repoRoot) {
3471
3759
  });
3472
3760
  continue;
3473
3761
  }
3474
- const content = readFileSync5(p, "utf-8");
3762
+ const content = readFileSync6(p, "utf-8");
3475
3763
  const stripped = stripBlockFromContent(content, b.marker, b.comment);
3476
3764
  const removesHost = stripped !== null && blockStripLeavesEmpty(stripped);
3477
3765
  actions.push({
@@ -3523,7 +3811,7 @@ function applyEject(plan, repoRoot) {
3523
3811
  const p = containedManagedBlockAbs(repoRoot, a.path);
3524
3812
  if (p === null)
3525
3813
  continue;
3526
- const content = readFileSync5(p, "utf-8");
3814
+ const content = readFileSync6(p, "utf-8");
3527
3815
  const stripped = stripBlockFromContent(content, a.marker, a.comment);
3528
3816
  if (stripped === null)
3529
3817
  continue;
@@ -3654,7 +3942,7 @@ function foreignRawHooks(repoRoot) {
3654
3942
  return activeRawHooks(repoRoot).filter((f) => {
3655
3943
  const abs2 = resolveManagedBlockPath(repoRoot, `.git/hooks/${f}`);
3656
3944
  try {
3657
- const stripped = stripBlockFromContent(readFileSync6(abs2, "utf-8"), f, "hash");
3945
+ const stripped = stripBlockFromContent(readFileSync7(abs2, "utf-8"), f, "hash");
3658
3946
  return stripped === null || !blockStripLeavesEmpty(stripped);
3659
3947
  } catch {
3660
3948
  return true;
@@ -3766,8 +4054,8 @@ function prompt(question) {
3766
4054
  setupStdinReader();
3767
4055
  if (stdinEnded && pendingResolvers.length === 0)
3768
4056
  return Promise.resolve("");
3769
- return new Promise((resolve2) => {
3770
- pendingResolvers.push(resolve2);
4057
+ return new Promise((resolve3) => {
4058
+ pendingResolvers.push(resolve3);
3771
4059
  if (stdinEnded)
3772
4060
  flushLines();
3773
4061
  });
@@ -3786,83 +4074,354 @@ function closeStdin() {
3786
4074
  }
3787
4075
  }
3788
4076
 
3789
- // src/commands/archive.ts
3790
- function ring2Accelerated() {
3791
- return loadConfig()?.rings?.ring2_asyncAudits === true;
3792
- }
3793
- function realDeps() {
3794
- return { detectRepo: detectGitRepo };
4077
+ // src/commands/tokens.ts
4078
+ import { execFileSync as execFileSync4 } from "node:child_process";
4079
+ import { resolve as resolvePath } from "node:path";
4080
+ var USAGE = [
4081
+ 'Usage: vinaya tokens --phase "<task-id>: develop" --role Developer',
4082
+ " [--model <id>] [--transcript <path>] [--in <tokens-in> --out <tokens-out>]",
4083
+ "",
4084
+ " --transcript Read this transcript directly. Supported primary route — use it whenever you know",
4085
+ " which transcript is yours, and always in a repo with no track-transcript.sh hook.",
4086
+ " (omitted) Resolve via the Stop-hook pointer file, if this repo installs that hook.",
4087
+ " --in/--out Manual entry: the exact token figures, for a host whose usage arrives by some other",
4088
+ " means than a Claude Code transcript. Both required together; skips transcript",
4089
+ " resolution entirely.",
4090
+ "",
4091
+ "Usage: vinaya tokens --trust-collect",
4092
+ " Approves this repo's declared tokens.collect command, pinned to the script's exact current",
4093
+ " content, on this machine. Run this once before tokens.collect will ever execute — a changed",
4094
+ " interpreter, script path, or script CONTENT needs its own fresh approval. Does not take",
4095
+ " --phase/--role and never emits a Tokens: line."
4096
+ ].join(`
4097
+ `);
4098
+ function parseArgs(argv) {
4099
+ let phase;
4100
+ let role;
4101
+ let model;
4102
+ let transcriptPath;
4103
+ let tokensInRaw;
4104
+ let tokensOutRaw;
4105
+ for (let i = 0;i < argv.length; i++) {
4106
+ const arg = argv[i];
4107
+ if (arg === "--phase")
4108
+ phase = argv[++i];
4109
+ else if (arg === "--role")
4110
+ role = argv[++i];
4111
+ else if (arg === "--model")
4112
+ model = argv[++i];
4113
+ else if (arg === "--transcript")
4114
+ transcriptPath = argv[++i];
4115
+ else if (arg === "--in")
4116
+ tokensInRaw = argv[++i];
4117
+ else if (arg === "--out")
4118
+ tokensOutRaw = argv[++i];
4119
+ }
4120
+ if (!phase || !role) {
4121
+ throw new Error(USAGE);
4122
+ }
4123
+ if (tokensInRaw === undefined !== (tokensOutRaw === undefined)) {
4124
+ throw new Error("vinaya tokens: --in and --out must be given together, or not at all.");
4125
+ }
4126
+ const tokensIn = tokensInRaw === undefined ? undefined : Number(tokensInRaw);
4127
+ const tokensOut = tokensOutRaw === undefined ? undefined : Number(tokensOutRaw);
4128
+ if (tokensIn !== undefined && (!Number.isFinite(tokensIn) || tokensIn < 0)) {
4129
+ throw new Error(`vinaya tokens: --in must be a non-negative integer, got "${tokensInRaw}".`);
4130
+ }
4131
+ if (tokensOut !== undefined && (!Number.isFinite(tokensOut) || tokensOut < 0)) {
4132
+ throw new Error(`vinaya tokens: --out must be a non-negative integer, got "${tokensOutRaw}".`);
4133
+ }
4134
+ return { phase, role, model, transcriptPath, tokensIn, tokensOut };
3795
4135
  }
3796
- function sh(args, input) {
3797
- return execFileSync4(args[0], args.slice(1), { encoding: "utf8", input }).trim();
4136
+ function manualSummary(tokensIn, tokensOut) {
4137
+ return {
4138
+ components: {
4139
+ inputTokens: tokensIn,
4140
+ outputTokens: tokensOut,
4141
+ cacheCreationInputTokens: 0,
4142
+ cacheReadInputTokens: 0
4143
+ },
4144
+ model: null,
4145
+ messageCount: 1
4146
+ };
3798
4147
  }
3799
- function shJson(args) {
3800
- return JSON.parse(sh(args));
4148
+ function declaredSummary(components, model) {
4149
+ return { components, model, messageCount: 1 };
3801
4150
  }
3802
- function parseMergeSha(args) {
3803
- for (const a of args) {
3804
- const m = a.match(/^--merge-sha=(.+)$/);
3805
- if (m)
3806
- return m[1];
4151
+ var COLLECT_FIELDS = ["inputTokens", "outputTokens", "cacheCreationInputTokens", "cacheReadInputTokens"];
4152
+ function parseDeclaredCollectOutput(raw, command) {
4153
+ const trimmed = raw.trim();
4154
+ const fail = (why) => {
4155
+ throw new Error(`vinaya tokens: declared tokens.collect command "${command}" ${why}
4156
+ ` + "Expected stdout to be a JSON object: " + `{"inputTokens":N,"outputTokens":N,"cacheCreationInputTokens":N,"cacheReadInputTokens":N,"model":"…"|null}
4157
+ ` + `Got: ${trimmed.slice(0, 500)}`);
4158
+ };
4159
+ let parsed;
4160
+ try {
4161
+ parsed = JSON.parse(trimmed);
4162
+ } catch (err) {
4163
+ fail(`did not print valid JSON on stdout (${err.message}).`);
3807
4164
  }
3808
- return null;
3809
- }
3810
- async function runArchive(args, deps) {
3811
- if (ring2Accelerated()) {
3812
- process.stdout.write("[vinaya archive] rings.ring2_asyncAudits is `true` (opt-in accelerator) — skipping, nothing changed.\n");
3813
- return 0;
4165
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
4166
+ fail("printed JSON that is not an object.");
3814
4167
  }
3815
- const repo = await deps.detectRepo();
3816
- if (!repo) {
3817
- console.error("Error: not a git repository. Run `vinaya archive` from inside your repo.");
3818
- return 1;
4168
+ const obj = parsed;
4169
+ const components = {};
4170
+ for (const field of COLLECT_FIELDS) {
4171
+ const value = obj[field];
4172
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
4173
+ fail(`printed an invalid "${field}" (must be a non-negative number), got ${JSON.stringify(value)}.`);
4174
+ }
4175
+ components[field] = value;
3819
4176
  }
3820
- if (!repo.owner || !repo.repo) {
3821
- console.error("Error: could not resolve a GitHub owner/repo from the `origin` remote.");
3822
- return 1;
4177
+ const model = obj.model;
4178
+ if (model !== undefined && model !== null && typeof model !== "string") {
4179
+ fail(`printed a non-string "model" field: ${JSON.stringify(model)}.`);
3823
4180
  }
3824
- const repoFlag = `${repo.owner}/${repo.repo}`;
3825
- const mergeSha = parseMergeSha(args) ?? sh(["git", "rev-parse", "HEAD"]);
3826
- const associated = shJson(["gh", "api", `repos/${repoFlag}/commits/${mergeSha}/pulls`]);
3827
- if (associated.length === 0) {
3828
- process.stdout.write(`[vinaya archive] no associated PR for merge ${mergeSha} — skip.
3829
- `);
3830
- return 0;
4181
+ return declaredSummary(components, typeof model === "string" ? model : null);
4182
+ }
4183
+ function resolveDeclaredCollect(command, deps) {
4184
+ const parsed = parseTokensCollectDeclaration(command);
4185
+ if (!parsed) {
4186
+ throw new Error(`vinaya tokens: declared tokens.collect "${command}" is not shaped "<interpreter> <script-path>" — ` + "this should have been rejected at config-load time; the config may have been edited after loading.");
3831
4187
  }
3832
- const prNumber = associated[0].number;
3833
- const pr = shJson([
3834
- "gh",
3835
- "pr",
3836
- "view",
3837
- String(prNumber),
3838
- "-R",
3839
- repoFlag,
3840
- "--json",
3841
- "number,headRefName,body,mergedAt,comments"
3842
- ]);
3843
- const ref = taskRefFromBranch(pr.headRefName);
3844
- let issueLabels = [];
3845
- if (ref === null) {
3846
- const { issue: candidateIssue } = extractIssue(pr.body);
3847
- if (candidateIssue !== null) {
3848
- issueLabels = shJson([
3849
- "gh",
3850
- "issue",
3851
- "view",
3852
- String(candidateIssue),
3853
- "-R",
3854
- repoFlag,
3855
- "--json",
3856
- "labels"
3857
- ]).labels.map((l) => l.name);
3858
- }
4188
+ const repoConfigDir = deps.repoConfigDir();
4189
+ if (repoConfigDir === null) {
4190
+ throw new Error("vinaya tokens: declared tokens.collect command could not be resolved — this repo's local " + "vinaya.config.json directory could not be found. Refusing to run an unresolvable command.");
3859
4191
  }
3860
- if (!isEligibleForProvenance(ref, issueLabels)) {
3861
- process.stdout.write(`[vinaya archive] non-task branch (${pr.headRefName}) and closing Issue carries no vinaya/tranche:* label — skip.
3862
- `);
3863
- return 0;
4192
+ const repoGitCommonDir = deps.gitCommonDir();
4193
+ if (repoGitCommonDir === null) {
4194
+ throw new Error("vinaya tokens: declared tokens.collect command could not be verified — this repo's git common " + "directory could not be resolved. Refusing to run an unverifiable command.");
3864
4195
  }
3865
- const comments = pr.comments.map((c) => c.body);
4196
+ const scriptAbsolutePath = resolvePath(repoConfigDir, parsed.script);
4197
+ if (!deps.exists(scriptAbsolutePath)) {
4198
+ throw new Error(`vinaya tokens: declared tokens.collect script "${parsed.script}" does not exist at ${scriptAbsolutePath}.`);
4199
+ }
4200
+ return { interpreter: parsed.interpreter, script: parsed.script, scriptAbsolutePath, repoGitCommonDir };
4201
+ }
4202
+ function runDeclaredCollect(command, deps) {
4203
+ const { interpreter, script, scriptAbsolutePath, repoGitCommonDir } = resolveDeclaredCollect(command, deps);
4204
+ const currentHash = deps.scriptContentHash(scriptAbsolutePath, deps.cwd);
4205
+ if (currentHash === null) {
4206
+ throw new Error(`vinaya tokens: declared tokens.collect script "${script}" could not be hashed for verification ` + "(git unavailable, or the file could not be read). Refusing to run an unverifiable script.");
4207
+ }
4208
+ const trust = deps.getTrust(repoGitCommonDir, interpreter, script);
4209
+ if (trust === null) {
4210
+ throw new Error(`vinaya tokens: declared tokens.collect is not yet trusted on this machine:
4211
+ ` + ` ${interpreter} ${script}
4212
+ ` + "Run `vinaya tokens --trust-collect` once to approve it. Refusing — no `Tokens:` line emitted, no zeros.");
4213
+ }
4214
+ if (trust.scriptBlobHash !== currentHash) {
4215
+ throw new Error(`vinaya tokens: declared tokens.collect script "${script}" no longer matches its trusted content ` + `(approved at ${trust.scriptBlobHash}, now ${currentHash}).
4216
+ ` + "Run `vinaya tokens --trust-collect` again to approve the new content. Refusing — no `Tokens:` line emitted, no zeros.");
4217
+ }
4218
+ deps.warn(`⚠ vinaya tokens: running declared tokens.collect: ${interpreter} ${script}`);
4219
+ let raw;
4220
+ try {
4221
+ raw = deps.runScript(interpreter, scriptAbsolutePath, deps.cwd);
4222
+ } catch (err) {
4223
+ throw new Error(`vinaya tokens: declared tokens.collect "${interpreter} ${script}" failed: ${err.message}`);
4224
+ }
4225
+ return parseDeclaredCollectOutput(raw, command);
4226
+ }
4227
+ var COLLECT_COMMAND_TIMEOUT_MS = 30000;
4228
+ function realDeps() {
4229
+ return {
4230
+ ...hardenedMeteringDeps(),
4231
+ loadConfig,
4232
+ repoConfigDir: repoLocalConfigDir,
4233
+ runScript: (interpreter, scriptAbsolutePath, cwd) => execFileSync4(interpreter, [scriptAbsolutePath], { cwd, encoding: "utf-8", timeout: COLLECT_COMMAND_TIMEOUT_MS }),
4234
+ warn: (message) => console.error(message),
4235
+ gitCommonDir,
4236
+ scriptContentHash: gitBlobHash,
4237
+ getTrust: getTokensCollectTrust,
4238
+ trustCollect: trustTokensCollectCommand
4239
+ };
4240
+ }
4241
+ function buildTokensResult(parsed, deps) {
4242
+ if (parsed.tokensIn !== undefined && parsed.tokensOut !== undefined) {
4243
+ const summary = manualSummary(parsed.tokensIn, parsed.tokensOut);
4244
+ return {
4245
+ line: formatTokensLine({ phase: parsed.phase, role: parsed.role, summary, modelOverride: parsed.model }),
4246
+ breakdown: undefined
4247
+ };
4248
+ }
4249
+ const declaredCommand = deps.loadConfig()?.tokens?.collect;
4250
+ if (declaredCommand) {
4251
+ const summary = runDeclaredCollect(declaredCommand, deps);
4252
+ return {
4253
+ line: formatTokensLine({ phase: parsed.phase, role: parsed.role, summary, modelOverride: parsed.model }),
4254
+ breakdown: formatBreakdown(summary)
4255
+ };
4256
+ }
4257
+ const capability = resolveMeteringCapability(deps, parsed.transcriptPath);
4258
+ if (!capability.capable) {
4259
+ throw new Error(`vinaya tokens: refused — could not resolve real usage figures (${capability.reason}).
4260
+ ${capability.detail}
4261
+
4262
+ ${USAGE}`);
4263
+ }
4264
+ return {
4265
+ line: formatTokensLine({
4266
+ phase: parsed.phase,
4267
+ role: parsed.role,
4268
+ summary: capability.summary,
4269
+ modelOverride: parsed.model
4270
+ }),
4271
+ breakdown: formatBreakdown(capability.summary)
4272
+ };
4273
+ }
4274
+ function runTrustCollect(deps) {
4275
+ const declaredCommand = deps.loadConfig()?.tokens?.collect;
4276
+ if (!declaredCommand) {
4277
+ return {
4278
+ ok: false,
4279
+ message: "vinaya tokens --trust-collect: no tokens.collect declared in this repo's vinaya.config.json — nothing to trust."
4280
+ };
4281
+ }
4282
+ let resolved;
4283
+ try {
4284
+ resolved = resolveDeclaredCollect(declaredCommand, deps);
4285
+ } catch (err) {
4286
+ return { ok: false, message: err.message };
4287
+ }
4288
+ const { interpreter, script, scriptAbsolutePath, repoGitCommonDir } = resolved;
4289
+ const scriptBlobHash = deps.scriptContentHash(scriptAbsolutePath, deps.cwd);
4290
+ if (scriptBlobHash === null) {
4291
+ return {
4292
+ ok: false,
4293
+ message: `vinaya tokens --trust-collect: script "${script}" could not be hashed (git unavailable, or the file could not be read) — refusing to trust blindly.`
4294
+ };
4295
+ }
4296
+ deps.trustCollect(repoGitCommonDir, interpreter, script, scriptBlobHash);
4297
+ return { ok: true, interpreter, script, scriptBlobHash };
4298
+ }
4299
+ function tokensCommand(argv) {
4300
+ if (argv.includes("--trust-collect")) {
4301
+ const outcome = runTrustCollect(realDeps());
4302
+ if (!outcome.ok) {
4303
+ console.error(outcome.message);
4304
+ process.exit(1);
4305
+ }
4306
+ process.stdout.write("Trusted. Future `vinaya tokens` runs on this machine execute it without asking again until the " + `interpreter, script path, or script content changes.
4307
+ ${outcome.interpreter} ${outcome.script}
4308
+ ` + ` content hash: ${outcome.scriptBlobHash}
4309
+ `);
4310
+ return;
4311
+ }
4312
+ let parsed;
4313
+ try {
4314
+ parsed = parseArgs(argv);
4315
+ } catch (err) {
4316
+ console.error(err.message);
4317
+ process.exit(2);
4318
+ }
4319
+ let result;
4320
+ try {
4321
+ result = buildTokensResult(parsed, realDeps());
4322
+ } catch (err) {
4323
+ console.error(err.message);
4324
+ process.exit(1);
4325
+ }
4326
+ if (result.breakdown)
4327
+ console.error(result.breakdown);
4328
+ process.stdout.write(`${result.line}
4329
+ `);
4330
+ }
4331
+
4332
+ // src/commands/archive.ts
4333
+ function ring2Accelerated() {
4334
+ return loadConfig()?.rings?.ring2_asyncAudits === true;
4335
+ }
4336
+ function realDeps2() {
4337
+ return { detectRepo: detectGitRepo, meteringCapability: () => resolveMeteringCapability(realDeps()) };
4338
+ }
4339
+ function isEmptySummary(summary) {
4340
+ const { inputTokens, outputTokens, cacheCreationInputTokens, cacheReadInputTokens } = summary.components;
4341
+ return inputTokens === 0 && outputTokens === 0 && cacheCreationInputTokens === 0 && cacheReadInputTokens === 0;
4342
+ }
4343
+ function renderArchiveTokensLine(capability, phase, role) {
4344
+ if (!capability.capable) {
4345
+ return { line: formatTokensLine({ phase, role, summary: null }), dangling: null };
4346
+ }
4347
+ if (isEmptySummary(capability.summary)) {
4348
+ return {
4349
+ line: null,
4350
+ dangling: `Archivist Tokens: line omitted — metering probe reports capable (transcript ${capability.transcriptPath}) ` + `but summarized to zero tokens across ${capability.summary.messageCount} message(s); posting a real zero ` + "would misrepresent the turn, and `—` is sanctioned only for a genuinely incapable host."
4351
+ };
4352
+ }
4353
+ return { line: formatTokensLine({ phase, role, summary: capability.summary }), dangling: null };
4354
+ }
4355
+ function sh(args, input) {
4356
+ return execFileSync5(args[0], args.slice(1), { encoding: "utf8", input, env: process.env }).trim();
4357
+ }
4358
+ function shJson(args) {
4359
+ return JSON.parse(sh(args));
4360
+ }
4361
+ function parseMergeSha(args) {
4362
+ for (const a of args) {
4363
+ const m = a.match(/^--merge-sha=(.+)$/);
4364
+ if (m)
4365
+ return m[1];
4366
+ }
4367
+ return null;
4368
+ }
4369
+ async function runArchive(args, deps) {
4370
+ if (ring2Accelerated()) {
4371
+ process.stdout.write("[vinaya archive] rings.ring2_asyncAudits is `true` (opt-in accelerator) — skipping, nothing changed.\n");
4372
+ return 0;
4373
+ }
4374
+ const repo = await deps.detectRepo();
4375
+ if (!repo) {
4376
+ console.error("Error: not a git repository. Run `vinaya archive` from inside your repo.");
4377
+ return 1;
4378
+ }
4379
+ if (!repo.owner || !repo.repo) {
4380
+ console.error("Error: could not resolve a GitHub owner/repo from the `origin` remote.");
4381
+ return 1;
4382
+ }
4383
+ const repoFlag = `${repo.owner}/${repo.repo}`;
4384
+ const mergeSha = parseMergeSha(args) ?? sh(["git", "rev-parse", "HEAD"]);
4385
+ const associated = shJson(["gh", "api", `repos/${repoFlag}/commits/${mergeSha}/pulls`]);
4386
+ if (associated.length === 0) {
4387
+ process.stdout.write(`[vinaya archive] no associated PR for merge ${mergeSha} — skip.
4388
+ `);
4389
+ return 0;
4390
+ }
4391
+ const prNumber = associated[0].number;
4392
+ const pr = shJson([
4393
+ "gh",
4394
+ "pr",
4395
+ "view",
4396
+ String(prNumber),
4397
+ "-R",
4398
+ repoFlag,
4399
+ "--json",
4400
+ "number,headRefName,body,mergedAt,comments"
4401
+ ]);
4402
+ const ref = taskRefFromBranch(pr.headRefName);
4403
+ let issueLabels = [];
4404
+ if (ref === null) {
4405
+ const { issue: candidateIssue } = extractIssue(pr.body);
4406
+ if (candidateIssue !== null) {
4407
+ issueLabels = shJson([
4408
+ "gh",
4409
+ "issue",
4410
+ "view",
4411
+ String(candidateIssue),
4412
+ "-R",
4413
+ repoFlag,
4414
+ "--json",
4415
+ "labels"
4416
+ ]).labels.map((l) => l.name);
4417
+ }
4418
+ }
4419
+ if (!isEligibleForProvenance(ref, issueLabels)) {
4420
+ process.stdout.write(`[vinaya archive] non-task branch (${pr.headRefName}) and closing Issue carries no vinaya/tranche:* label — skip.
4421
+ `);
4422
+ return 0;
4423
+ }
4424
+ const comments = pr.comments.map((c) => c.body);
3866
4425
  if (hasProvenance(comments)) {
3867
4426
  process.stdout.write(`[vinaya archive] provenance already present on PR #${pr.number} — skip (idempotent).
3868
4427
  `);
@@ -3877,13 +4436,23 @@ async function runArchive(args, deps) {
3877
4436
  comments
3878
4437
  };
3879
4438
  const { block, issue, dangling } = buildProvenanceBlock(facts);
4439
+ const phase = `${ref ? ref.taskId : pr.headRefName}: archive`;
4440
+ const tokensResult = renderArchiveTokensLine(deps.meteringCapability(), phase, "Archivist");
4441
+ const tokensAddition = tokensResult.line !== null ? tokensResult.line : `DANGLING (tokens): ${tokensResult.dangling}`;
4442
+ const blockWithTokens = `${block}
4443
+
4444
+ ${tokensAddition}`;
3880
4445
  process.stdout.write(`[vinaya archive] posting provenance block to PR #${pr.number}...
3881
4446
  `);
3882
- sh(["gh", "pr", "comment", String(pr.number), "-R", repoFlag, "--body-file", "-"], block);
4447
+ sh(["gh", "pr", "comment", String(pr.number), "-R", repoFlag, "--body-file", "-"], blockWithTokens);
3883
4448
  process.stdout.write(`[vinaya archive] provenance block posted to PR #${pr.number}.
3884
4449
  `);
3885
4450
  if (dangling.length > 0) {
3886
4451
  process.stdout.write(`[vinaya archive] DANGLING (${dangling.length}): ${dangling.join("; ")}
4452
+ `);
4453
+ }
4454
+ if (tokensResult.dangling !== null) {
4455
+ process.stdout.write(`[vinaya archive] DANGLING (tokens): ${tokensResult.dangling}
3887
4456
  `);
3888
4457
  }
3889
4458
  if (issue !== null) {
@@ -3915,7 +4484,7 @@ async function runArchive(args, deps) {
3915
4484
  return 0;
3916
4485
  }
3917
4486
  async function archiveCommand(args) {
3918
- process.exit(await runArchive(args, realDeps()));
4487
+ process.exit(await runArchive(args, realDeps2()));
3919
4488
  }
3920
4489
  function parseTrancheArgs(args) {
3921
4490
  const yes = args.includes("--yes");
@@ -3994,19 +4563,19 @@ async function runArchiveTranche(args, deps) {
3994
4563
  return 0;
3995
4564
  }
3996
4565
  async function archiveTrancheCommand(args) {
3997
- process.exit(await runArchiveTranche(args, realDeps()));
4566
+ process.exit(await runArchiveTranche(args, realDeps2()));
3998
4567
  }
3999
4568
 
4000
4569
  // src/commands/audit.ts
4001
- import { execFileSync as execFileSync5 } from "node:child_process";
4570
+ import { execFileSync as execFileSync6 } from "node:child_process";
4002
4571
  function ring2Accelerated2() {
4003
4572
  return loadConfig()?.rings?.ring2_asyncAudits === true;
4004
4573
  }
4005
- function realDeps2() {
4574
+ function realDeps3() {
4006
4575
  return { detectRepo: detectGitRepo };
4007
4576
  }
4008
4577
  function defaultSleep(ms) {
4009
- return new Promise((resolve2) => setTimeout(resolve2, ms));
4578
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
4010
4579
  }
4011
4580
  var COMMAND_TIMEOUT_MS = 20000;
4012
4581
  var DEAD_BRANCH_LABEL = label("dead-branch-push");
@@ -4017,7 +4586,7 @@ var DIRECT_PUSH_MARKER = `<!-- ${DIRECT_PUSH_LABEL} -->`;
4017
4586
  var DIRECT_PUSH_LABEL_DESCRIPTION = "A commit landed on main with no associated merged PR (ring-2 detection)";
4018
4587
  function shSoft(args, input) {
4019
4588
  try {
4020
- return execFileSync5(args[0], args.slice(1), {
4589
+ return execFileSync6(args[0], args.slice(1), {
4021
4590
  encoding: "utf8",
4022
4591
  input,
4023
4592
  timeout: COMMAND_TIMEOUT_MS
@@ -4041,7 +4610,7 @@ function shSoftJson(args) {
4041
4610
  }
4042
4611
  }
4043
4612
  function sh2(args, input) {
4044
- return execFileSync5(args[0], args.slice(1), { encoding: "utf8", input }).trim();
4613
+ return execFileSync6(args[0], args.slice(1), { encoding: "utf8", input }).trim();
4045
4614
  }
4046
4615
  function listTaskBranches() {
4047
4616
  const out = shSoft(["git", "ls-remote", "--heads", "origin", "task/*"]);
@@ -4297,11 +4866,11 @@ async function runAudit(args, deps) {
4297
4866
  return failed ? 1 : 0;
4298
4867
  }
4299
4868
  async function auditCommand(args) {
4300
- process.exit(await runAudit(args, realDeps2()));
4869
+ process.exit(await runAudit(args, realDeps3()));
4301
4870
  }
4302
4871
 
4303
4872
  // src/commands/check.ts
4304
- import { execFileSync as execFileSync6 } from "node:child_process";
4873
+ import { execFileSync as execFileSync7 } from "node:child_process";
4305
4874
  import { dirname as dirname5 } from "node:path";
4306
4875
 
4307
4876
  // src/checks/contract.ts
@@ -4451,6 +5020,21 @@ var REGISTRY = [
4451
5020
  },
4452
5021
  1
4453
5022
  ],
5023
+ [
5024
+ {
5025
+ name: "token-report",
5026
+ run: bin("check-token-report"),
5027
+ scope: "diff",
5028
+ timeoutMs: 15000,
5029
+ requiresOpenPr: true,
5030
+ env: {
5031
+ PR_BODY: { optional: true },
5032
+ CLAUDE_PROJECT_DIR: { optional: true },
5033
+ CLAUDE_CODE_SESSION_ID: { optional: true }
5034
+ }
5035
+ },
5036
+ 1
5037
+ ],
4454
5038
  [
4455
5039
  {
4456
5040
  name: "no-disk-state",
@@ -4627,6 +5211,26 @@ var REGISTRY = [
4627
5211
  },
4628
5212
  0
4629
5213
  ],
5214
+ [
5215
+ {
5216
+ name: "changeset-coverage",
5217
+ run: bin("check-changeset-coverage"),
5218
+ scope: "diff",
5219
+ timeoutMs: 15000,
5220
+ env: {}
5221
+ },
5222
+ 0
5223
+ ],
5224
+ [
5225
+ {
5226
+ name: "quoted-command",
5227
+ run: bin("check-quoted-command"),
5228
+ scope: "diff",
5229
+ timeoutMs: 15000,
5230
+ env: {}
5231
+ },
5232
+ 0
5233
+ ],
4630
5234
  [
4631
5235
  {
4632
5236
  name: "main-branch-refusal",
@@ -4636,6 +5240,19 @@ var REGISTRY = [
4636
5240
  env: {}
4637
5241
  },
4638
5242
  0
5243
+ ],
5244
+ [
5245
+ {
5246
+ name: "token-collection-wired",
5247
+ run: bin("check-token-collection-wired"),
5248
+ scope: "full",
5249
+ timeoutMs: 15000,
5250
+ env: {
5251
+ CLAUDE_PROJECT_DIR: { optional: true },
5252
+ CLAUDE_CODE_SESSION_ID: { optional: true }
5253
+ }
5254
+ },
5255
+ 0
4639
5256
  ]
4640
5257
  ];
4641
5258
  function coreCheckRegistry() {
@@ -4843,16 +5460,16 @@ async function runOne(spec, timeoutMs, callerEnv) {
4843
5460
  const timer = setTimeout(() => {
4844
5461
  timedOut = true;
4845
5462
  killTree("SIGTERM");
4846
- escalationDone = new Promise((resolve2) => {
5463
+ escalationDone = new Promise((resolve3) => {
4847
5464
  const graceTimer = setTimeout(() => {
4848
5465
  const pid = proc.pid;
4849
5466
  if (pid !== undefined && groupStillAlive(pid))
4850
5467
  killTree("SIGKILL");
4851
- resolve2();
5468
+ resolve3();
4852
5469
  }, KILL_GRACE_MS);
4853
5470
  cancelEscalation = () => {
4854
5471
  clearTimeout(graceTimer);
4855
- resolve2();
5472
+ resolve3();
4856
5473
  };
4857
5474
  });
4858
5475
  }, timeoutMs);
@@ -4863,11 +5480,11 @@ async function runOne(spec, timeoutMs, callerEnv) {
4863
5480
  });
4864
5481
  proc.stdout.resume();
4865
5482
  let spawnError;
4866
- const exitCode = await new Promise((resolve2) => {
4867
- proc.on("close", (code) => resolve2(code));
5483
+ const exitCode = await new Promise((resolve3) => {
5484
+ proc.on("close", (code) => resolve3(code));
4868
5485
  proc.on("error", (err) => {
4869
5486
  spawnError = err;
4870
- resolve2(null);
5487
+ resolve3(null);
4871
5488
  });
4872
5489
  });
4873
5490
  clearTimeout(timer);
@@ -4955,7 +5572,7 @@ async function runChecks(specs, opts) {
4955
5572
  }
4956
5573
 
4957
5574
  // src/roles/plan.ts
4958
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "node:fs";
5575
+ import { existsSync as existsSync8, readFileSync as readFileSync9 } from "node:fs";
4959
5576
  import { join as join10 } from "node:path";
4960
5577
 
4961
5578
  // ../../packages/sources/src/commands.ts
@@ -5110,6 +5727,18 @@ var COMMANDS = [
5110
5727
  ],
5111
5728
  status: "shipped"
5112
5729
  },
5730
+ {
5731
+ name: "pr verify-evidence",
5732
+ description: "Prove a pull request's AEG:EVIDENCE region was machine-generated — regenerate it and compare",
5733
+ flags: [],
5734
+ details: [
5735
+ "Deliberately a command, not a check: `evidence-fresh` runs inside `vinaya check --all`, and `pr report` runs `vinaya check --all --diff-only`, so a check that regenerated the block would run the suite containing itself. That recursion is why `evidence-fresh` verifies Group A only and leaves Group B attested.",
5736
+ "Must be run from the repository ROOT of a CLEAN checkout at the pull request's head, and refuses otherwise. The regeneration inherits the working directory and several gates resolve their scan root from it, so a subdirectory silently narrows what the fresh run inspects. On cleanliness: Group B is regenerated by diff-scoped checks, so uncommitted or untracked files change which files are scanned and can produce a MATCH a clean checkout would not. Ignored paths are deliberately NOT inspected — `git diff` never reports one, so they cannot change the scope, and including them would refuse in every real checkout since `node_modules` is ignored and always present. The head is read from the forge and both a mismatch AND an unreadable head refuse — the verdict is bound to a commit, never merely attested. `BASE_SHA` is refused as well, since it steers the regeneration's merge-base.",
5737
+ "Exits 0 on MATCH; 1 on DIFFERS, HIDDEN, or no block; 2 on a refusal (dirty worktree, wrong head, usage error). HIDDEN means the anchor pair sits inside a collapsed `<details>` block, where `body-bare-digits` blanks every digit and nothing can verify what it claims.",
5738
+ "Comparison is a multiset of repo-relative, control-stripped lines: absolute paths (local AND foreign, so a block generated in CI compares against one generated on a laptop) and line order are ignored — a reorder is not a fabrication. Only the AEG:EVIDENCE region is read, since AEG:TOKENS appends by design. A moved merge-base is reported ALONGSIDE the differing lines, never instead of them."
5739
+ ],
5740
+ status: "shipped"
5741
+ },
5113
5742
  {
5114
5743
  name: "issue create",
5115
5744
  description: "Open an issue after full brief-schema validation",
@@ -5186,6 +5815,19 @@ var COMMANDS = [
5186
5815
  ],
5187
5816
  status: "shipped"
5188
5817
  },
5818
+ {
5819
+ name: "milestone close",
5820
+ description: "Close a tranche's Milestone after verifying every labeled Issue is actually attached",
5821
+ flags: [
5822
+ { flag: "--slug", description: "The tranche whose Milestone is being closed" },
5823
+ { flag: "--validate-only", description: "Run every gate and report PASS without closing the Milestone" },
5824
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
5825
+ ],
5826
+ details: [
5827
+ "Resolves the target Milestone the same legacy-or-intent-declared way Issue create auto-attach does, then refuses to close on any mismatch between the label's Issues and the Milestone's natively attached Issues — naming each unattached or foreign Issue and its repair path (`gh issue edit <n> --milestone <title>`, or `vinaya milestone adopt`) — before the PATCH ever reaches the forge."
5828
+ ],
5829
+ status: "shipped"
5830
+ },
5189
5831
  {
5190
5832
  name: "review post",
5191
5833
  description: "Render, post, and self-verify a code-reviewer or security-review verdict comment on a PR",
@@ -5373,7 +6015,7 @@ var COMMANDS = [
5373
6015
  }
5374
6016
  ];
5375
6017
  // ../../packages/sources/src/doctrine-file-adapter.ts
5376
- import { readdirSync as readdirSync4, readFileSync as readFileSync7 } from "node:fs";
6018
+ import { readdirSync as readdirSync4, readFileSync as readFileSync8 } from "node:fs";
5377
6019
  import { join as join9 } from "node:path";
5378
6020
 
5379
6021
  // ../../packages/sources/src/file-adapter.ts
@@ -5384,14 +6026,14 @@ function readMarkdownDir(root, dir) {
5384
6026
  const dirPath = join9(root, dir);
5385
6027
  return readdirSync4(dirPath).filter((f) => f.endsWith(".md")).sort().map((file) => {
5386
6028
  const path = join9(dir, file);
5387
- return { path, content: readFileSync7(join9(root, path), "utf-8") };
6029
+ return { path, content: readFileSync8(join9(root, path), "utf-8") };
5388
6030
  });
5389
6031
  }
5390
6032
  function createFileDoctrineSource(config = {}) {
5391
6033
  const root = config.root ?? DEFAULT_GOVERNANCE_ROOT;
5392
6034
  return {
5393
6035
  async getDoctrine() {
5394
- const enforcement = readFileSync7(join9(root, "enforcement.md"), "utf-8");
6036
+ const enforcement = readFileSync8(join9(root, "enforcement.md"), "utf-8");
5395
6037
  const roles = readMarkdownDir(root, "roles");
5396
6038
  const contracts = readMarkdownDir(root, "contracts");
5397
6039
  return { enforcement, roles, contracts };
@@ -5570,7 +6212,7 @@ function readContract(configDir, contractPath) {
5570
6212
  return { ok: false, errors: [`contract file "${contractPath}" does not exist (resolved to "${absolute}")`] };
5571
6213
  }
5572
6214
  try {
5573
- return validateRoleContract(readFileSync8(absolute, "utf-8"));
6215
+ return validateRoleContract(readFileSync9(absolute, "utf-8"));
5574
6216
  } catch (err) {
5575
6217
  return { ok: false, errors: [`could not read "${contractPath}": ${err.message}`] };
5576
6218
  }
@@ -5603,7 +6245,7 @@ async function buildRolePlan(configDir, configRoles) {
5603
6245
  // src/commands/check.ts
5604
6246
  function git(args) {
5605
6247
  try {
5606
- return execFileSync6("git", args, { encoding: "utf8" }).trim();
6248
+ return execFileSync7("git", args, { encoding: "utf8" }).trim();
5607
6249
  } catch {
5608
6250
  return "";
5609
6251
  }
@@ -5841,7 +6483,7 @@ ${renderRolesTable(rolePlan)}
5841
6483
  }
5842
6484
 
5843
6485
  // src/commands/commit-msg.ts
5844
- import { readFileSync as readFileSync9 } from "node:fs";
6486
+ import { readFileSync as readFileSync10 } from "node:fs";
5845
6487
  function commitMsgCommand(args) {
5846
6488
  const [messageFile, source] = args;
5847
6489
  if (!messageFile) {
@@ -5852,7 +6494,7 @@ function commitMsgCommand(args) {
5852
6494
  }
5853
6495
  if (source === "merge")
5854
6496
  return;
5855
- const firstLine2 = readFileSync9(messageFile, "utf-8").split(`
6497
+ const firstLine2 = readFileSync10(messageFile, "utf-8").split(`
5856
6498
  `, 1)[0] ?? "";
5857
6499
  if (COMMIT_TYPE_STYLE.test(firstLine2))
5858
6500
  return;
@@ -5864,14 +6506,14 @@ function commitMsgCommand(args) {
5864
6506
  }
5865
6507
 
5866
6508
  // src/commands/demo.ts
5867
- import { execFileSync as execFileSync7, spawnSync } from "node:child_process";
5868
- import { existsSync as existsSync9, readFileSync as readFileSync10, rmSync as rmSync2, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
6509
+ import { execFileSync as execFileSync8, spawnSync } from "node:child_process";
6510
+ import { existsSync as existsSync9, readFileSync as readFileSync11, rmSync as rmSync2, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
5869
6511
  import { isAbsolute as isAbsolute2, join as join11 } from "node:path";
5870
6512
  var DEMO_BRANCH_PREFIX = "vinaya/demo-break-";
5871
6513
  var FIXTURE_PATH = ".vinaya-demo-brief.md";
5872
6514
  var STATE_FILENAME = "vinaya-demo-state.json";
5873
6515
  function gitCapture(repoRoot, args) {
5874
- return execFileSync7("git", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
6516
+ return execFileSync8("git", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
5875
6517
  }
5876
6518
  function currentBranchName(repoRoot) {
5877
6519
  try {
@@ -5882,7 +6524,7 @@ function currentBranchName(repoRoot) {
5882
6524
  }
5883
6525
  function branchExists(repoRoot, name) {
5884
6526
  try {
5885
- execFileSync7("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${name}`], {
6527
+ execFileSync8("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${name}`], {
5886
6528
  cwd: repoRoot,
5887
6529
  stdio: ["ignore", "pipe", "pipe"]
5888
6530
  });
@@ -5910,7 +6552,7 @@ function recoverFromCrash(repoRoot, statePath) {
5910
6552
  let state = null;
5911
6553
  if (existsSync9(statePath)) {
5912
6554
  try {
5913
- state = JSON.parse(readFileSync10(statePath, "utf-8"));
6555
+ state = JSON.parse(readFileSync11(statePath, "utf-8"));
5914
6556
  } catch {
5915
6557
  state = null;
5916
6558
  }
@@ -6084,12 +6726,12 @@ async function demoBreakCommand(args) {
6084
6726
  }
6085
6727
 
6086
6728
  // src/commands/doctor.ts
6087
- import { execFileSync as execFileSync8 } from "node:child_process";
6088
- import { existsSync as existsSync11, readdirSync as readdirSync6, readFileSync as readFileSync13, statSync as statSync2 } from "node:fs";
6089
- import { delimiter, join as join13 } from "node:path";
6729
+ import { execFileSync as execFileSync9 } from "node:child_process";
6730
+ import { existsSync as existsSync12, readdirSync as readdirSync6, readFileSync as readFileSync15, statSync as statSync2 } from "node:fs";
6731
+ import { delimiter, join as join14 } from "node:path";
6090
6732
 
6091
6733
  // src/lib/self-host.ts
6092
- import { readdirSync as readdirSync5, readFileSync as readFileSync11, realpathSync } from "node:fs";
6734
+ import { readdirSync as readdirSync5, readFileSync as readFileSync12, realpathSync as realpathSync2 } from "node:fs";
6093
6735
  import { join as join12, sep as sep3 } from "node:path";
6094
6736
  var VINAYA_PACKAGE_NAME = "@attalabs/vinaya";
6095
6737
  var DEFAULT_BIN = "dist/index.js";
@@ -6108,7 +6750,7 @@ function isSafeRelPath(p) {
6108
6750
  }
6109
6751
  function readPackageJson(path) {
6110
6752
  try {
6111
- const parsed = JSON.parse(readFileSync11(path, "utf-8"));
6753
+ const parsed = JSON.parse(readFileSync12(path, "utf-8"));
6112
6754
  return parsed && typeof parsed === "object" ? parsed : null;
6113
6755
  } catch {
6114
6756
  return null;
@@ -6162,8 +6804,8 @@ function expandPattern(repoRoot, pattern) {
6162
6804
  }
6163
6805
  function resolvesInsideRepo(repoRoot, rel) {
6164
6806
  try {
6165
- const root = realpathSync(repoRoot);
6166
- const target = realpathSync(join12(repoRoot, rel));
6807
+ const root = realpathSync2(repoRoot);
6808
+ const target = realpathSync2(join12(repoRoot, rel));
6167
6809
  return target === root || target.startsWith(`${root}${sep3}`);
6168
6810
  } catch {
6169
6811
  return false;
@@ -6203,7 +6845,7 @@ function detectVendoredVinaya(repoRoot) {
6203
6845
  }
6204
6846
 
6205
6847
  // src/lib/env-lint.ts
6206
- import { existsSync as existsSync10, readFileSync as readFileSync12 } from "node:fs";
6848
+ import { existsSync as existsSync10, readFileSync as readFileSync13 } from "node:fs";
6207
6849
  var ENV_READ_PATTERN = /\b(?:process\.env|Bun\.env|Deno\.env)\b/;
6208
6850
  function checksMissingEnvDeclaration(specs) {
6209
6851
  const names = [];
@@ -6214,7 +6856,7 @@ function checksMissingEnvDeclaration(specs) {
6214
6856
  continue;
6215
6857
  let source;
6216
6858
  try {
6217
- source = readFileSync12(spec.run, "utf8");
6859
+ source = readFileSync13(spec.run, "utf8");
6218
6860
  } catch {
6219
6861
  continue;
6220
6862
  }
@@ -6227,49 +6869,175 @@ function envDeclarationWarning(name) {
6227
6869
  return `check "${name}" reads environment variables directly but declares no \`env\` — it will lose environment access at the next minor. Declare \`env\` for it (CheckSpec['env'] in src/checks/contract.ts, or vinaya.config.json's checks.<name>.env).`;
6228
6870
  }
6229
6871
 
6230
- // src/commands/doctor.ts
6231
- function readVersion() {
6232
- const pkg = JSON.parse(readFileSync13(join13(packageRoot(import.meta.url), "package.json"), "utf-8"));
6233
- return pkg.version;
6234
- }
6235
- function realDeps3() {
6236
- return {
6237
- detectRepo: detectGitRepo,
6238
- ghAuthStatus,
6239
- branchProtectionConfigured,
6240
- hookDirFor: resolveHookDir,
6241
- readHooksPath: readCoreHooksPath,
6242
- nodeVersion: () => process.version,
6243
- bunVersion: () => typeof Bun === "undefined" ? null : Bun.version,
6244
- packageVersion: readVersion,
6245
- meteringCapability: () => resolveMeteringCapability({
6246
- env: process.env,
6247
- cwd: process.cwd(),
6248
- exists: existsSync11,
6249
- readFile: (path) => readFileSync13(path, "utf8")
6250
- })
6251
- };
6872
+ // src/lib/registry-write.ts
6873
+ import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync14, writeFileSync as writeFileSync4 } from "node:fs";
6874
+ import { dirname as dirname6, join as join13 } from "node:path";
6875
+ var PROJECTS_REGISTRY_PATH = ".vinaya/projects.md";
6876
+ var CONFIG_FILE_PATH = "vinaya.config.json";
6877
+ function rowLine(name, path, specsPath) {
6878
+ return `| ${name} | \`${path}\` | \`${specsPath}\` | (state tracked globally) |`;
6252
6879
  }
6253
- var ok = (check, message) => ({ check, severity: "ok", message });
6254
- var info = (check, message) => ({ check, severity: "info", message });
6255
- var warn = (check, message) => ({ check, severity: "warn", message });
6256
- var error = (check, message) => ({ check, severity: "error", message });
6257
- function readConfig(repoRoot) {
6258
- const p = join13(repoRoot, CONFIG_PATH);
6259
- if (!existsSync11(p))
6260
- return { kind: "missing" };
6261
- let raw;
6262
- try {
6263
- raw = JSON.parse(readFileSync13(p, "utf-8"));
6264
- } catch (err) {
6265
- return { kind: "invalid", error: `invalid JSON: ${err.message}` };
6266
- }
6267
- const parsed = VinayaConfigSchema.safeParse(raw);
6268
- if (!parsed.success) {
6269
- return {
6270
- kind: "invalid",
6271
- error: parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")
6272
- };
6880
+ function freshProjectsRegistry(name, path, specsPath) {
6881
+ return `---
6882
+ sidebar_title: Projects
6883
+ ---
6884
+ # Projects in this repo
6885
+
6886
+ **The project registry.** Declares the projects in this repo and where each
6887
+ one's specs and per-project state live. The \`Project\` field on a task (a
6888
+ forge Issue) resolves against this file.
6889
+
6890
+ **Presence of this file means this is a multi-project repo** — the
6891
+ \`Project\` field is required on task Issues/PRs that touch a registered
6892
+ project's path. A project is a \`(name, path)\` pair you declared — nothing
6893
+ is derived from the folder tree.
6894
+
6895
+ ## Registry
6896
+
6897
+ | Project | Path | Specs | Per-project state |
6898
+ |---------|------|-------|---------------------|
6899
+ ${rowLine(name, path, specsPath)}
6900
+ `;
6901
+ }
6902
+ var REGISTRY_HEADING_RE = /^##\s+Registry\b/i;
6903
+ function appendRegistryRow(content, name, path, specsPath) {
6904
+ const lines = content.split(/\r?\n/);
6905
+ const headingIdx = lines.findIndex((l) => REGISTRY_HEADING_RE.test(l));
6906
+ const newRow = rowLine(name, path, specsPath);
6907
+ if (headingIdx === -1) {
6908
+ const sep4 = content.endsWith(`
6909
+ `) ? "" : `
6910
+ `;
6911
+ return `${content}${sep4}
6912
+ ## Registry
6913
+
6914
+ | Project | Path | Specs | Per-project state |
6915
+ |---------|------|-------|---------------------|
6916
+ ${newRow}
6917
+ `;
6918
+ }
6919
+ let i = headingIdx + 1;
6920
+ for (;i < lines.length; i++) {
6921
+ if ((lines[i] ?? "").trim().startsWith("|"))
6922
+ break;
6923
+ }
6924
+ if (i >= lines.length) {
6925
+ lines.splice(headingIdx + 1, 0, "", "| Project | Path | Specs | Per-project state |", "|---------|------|-------|---------------------|", newRow);
6926
+ return lines.join(`
6927
+ `);
6928
+ }
6929
+ let last = i + 1;
6930
+ for (let j = i + 2;j < lines.length; j++) {
6931
+ if (!(lines[j] ?? "").trim().startsWith("|"))
6932
+ break;
6933
+ last = j;
6934
+ }
6935
+ lines.splice(last + 1, 0, newRow);
6936
+ return lines.join(`
6937
+ `);
6938
+ }
6939
+ function planRegistryRow(repoRoot, name, path, specsPath) {
6940
+ const abs2 = join13(repoRoot, PROJECTS_REGISTRY_PATH);
6941
+ const line = rowLine(name, path, specsPath);
6942
+ if (!existsSync11(abs2)) {
6943
+ return { action: "create-host", rowLine: line, path: PROJECTS_REGISTRY_PATH };
6944
+ }
6945
+ const existing = readFileSync14(abs2, "utf-8");
6946
+ const already = parseRegistry(existing).some((p) => p.name === name);
6947
+ return { action: already ? "skip-present" : "append-row", rowLine: line, path: PROJECTS_REGISTRY_PATH };
6948
+ }
6949
+ function applyRegistryRow(repoRoot, plan, name, path, specsPath) {
6950
+ const abs2 = join13(repoRoot, PROJECTS_REGISTRY_PATH);
6951
+ if (plan.action === "create-host") {
6952
+ mkdirSync3(dirname6(abs2), { recursive: true });
6953
+ writeFileSync4(abs2, freshProjectsRegistry(name, path, specsPath), "utf-8");
6954
+ } else if (plan.action === "append-row") {
6955
+ const existing = readFileSync14(abs2, "utf-8");
6956
+ writeFileSync4(abs2, appendRegistryRow(existing, name, path, specsPath), "utf-8");
6957
+ }
6958
+ }
6959
+ function renderRegistryRowDiffLine(plan) {
6960
+ if (plan.action === "skip-present")
6961
+ return ` = keep ${plan.path} (project already registered)`;
6962
+ if (plan.action === "append-row")
6963
+ return ` ~ append row to ${plan.path}
6964
+ ${plan.rowLine}`;
6965
+ return ` + create ${plan.path} (with registry row)
6966
+ ${plan.rowLine}`;
6967
+ }
6968
+ function readProjectEntries(repoRoot) {
6969
+ const abs2 = join13(repoRoot, CONFIG_FILE_PATH);
6970
+ if (!existsSync11(abs2))
6971
+ return [];
6972
+ try {
6973
+ return VinayaConfigSchema.parse(JSON.parse(readFileSync14(abs2, "utf-8"))).projects ?? [];
6974
+ } catch {
6975
+ return [];
6976
+ }
6977
+ }
6978
+ function planConfigProjectEntry(repoRoot, entry2) {
6979
+ const already = readProjectEntries(repoRoot).some((p) => p.name === entry2.name);
6980
+ return { action: already ? "skip-present" : "add-entry", entry: entry2 };
6981
+ }
6982
+ function applyConfigProjectEntry(repoRoot, plan) {
6983
+ if (plan.action === "skip-present")
6984
+ return;
6985
+ const abs2 = join13(repoRoot, CONFIG_FILE_PATH);
6986
+ const seed = JSON.parse(readFileSync14(abs2, "utf-8"));
6987
+ const projects = Array.isArray(seed.projects) ? seed.projects : [];
6988
+ writeFileSync4(abs2, `${JSON.stringify({ ...seed, projects: [...projects, plan.entry] }, null, 2)}
6989
+ `, "utf-8");
6990
+ }
6991
+ function renderConfigProjectEntryDiffLine(plan) {
6992
+ if (plan.action === "skip-present")
6993
+ return ` = keep ${CONFIG_FILE_PATH} (project already declared under "projects")`;
6994
+ const bits = [`"name": ${JSON.stringify(plan.entry.name)}`];
6995
+ if (plan.entry.path)
6996
+ bits.push(`"path": ${JSON.stringify(plan.entry.path)}`);
6997
+ if (plan.entry.description)
6998
+ bits.push(`"description": ${JSON.stringify(plan.entry.description)}`);
6999
+ return ` ~ append entry to ${CONFIG_FILE_PATH}'s "projects"
7000
+ { ${bits.join(", ")} }`;
7001
+ }
7002
+
7003
+ // src/commands/doctor.ts
7004
+ function readVersion() {
7005
+ const pkg = JSON.parse(readFileSync15(join14(packageRoot(import.meta.url), "package.json"), "utf-8"));
7006
+ return pkg.version;
7007
+ }
7008
+ function realDeps4() {
7009
+ return {
7010
+ detectRepo: detectGitRepo,
7011
+ ghAuthStatus,
7012
+ branchProtectionConfigured,
7013
+ hookDirFor: resolveHookDir,
7014
+ readHooksPath: readCoreHooksPath,
7015
+ nodeVersion: () => process.version,
7016
+ bunVersion: () => typeof Bun === "undefined" ? null : Bun.version,
7017
+ packageVersion: readVersion,
7018
+ meteringCapability: () => resolveMeteringCapability(hardenedMeteringDeps())
7019
+ };
7020
+ }
7021
+ var ok = (check, message) => ({ check, severity: "ok", message });
7022
+ var info = (check, message) => ({ check, severity: "info", message });
7023
+ var warn = (check, message) => ({ check, severity: "warn", message });
7024
+ var error = (check, message) => ({ check, severity: "error", message });
7025
+ function readConfig(repoRoot) {
7026
+ const p = join14(repoRoot, CONFIG_PATH);
7027
+ if (!existsSync12(p))
7028
+ return { kind: "missing" };
7029
+ let raw;
7030
+ try {
7031
+ raw = JSON.parse(readFileSync15(p, "utf-8"));
7032
+ } catch (err) {
7033
+ return { kind: "invalid", error: `invalid JSON: ${err.message}` };
7034
+ }
7035
+ const parsed = VinayaConfigSchema.safeParse(raw);
7036
+ if (!parsed.success) {
7037
+ return {
7038
+ kind: "invalid",
7039
+ error: parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")
7040
+ };
6273
7041
  }
6274
7042
  return { kind: "ok", config: parsed.data };
6275
7043
  }
@@ -6299,14 +7067,14 @@ function diagnoseInstall(repoRoot, ctx, manifest) {
6299
7067
  for (const op of buildInitOps(ctx)) {
6300
7068
  if (op.kind === "create-file") {
6301
7069
  const check = labelForPath(op.path);
6302
- const abs2 = join13(repoRoot, op.path);
6303
- const exists = existsSync11(abs2);
7070
+ const abs2 = join14(repoRoot, op.path);
7071
+ const exists = existsSync12(abs2);
6304
7072
  const owned = ownedFiles.has(op.path) || isDefaultedAgentVendorPath(op.path, manifest);
6305
7073
  if (!exists) {
6306
7074
  findings.push(owned ? error(check, `${op.path} is recorded as vinaya-managed but missing on disk — run \`vinaya upgrade\`.`) : error(check, `${op.path} is not installed — run \`vinaya init\`.`));
6307
7075
  continue;
6308
7076
  }
6309
- const content = readFileSync13(abs2, "utf-8");
7077
+ const content = readFileSync15(abs2, "utf-8");
6310
7078
  if (!owned) {
6311
7079
  findings.push(content === op.content ? warn(check, `${op.path} has vinaya's own content but isn't recorded in the manifest.`) : info(check, `${op.path} exists but is foreign content — not vinaya-managed, left untouched.`));
6312
7080
  continue;
@@ -6325,11 +7093,11 @@ function diagnoseInstall(repoRoot, ctx, manifest) {
6325
7093
  const check = "hooks";
6326
7094
  const abs2 = resolveManagedBlockPath(repoRoot, op.path);
6327
7095
  const owned = ownedBlocks.has(blockKey(op.path, op.marker));
6328
- if (!existsSync11(abs2)) {
7096
+ if (!existsSync12(abs2)) {
6329
7097
  findings.push(owned ? error(check, `${op.path} is missing — likely a fresh clone (raw git hooks aren't tracked by git). Run \`vinaya upgrade\` to restore it.`) : info(check, `${op.path} is not installed.`));
6330
7098
  continue;
6331
7099
  }
6332
- const content = readFileSync13(abs2, "utf-8");
7100
+ const content = readFileSync15(abs2, "utf-8");
6333
7101
  const { begin, end } = markerLines(op.marker, op.comment);
6334
7102
  const hasMarkers = content.includes(begin) && content.includes(end);
6335
7103
  if (!hasMarkers) {
@@ -6377,22 +7145,22 @@ async function diagnoseHookRouting(repoRoot, hookDir, readHooksPath) {
6377
7145
  function diagnoseCustomChecks(repoRoot, config) {
6378
7146
  const findings = [];
6379
7147
  for (const [name, entry2] of Object.entries(config.checks ?? {})) {
6380
- const scriptAbs = join13(repoRoot, entry2.run);
6381
- findings.push(existsSync11(scriptAbs) ? ok("checks", `custom check '${name}' → ${entry2.run}`) : error("checks", `custom check '${name}' points at a missing script: ${entry2.run}`));
7148
+ const scriptAbs = join14(repoRoot, entry2.run);
7149
+ findings.push(existsSync12(scriptAbs) ? ok("checks", `custom check '${name}' → ${entry2.run}`) : error("checks", `custom check '${name}' points at a missing script: ${entry2.run}`));
6382
7150
  }
6383
7151
  return findings;
6384
7152
  }
6385
7153
  function listTrackedFiles(repoRoot) {
6386
7154
  try {
6387
- return execFileSync8("git", ["ls-files"], { cwd: repoRoot, encoding: "utf-8" }).split(`
7155
+ return execFileSync9("git", ["ls-files"], { cwd: repoRoot, encoding: "utf-8" }).split(`
6388
7156
  `).map((s) => s.trim()).filter(Boolean);
6389
7157
  } catch {
6390
7158
  return [];
6391
7159
  }
6392
7160
  }
6393
7161
  function diagnoseDocOwnersHealth(repoRoot) {
6394
- const path = join13(repoRoot, DOC_OWNERS_PATH);
6395
- const content = existsSync11(path) ? readFileSync13(path, "utf-8") : null;
7162
+ const path = join14(repoRoot, DOC_OWNERS_PATH);
7163
+ const content = existsSync12(path) ? readFileSync15(path, "utf-8") : null;
6396
7164
  const state = classifyDocOwnersManifest(content);
6397
7165
  if (state === "absent" || state === "empty")
6398
7166
  return [];
@@ -6409,8 +7177,8 @@ function diagnoseDocOwnersHealth(repoRoot) {
6409
7177
  findings.push(warn("doc-owners", `${DOC_OWNERS_PATH}:${b.lineNum} binds glob '${b.glob}', which matches none of the ${codeFiles.length} ` + "tracked code file(s) in this repo — the code it names may have been deleted, renamed, or never " + "existed. Repoint the binding to the code's new location, or remove it."));
6410
7178
  }
6411
7179
  if (!isUrlPointer(b.pointer)) {
6412
- const pointerPath = join13(repoRoot, pointerToPath(b.pointer));
6413
- if (!existsSync11(pointerPath)) {
7180
+ const pointerPath = join14(repoRoot, pointerToPath(b.pointer));
7181
+ if (!existsSync12(pointerPath)) {
6414
7182
  flagged = true;
6415
7183
  findings.push(warn("doc-owners", `${DOC_OWNERS_PATH}:${b.lineNum} points to ${b.pointer}, which does not exist on disk. Repoint the ` + "binding, or add the missing doc."));
6416
7184
  }
@@ -6425,7 +7193,7 @@ var LEGACY_AEG_PACKAGES_PATH = ".aeg/packages";
6425
7193
  function readWorkspacesForDoctor(repoRoot) {
6426
7194
  const fromPackageJson = () => {
6427
7195
  try {
6428
- const pkg = JSON.parse(readFileSync13(join13(repoRoot, "package.json"), "utf-8"));
7196
+ const pkg = JSON.parse(readFileSync15(join14(repoRoot, "package.json"), "utf-8"));
6429
7197
  return Array.isArray(pkg.workspaces) ? pkg.workspaces.filter((w) => typeof w === "string") : [];
6430
7198
  } catch {
6431
7199
  return [];
@@ -6433,7 +7201,7 @@ function readWorkspacesForDoctor(repoRoot) {
6433
7201
  };
6434
7202
  const fromPnpmWorkspaceYaml = () => {
6435
7203
  try {
6436
- return parsePnpmWorkspaceYaml(readFileSync13(join13(repoRoot, "pnpm-workspace.yaml"), "utf-8"));
7204
+ return parsePnpmWorkspaceYaml(readFileSync15(join14(repoRoot, "pnpm-workspace.yaml"), "utf-8"));
6437
7205
  } catch {
6438
7206
  return [];
6439
7207
  }
@@ -6442,23 +7210,23 @@ function readWorkspacesForDoctor(repoRoot) {
6442
7210
  }
6443
7211
  function listChildDirsForDoctor(dir, repoRoot) {
6444
7212
  try {
6445
- return readdirSync6(join13(repoRoot, dir), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
7213
+ return readdirSync6(join14(repoRoot, dir), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
6446
7214
  } catch {
6447
7215
  return [];
6448
7216
  }
6449
7217
  }
6450
7218
  function diagnoseBlastRadiusDeprecation(repoRoot, config) {
6451
7219
  const derived = deriveWorkspacePackageDomains(readWorkspacesForDoctor(repoRoot), (dir) => listChildDirsForDoctor(dir, repoRoot));
6452
- const defaults = deriveBuiltinCrossCuttingDefaults((p) => existsSync11(join13(repoRoot, p)));
7220
+ const defaults = deriveBuiltinCrossCuttingDefaults((p) => existsSync12(join14(repoRoot, p)));
6453
7221
  const covered = new Set([...derived, ...defaults]);
6454
7222
  const configExtra = new Set(config?.blastRadius?.extraDomains ?? []);
6455
- const legacyPath = join13(repoRoot, LEGACY_AEG_PACKAGES_PATH);
6456
- if (!existsSync11(legacyPath)) {
7223
+ const legacyPath = join14(repoRoot, LEGACY_AEG_PACKAGES_PATH);
7224
+ if (!existsSync12(legacyPath)) {
6457
7225
  return [
6458
7226
  info("blast-radius", `checkBlastRadiusScope is active via live derivation (${derived.length} packages/* domain(s)) + built-in defaults (${defaults.length} present) — no ${LEGACY_AEG_PACKAGES_PATH}, and the check is not dormant.`)
6459
7227
  ];
6460
7228
  }
6461
- const legacyEntries = readFileSync13(legacyPath, "utf-8").split(`
7229
+ const legacyEntries = readFileSync15(legacyPath, "utf-8").split(`
6462
7230
  `).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
6463
7231
  const uncovered = legacyEntries.filter((e) => !covered.has(e) && !configExtra.has(e));
6464
7232
  const migrationNote = uncovered.length === 0 ? "every entry is already covered by live derivation, the built-in defaults, or vinaya.config.json blastRadius.extraDomains — the file can be deleted." : `migrate ${uncovered.length} entr${uncovered.length === 1 ? "y" : "ies"} not yet covered before deleting it: ${uncovered.join(", ")}.`;
@@ -6493,7 +7261,7 @@ function diagnoseBriefSchemaDrift(config) {
6493
7261
  }
6494
7262
  function diagnoseEnvDeclarations(repoRoot, config) {
6495
7263
  const resolved = resolveChecks(coreCheckRegistry(), config?.checks).resolved;
6496
- const specs = resolved.map((entry2) => entry2.source === "config" ? { ...entry2.spec, run: join13(repoRoot, entry2.spec.run) } : entry2.spec);
7264
+ const specs = resolved.map((entry2) => entry2.source === "config" ? { ...entry2.spec, run: join14(repoRoot, entry2.spec.run) } : entry2.spec);
6497
7265
  const missing = checksMissingEnvDeclaration(specs);
6498
7266
  const findings = missing.map((name) => info("env", envDeclarationWarning(name)));
6499
7267
  for (const message of lintEnvDeclarations(config?.checks)) {
@@ -6514,11 +7282,11 @@ function diagnoseCheckClassification(config) {
6514
7282
  return findings;
6515
7283
  }
6516
7284
  function diagnoseGlobalConfigChecks() {
6517
- if (!existsSync11(GLOBAL_CONFIG_PATH))
7285
+ if (!existsSync12(GLOBAL_CONFIG_PATH))
6518
7286
  return [];
6519
7287
  let raw;
6520
7288
  try {
6521
- raw = JSON.parse(readFileSync13(GLOBAL_CONFIG_PATH, "utf-8"));
7289
+ raw = JSON.parse(readFileSync15(GLOBAL_CONFIG_PATH, "utf-8"));
6522
7290
  } catch (err) {
6523
7291
  return [info("checks", `${GLOBAL_CONFIG_PATH} is invalid JSON — ${err.message}`)];
6524
7292
  }
@@ -6567,13 +7335,13 @@ function coversWorkflowsDir(pattern) {
6567
7335
  return bare === ".github/workflows" || bare === "/.github/workflows";
6568
7336
  }
6569
7337
  function diagnoseCodeowners(repoRoot) {
6570
- const path = join13(repoRoot, ".github", "CODEOWNERS");
6571
- if (!existsSync11(path)) {
7338
+ const path = join14(repoRoot, ".github", "CODEOWNERS");
7339
+ if (!existsSync12(path)) {
6572
7340
  return info("codeowners", "no .github/CODEOWNERS — vinaya's workflow files have no required-review protection; see `vinaya init`'s printed recommendation.");
6573
7341
  }
6574
7342
  let body;
6575
7343
  try {
6576
- body = readFileSync13(path, "utf-8");
7344
+ body = readFileSync15(path, "utf-8");
6577
7345
  } catch (err) {
6578
7346
  return warn("codeowners", `.github/CODEOWNERS exists but could not be read: ${err.message}`);
6579
7347
  }
@@ -6586,7 +7354,7 @@ function diagnoseVinayaOnPath(agents) {
6586
7354
  return [];
6587
7355
  const names = process.platform === "win32" ? ["vinaya.cmd", "vinaya.exe", "vinaya.bat"] : ["vinaya"];
6588
7356
  const dirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
6589
- const onPath = dirs.some((dir) => names.some((name) => existsSync11(join13(dir, name))));
7357
+ const onPath = dirs.some((dir) => names.some((name) => existsSync12(join14(dir, name))));
6590
7358
  return [
6591
7359
  onPath ? ok("vinaya-on-path", "`vinaya` resolves on PATH — agent-native commands will work.") : info("vinaya-on-path", "`vinaya` is not resolvable on PATH — `/vinaya <role>` and the .agents/skills/vinaya-*/SKILL.md files " + 'installed here will fail "command not found" on first use. Run `npm install -g @attalabs/vinaya`.')
6592
7360
  ];
@@ -6597,21 +7365,38 @@ function diagnosePrincipals(config) {
6597
7365
  }
6598
7366
  return info("principals", 'vinaya.config.json has no "principals" — review-gate and the waiver-label actor check both fall back to a ' + "hardcoded placeholder allowlist that will not include anyone on this repo. Every reviewer's verdict is " + 'silently ignored (DANGLING) until you add `"principals": ["<your-github-login>", ...]` to vinaya.config.json.');
6599
7367
  }
7368
+ function diagnoseProjectsCoherence(repoRoot, config) {
7369
+ const registryAbs = join14(repoRoot, PROJECTS_REGISTRY_PATH);
7370
+ const registryNames = existsSync12(registryAbs) ? new Set(parseRegistry(readFileSync15(registryAbs, "utf-8")).map((p) => p.name)) : new Set;
7371
+ const configNames = new Set((config?.projects ?? []).map((p) => p.name));
7372
+ if (registryNames.size === 0 && configNames.size === 0)
7373
+ return [];
7374
+ const registryOnly = [...registryNames].filter((n) => !configNames.has(n)).sort();
7375
+ const configOnly = [...configNames].filter((n) => !registryNames.has(n)).sort();
7376
+ const findings = [];
7377
+ if (registryOnly.length > 0) {
7378
+ findings.push(info("projects", `${PROJECTS_REGISTRY_PATH} declares ${registryOnly.join(", ")} with no matching vinaya.config.json "projects" entry.`));
7379
+ }
7380
+ if (configOnly.length > 0) {
7381
+ findings.push(info("projects", `vinaya.config.json "projects" declares ${configOnly.join(", ")} with no matching ${PROJECTS_REGISTRY_PATH} row.`));
7382
+ }
7383
+ return findings;
7384
+ }
6600
7385
  var TEST_INVOCATION_SUBSTRINGS = ["npm test", "npm run test", "bun test", "bunx turbo test"];
6601
7386
  function diagnoseTestCi(repoRoot) {
6602
7387
  let pkg;
6603
7388
  try {
6604
- pkg = JSON.parse(readFileSync13(join13(repoRoot, "package.json"), "utf-8"));
7389
+ pkg = JSON.parse(readFileSync15(join14(repoRoot, "package.json"), "utf-8"));
6605
7390
  } catch {
6606
7391
  return [];
6607
7392
  }
6608
7393
  const testScript = pkg?.scripts?.test;
6609
7394
  if (typeof testScript !== "string" || testScript.trim() === "")
6610
7395
  return [];
6611
- const workflowsDir = join13(repoRoot, ".github/workflows");
6612
- const files = existsSync11(workflowsDir) ? readdirSync6(workflowsDir).filter((name) => statSync2(join13(workflowsDir, name)).isFile()) : [];
7396
+ const workflowsDir = join14(repoRoot, ".github/workflows");
7397
+ const files = existsSync12(workflowsDir) ? readdirSync6(workflowsDir).filter((name) => statSync2(join14(workflowsDir, name)).isFile()) : [];
6613
7398
  const invoked = files.some((name) => {
6614
- const content = readFileSync13(join13(workflowsDir, name), "utf-8");
7399
+ const content = readFileSync15(join14(workflowsDir, name), "utf-8");
6615
7400
  return TEST_INVOCATION_SUBSTRINGS.some((s) => content.includes(s));
6616
7401
  });
6617
7402
  if (invoked)
@@ -6678,6 +7463,7 @@ async function runDoctor(args, deps) {
6678
7463
  findings.push(diagnosePrincipals(configRead.config));
6679
7464
  findings.push(...diagnoseVinayaOnPath(ctx.agents));
6680
7465
  }
7466
+ findings.push(...diagnoseProjectsCoherence(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
6681
7467
  findings.push(...diagnoseBlastRadiusDeprecation(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
6682
7468
  findings.push(...diagnoseBriefSchemaDrift(configRead.kind === "ok" ? configRead.config : null));
6683
7469
  findings.push(...diagnoseEnvDeclarations(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
@@ -6697,13 +7483,13 @@ async function runDoctor(args, deps) {
6697
7483
  return healthy ? 0 : 1;
6698
7484
  }
6699
7485
  async function doctorCommand(args) {
6700
- process.exit(await runDoctor(args, realDeps3()));
7486
+ process.exit(await runDoctor(args, realDeps4()));
6701
7487
  }
6702
7488
 
6703
7489
  // src/commands/eject.ts
6704
- import { existsSync as existsSync12, readFileSync as readFileSync14 } from "node:fs";
6705
- import { join as join14 } from "node:path";
6706
- function realDeps4() {
7490
+ import { existsSync as existsSync13, readFileSync as readFileSync16 } from "node:fs";
7491
+ import { join as join15 } from "node:path";
7492
+ function realDeps5() {
6707
7493
  return {
6708
7494
  detectRepo: detectGitRepo,
6709
7495
  readHooksPath: readCoreHooksPath,
@@ -6719,12 +7505,12 @@ function parse(args) {
6719
7505
  return { dryRun: args.includes("--dry-run"), yes: args.includes("--yes") };
6720
7506
  }
6721
7507
  function readManifest(repoRoot) {
6722
- const p = join14(repoRoot, CONFIG_PATH);
6723
- if (!existsSync12(p))
7508
+ const p = join15(repoRoot, CONFIG_PATH);
7509
+ if (!existsSync13(p))
6724
7510
  return { kind: "none" };
6725
7511
  let raw;
6726
7512
  try {
6727
- raw = JSON.parse(readFileSync14(p, "utf-8"));
7513
+ raw = JSON.parse(readFileSync16(p, "utf-8"));
6728
7514
  } catch (err) {
6729
7515
  return { kind: "orphan", reason: `vinaya.config.json is not valid JSON: ${err.message}` };
6730
7516
  }
@@ -6809,112 +7595,14 @@ Vinaya ejected.
6809
7595
  return 0;
6810
7596
  }
6811
7597
  async function ejectCommand(args) {
6812
- const code = await runEject(args, realDeps4());
7598
+ const code = await runEject(args, realDeps5());
6813
7599
  process.exit(code);
6814
7600
  }
6815
7601
 
6816
7602
  // src/commands/init.ts
6817
- import { existsSync as existsSync14, readFileSync as readFileSync16, writeFileSync as writeFileSync5 } from "node:fs";
7603
+ import { existsSync as existsSync14, readFileSync as readFileSync17, writeFileSync as writeFileSync5 } from "node:fs";
6818
7604
  import { join as join16 } from "node:path";
6819
-
6820
- // src/lib/registry-write.ts
6821
- import { existsSync as existsSync13, mkdirSync as mkdirSync3, readFileSync as readFileSync15, writeFileSync as writeFileSync4 } from "node:fs";
6822
- import { dirname as dirname6, join as join15 } from "node:path";
6823
- var PROJECTS_REGISTRY_PATH = ".vinaya/projects.md";
6824
- function rowLine(name, path, specsPath) {
6825
- return `| ${name} | \`${path}\` | \`${specsPath}\` | (state tracked globally) |`;
6826
- }
6827
- function freshProjectsRegistry(name, path, specsPath) {
6828
- return `---
6829
- sidebar_title: Projects
6830
- ---
6831
- # Projects in this repo
6832
-
6833
- **The project registry.** Declares the projects in this repo and where each
6834
- one's specs and per-project state live. The \`Project\` field on a task (a
6835
- forge Issue) resolves against this file.
6836
-
6837
- **Presence of this file means this is a multi-project repo** — the
6838
- \`Project\` field is required on task Issues/PRs that touch a registered
6839
- project's path. A project is a \`(name, path)\` pair you declared — nothing
6840
- is derived from the folder tree.
6841
-
6842
- ## Registry
6843
-
6844
- | Project | Path | Specs | Per-project state |
6845
- |---------|------|-------|---------------------|
6846
- ${rowLine(name, path, specsPath)}
6847
- `;
6848
- }
6849
- var REGISTRY_HEADING_RE = /^##\s+Registry\b/i;
6850
- function appendRegistryRow(content, name, path, specsPath) {
6851
- const lines = content.split(/\r?\n/);
6852
- const headingIdx = lines.findIndex((l) => REGISTRY_HEADING_RE.test(l));
6853
- const newRow = rowLine(name, path, specsPath);
6854
- if (headingIdx === -1) {
6855
- const sep4 = content.endsWith(`
6856
- `) ? "" : `
6857
- `;
6858
- return `${content}${sep4}
6859
- ## Registry
6860
-
6861
- | Project | Path | Specs | Per-project state |
6862
- |---------|------|-------|---------------------|
6863
- ${newRow}
6864
- `;
6865
- }
6866
- let i = headingIdx + 1;
6867
- for (;i < lines.length; i++) {
6868
- if ((lines[i] ?? "").trim().startsWith("|"))
6869
- break;
6870
- }
6871
- if (i >= lines.length) {
6872
- lines.splice(headingIdx + 1, 0, "", "| Project | Path | Specs | Per-project state |", "|---------|------|-------|---------------------|", newRow);
6873
- return lines.join(`
6874
- `);
6875
- }
6876
- let last = i + 1;
6877
- for (let j = i + 2;j < lines.length; j++) {
6878
- if (!(lines[j] ?? "").trim().startsWith("|"))
6879
- break;
6880
- last = j;
6881
- }
6882
- lines.splice(last + 1, 0, newRow);
6883
- return lines.join(`
6884
- `);
6885
- }
6886
- function planRegistryRow(repoRoot, name, path, specsPath) {
6887
- const abs2 = join15(repoRoot, PROJECTS_REGISTRY_PATH);
6888
- const line = rowLine(name, path, specsPath);
6889
- if (!existsSync13(abs2)) {
6890
- return { action: "create-host", rowLine: line, path: PROJECTS_REGISTRY_PATH };
6891
- }
6892
- const existing = readFileSync15(abs2, "utf-8");
6893
- const already = parseRegistry(existing).some((p) => p.name === name);
6894
- return { action: already ? "skip-present" : "append-row", rowLine: line, path: PROJECTS_REGISTRY_PATH };
6895
- }
6896
- function applyRegistryRow(repoRoot, plan, name, path, specsPath) {
6897
- const abs2 = join15(repoRoot, PROJECTS_REGISTRY_PATH);
6898
- if (plan.action === "create-host") {
6899
- mkdirSync3(dirname6(abs2), { recursive: true });
6900
- writeFileSync4(abs2, freshProjectsRegistry(name, path, specsPath), "utf-8");
6901
- } else if (plan.action === "append-row") {
6902
- const existing = readFileSync15(abs2, "utf-8");
6903
- writeFileSync4(abs2, appendRegistryRow(existing, name, path, specsPath), "utf-8");
6904
- }
6905
- }
6906
- function renderRegistryRowDiffLine(plan) {
6907
- if (plan.action === "skip-present")
6908
- return ` = keep ${plan.path} (project already registered)`;
6909
- if (plan.action === "append-row")
6910
- return ` ~ append row to ${plan.path}
6911
- ${plan.rowLine}`;
6912
- return ` + create ${plan.path} (with registry row)
6913
- ${plan.rowLine}`;
6914
- }
6915
-
6916
- // src/commands/init.ts
6917
- function realDeps5() {
7605
+ function realDeps6() {
6918
7606
  return {
6919
7607
  detectRepo: detectGitRepo,
6920
7608
  checkGhAuth,
@@ -6957,14 +7645,14 @@ function readManifest2(repoRoot) {
6957
7645
  if (!existsSync14(p))
6958
7646
  return null;
6959
7647
  try {
6960
- return VinayaConfigSchema.parse(JSON.parse(readFileSync16(p, "utf-8"))).managed ?? null;
7648
+ return VinayaConfigSchema.parse(JSON.parse(readFileSync17(p, "utf-8"))).managed ?? null;
6961
7649
  } catch {
6962
7650
  return null;
6963
7651
  }
6964
7652
  }
6965
7653
  function writeManifest(repoRoot, manifest) {
6966
7654
  const configAbs = join16(repoRoot, CONFIG_PATH);
6967
- const seed = JSON.parse(readFileSync16(configAbs, "utf-8"));
7655
+ const seed = JSON.parse(readFileSync17(configAbs, "utf-8"));
6968
7656
  writeFileSync5(configAbs, `${JSON.stringify({ ...seed, managed: manifest }, null, 2)}
6969
7657
  `, "utf-8");
6970
7658
  }
@@ -7072,6 +7760,7 @@ async function runInitProduct(args, deps) {
7072
7760
  return 1;
7073
7761
  }
7074
7762
  const registryPlan = planRegistryRow(repo.repoRoot, name, productPath, specsPath);
7763
+ const configPlan = planConfigProjectEntry(repo.repoRoot, { name, path: productPath });
7075
7764
  process.stdout.write(`vinaya init product ${name} — the full diff:
7076
7765
 
7077
7766
  `);
@@ -7079,6 +7768,11 @@ async function runInitProduct(args, deps) {
7079
7768
  `);
7080
7769
  process.stdout.write(`${renderRegistryRowDiffLine(registryPlan)}
7081
7770
 
7771
+ `);
7772
+ process.stdout.write(`── Config ────────────────────────────────────────
7773
+ `);
7774
+ process.stdout.write(`${renderConfigProjectEntryDiffLine(configPlan)}
7775
+
7082
7776
  `);
7083
7777
  if (dryRun) {
7084
7778
  process.stdout.write(`--dry-run: nothing was written.
@@ -7094,25 +7788,26 @@ async function runInitProduct(args, deps) {
7094
7788
  }
7095
7789
  }
7096
7790
  applyRegistryRow(repo.repoRoot, registryPlan, name, productPath, specsPath);
7791
+ applyConfigProjectEntry(repo.repoRoot, configPlan);
7097
7792
  process.stdout.write(`
7098
7793
  Governed product area '${name}' scaffolded.
7099
7794
  `);
7100
7795
  return 0;
7101
7796
  }
7102
7797
  async function initCommand(args) {
7103
- process.exit(await runInit(args, realDeps5()));
7798
+ process.exit(await runInit(args, realDeps6()));
7104
7799
  }
7105
7800
  async function initProductCommand(args) {
7106
- process.exit(await runInitProduct(args, realDeps5()));
7801
+ process.exit(await runInitProduct(args, realDeps6()));
7107
7802
  }
7108
7803
 
7109
7804
  // src/commands/issue.ts
7110
- import { execFileSync as execFileSync10 } from "node:child_process";
7805
+ import { execFileSync as execFileSync11 } from "node:child_process";
7111
7806
 
7112
7807
  // src/lib/forge-write.ts
7113
- import { execFileSync as execFileSync9 } from "node:child_process";
7808
+ import { execFileSync as execFileSync10 } from "node:child_process";
7114
7809
  import { existsSync as existsSync15, mkdtempSync, readdirSync as readdirSync7, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
7115
- import { readFileSync as readFileSync17 } from "node:fs";
7810
+ import { readFileSync as readFileSync18 } from "node:fs";
7116
7811
  import { tmpdir } from "node:os";
7117
7812
  import { join as join17 } from "node:path";
7118
7813
  class ForgeArgError extends Error {
@@ -7124,13 +7819,13 @@ function locateBody(args) {
7124
7819
  const p = args[i + 1];
7125
7820
  if (!p)
7126
7821
  throw new ForgeArgError("`--body-file` was given with no path.");
7127
- return { body: readFileSync17(p, "utf8"), source: { kind: "file", argIndex: i + 1, inlineForm: false } };
7822
+ return { body: readFileSync18(p, "utf8"), source: { kind: "file", argIndex: i + 1, inlineForm: false } };
7128
7823
  }
7129
7824
  if (a.startsWith("--body-file=")) {
7130
7825
  const p = a.slice("--body-file=".length);
7131
7826
  if (!p)
7132
7827
  throw new ForgeArgError("`--body-file=` was given with no path.");
7133
- return { body: readFileSync17(p, "utf8"), source: { kind: "file", argIndex: i, inlineForm: true } };
7828
+ return { body: readFileSync18(p, "utf8"), source: { kind: "file", argIndex: i, inlineForm: true } };
7134
7829
  }
7135
7830
  if (a === "--body" || a === "-b") {
7136
7831
  const v = args[i + 1];
@@ -7204,7 +7899,7 @@ function ensureTrancheLabelExists(slug) {
7204
7899
  const name = trancheLabel(slug);
7205
7900
  let existing;
7206
7901
  try {
7207
- const out = execFileSync9("gh", ["label", "list", "--json", "name", "--limit", "200"], {
7902
+ const out = execFileSync10("gh", ["label", "list", "--json", "name", "--limit", "200"], {
7208
7903
  encoding: "utf8",
7209
7904
  stdio: ["ignore", "pipe", "pipe"]
7210
7905
  });
@@ -7217,7 +7912,7 @@ function ensureTrancheLabelExists(slug) {
7217
7912
  if (existing.some((l) => l.name === name))
7218
7913
  return;
7219
7914
  try {
7220
- execFileSync9("gh", ["label", "create", name, "--color", TRANCHE_LABEL_COLOR, "--description", `Tranche: ${slug}`], {
7915
+ execFileSync10("gh", ["label", "create", name, "--color", TRANCHE_LABEL_COLOR, "--description", `Tranche: ${slug}`], {
7221
7916
  encoding: "utf8",
7222
7917
  stdio: ["ignore", "pipe", "pipe"]
7223
7918
  });
@@ -7227,6 +7922,24 @@ function ensureTrancheLabelExists(slug) {
7227
7922
  ]);
7228
7923
  }
7229
7924
  }
7925
+ function resolveMilestoneAttachArgs(ghArgs, labels) {
7926
+ if (!isTaskIssueLabelSet(labels))
7927
+ return ghArgs;
7928
+ if (hasExplicitMilestoneFlag(ghArgs))
7929
+ return ghArgs;
7930
+ const slug = findTrancheSlug(labels);
7931
+ if (!slug)
7932
+ return ghArgs;
7933
+ let target;
7934
+ try {
7935
+ target = findMilestoneAttachTargetForSlug("{owner}", "{repo}", slug);
7936
+ } catch {
7937
+ process.stderr.write(`vinaya: milestone lookup for '${slug}' failed (\`gh api\`) — creating without --milestone.
7938
+ `);
7939
+ return ghArgs;
7940
+ }
7941
+ return target ? [...ghArgs, "--milestone", target.title] : ghArgs;
7942
+ }
7230
7943
  function resolveSections(kind, retryCommand) {
7231
7944
  const result = loadConfigChecked();
7232
7945
  if (!result.ok) {
@@ -7351,7 +8064,7 @@ function validateForgeWrite(input) {
7351
8064
  }
7352
8065
  function git2(args) {
7353
8066
  try {
7354
- return execFileSync9("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
8067
+ return execFileSync10("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
7355
8068
  } catch {
7356
8069
  return "";
7357
8070
  }
@@ -7362,7 +8075,7 @@ function repoRoot() {
7362
8075
  function readWorkspaceGlobs(root) {
7363
8076
  const fromPackageJson = () => {
7364
8077
  try {
7365
- const pkg = JSON.parse(readFileSync17(join17(root, "package.json"), "utf8"));
8078
+ const pkg = JSON.parse(readFileSync18(join17(root, "package.json"), "utf8"));
7366
8079
  return Array.isArray(pkg.workspaces) ? pkg.workspaces.filter((w) => typeof w === "string") : [];
7367
8080
  } catch {
7368
8081
  return [];
@@ -7370,7 +8083,7 @@ function readWorkspaceGlobs(root) {
7370
8083
  };
7371
8084
  const fromPnpmWorkspaceYaml = () => {
7372
8085
  try {
7373
- return parsePnpmWorkspaceYaml(readFileSync17(join17(root, "pnpm-workspace.yaml"), "utf8"));
8086
+ return parsePnpmWorkspaceYaml(readFileSync18(join17(root, "pnpm-workspace.yaml"), "utf8"));
7374
8087
  } catch {
7375
8088
  return [];
7376
8089
  }
@@ -7386,7 +8099,7 @@ function listWorkspaceChildDirs(dir, root) {
7386
8099
  }
7387
8100
  function readConfigExtraDomains(root) {
7388
8101
  try {
7389
- const parsed = VinayaConfigSchema.safeParse(JSON.parse(readFileSync17(join17(root, "vinaya.config.json"), "utf8")));
8102
+ const parsed = VinayaConfigSchema.safeParse(JSON.parse(readFileSync18(join17(root, "vinaya.config.json"), "utf8")));
7390
8103
  return parsed.success ? parsed.data.blastRadius?.extraDomains ?? [] : [];
7391
8104
  } catch {
7392
8105
  return [];
@@ -7404,7 +8117,7 @@ function readProjectPaths(root = repoRoot()) {
7404
8117
  if (!root)
7405
8118
  return [];
7406
8119
  try {
7407
- return parseRegistry(readFileSync17(join17(root, ".vinaya/projects.md"), "utf8"));
8120
+ return parseRegistry(readFileSync18(join17(root, ".vinaya/projects.md"), "utf8"));
7408
8121
  } catch {
7409
8122
  return [];
7410
8123
  }
@@ -7446,7 +8159,7 @@ function locateBodyOrRefuse(ghArgs, retryCommand) {
7446
8159
  function fetchForgeLabels(issueRef) {
7447
8160
  let out;
7448
8161
  try {
7449
- out = execFileSync10("gh", ["issue", "view", issueRef, "--json", "labels"], {
8162
+ out = execFileSync11("gh", ["issue", "view", issueRef, "--json", "labels"], {
7450
8163
  encoding: "utf8",
7451
8164
  stdio: ["ignore", "pipe", "pipe"]
7452
8165
  });
@@ -7474,7 +8187,7 @@ function reportPass(json, command) {
7474
8187
  function runGhWrite(ghCmd, ghArgs, bodyResult, json) {
7475
8188
  const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
7476
8189
  try {
7477
- const out = execFileSync10("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
8190
+ const out = execFileSync11("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
7478
8191
  const url = out.trim();
7479
8192
  if (json)
7480
8193
  printJson({ validated: true, written: true, url });
@@ -7529,7 +8242,7 @@ function issueCreateCommand(args) {
7529
8242
  const slugToEnsure = findTrancheSlug(labels);
7530
8243
  if (slugToEnsure)
7531
8244
  ensureTrancheLabelExists(slugToEnsure);
7532
- runGhWrite(["issue", "create"], ghArgs, bodyResult, json);
8245
+ runGhWrite(["issue", "create"], resolveMilestoneAttachArgs(ghArgs, labels), bodyResult, json);
7533
8246
  }
7534
8247
  function issueEditCommand(args) {
7535
8248
  const json = args.includes("--json");
@@ -7560,12 +8273,13 @@ function issueEditCommand(args) {
7560
8273
  }
7561
8274
 
7562
8275
  // src/commands/milestone.ts
7563
- import { execFileSync as execFileSync11 } from "node:child_process";
8276
+ import { execFileSync as execFileSync12 } from "node:child_process";
7564
8277
  var RETRY_CREATE2 = "vinaya milestone create --title <title> --body-file <path>";
7565
8278
  var RETRY_ADOPT = "vinaya milestone adopt --target <title> --slug <slug> [--slug <slug> ...]";
7566
8279
  var RETRY_EDIT2 = "vinaya milestone edit <n> --body-file <path>";
8280
+ var RETRY_CLOSE = "vinaya milestone close --slug <slug>";
7567
8281
  function sh3(args, input) {
7568
- return execFileSync11(args[0], args.slice(1), {
8282
+ return execFileSync12(args[0], args.slice(1), {
7569
8283
  encoding: "utf8",
7570
8284
  input,
7571
8285
  env: process.env,
@@ -7723,18 +8437,18 @@ function extractSlugs(args) {
7723
8437
  }
7724
8438
  return slugs;
7725
8439
  }
7726
- function ghJsonOrRefuse(args, what) {
8440
+ function ghJsonOrRefuse(args, what, retryCommand) {
7727
8441
  try {
7728
8442
  return shJson2(args);
7729
8443
  } catch (e) {
7730
8444
  refuse([
7731
- makeCheckError("forge-fetch", `could not fetch ${what} from the forge: ${ghErrorDetail(e)}`, `Check \`gh auth status\` and network, then re-run \`${RETRY_ADOPT}\`.`)
8445
+ makeCheckError("forge-fetch", `could not fetch ${what} from the forge: ${ghErrorDetail(e)}`, `Check \`gh auth status\` and network, then re-run \`${retryCommand}\`.`)
7732
8446
  ]);
7733
8447
  }
7734
8448
  }
7735
8449
  function fetchAdoptFacts(repoFlag, target, slugs) {
7736
- const labels = ghJsonOrRefuse(["gh", "api", `repos/${repoFlag}/labels?per_page=100`], "this repo's labels");
7737
- const milestones = ghJsonOrRefuse(["gh", "api", `repos/${repoFlag}/milestones?state=all&per_page=100`], "this repo's Milestones");
8450
+ const labels = ghJsonOrRefuse(["gh", "api", `repos/${repoFlag}/labels?per_page=100`], "this repo's labels", RETRY_ADOPT);
8451
+ const milestones = ghJsonOrRefuse(["gh", "api", `repos/${repoFlag}/milestones?state=all&per_page=100`], "this repo's Milestones", RETRY_ADOPT);
7738
8452
  const targetMilestone = milestones.find((m) => m.title === target);
7739
8453
  const targetFacts = {
7740
8454
  title: target,
@@ -7757,7 +8471,7 @@ function fetchAdoptFacts(repoFlag, target, slugs) {
7757
8471
  "number,state,milestone",
7758
8472
  "--limit",
7759
8473
  "200"
7760
- ], `Issues for \`${trancheLabel(slug)}\``) : [];
8474
+ ], `Issues for \`${trancheLabel(slug)}\``, RETRY_ADOPT) : [];
7761
8475
  const elsewhere = new Set;
7762
8476
  for (const issue of issues) {
7763
8477
  const title = issue.milestone?.title;
@@ -7803,56 +8517,149 @@ async function milestoneAdoptCommand(args) {
7803
8517
  }
7804
8518
  if (validateOnly) {
7805
8519
  if (json)
7806
- printJson({ validated: true, written: false, command: "milestone adopt" });
7807
- else
7808
- process.stdout.write(`✓ adopt is safe — nothing written (--validate-only).
8520
+ printJson({ validated: true, written: false, command: "milestone adopt" });
8521
+ else
8522
+ process.stdout.write(`✓ adopt is safe — nothing written (--validate-only).
8523
+ `);
8524
+ return;
8525
+ }
8526
+ const adopted = [];
8527
+ for (const slugFact of facts.slugs) {
8528
+ for (const issueNumber of slugFact.issueNumbers) {
8529
+ try {
8530
+ sh3(["gh", "issue", "edit", String(issueNumber), "-R", repoFlag, "--milestone", target]);
8531
+ } catch (e) {
8532
+ failMidWrite(`failed to attach Issue #${issueNumber} (tranche \`${slugFact.slug}\`) to Milestone "${target}": ` + `${ghErrorDetail(e)}. Slugs already adopted this run: ${adopted.map((a) => a.slug).join(", ") || "(none)"}.`);
8533
+ }
8534
+ }
8535
+ const legacy = milestones.find((m) => m.title === slugFact.slug && m.title !== target);
8536
+ let closedMilestone = null;
8537
+ if (legacy) {
8538
+ closedMilestone = legacy.number;
8539
+ if (legacy.state !== "closed") {
8540
+ try {
8541
+ sh3(["gh", "api", "-X", "PATCH", `repos/${repoFlag}/milestones/${legacy.number}`, "-f", "state=closed"]);
8542
+ } catch (e) {
8543
+ failMidWrite(`attached tranche \`${slugFact.slug}\`'s Issue(s) to Milestone "${target}", but failed to close its old ` + `Milestone #${legacy.number}: ${ghErrorDetail(e)}. Slugs already adopted this run: ` + `${adopted.map((a) => a.slug).join(", ") || "(none)"}.`);
8544
+ }
8545
+ }
8546
+ }
8547
+ adopted.push({ slug: slugFact.slug, issueCount: slugFact.issueNumbers.length, closedMilestone });
8548
+ }
8549
+ if (json)
8550
+ printJson({ validated: true, written: true, target, adopted });
8551
+ else {
8552
+ for (const a of adopted) {
8553
+ const closedNote = a.closedMilestone ? ` (old Milestone #${a.closedMilestone} closed)` : "";
8554
+ process.stdout.write(`${a.slug}: ${a.issueCount} Issue(s) → "${target}"${closedNote}
8555
+ `);
8556
+ }
8557
+ }
8558
+ }
8559
+ function extractSlug(args) {
8560
+ for (let i = 0;i < args.length; i++) {
8561
+ const a = args[i];
8562
+ if (a === "--slug")
8563
+ return args[i + 1] ?? null;
8564
+ if (a.startsWith("--slug="))
8565
+ return a.slice("--slug=".length);
8566
+ }
8567
+ return null;
8568
+ }
8569
+ async function milestoneCloseCommand(args) {
8570
+ const json = args.includes("--json");
8571
+ const validateOnly = args.includes("--validate-only");
8572
+ const rest = args.filter((a) => a !== "--json" && a !== "--validate-only");
8573
+ const slug = extractSlug(rest);
8574
+ if (!slug) {
8575
+ refuse([
8576
+ makeCheckError("forge-args", "`vinaya milestone close` requires a `--slug <slug>` — the tranche whose Milestone is being closed.", `Add \`--slug <slug>\`, then re-run \`${RETRY_CLOSE}\`.`)
8577
+ ]);
8578
+ }
8579
+ const repoFlag = await resolveRepoFlagOrRefuse(RETRY_CLOSE);
8580
+ const milestones = ghJsonOrRefuse(["gh", "api", `repos/${repoFlag}/milestones?state=all&per_page=100`], "this repo's Milestones", RETRY_CLOSE);
8581
+ const target = resolveMilestoneAttachTarget(milestones, slug);
8582
+ if (!target) {
8583
+ refuse([
8584
+ makeCheckError("milestone-close", `No OPEN Milestone resolves for tranche \`${slug}\` — it may already be closed, or never had one attached.`, `Confirm with \`gh issue list --label ${trancheLabel(slug)} --json milestone\`, or run \`vinaya milestone ` + `create\`/\`${RETRY_ADOPT}\` if a Milestone is genuinely still owed, then re-run \`${RETRY_CLOSE}\`.`)
8585
+ ]);
8586
+ }
8587
+ const labeledIssues = ghJsonOrRefuse([
8588
+ "gh",
8589
+ "issue",
8590
+ "list",
8591
+ "-R",
8592
+ repoFlag,
8593
+ "--label",
8594
+ trancheLabel(slug),
8595
+ "--state",
8596
+ "all",
8597
+ "--json",
8598
+ "number,state,milestone",
8599
+ "--limit",
8600
+ "200"
8601
+ ], `Issues for \`${trancheLabel(slug)}\``, RETRY_CLOSE);
8602
+ const attachedIssues = ghJsonOrRefuse([
8603
+ "gh",
8604
+ "issue",
8605
+ "list",
8606
+ "-R",
8607
+ repoFlag,
8608
+ "--milestone",
8609
+ target.title,
8610
+ "--state",
8611
+ "all",
8612
+ "--json",
8613
+ "number,state,milestone",
8614
+ "--limit",
8615
+ "200"
8616
+ ], `Issues attached to Milestone "${target.title}"`, RETRY_CLOSE);
8617
+ const report = checkMilestoneAttachment(labeledIssues.map((i) => i.number), attachedIssues.map((i) => i.number));
8618
+ if (report.status === "mismatch") {
8619
+ const errors = [];
8620
+ if (report.unattached.length > 0) {
8621
+ errors.push(makeCheckError("milestone-close", `Issue(s) ${report.unattached.map((n) => `#${n}`).join(", ")} carry \`${trancheLabel(slug)}\` but ` + `are not attached to Milestone "${target.title}".`, `Attach each with \`gh issue edit <n> --milestone "${target.title}"\`, then re-run \`${RETRY_CLOSE}\`.`));
8622
+ }
8623
+ if (report.foreign.length > 0) {
8624
+ errors.push(makeCheckError("milestone-close", `Issue(s) ${report.foreign.map((n) => `#${n}`).join(", ")} are attached to Milestone "${target.title}" ` + `but do not carry \`${trancheLabel(slug)}\` — closing would end tracking for whatever they belong to as well.`, `Move them with \`${RETRY_ADOPT}\` if they belong to a different tranche, or \`gh issue edit <n> ` + `--milestone <title>\` to relocate them, then re-run \`${RETRY_CLOSE}\`.`));
8625
+ }
8626
+ refuse(errors);
8627
+ }
8628
+ if (validateOnly) {
8629
+ if (json)
8630
+ printJson({ validated: true, written: false, command: "milestone close", number: target.number });
8631
+ else {
8632
+ process.stdout.write(`✓ Milestone "${target.title}" (#${target.number}) attachment verified — nothing written (--validate-only).
7809
8633
  `);
8634
+ }
7810
8635
  return;
7811
8636
  }
7812
- const adopted = [];
7813
- for (const slugFact of facts.slugs) {
7814
- for (const issueNumber of slugFact.issueNumbers) {
7815
- try {
7816
- sh3(["gh", "issue", "edit", String(issueNumber), "-R", repoFlag, "--milestone", target]);
7817
- } catch (e) {
7818
- failMidWrite(`failed to attach Issue #${issueNumber} (tranche \`${slugFact.slug}\`) to Milestone "${target}": ` + `${ghErrorDetail(e)}. Slugs already adopted this run: ${adopted.map((a) => a.slug).join(", ") || "(none)"}.`);
7819
- }
7820
- }
7821
- const legacy = milestones.find((m) => m.title === slugFact.slug && m.title !== target);
7822
- let closedMilestone = null;
7823
- if (legacy) {
7824
- closedMilestone = legacy.number;
7825
- if (legacy.state !== "closed") {
7826
- try {
7827
- sh3(["gh", "api", "-X", "PATCH", `repos/${repoFlag}/milestones/${legacy.number}`, "-f", "state=closed"]);
7828
- } catch (e) {
7829
- failMidWrite(`attached tranche \`${slugFact.slug}\`'s Issue(s) to Milestone "${target}", but failed to close its old ` + `Milestone #${legacy.number}: ${ghErrorDetail(e)}. Slugs already adopted this run: ` + `${adopted.map((a) => a.slug).join(", ") || "(none)"}.`);
7830
- }
7831
- }
7832
- }
7833
- adopted.push({ slug: slugFact.slug, issueCount: slugFact.issueNumbers.length, closedMilestone });
8637
+ let out;
8638
+ try {
8639
+ out = sh3(["gh", "api", "-X", "PATCH", `repos/${repoFlag}/milestones/${target.number}`, "-f", "state=closed"]);
8640
+ } catch (e) {
8641
+ refuse([
8642
+ makeCheckError("forge-fetch", `\`gh api repos/${repoFlag}/milestones/${target.number}\` failed: ${ghErrorDetail(e)}`, `Check \`gh auth status\` and network, then re-run \`${RETRY_CLOSE}\`.`)
8643
+ ]);
7834
8644
  }
8645
+ const closed = JSON.parse(out);
7835
8646
  if (json)
7836
- printJson({ validated: true, written: true, target, adopted });
7837
- else {
7838
- for (const a of adopted) {
7839
- const closedNote = a.closedMilestone ? ` (old Milestone #${a.closedMilestone} closed)` : "";
7840
- process.stdout.write(`${a.slug}: ${a.issueCount} Issue(s) → "${target}"${closedNote}
8647
+ printJson({ validated: true, written: true, number: closed.number, url: closed.html_url });
8648
+ else
8649
+ process.stdout.write(`${closed.html_url}
7841
8650
  `);
7842
- }
7843
- }
7844
8651
  }
7845
8652
 
7846
8653
  // src/commands/new-check.ts
7847
- import { chmodSync as chmodSync2, existsSync as existsSync16, mkdirSync as mkdirSync4, readFileSync as readFileSync18, writeFileSync as writeFileSync7 } from "node:fs";
8654
+ import { chmodSync as chmodSync2, existsSync as existsSync16, mkdirSync as mkdirSync4, readFileSync as readFileSync19, writeFileSync as writeFileSync7 } from "node:fs";
7848
8655
  import { join as join18 } from "node:path";
7849
8656
  var TEMPLATE_PATH = join18(packageRoot(import.meta.url), "templates", "custom-check.template.ts");
7850
8657
  var CHECKS_DIR = join18("scripts", "vinaya-checks");
7851
- var USAGE = "Usage: vinaya new check <yourname>/<id> (both segments: lowercase letters, digits, hyphens; e.g. myteam/vocab-check)";
8658
+ var USAGE2 = "Usage: vinaya new check <yourname>/<id> (both segments: lowercase letters, digits, hyphens; e.g. myteam/vocab-check)";
7852
8659
  function newCheckCommand(args) {
7853
8660
  const name = args[0];
7854
8661
  if (!name) {
7855
- console.error(USAGE);
8662
+ console.error(USAGE2);
7856
8663
  process.exit(2);
7857
8664
  }
7858
8665
  if (coreCheckRegistry().some((spec) => spec.name === name)) {
@@ -7860,7 +8667,7 @@ function newCheckCommand(args) {
7860
8667
  process.exit(2);
7861
8668
  }
7862
8669
  if (!isValidNamespacedKey(name)) {
7863
- console.error(`Refusing: "${name}" is not a valid check id. ${USAGE.slice("Usage: ".length)}
8670
+ console.error(`Refusing: "${name}" is not a valid check id. ${USAGE2.slice("Usage: ".length)}
7864
8671
  \`vinaya check\` refuses the entire run over a key it cannot resolve, so a bare, un-namespaced name would brick every check invocation in this repo.`);
7865
8672
  process.exit(2);
7866
8673
  }
@@ -7873,7 +8680,7 @@ function newCheckCommand(args) {
7873
8680
  console.error(`Error: ${targetPath} already exists.`);
7874
8681
  process.exit(1);
7875
8682
  }
7876
- const template = readFileSync18(TEMPLATE_PATH, "utf-8");
8683
+ const template = readFileSync19(TEMPLATE_PATH, "utf-8");
7877
8684
  const contents = template.split("{{CHECK_NAME}}").join(name);
7878
8685
  writeFileSync7(targetPath, contents, "utf-8");
7879
8686
  chmodSync2(targetPath, 493);
@@ -7887,20 +8694,20 @@ ${registration}
7887
8694
  }
7888
8695
 
7889
8696
  // src/commands/new-noop-check.ts
7890
- import { chmodSync as chmodSync3, existsSync as existsSync17, mkdirSync as mkdirSync5, readFileSync as readFileSync19, writeFileSync as writeFileSync8 } from "node:fs";
8697
+ import { chmodSync as chmodSync3, existsSync as existsSync17, mkdirSync as mkdirSync5, readFileSync as readFileSync20, writeFileSync as writeFileSync8 } from "node:fs";
7891
8698
  import { join as join19 } from "node:path";
7892
8699
  var TEMPLATE_PATH2 = join19(packageRoot(import.meta.url), "templates", "noop-check.template.ts");
7893
8700
  var CHECKS_DIR2 = join19("vinaya", "checks");
7894
- var USAGE2 = "Usage: vinaya new noop-check <core-check-id> (the exact id of a core check — run `vinaya check --plan` to list them)";
8701
+ var USAGE3 = "Usage: vinaya new noop-check <core-check-id> (the exact id of a core check — run `vinaya check --plan` to list them)";
7895
8702
  function newNoopCheckCommand(args) {
7896
8703
  const name = args[0];
7897
8704
  if (!name) {
7898
- console.error(USAGE2);
8705
+ console.error(USAGE3);
7899
8706
  process.exit(2);
7900
8707
  }
7901
8708
  const coreSpec = coreCheckRegistry().find((spec) => spec.name === name);
7902
8709
  if (!coreSpec) {
7903
- console.error(`Refusing: "${name}" is not a core check id. ${USAGE2}
8710
+ console.error(`Refusing: "${name}" is not a core check id. ${USAGE3}
7904
8711
  \`new noop-check\` only silences a CORE check — a \`checks\` key that exactly matches a core check id REPLACES it. A namespaced or unknown name has nothing to replace; use \`vinaya new check\` to scaffold a new, additive check instead.`);
7905
8712
  process.exit(2);
7906
8713
  }
@@ -7912,7 +8719,7 @@ function newNoopCheckCommand(args) {
7912
8719
  console.error(`Error: ${targetPath} already exists.`);
7913
8720
  process.exit(1);
7914
8721
  }
7915
- const template = readFileSync19(TEMPLATE_PATH2, "utf-8");
8722
+ const template = readFileSync20(TEMPLATE_PATH2, "utf-8");
7916
8723
  const contents = template.split("{{CHECK_NAME}}").join(name);
7917
8724
  writeFileSync8(targetPath, contents, "utf-8");
7918
8725
  chmodSync3(targetPath, 493);
@@ -7934,19 +8741,19 @@ ${registration}
7934
8741
  }
7935
8742
 
7936
8743
  // src/commands/new-role.ts
7937
- import { existsSync as existsSync18, mkdirSync as mkdirSync6, readFileSync as readFileSync20, writeFileSync as writeFileSync9 } from "node:fs";
8744
+ import { existsSync as existsSync18, mkdirSync as mkdirSync6, readFileSync as readFileSync21, writeFileSync as writeFileSync9 } from "node:fs";
7938
8745
  import { join as join20 } from "node:path";
7939
8746
  var TEMPLATE_PATH3 = join20(packageRoot(import.meta.url), "templates", "role.template.md");
7940
8747
  var ROLES_DIR = join20("vinaya", "roles");
7941
- var USAGE3 = "Usage: vinaya new role <yourname>/<id> (both segments: lowercase letters, digits, hyphens; e.g. acme/qa-lead)";
8748
+ var USAGE4 = "Usage: vinaya new role <yourname>/<id> (both segments: lowercase letters, digits, hyphens; e.g. acme/qa-lead)";
7942
8749
  function newRoleCommand(args) {
7943
8750
  const name = args[0];
7944
8751
  if (!name) {
7945
- console.error(USAGE3);
8752
+ console.error(USAGE4);
7946
8753
  process.exit(2);
7947
8754
  }
7948
8755
  if (!isValidNamespacedKey(name)) {
7949
- console.error(`Refusing: "${name}" is not a valid role key. ${USAGE3.slice("Usage: ".length)}
8756
+ console.error(`Refusing: "${name}" is not a valid role key. ${USAGE4.slice("Usage: ".length)}
7950
8757
  A bare, un-namespaced key resolves as an OVERRIDE of a core role — a complete replacement of that role's contract, and a real governance decision this scaffolder does not make for you. Pick a namespaced name, or write the override entry by hand if you really mean to replace a core role's contract.`);
7951
8758
  process.exit(2);
7952
8759
  }
@@ -7959,7 +8766,7 @@ A bare, un-namespaced key resolves as an OVERRIDE of a core role — a complete
7959
8766
  console.error(`Error: ${targetPath} already exists.`);
7960
8767
  process.exit(1);
7961
8768
  }
7962
- const template = readFileSync20(TEMPLATE_PATH3, "utf-8");
8769
+ const template = readFileSync21(TEMPLATE_PATH3, "utf-8");
7963
8770
  const contents = template.split("{{ROLE_ID}}").join(roleId);
7964
8771
  writeFileSync9(targetPath, contents, "utf-8");
7965
8772
  const relPath = join20(ROLES_DIR, `${roleId}.md`);
@@ -7972,7 +8779,7 @@ ${registration}
7972
8779
  }
7973
8780
 
7974
8781
  // src/commands/pr.ts
7975
- import { execFileSync as execFileSync12 } from "node:child_process";
8782
+ import { execFileSync as execFileSync13 } from "node:child_process";
7976
8783
 
7977
8784
  // src/lib/numstat.ts
7978
8785
  function summariseNumstat(numstat) {
@@ -8033,6 +8840,16 @@ class ScanContext {
8033
8840
  return rawHasPair === (anchoredRegionBounds(this.masked, field) !== null);
8034
8841
  }
8035
8842
  }
8843
+ function resolveAnchoredRegion(ctx, field) {
8844
+ const bounds = anchoredRegionBounds(ctx.masked, field);
8845
+ if (bounds === null) {
8846
+ return anchoredRegionBounds(ctx.normalised, field) !== null ? "hidden" : null;
8847
+ }
8848
+ return {
8849
+ region: ctx.normalised.slice(bounds.innerStart, bounds.innerEnd),
8850
+ maskedRegion: ctx.masked.slice(bounds.innerStart, bounds.innerEnd)
8851
+ };
8852
+ }
8036
8853
 
8037
8854
  // src/checks/body-bare-digits-logic.ts
8038
8855
  function blankRange(text, start, end) {
@@ -8254,7 +9071,7 @@ var RETRY_CREATE3 = "vinaya pr create --validate-only …";
8254
9071
  var RETRY_EDIT3 = "vinaya pr edit <n> --validate-only …";
8255
9072
  function git3(args) {
8256
9073
  try {
8257
- return execFileSync12("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
9074
+ return execFileSync13("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
8258
9075
  } catch {
8259
9076
  return "";
8260
9077
  }
@@ -8288,7 +9105,7 @@ function reportPass2(json, command) {
8288
9105
  function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
8289
9106
  const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
8290
9107
  try {
8291
- const out = execFileSync12("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
9108
+ const out = execFileSync13("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
8292
9109
  const url = out.trim();
8293
9110
  if (json)
8294
9111
  printJson({ validated: true, written: true, url });
@@ -8302,7 +9119,7 @@ function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
8302
9119
  function fetchPrForgeContext(prRef) {
8303
9120
  let viewOut;
8304
9121
  try {
8305
- viewOut = execFileSync12("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
9122
+ viewOut = execFileSync13("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
8306
9123
  encoding: "utf8",
8307
9124
  stdio: ["ignore", "pipe", "pipe"]
8308
9125
  });
@@ -8413,14 +9230,14 @@ function prEditCommand(args) {
8413
9230
  }
8414
9231
 
8415
9232
  // src/commands/pr-report.ts
8416
- import { execFileSync as execFileSync13, spawnSync as spawnSync2 } from "node:child_process";
8417
- import { existsSync as existsSync19, readFileSync as readFileSync21, writeFileSync as writeFileSync10 } from "node:fs";
9233
+ import { execFileSync as execFileSync14, spawnSync as spawnSync2 } from "node:child_process";
9234
+ import { existsSync as existsSync19, readFileSync as readFileSync22, writeFileSync as writeFileSync10 } from "node:fs";
8418
9235
  import { join as join21 } from "node:path";
8419
9236
  var EVIDENCE_START = "<!-- AEG:EVIDENCE:START -->";
8420
9237
  var EVIDENCE_END = "<!-- AEG:EVIDENCE:END -->";
8421
9238
  function git4(args) {
8422
9239
  try {
8423
- return execFileSync13("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
9240
+ return execFileSync14("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
8424
9241
  } catch {
8425
9242
  return "";
8426
9243
  }
@@ -8438,7 +9255,7 @@ class GitCommandError extends Error {
8438
9255
  }
8439
9256
  function gitStrict(args) {
8440
9257
  try {
8441
- return execFileSync13("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
9258
+ return execFileSync14("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
8442
9259
  } catch (err) {
8443
9260
  const stderr = err.stderr;
8444
9261
  throw new GitCommandError(args, String(stderr ?? err.message).trim() || "non-zero exit");
@@ -8567,6 +9384,84 @@ ${EVIDENCE_END}`;
8567
9384
  ${full}
8568
9385
  `;
8569
9386
  }
9387
+ var TOKENS_START = "<!-- AEG:TOKENS:START -->";
9388
+ var TOKENS_END = "<!-- AEG:TOKENS:END -->";
9389
+ var TOKEN_REPORT_HEADING = /^#{1,6}\s*token report\s*$/i;
9390
+ var HEADING_LINE = /^#{1,6}\s/;
9391
+ var TOKEN_TABLE_HEADER = "| Phase | Role | Agent/Model | Tokens in | Tokens out | Cost | Date |";
9392
+ var TOKEN_TABLE_SEPARATOR = "|---|---|---|---|---|---|---|";
9393
+ function tokensBlockLineBounds(maskedLines) {
9394
+ const startIdx = maskedLines.findIndex((l) => l.trim() === TOKENS_START);
9395
+ if (startIdx === -1)
9396
+ return null;
9397
+ for (let i = startIdx + 1;i < maskedLines.length; i++) {
9398
+ if (maskedLines[i].trim() === TOKENS_END)
9399
+ return { startIdx, endIdx: i };
9400
+ }
9401
+ return null;
9402
+ }
9403
+ function writeTokensBlock(body, addition) {
9404
+ const rawLines = body.split(`
9405
+ `);
9406
+ const maskedLines = maskCode(body).split(`
9407
+ `);
9408
+ const additionLines = addition.split(`
9409
+ `);
9410
+ const existing = tokensBlockLineBounds(maskedLines);
9411
+ if (existing) {
9412
+ const before = rawLines.slice(0, existing.endIdx);
9413
+ const after = rawLines.slice(existing.endIdx);
9414
+ return [...before, ...additionLines, ...after].join(`
9415
+ `);
9416
+ }
9417
+ const freshBlock = [TOKENS_START, TOKEN_TABLE_HEADER, TOKEN_TABLE_SEPARATOR, ...additionLines, TOKENS_END];
9418
+ const headingIdx = rawLines.findIndex((l) => TOKEN_REPORT_HEADING.test(l));
9419
+ if (headingIdx !== -1) {
9420
+ let sectionEnd = rawLines.length;
9421
+ for (let i = headingIdx + 1;i < rawLines.length; i++) {
9422
+ if (HEADING_LINE.test(rawLines[i])) {
9423
+ sectionEnd = i;
9424
+ break;
9425
+ }
9426
+ }
9427
+ const before = rawLines.slice(0, headingIdx + 1);
9428
+ const after = rawLines.slice(sectionEnd);
9429
+ return [...before, "", ...freshBlock, "", ...after].join(`
9430
+ `);
9431
+ }
9432
+ const sep4 = body.length === 0 || body.endsWith(`
9433
+ `) ? [] : [""];
9434
+ return [...rawLines, ...sep4, "", "## Token report", "", ...freshBlock, ""].join(`
9435
+ `);
9436
+ }
9437
+ function derivePhase() {
9438
+ const branch = git4(["rev-parse", "--abbrev-ref", "HEAD"]);
9439
+ const m = /^task\/[^/]+\/(.+)$/.exec(branch);
9440
+ const taskId = m ? m[1] : branch || "unknown";
9441
+ return `${taskId}: develop`;
9442
+ }
9443
+ function isoToday() {
9444
+ return new Date().toISOString().slice(0, 10);
9445
+ }
9446
+ function collectTokensAddition(opts) {
9447
+ const capability = resolveMeteringCapability(realDeps(), opts.transcriptPath);
9448
+ if (!capability.capable) {
9449
+ return formatTokenReportRow({
9450
+ phase: opts.phase,
9451
+ role: opts.role,
9452
+ summary: null,
9453
+ modelOverride: `— (${capability.reason})`,
9454
+ date: opts.date
9455
+ });
9456
+ }
9457
+ return formatTokenReportRow({
9458
+ phase: opts.phase,
9459
+ role: opts.role,
9460
+ summary: capability.summary,
9461
+ modelOverride: opts.modelOverride,
9462
+ date: opts.date
9463
+ });
9464
+ }
8570
9465
  async function buildReport(opts = {}) {
8571
9466
  const groupA = opts.groupA ?? computeGroupA();
8572
9467
  const gateRunner = opts.gateRunner ?? runRealGates;
@@ -8580,8 +9475,16 @@ ${EVIDENCE_END}`;
8580
9475
  async function prReportCommand(args) {
8581
9476
  const writeIdx = args.indexOf("--write");
8582
9477
  const writePath = writeIdx !== -1 ? args[writeIdx + 1] : undefined;
9478
+ const transcriptIdx = args.indexOf("--transcript");
9479
+ const transcriptPath = transcriptIdx !== -1 ? args[transcriptIdx + 1] : undefined;
9480
+ const phaseIdx = args.indexOf("--phase");
9481
+ const phaseOverride = phaseIdx !== -1 ? args[phaseIdx + 1] : undefined;
9482
+ const roleIdx = args.indexOf("--role");
9483
+ const roleOverride = roleIdx !== -1 ? args[roleIdx + 1] : undefined;
9484
+ const modelIdx = args.indexOf("--model");
9485
+ const modelOverride = modelIdx !== -1 ? args[modelIdx + 1] : undefined;
8583
9486
  if (writeIdx !== -1 && !writePath) {
8584
- console.error("Usage: vinaya pr report [--write <body-file>]");
9487
+ console.error("Usage: vinaya pr report [--write <body-file>] [--phase <phase>] [--role <role>] " + "[--model <id>] [--transcript <path>]");
8585
9488
  process.exit(2);
8586
9489
  }
8587
9490
  let result;
@@ -8595,10 +9498,18 @@ async function prReportCommand(args) {
8595
9498
  throw err;
8596
9499
  }
8597
9500
  if (writePath) {
8598
- const existing = existsSync19(writePath) ? readFileSync21(writePath, "utf8") : "";
8599
- const updated = replaceEvidenceBlock(existing, result.blockInner);
9501
+ const existing = existsSync19(writePath) ? readFileSync22(writePath, "utf8") : "";
9502
+ const withEvidence = replaceEvidenceBlock(existing, result.blockInner);
9503
+ const tokensAddition = collectTokensAddition({
9504
+ phase: phaseOverride ?? derivePhase(),
9505
+ role: roleOverride ?? "Developer",
9506
+ date: isoToday(),
9507
+ transcriptPath,
9508
+ modelOverride
9509
+ });
9510
+ const updated = writeTokensBlock(withEvidence, tokensAddition);
8600
9511
  writeFileSync10(writePath, updated);
8601
- process.stdout.write(`Wrote AEG:EVIDENCE block to ${writePath}
9512
+ process.stdout.write(`Wrote AEG:EVIDENCE and AEG:TOKENS blocks to ${writePath}
8602
9513
  `);
8603
9514
  } else {
8604
9515
  process.stdout.write(`${result.block}
@@ -8607,13 +9518,187 @@ async function prReportCommand(args) {
8607
9518
  process.exit(result.gatesFailed ? 1 : 0);
8608
9519
  }
8609
9520
 
9521
+ // src/commands/pr-verify-evidence.ts
9522
+ import { execFileSync as execFileSync15 } from "node:child_process";
9523
+
9524
+ // src/commands/pr-verify-evidence-logic.ts
9525
+ function publishedMergeBase(region) {
9526
+ const m = region.match(/^[ \t]*`git diff ([0-9a-f]{7,40})\.\.\.[0-9a-f]{7,40} --numstat`[ \t]*$/m);
9527
+ return m ? m[1] : null;
9528
+ }
9529
+ function normaliseLines(region, repoRoot2) {
9530
+ const root = repoRoot2.replace(/\/+$/, "");
9531
+ const out = [];
9532
+ for (const raw of region.split(`
9533
+ `)) {
9534
+ const localStripped = root ? raw.split(`${root}/`).join("").split(root).join("") : raw;
9535
+ const stripped = localStripped.replace(/(^|\s)\/\S*?\/(?=(?:apps|packages|aeg-root|scripts|\.changeset|\.vinaya|\.github)\/)/g, "$1");
9536
+ const line = stripped.replace(/[\u0000-\u001F\u007F-\u009F\u2028\u2029\u202A-\u202E\u2066-\u2069]/g, " ").trim();
9537
+ if (line)
9538
+ out.push(line);
9539
+ }
9540
+ return out;
9541
+ }
9542
+ function compareEvidence(published, freshRegion, repoRoot2) {
9543
+ if (published === null)
9544
+ return { status: "no-block" };
9545
+ if (published === "hidden")
9546
+ return { status: "hidden" };
9547
+ const publishedLines = normaliseLines(published.region, repoRoot2);
9548
+ const freshLines = normaliseLines(freshRegion, repoRoot2);
9549
+ const missing = subtractOnce(freshLines, publishedLines);
9550
+ const unexpected = subtractOnce(publishedLines, freshLines);
9551
+ if (missing.length === 0 && unexpected.length === 0)
9552
+ return { status: "match" };
9553
+ const publishedBase = publishedMergeBase(published.region);
9554
+ const currentBase = publishedMergeBase(freshRegion);
9555
+ const baseDrift = publishedBase && currentBase && publishedBase !== currentBase ? { publishedBase, currentBase } : undefined;
9556
+ return baseDrift ? { status: "differs", missing, unexpected, baseDrift } : { status: "differs", missing, unexpected };
9557
+ }
9558
+ function subtractOnce(a, b) {
9559
+ const counts = new Map;
9560
+ for (const line of b)
9561
+ counts.set(line, (counts.get(line) ?? 0) + 1);
9562
+ const out = [];
9563
+ for (const line of a) {
9564
+ const n = counts.get(line) ?? 0;
9565
+ if (n > 0)
9566
+ counts.set(line, n - 1);
9567
+ else
9568
+ out.push(line);
9569
+ }
9570
+ return out;
9571
+ }
9572
+ function renderVerdict(verdict) {
9573
+ if (verdict.status === "match") {
9574
+ return "pr verify-evidence: MATCH — every line of the published AEG:EVIDENCE region is reproduced by a fresh run at this head, and vice versa (compared as a multiset of repo-relative, control-stripped lines: order and whitespace are not compared).";
9575
+ }
9576
+ if (verdict.status === "no-block") {
9577
+ return "pr verify-evidence: NO BLOCK — this pull request body carries no AEG:EVIDENCE anchors. Run `vinaya pr report --write <body-file>` to generate one.";
9578
+ }
9579
+ if (verdict.status === "hidden") {
9580
+ return [
9581
+ "pr verify-evidence: HIDDEN — the AEG:EVIDENCE pair sits inside a collapsed `<details>` block.",
9582
+ "`body-bare-digits` blanks every digit in there, so nothing can verify what it claims. Move the block out of `<details>` and regenerate it with `vinaya pr report --write <body-file>`."
9583
+ ].join(`
9584
+ `);
9585
+ }
9586
+ const lines = ["pr verify-evidence: DIFFERS — the published AEG:EVIDENCE region does not reproduce at this head."];
9587
+ if (verdict.baseDrift) {
9588
+ lines.push("", `The published block also records a different merge-base (${verdict.baseDrift.publishedBase}) than the one resolving now (${verdict.baseDrift.currentBase}).`, "That explains SOME differences — the gates ran over a different diff — but it is not on its own evidence the content was honest, and the differing lines are still listed below. Rebase or regenerate, then re-run this command to compare at a shared base.");
9589
+ } else {
9590
+ lines.push("No merge-base difference was detected — either the base is unchanged, or one side carries no readable Group A command line to compare. Drift therefore does not account for this difference.");
9591
+ }
9592
+ lines.push("", "Regenerate with `vinaya pr report --write <body-file>` and push.");
9593
+ if (verdict.missing.length > 0) {
9594
+ lines.push("", `Present in a fresh run, absent from the published block (${verdict.missing.length}):`);
9595
+ for (const line of verdict.missing)
9596
+ lines.push(` + ${line}`);
9597
+ }
9598
+ if (verdict.unexpected.length > 0) {
9599
+ lines.push("", `Published but not reproducible (${verdict.unexpected.length}):`);
9600
+ for (const line of verdict.unexpected)
9601
+ lines.push(` - ${line}`);
9602
+ }
9603
+ return lines.join(`
9604
+ `);
9605
+ }
9606
+
9607
+ // src/commands/pr-verify-evidence.ts
9608
+ function gh(args) {
9609
+ return execFileSync15("gh", args, { encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
9610
+ }
9611
+ function assertCleanWorktree() {
9612
+ const dirty = execFileSync15("git", ["status", "--porcelain"], {
9613
+ encoding: "utf8",
9614
+ cwd: repoRoot2()
9615
+ }).trim();
9616
+ if (!dirty)
9617
+ return;
9618
+ process.stderr.write(`pr verify-evidence: REFUSED — the worktree has uncommitted or untracked changes.
9619
+ ` + `Group B is regenerated by diff-scoped checks, so local changes alter which files are scanned and can
9620
+ ` + `produce a MATCH that a clean checkout would not. Verify from a clean checkout at this head instead.
9621
+
9622
+ ` + `${dirty}
9623
+ `);
9624
+ process.exit(2);
9625
+ }
9626
+ function assertNoBaseOverride() {
9627
+ const override = process.env.BASE_SHA;
9628
+ if (!override)
9629
+ return;
9630
+ process.stderr.write(`pr verify-evidence: REFUSED — BASE_SHA is set (${override}).
9631
+ ` + `The regeneration resolves its merge-base from that variable, so an override changes what a "fresh run"
9632
+ ` + `means and can turn a drifted comparison into an apparently shared-base one. Unset it and re-run.
9633
+ `);
9634
+ process.exit(2);
9635
+ }
9636
+ function assertRepoRootCwd() {
9637
+ const root = repoRoot2();
9638
+ const cwd = process.cwd();
9639
+ if (cwd === root)
9640
+ return;
9641
+ process.stderr.write(`pr verify-evidence: REFUSED — run this from the repository root (${root}), not ${cwd}.
9642
+ ` + `The regeneration inherits this working directory, and several gates resolve their scan root from it,
9643
+ ` + `so a subdirectory silently narrows what the fresh run inspects and can report MATCH on a block that
9644
+ ` + `omits real findings.
9645
+ `);
9646
+ process.exit(2);
9647
+ }
9648
+ function repoRoot2() {
9649
+ return execFileSync15("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
9650
+ }
9651
+ async function prVerifyEvidenceCommand(args) {
9652
+ const prRef = args[0] && !args[0].startsWith("-") ? args[0] : undefined;
9653
+ if (!prRef) {
9654
+ process.stderr.write(`Usage: vinaya pr verify-evidence <pr-number>
9655
+ `);
9656
+ process.exit(2);
9657
+ }
9658
+ assertRepoRootCwd();
9659
+ assertCleanWorktree();
9660
+ assertNoBaseOverride();
9661
+ let body;
9662
+ let forgeHead;
9663
+ try {
9664
+ const view = JSON.parse(gh(["pr", "view", prRef, "--json", "body,headRefOid"]));
9665
+ body = view.body ?? "";
9666
+ forgeHead = view.headRefOid ?? "";
9667
+ } catch (err) {
9668
+ process.stderr.write(`pr verify-evidence: could not read pull request #${prRef} — ${err.message}
9669
+ `);
9670
+ process.exit(1);
9671
+ return;
9672
+ }
9673
+ const localHead = execFileSync15("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim();
9674
+ if (!forgeHead) {
9675
+ process.stderr.write(`pr verify-evidence: REFUSED — could not read #${prRef}'s head from the forge, so the verdict cannot be bound to a commit.
9676
+ `);
9677
+ process.exit(2);
9678
+ }
9679
+ if (forgeHead !== localHead) {
9680
+ process.stderr.write(`pr verify-evidence: REFUSED — this checkout is at ${localHead}, but #${prRef}'s head is ${forgeHead}.
9681
+ ` + `Comparing against a different tree produces a difference that is real and uninteresting. Check out the head first.
9682
+ `);
9683
+ process.exit(2);
9684
+ }
9685
+ const fresh = await buildReport();
9686
+ const resolved = resolveAnchoredRegion(ScanContext.from(body), "EVIDENCE");
9687
+ const verdict = compareEvidence(resolved === null || resolved === "hidden" ? resolved : { region: resolved.region }, fresh.blockInner, repoRoot2());
9688
+ process.stdout.write(`${renderVerdict(verdict)}
9689
+ `);
9690
+ process.stdout.write(` compared at head ${localHead}
9691
+ `);
9692
+ process.exit(verdict.status === "match" ? 0 : 1);
9693
+ }
9694
+
8610
9695
  // src/commands/quickstart.ts
8611
- import { execFileSync as execFileSync14 } from "node:child_process";
8612
- import { existsSync as existsSync21, readFileSync as readFileSync23 } from "node:fs";
9696
+ import { execFileSync as execFileSync16 } from "node:child_process";
9697
+ import { existsSync as existsSync21, readFileSync as readFileSync24 } from "node:fs";
8613
9698
  import { join as join23 } from "node:path";
8614
9699
 
8615
9700
  // src/lib/doc-owners-write.ts
8616
- import { existsSync as existsSync20, mkdirSync as mkdirSync7, readFileSync as readFileSync22, writeFileSync as writeFileSync11 } from "node:fs";
9701
+ import { existsSync as existsSync20, mkdirSync as mkdirSync7, readFileSync as readFileSync23, writeFileSync as writeFileSync11 } from "node:fs";
8617
9702
  import { dirname as dirname7, join as join22 } from "node:path";
8618
9703
  function bindingLine(glob, pointer) {
8619
9704
  return `${glob} ${pointer}`;
@@ -8629,23 +9714,23 @@ function appendDocOwnersBinding(content, glob, pointer) {
8629
9714
  return `${content}${sep4}${bindingLine(glob, pointer)}
8630
9715
  `;
8631
9716
  }
8632
- function planDocOwnersBinding(repoRoot2, glob, pointer) {
8633
- const abs2 = join22(repoRoot2, DOC_OWNERS_PATH);
9717
+ function planDocOwnersBinding(repoRoot3, glob, pointer) {
9718
+ const abs2 = join22(repoRoot3, DOC_OWNERS_PATH);
8634
9719
  const line = bindingLine(glob, pointer);
8635
9720
  if (!existsSync20(abs2)) {
8636
9721
  return { action: "create-host", line, path: DOC_OWNERS_PATH };
8637
9722
  }
8638
- const existing = readFileSync22(abs2, "utf-8");
9723
+ const existing = readFileSync23(abs2, "utf-8");
8639
9724
  const already = parseDocOwners(existing).bindings.some((b) => b.glob === glob);
8640
9725
  return { action: already ? "skip-present" : "append-row", line, path: DOC_OWNERS_PATH };
8641
9726
  }
8642
- function applyDocOwnersBinding(repoRoot2, plan, glob, pointer) {
8643
- const abs2 = join22(repoRoot2, DOC_OWNERS_PATH);
9727
+ function applyDocOwnersBinding(repoRoot3, plan, glob, pointer) {
9728
+ const abs2 = join22(repoRoot3, DOC_OWNERS_PATH);
8644
9729
  if (plan.action === "create-host") {
8645
9730
  mkdirSync7(dirname7(abs2), { recursive: true });
8646
9731
  writeFileSync11(abs2, freshDocOwners(glob, pointer), "utf-8");
8647
9732
  } else if (plan.action === "append-row") {
8648
- const existing = readFileSync22(abs2, "utf-8");
9733
+ const existing = readFileSync23(abs2, "utf-8");
8649
9734
  writeFileSync11(abs2, appendDocOwnersBinding(existing, glob, pointer), "utf-8");
8650
9735
  }
8651
9736
  }
@@ -8705,13 +9790,13 @@ var GLOB_INJECTION_RE = /[\s\r\n]/;
8705
9790
  var POINTER_INJECTION_RE = /[\r\n]/;
8706
9791
  var NAME_INJECTION_RE = /[\s|\r\n]/;
8707
9792
  var PATH_INJECTION_RE2 = /[|\r\n]/;
8708
- function pointerIsMissingLocalFile(repoRoot2, pointer) {
9793
+ function pointerIsMissingLocalFile(repoRoot3, pointer) {
8709
9794
  if (/^https?:\/\//i.test(pointer))
8710
9795
  return false;
8711
9796
  const path = pointer.split("#")[0] ?? pointer;
8712
- return !existsSync21(join23(repoRoot2, path));
9797
+ return !existsSync21(join23(repoRoot3, path));
8713
9798
  }
8714
- async function bindDocOwnerLoop(deps, repoRoot2) {
9799
+ async function bindDocOwnerLoop(deps, repoRoot3) {
8715
9800
  let any = false;
8716
9801
  for (;; ) {
8717
9802
  if (any && !await deps.confirm("Bind another doc→code pair?", false))
@@ -8726,11 +9811,11 @@ async function bindDocOwnerLoop(deps, repoRoot2) {
8726
9811
  }
8727
9812
  if (GLOB_INJECTION_RE.test(glob) || POINTER_INJECTION_RE.test(pointer)) {
8728
9813
  console.error(red("Error: glob must not contain whitespace, and the pointer must not contain a newline."));
8729
- } else if (pointerIsMissingLocalFile(repoRoot2, pointer)) {
9814
+ } else if (pointerIsMissingLocalFile(repoRoot3, pointer)) {
8730
9815
  console.error(red(`✗ "${pointer}" does not exist in this repo (and isn't a URL).`));
8731
9816
  } else {
8732
- const plan = planDocOwnersBinding(repoRoot2, glob, pointer);
8733
- applyDocOwnersBinding(repoRoot2, plan, glob, pointer);
9817
+ const plan = planDocOwnersBinding(repoRoot3, glob, pointer);
9818
+ applyDocOwnersBinding(repoRoot3, plan, glob, pointer);
8734
9819
  process.stdout.write(`${green(renderDocOwnersBindingDiffLine(plan))}
8735
9820
  `);
8736
9821
  any = true;
@@ -8776,10 +9861,10 @@ async function registerProjectLoop(deps) {
8776
9861
  }
8777
9862
  }
8778
9863
  function readPackageVersion() {
8779
- const pkg = JSON.parse(readFileSync23(join23(packageRoot(import.meta.url), "package.json"), "utf-8"));
9864
+ const pkg = JSON.parse(readFileSync24(join23(packageRoot(import.meta.url), "package.json"), "utf-8"));
8780
9865
  return pkg.version;
8781
9866
  }
8782
- function realDeps6() {
9867
+ function realDeps7() {
8783
9868
  return {
8784
9869
  detectRepo: detectGitRepo,
8785
9870
  initDeps: {
@@ -8800,20 +9885,15 @@ function realDeps6() {
8800
9885
  nodeVersion: () => process.version,
8801
9886
  bunVersion: () => typeof Bun === "undefined" ? null : Bun.version,
8802
9887
  packageVersion: readPackageVersion,
8803
- meteringCapability: () => resolveMeteringCapability({
8804
- env: process.env,
8805
- cwd: process.cwd(),
8806
- exists: existsSync21,
8807
- readFile: (path) => readFileSync23(path, "utf8")
8808
- })
9888
+ meteringCapability: () => resolveMeteringCapability(hardenedMeteringDeps())
8809
9889
  },
8810
9890
  confirm: async (q, defaultYes) => promptYesNo(q, defaultYes),
8811
9891
  ask: async (q) => prompt(q),
8812
9892
  closeStdin
8813
9893
  };
8814
9894
  }
8815
- function execGit(repoRoot2, args) {
8816
- return execFileSync14("git", args, { cwd: repoRoot2, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
9895
+ function execGit(repoRoot3, args) {
9896
+ return execFileSync16("git", args, { cwd: repoRoot3, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
8817
9897
  }
8818
9898
  function errorDetail(err) {
8819
9899
  const stderr = err.stderr;
@@ -8821,37 +9901,37 @@ function errorDetail(err) {
8821
9901
  return stderr.toString().trim();
8822
9902
  return err instanceof Error ? err.message : String(err);
8823
9903
  }
8824
- function gitStatusShort(repoRoot2) {
8825
- return execGit(repoRoot2, ["status", "--short"]);
9904
+ function gitStatusShort(repoRoot3) {
9905
+ return execGit(repoRoot3, ["status", "--short"]);
8826
9906
  }
8827
- function commitInstall(repoRoot2) {
8828
- execGit(repoRoot2, ["add", "-A"]);
8829
- const staged = execGit(repoRoot2, ["diff", "--cached", "--stat"]);
9907
+ function commitInstall(repoRoot3) {
9908
+ execGit(repoRoot3, ["add", "-A"]);
9909
+ const staged = execGit(repoRoot3, ["diff", "--cached", "--stat"]);
8830
9910
  if (!staged)
8831
9911
  return { committed: false, message: "Nothing to commit — working tree already clean." };
8832
9912
  try {
8833
- execGit(repoRoot2, ["commit", "-m", "Chore: install Vinaya"]);
9913
+ execGit(repoRoot3, ["commit", "-m", "Chore: install Vinaya"]);
8834
9914
  return { committed: true, message: "✓ Committed `Chore: install Vinaya`." };
8835
9915
  } catch (err) {
8836
9916
  return { committed: false, message: `git commit failed: ${errorDetail(err)}` };
8837
9917
  }
8838
9918
  }
8839
- function pushInstall(repoRoot2) {
9919
+ function pushInstall(repoRoot3) {
8840
9920
  try {
8841
- const branch = execGit(repoRoot2, ["symbolic-ref", "--short", "HEAD"]);
9921
+ const branch = execGit(repoRoot3, ["symbolic-ref", "--short", "HEAD"]);
8842
9922
  let hasUpstream = true;
8843
9923
  try {
8844
- execGit(repoRoot2, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
9924
+ execGit(repoRoot3, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
8845
9925
  } catch {
8846
9926
  hasUpstream = false;
8847
9927
  }
8848
9928
  if (hasUpstream) {
8849
- const ahead = execGit(repoRoot2, ["rev-list", "--count", "@{u}..HEAD"]);
9929
+ const ahead = execGit(repoRoot3, ["rev-list", "--count", "@{u}..HEAD"]);
8850
9930
  if (ahead === "0") {
8851
9931
  return { status: "up-to-date", message: "Nothing to push — already up to date with the remote." };
8852
9932
  }
8853
9933
  }
8854
- execGit(repoRoot2, hasUpstream ? ["push"] : ["push", "-u", "origin", branch]);
9934
+ execGit(repoRoot3, hasUpstream ? ["push"] : ["push", "-u", "origin", branch]);
8855
9935
  return { status: "pushed", message: "✓ Pushed." };
8856
9936
  } catch (err) {
8857
9937
  return { status: "failed", message: `git push failed: ${errorDetail(err)}` };
@@ -8943,12 +10023,12 @@ ${bold("Next steps:")}
8943
10023
  }
8944
10024
  }
8945
10025
  async function quickstartCommand(args) {
8946
- process.exit(await runQuickstart(args, realDeps6()));
10026
+ process.exit(await runQuickstart(args, realDeps7()));
8947
10027
  }
8948
10028
 
8949
10029
  // src/commands/review-post.ts
8950
- import { execFileSync as execFileSync15 } from "node:child_process";
8951
- import { readFileSync as readFileSync24, rmSync as rmSync4, writeFileSync as writeFileSync12 } from "node:fs";
10030
+ import { execFileSync as execFileSync17 } from "node:child_process";
10031
+ import { readFileSync as readFileSync25, rmSync as rmSync4, writeFileSync as writeFileSync12 } from "node:fs";
8952
10032
  import { tmpdir as tmpdir2 } from "node:os";
8953
10033
  import { join as join24 } from "node:path";
8954
10034
  class FindingsParseError extends Error {
@@ -9130,7 +10210,7 @@ function readFindingsFile(path, allowedSeverities) {
9130
10210
  return [];
9131
10211
  let content;
9132
10212
  try {
9133
- content = readFileSync24(path, "utf8");
10213
+ content = readFileSync25(path, "utf8");
9134
10214
  } catch {
9135
10215
  refuseCmd(`Could not read findings file at ${path}.`, "Check the path and re-run.");
9136
10216
  }
@@ -9159,9 +10239,9 @@ function normalizeSecurityVerdict(raw) {
9159
10239
  return "FAIL";
9160
10240
  refuseCmd(`\`--verdict ${raw}\` is not PASS or FAIL.`, "Pass `--verdict PASS` or `--verdict FAIL`.");
9161
10241
  }
9162
- function gh(args) {
10242
+ function gh2(args) {
9163
10243
  try {
9164
- return execFileSync15("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
10244
+ return execFileSync17("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
9165
10245
  } catch (err) {
9166
10246
  const stderr = err.stderr;
9167
10247
  throw new Error(String(stderr ?? err.message).trim() || "gh command failed");
@@ -9170,7 +10250,7 @@ function gh(args) {
9170
10250
  function resolveHeadSha(pr) {
9171
10251
  let out;
9172
10252
  try {
9173
- out = gh(["pr", "view", pr, "--json", "headRefOid", "-q", ".headRefOid"]);
10253
+ out = gh2(["pr", "view", pr, "--json", "headRefOid", "-q", ".headRefOid"]);
9174
10254
  } catch (err) {
9175
10255
  refuseCmd(`Could not resolve PR ${pr}'s head via \`gh pr view --json headRefOid\`: ${err instanceof Error ? err.message : String(err)}`, "Confirm `gh auth status` passes and the PR number is correct, then re-run.");
9176
10256
  }
@@ -9183,7 +10263,7 @@ function postComment(pr, body) {
9183
10263
  const tmp = join24(tmpdir2(), `vinaya-review-post-${process.pid}-${Date.now()}.md`);
9184
10264
  writeFileSync12(tmp, body);
9185
10265
  try {
9186
- return gh(["pr", "comment", pr, "--body-file", tmp]).trim();
10266
+ return gh2(["pr", "comment", pr, "--body-file", tmp]).trim();
9187
10267
  } catch (err) {
9188
10268
  refuseCmd(`Rendered comment failed to post via \`gh pr comment\`: ${err instanceof Error ? err.message : String(err)}`, "Check `gh auth status`/network, then re-run — nothing was posted.");
9189
10269
  } finally {
@@ -9191,7 +10271,7 @@ function postComment(pr, body) {
9191
10271
  }
9192
10272
  }
9193
10273
  function fetchComments(pr) {
9194
- const out = gh(["pr", "view", pr, "--json", "comments"]);
10274
+ const out = gh2(["pr", "view", pr, "--json", "comments"]);
9195
10275
  const parsed = JSON.parse(out);
9196
10276
  return parsed.comments.map((c) => ({ body: c.body, author: c.author?.login ?? null }));
9197
10277
  }
@@ -9299,7 +10379,7 @@ async function reviewPostCommand(args) {
9299
10379
  let secretsEvidence = null;
9300
10380
  if (secretsEvidenceFile) {
9301
10381
  try {
9302
- secretsEvidence = readFileSync24(secretsEvidenceFile, "utf8");
10382
+ secretsEvidence = readFileSync25(secretsEvidenceFile, "utf8");
9303
10383
  } catch {
9304
10384
  refuseCmd(`Could not read secrets evidence file at ${secretsEvidenceFile}.`, "Check the path and re-run.");
9305
10385
  }
@@ -9340,7 +10420,7 @@ Self-verification: clean — re-parsed VERDICT is bound to head ${headSha}.
9340
10420
 
9341
10421
  // src/commands/studio.ts
9342
10422
  import { execFile as execFile5, spawn as spawn2 } from "node:child_process";
9343
- import { existsSync as existsSync22, readFileSync as readFileSync25, renameSync } from "node:fs";
10423
+ import { existsSync as existsSync22, readFileSync as readFileSync26, renameSync } from "node:fs";
9344
10424
  import net from "node:net";
9345
10425
  import { dirname as dirname8, join as join25 } from "node:path";
9346
10426
  import { promisify as promisify5 } from "node:util";
@@ -9399,7 +10479,7 @@ function resolveStudioTarget(cwd, moduleUrl = import.meta.url) {
9399
10479
  const pkgPath = join25(webDir, "package.json");
9400
10480
  if (existsSync22(pkgPath)) {
9401
10481
  try {
9402
- const pkg = JSON.parse(readFileSync25(pkgPath, "utf-8"));
10482
+ const pkg = JSON.parse(readFileSync26(pkgPath, "utf-8"));
9403
10483
  if (pkg.name === "@atta/vinaya-studio-web") {
9404
10484
  return { kind: "workspace", webDir };
9405
10485
  }
@@ -9419,16 +10499,16 @@ function resolveStudioTarget(cwd, moduleUrl = import.meta.url) {
9419
10499
  return { kind: "missing" };
9420
10500
  }
9421
10501
  function spawnDev(webDir, args) {
9422
- return new Promise((resolve2) => {
10502
+ return new Promise((resolve3) => {
9423
10503
  const child = spawn2("bun", ["run", "dev", ...args], { cwd: webDir, stdio: "inherit" });
9424
- child.on("exit", (code) => resolve2(code ?? 0));
10504
+ child.on("exit", (code) => resolve3(code ?? 0));
9425
10505
  });
9426
10506
  }
9427
10507
  function isPortFree(port) {
9428
- return new Promise((resolve2) => {
10508
+ return new Promise((resolve3) => {
9429
10509
  const tester = net.createServer();
9430
- tester.once("error", () => resolve2(false));
9431
- tester.once("listening", () => tester.close(() => resolve2(true)));
10510
+ tester.once("error", () => resolve3(false));
10511
+ tester.once("listening", () => tester.close(() => resolve3(true)));
9432
10512
  tester.listen(port, "127.0.0.1");
9433
10513
  });
9434
10514
  }
@@ -9459,9 +10539,9 @@ async function spawnStandalone(cwd, serverPath, bundleRoot, explicitPort) {
9459
10539
  const env = { HOSTNAME: "127.0.0.1", ...process.env, PORT: String(port), VINAYA_REPO_ROOT: cwd };
9460
10540
  if (repo)
9461
10541
  env.AEG_REPO = `${repo.owner}/${repo.repo}`;
9462
- return new Promise((resolve2) => {
10542
+ return new Promise((resolve3) => {
9463
10543
  const child = spawn2("node", [serverPath], { cwd, stdio: "inherit", env });
9464
- child.on("exit", (code) => resolve2(code ?? 0));
10544
+ child.on("exit", (code) => resolve3(code ?? 0));
9465
10545
  });
9466
10546
  }
9467
10547
  async function runStudio(cwd, args, moduleUrl = import.meta.url) {
@@ -9493,126 +10573,8 @@ async function runStudio(cwd, args, moduleUrl = import.meta.url) {
9493
10573
  }
9494
10574
  }
9495
10575
 
9496
- // src/commands/tokens.ts
9497
- import { existsSync as existsSync23, readFileSync as readFileSync26 } from "node:fs";
9498
- var USAGE4 = [
9499
- 'Usage: vinaya tokens --phase "<task-id>: develop" --role Developer',
9500
- " [--model <id>] [--transcript <path>] [--in <tokens-in> --out <tokens-out>]",
9501
- "",
9502
- " --transcript Read this transcript directly. Supported primary route — use it whenever you know",
9503
- " which transcript is yours, and always in a repo with no track-transcript.sh hook.",
9504
- " (omitted) Resolve via the Stop-hook pointer file, if this repo installs that hook.",
9505
- " --in/--out Manual entry: the exact token figures, for a host whose usage arrives by some other",
9506
- " means than a Claude Code transcript. Both required together; skips transcript",
9507
- " resolution entirely."
9508
- ].join(`
9509
- `);
9510
- function parseArgs(argv) {
9511
- let phase;
9512
- let role;
9513
- let model;
9514
- let transcriptPath;
9515
- let tokensInRaw;
9516
- let tokensOutRaw;
9517
- for (let i = 0;i < argv.length; i++) {
9518
- const arg = argv[i];
9519
- if (arg === "--phase")
9520
- phase = argv[++i];
9521
- else if (arg === "--role")
9522
- role = argv[++i];
9523
- else if (arg === "--model")
9524
- model = argv[++i];
9525
- else if (arg === "--transcript")
9526
- transcriptPath = argv[++i];
9527
- else if (arg === "--in")
9528
- tokensInRaw = argv[++i];
9529
- else if (arg === "--out")
9530
- tokensOutRaw = argv[++i];
9531
- }
9532
- if (!phase || !role) {
9533
- throw new Error(USAGE4);
9534
- }
9535
- if (tokensInRaw === undefined !== (tokensOutRaw === undefined)) {
9536
- throw new Error("vinaya tokens: --in and --out must be given together, or not at all.");
9537
- }
9538
- const tokensIn = tokensInRaw === undefined ? undefined : Number(tokensInRaw);
9539
- const tokensOut = tokensOutRaw === undefined ? undefined : Number(tokensOutRaw);
9540
- if (tokensIn !== undefined && (!Number.isFinite(tokensIn) || tokensIn < 0)) {
9541
- throw new Error(`vinaya tokens: --in must be a non-negative integer, got "${tokensInRaw}".`);
9542
- }
9543
- if (tokensOut !== undefined && (!Number.isFinite(tokensOut) || tokensOut < 0)) {
9544
- throw new Error(`vinaya tokens: --out must be a non-negative integer, got "${tokensOutRaw}".`);
9545
- }
9546
- return { phase, role, model, transcriptPath, tokensIn, tokensOut };
9547
- }
9548
- function manualSummary(tokensIn, tokensOut) {
9549
- return {
9550
- components: {
9551
- inputTokens: tokensIn,
9552
- outputTokens: tokensOut,
9553
- cacheCreationInputTokens: 0,
9554
- cacheReadInputTokens: 0
9555
- },
9556
- model: null,
9557
- messageCount: 1
9558
- };
9559
- }
9560
- function realDeps7() {
9561
- return {
9562
- env: process.env,
9563
- cwd: process.cwd(),
9564
- exists: existsSync23,
9565
- readFile: (path) => readFileSync26(path, "utf8")
9566
- };
9567
- }
9568
- function buildTokensResult(parsed, deps) {
9569
- if (parsed.tokensIn !== undefined && parsed.tokensOut !== undefined) {
9570
- const summary = manualSummary(parsed.tokensIn, parsed.tokensOut);
9571
- return {
9572
- line: formatTokensLine({ phase: parsed.phase, role: parsed.role, summary, modelOverride: parsed.model }),
9573
- breakdown: undefined
9574
- };
9575
- }
9576
- const capability = resolveMeteringCapability(deps, parsed.transcriptPath);
9577
- if (!capability.capable) {
9578
- throw new Error(`vinaya tokens: refused — could not resolve real usage figures (${capability.reason}).
9579
- ${capability.detail}
9580
-
9581
- ${USAGE4}`);
9582
- }
9583
- return {
9584
- line: formatTokensLine({
9585
- phase: parsed.phase,
9586
- role: parsed.role,
9587
- summary: capability.summary,
9588
- modelOverride: parsed.model
9589
- }),
9590
- breakdown: formatBreakdown(capability.summary)
9591
- };
9592
- }
9593
- function tokensCommand(argv) {
9594
- let parsed;
9595
- try {
9596
- parsed = parseArgs(argv);
9597
- } catch (err) {
9598
- console.error(err.message);
9599
- process.exit(2);
9600
- }
9601
- let result;
9602
- try {
9603
- result = buildTokensResult(parsed, realDeps7());
9604
- } catch (err) {
9605
- console.error(err.message);
9606
- process.exit(1);
9607
- }
9608
- if (result.breakdown)
9609
- console.error(result.breakdown);
9610
- process.stdout.write(`${result.line}
9611
- `);
9612
- }
9613
-
9614
10576
  // src/commands/upgrade.ts
9615
- import { existsSync as existsSync24, readFileSync as readFileSync27, rmSync as rmSync5, writeFileSync as writeFileSync13 } from "node:fs";
10577
+ import { existsSync as existsSync23, readFileSync as readFileSync27, rmSync as rmSync5, writeFileSync as writeFileSync13 } from "node:fs";
9616
10578
  import { join as join26 } from "node:path";
9617
10579
  function realDeps8() {
9618
10580
  return {
@@ -9630,9 +10592,9 @@ function realDeps8() {
9630
10592
  function flags2(args) {
9631
10593
  return { dryRun: args.includes("--dry-run"), yes: args.includes("--yes") };
9632
10594
  }
9633
- function readManifest3(repoRoot2) {
9634
- const p = join26(repoRoot2, CONFIG_PATH);
9635
- if (!existsSync24(p))
10595
+ function readManifest3(repoRoot3) {
10596
+ const p = join26(repoRoot3, CONFIG_PATH);
10597
+ if (!existsSync23(p))
9636
10598
  return { kind: "missing" };
9637
10599
  let raw;
9638
10600
  try {
@@ -9651,31 +10613,31 @@ function readManifest3(repoRoot2) {
9651
10613
  return { kind: "not-initialized" };
9652
10614
  return { kind: "ok", manifest: parsed.data.managed };
9653
10615
  }
9654
- function writeManifestVersion(repoRoot2, manifest) {
9655
- const configAbs = join26(repoRoot2, CONFIG_PATH);
10616
+ function writeManifestVersion(repoRoot3, manifest) {
10617
+ const configAbs = join26(repoRoot3, CONFIG_PATH);
9656
10618
  const seed = JSON.parse(readFileSync27(configAbs, "utf-8"));
9657
10619
  const updated = { ...manifest, version: MANAGED_MANIFEST_VERSION };
9658
10620
  writeFileSync13(configAbs, `${JSON.stringify({ ...seed, managed: updated }, null, 2)}
9659
10621
  `, "utf-8");
9660
10622
  }
9661
- function stripFor(repoRoot2, path, marker, comment) {
9662
- const abs2 = resolveManagedBlockPath(repoRoot2, path);
9663
- if (!existsSync24(abs2))
10623
+ function stripFor(repoRoot3, path, marker, comment) {
10624
+ const abs2 = resolveManagedBlockPath(repoRoot3, path);
10625
+ if (!existsSync23(abs2))
9664
10626
  return { path, marker, comment, present: false, removesHost: false };
9665
10627
  const stripped = stripBlockFromContent(readFileSync27(abs2, "utf-8"), marker, comment);
9666
10628
  if (stripped === null)
9667
10629
  return { path, marker, comment, present: false, removesHost: false };
9668
10630
  return { path, marker, comment, present: true, removesHost: blockStripLeavesEmpty(stripped) };
9669
10631
  }
9670
- function planHookRouting(repoRoot2, manifest, recorded, hooksPathValue) {
10632
+ function planHookRouting(repoRoot3, manifest, recorded, hooksPathValue) {
9671
10633
  const none = { target: recorded, strips: [], arm: false, migratesManifest: false, blockedReason: null };
9672
10634
  if (recorded === ".husky")
9673
10635
  return none;
9674
10636
  if (recorded === TRACKED_HOOK_DIR) {
9675
- 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);
10637
+ const strips = manifest.blocks.filter((b) => b.path.startsWith(`${TRACKED_HOOK_DIR}/`)).map((b) => stripFor(repoRoot3, `.git/hooks/${b.marker}`, b.marker, b.comment)).filter((s) => s.present);
9676
10638
  if (hooksPathValue === TRACKED_HOOK_DIR)
9677
10639
  return { ...none, strips };
9678
- const foreign = foreignRawHooks(repoRoot2);
10640
+ const foreign = foreignRawHooks(repoRoot3);
9679
10641
  if (foreign.length > 0) {
9680
10642
  return {
9681
10643
  ...none,
@@ -9688,8 +10650,8 @@ function planHookRouting(repoRoot2, manifest, recorded, hooksPathValue) {
9688
10650
  const legacy = manifest.blocks.filter((b) => b.path.startsWith(".git/hooks/"));
9689
10651
  const blocked = [];
9690
10652
  for (const b of legacy) {
9691
- const abs2 = resolveManagedBlockPath(repoRoot2, b.path);
9692
- if (!existsSync24(abs2))
10653
+ const abs2 = resolveManagedBlockPath(repoRoot3, b.path);
10654
+ if (!existsSync23(abs2))
9693
10655
  continue;
9694
10656
  const stripped = stripBlockFromContent(readFileSync27(abs2, "utf-8"), b.marker, b.comment);
9695
10657
  if (stripped === null)
@@ -9698,7 +10660,7 @@ function planHookRouting(repoRoot2, manifest, recorded, hooksPathValue) {
9698
10660
  blocked.push(`${b.path} carries your own lines alongside the managed block`);
9699
10661
  }
9700
10662
  const legacyNames = new Set(legacy.map((b) => b.path.slice(".git/hooks/".length)));
9701
- for (const f of activeRawHooks(repoRoot2)) {
10663
+ for (const f of activeRawHooks(repoRoot3)) {
9702
10664
  if (!legacyNames.has(f))
9703
10665
  blocked.push(`.git/hooks/${f} is an active raw hook vinaya does not manage`);
9704
10666
  }
@@ -9706,7 +10668,7 @@ function planHookRouting(repoRoot2, manifest, recorded, hooksPathValue) {
9706
10668
  return { ...none, blockedReason: blocked.join("; ") };
9707
10669
  return {
9708
10670
  target: TRACKED_HOOK_DIR,
9709
- strips: legacy.map((b) => stripFor(repoRoot2, b.path, b.marker, b.comment)).filter((s) => s.present),
10671
+ strips: legacy.map((b) => stripFor(repoRoot3, b.path, b.marker, b.comment)).filter((s) => s.present),
9710
10672
  arm: hooksPathValue !== TRACKED_HOOK_DIR,
9711
10673
  migratesManifest: true,
9712
10674
  blockedReason: null
@@ -9722,7 +10684,7 @@ function extractWorkflowTrigger(content) {
9722
10684
  const match = content.match(/^on:\r?\n {2}([A-Za-z0-9_]+):/m);
9723
10685
  return match?.[1] ?? null;
9724
10686
  }
9725
- function planUpgrade(ops, repoRoot2, manifest, routing) {
10687
+ function planUpgrade(ops, repoRoot3, manifest, routing) {
9726
10688
  const entries = [];
9727
10689
  let hasChanges = routing.arm || routing.migratesManifest || routing.strips.some((s) => s.present);
9728
10690
  const ownedFiles = new Set(manifest.files);
@@ -9730,8 +10692,8 @@ function planUpgrade(ops, repoRoot2, manifest, routing) {
9730
10692
  const ownedBlocks = new Set(manifest.blocks.map((b) => blockKey(b.path, b.marker)));
9731
10693
  for (const op of ops) {
9732
10694
  if (op.kind === "create-file") {
9733
- const abs2 = join26(repoRoot2, op.path);
9734
- const exists = existsSync24(abs2);
10695
+ const abs2 = join26(repoRoot3, op.path);
10696
+ const exists = existsSync23(abs2);
9735
10697
  const owned = ownedFiles.has(op.path) || isDefaultedAgentVendorPath(op.path, manifest);
9736
10698
  let action;
9737
10699
  let triggerChange;
@@ -9759,12 +10721,12 @@ function planUpgrade(ops, repoRoot2, manifest, routing) {
9759
10721
  }
9760
10722
  entries.push({ kind: "create-file", op, action, ...triggerChange ? { triggerChange } : {} });
9761
10723
  } else if (op.kind === "managed-block") {
9762
- const abs2 = resolveManagedBlockPath(repoRoot2, op.path);
10724
+ const abs2 = resolveManagedBlockPath(repoRoot3, op.path);
9763
10725
  const owned = ownedBlocks.has(blockKey(op.path, op.marker));
9764
10726
  let action;
9765
10727
  if (!owned) {
9766
10728
  action = "not-installed";
9767
- } else if (!existsSync24(abs2)) {
10729
+ } else if (!existsSync23(abs2)) {
9768
10730
  action = "recreate-host";
9769
10731
  hasChanges = true;
9770
10732
  } else {
@@ -9872,8 +10834,8 @@ function renderUpgradeDiff(plan) {
9872
10834
  return lines.join(`
9873
10835
  `);
9874
10836
  }
9875
- function regenerateBlock(repoRoot2, op) {
9876
- const abs2 = resolveManagedBlockPath(repoRoot2, op.path);
10837
+ function regenerateBlock(repoRoot3, op) {
10838
+ const abs2 = resolveManagedBlockPath(repoRoot3, op.path);
9877
10839
  const content = readFileSync27(abs2, "utf-8");
9878
10840
  const stripped = stripBlockFromContent(content, op.marker, op.comment);
9879
10841
  if (stripped !== null) {
@@ -9881,28 +10843,28 @@ function regenerateBlock(repoRoot2, op) {
9881
10843
  `) ? stripped : `${stripped}
9882
10844
  `, "utf-8");
9883
10845
  }
9884
- appendBlock(repoRoot2, op);
10846
+ appendBlock(repoRoot3, op);
9885
10847
  }
9886
- function applyUpgrade(plan, repoRoot2) {
10848
+ function applyUpgrade(plan, repoRoot3) {
9887
10849
  for (const e of plan.entries) {
9888
10850
  if (e.kind === "create-file") {
9889
10851
  if (e.action === "regenerate" || e.action === "recreate") {
9890
- writeFileWithDirs(join26(repoRoot2, e.op.path), e.op.content, e.op.mode);
10852
+ writeFileWithDirs(join26(repoRoot3, e.op.path), e.op.content, e.op.mode);
9891
10853
  }
9892
10854
  } else {
9893
10855
  if (e.action === "recreate-host")
9894
- createHost(repoRoot2, e.op);
10856
+ createHost(repoRoot3, e.op);
9895
10857
  else if (e.action === "recreate-append")
9896
- appendBlock(repoRoot2, e.op);
10858
+ appendBlock(repoRoot3, e.op);
9897
10859
  else if (e.action === "regenerate-block")
9898
- regenerateBlock(repoRoot2, e.op);
10860
+ regenerateBlock(repoRoot3, e.op);
9899
10861
  }
9900
10862
  }
9901
10863
  for (const s of plan.routing.strips) {
9902
10864
  if (!s.present)
9903
10865
  continue;
9904
- const abs2 = resolveManagedBlockPath(repoRoot2, s.path);
9905
- if (!existsSync24(abs2))
10866
+ const abs2 = resolveManagedBlockPath(repoRoot3, s.path);
10867
+ if (!existsSync23(abs2))
9906
10868
  continue;
9907
10869
  const stripped = stripBlockFromContent(readFileSync27(abs2, "utf-8"), s.marker, s.comment);
9908
10870
  if (stripped === null)
@@ -9993,16 +10955,16 @@ async function upgradeCommand(args) {
9993
10955
  }
9994
10956
 
9995
10957
  // src/commands/waiver.ts
9996
- import { execFileSync as execFileSync16 } from "node:child_process";
10958
+ import { execFileSync as execFileSync18 } from "node:child_process";
9997
10959
  function labelFor(kind) {
9998
10960
  return kind === "docs" ? WAIVER_LABEL : WAIVER_LABEL_REVIEW;
9999
10961
  }
10000
- function gh2(args) {
10001
- return execFileSync16("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], env: process.env }).trim();
10962
+ function gh3(args) {
10963
+ return execFileSync18("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], env: process.env }).trim();
10002
10964
  }
10003
10965
  function detectPrNumber() {
10004
10966
  try {
10005
- const out = gh2(["pr", "view", "--json", "number", "-q", ".number"]);
10967
+ const out = gh3(["pr", "view", "--json", "number", "-q", ".number"]);
10006
10968
  return /^\d+$/.test(out) ? out : null;
10007
10969
  } catch {
10008
10970
  return null;
@@ -10098,8 +11060,8 @@ Reason: ${reason}`];
10098
11060
  }
10099
11061
  process.stdout.write(`Applying \`${label2}\` to PR #${prNumber} under your authenticated \`gh\` identity — vinaya never fabricates an actor.
10100
11062
  `);
10101
- execFileSync16("gh", editArgs, { stdio: "inherit", env: process.env });
10102
- execFileSync16("gh", commentArgs, { stdio: "inherit", env: process.env });
11063
+ execFileSync18("gh", editArgs, { stdio: "inherit", env: process.env });
11064
+ execFileSync18("gh", commentArgs, { stdio: "inherit", env: process.env });
10103
11065
  process.stdout.write(`
10104
11066
  ✓ Label applied and reason posted as a PR comment.
10105
11067
  `);
@@ -10223,8 +11185,10 @@ try {
10223
11185
  prEditCommand(rest);
10224
11186
  } else if (subcommand === "report") {
10225
11187
  await prReportCommand(rest);
11188
+ } else if (subcommand === "verify-evidence") {
11189
+ await prVerifyEvidenceCommand(rest);
10226
11190
  } else {
10227
- console.error(`Unknown 'pr' subcommand: ${subcommand ?? "(none)"} (expected 'create', 'edit', or 'report')`);
11191
+ console.error(`Unknown 'pr' subcommand: ${subcommand ?? "(none)"} (expected 'create', 'edit', 'report', or 'verify-evidence')`);
10228
11192
  process.exit(2);
10229
11193
  }
10230
11194
  break;
@@ -10249,8 +11213,10 @@ try {
10249
11213
  await milestoneAdoptCommand(rest);
10250
11214
  } else if (subcommand === "edit") {
10251
11215
  await milestoneEditCommand(rest);
11216
+ } else if (subcommand === "close") {
11217
+ await milestoneCloseCommand(rest);
10252
11218
  } else {
10253
- console.error(`Unknown 'milestone' subcommand: ${subcommand ?? "(none)"} (expected create/adopt/edit)`);
11219
+ console.error(`Unknown 'milestone' subcommand: ${subcommand ?? "(none)"} (expected create/adopt/edit/close)`);
10254
11220
  process.exit(2);
10255
11221
  }
10256
11222
  break;