@attalabs/vinaya 0.20.1 → 0.21.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 (139) hide show
  1. package/README.md +74 -0
  2. package/aeg-root/enforcement.md +13 -6
  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 +1 -1
  6. package/aeg-root/roles/developer.md +3 -2
  7. package/aeg-root/roles/planner.md +1 -1
  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/tranche-model.md +1 -1
  12. package/dist/checks/bin/check-body-bare-digits.js +281 -5
  13. package/dist/checks/bin/check-branch-topology.js +274 -0
  14. package/dist/checks/bin/check-brief-shape.js +249 -0
  15. package/dist/checks/bin/check-changeset-coverage.js +4156 -0
  16. package/dist/checks/bin/check-closes-n.js +274 -0
  17. package/dist/checks/bin/check-coherence.js +305 -3
  18. package/dist/checks/bin/check-dead-branch-push.js +249 -0
  19. package/dist/checks/bin/check-dispatch-readiness.js +381 -20
  20. package/dist/checks/bin/check-doc-coverage-push.js +280 -3
  21. package/dist/checks/bin/check-doc-coverage.js +280 -3
  22. package/dist/checks/bin/check-doctrine-portability.js +280 -3
  23. package/dist/checks/bin/check-evidence-fresh.js +249 -0
  24. package/dist/checks/bin/check-first-push-dispatch.js +372 -14
  25. package/dist/checks/bin/check-issue-assignment.js +274 -0
  26. package/dist/checks/bin/check-main-branch-refusal.js +249 -0
  27. package/dist/checks/bin/check-no-disk-state.js +249 -0
  28. package/dist/checks/bin/check-quoted-command.js +4405 -0
  29. package/dist/checks/bin/check-reader-resolvable-prose.js +282 -5
  30. package/dist/checks/bin/check-registry-gates.js +282 -0
  31. package/dist/checks/bin/check-retired-vocabulary.js +282 -5
  32. package/dist/checks/bin/check-review-gate.js +280 -3
  33. package/dist/checks/bin/check-single-plan-pr.js +249 -0
  34. package/dist/checks/bin/check-test-plan.js +249 -0
  35. package/dist/checks/bin/check-token-collection-wired.js +3975 -0
  36. package/dist/checks/bin/check-workspace-escape.js +249 -0
  37. package/dist/index.js +1523 -630
  38. package/package.json +1 -1
  39. package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
  40. package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
  41. package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
  42. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
  43. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
  44. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  45. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  46. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  47. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  48. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  49. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
  50. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js +2 -2
  51. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  52. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  53. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
  54. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
  55. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js +2 -2
  56. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
  57. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
  58. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
  59. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js +2 -2
  60. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
  61. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
  62. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
  63. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js +3 -3
  64. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
  65. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
  66. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
  67. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js +3 -3
  68. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
  69. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
  70. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
  71. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js +3 -3
  72. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
  73. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
  74. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
  75. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js +3 -3
  76. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
  77. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
  78. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
  79. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js +3 -3
  80. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
  81. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
  82. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
  83. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js +2 -2
  84. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
  85. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
  86. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/[root-of-the-server]__02a-3g8._.js +1 -1
  87. 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
  88. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0053k9k._.js +1 -1
  89. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0112h-k._.js +1 -1
  90. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__040smoa._.js +1 -1
  91. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0cxtq46._.js +25 -18
  92. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0o771t1._.js +1 -1
  93. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__16cuydh._.js → [root-of-the-server]__0ozbaq0._.js} +1 -1
  94. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0puovz5._.js +1 -1
  95. 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
  96. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__18kqy4q._.js +1 -1
  97. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__19jyp3m._.js +1 -1
  98. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1hs0dcu._.js +1 -1
  99. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1wc4-ip._.js +1 -1
  100. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1widio3._.js +1 -1
  101. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1yoa8tx._.js +3 -0
  102. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_03x_w6q._.js +1 -1
  103. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0gvm3og._.js +1 -1
  104. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0lwxg63._.js +1 -1
  105. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0wxycau._.js +1 -1
  106. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0x999pb._.js +1 -1
  107. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1-6rzip._.js +1 -1
  108. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +2 -2
  109. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_0zys35s._.js → _1mqrlbl._.js} +2 -2
  110. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_1c9b9j-._.js → _1mrtj0f._.js} +2 -2
  111. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1n0cnq-._.js +1 -1
  112. 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
  113. 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
  114. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
  115. package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
  116. package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
  117. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
  118. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
  119. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0r4i_ks9z02bw.js +1 -0
  120. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0ea8-8nopz29h.js → 1hfhxjy577l5d.js} +1 -1
  121. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1k916ih3p4nsg.js +1 -0
  122. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{29-foak16tjfu.js → 1t2kft7bxzuxn.js} +1 -1
  123. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1ybrihl5_aocy.js +1 -0
  124. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{30ojnsx3p4lp4.js → 1yqs2zgvwa4p1.js} +1 -1
  125. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0msr2a1i-x-br.js → 25kbt5k-xb2a5.js} +1 -1
  126. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{2twnwe77d4n-m.js → 2_k6j1o9q5nn4.js} +4 -4
  127. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/2hmannd4bjhzy.js +1 -0
  128. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/3mqu_uhluy19i.css +1 -0
  129. package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
  130. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1rku4gz._.js +0 -3
  131. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0la0hhmjprdxm.css +0 -1
  132. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1lk7h-0buf4ab.js +0 -1
  133. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/25j-y57ng11hl.js +0 -1
  134. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/30lyvn0yzwdiz.js +0 -1
  135. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/35u8inxa732is.js +0 -1
  136. /package/studio-standalone/_node_modules/@attalabs/{aeg-core → vinaya/studio-standalone/_node_modules/@attalabs/aeg-core}/bin/verify-coherence.ts +0 -0
  137. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{b2VfZK2AItZwlghK_pUN0 → crNCHQ9uAgKOYC6DKkZma}/_buildManifest.js +0 -0
  138. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{b2VfZK2AItZwlghK_pUN0 → crNCHQ9uAgKOYC6DKkZma}/_clientMiddlewareManifest.js +0 -0
  139. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{b2VfZK2AItZwlghK_pUN0 → crNCHQ9uAgKOYC6DKkZma}/_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,60 @@ 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
+ function sanitizeForTokensLine(value) {
805
+ const stripped = stripNewlines(value);
806
+ let result = "";
807
+ for (let i = 0;i < stripped.length; i++) {
808
+ const ch = stripped[i];
809
+ if (DASH_CHARS.has(ch)) {
810
+ const leftReachable = i === 0 || /\s/.test(stripped[i - 1]);
811
+ const rightReachable = i === stripped.length - 1 || /\s/.test(stripped[i + 1]);
812
+ if (leftReachable && rightReachable) {
813
+ result += DASH_LOOKALIKES[ch];
814
+ continue;
815
+ }
816
+ }
817
+ result += ch;
739
818
  }
819
+ return result;
820
+ }
821
+ function renderCells(input) {
822
+ const model = input.modelOverride ?? input.summary?.model ?? null;
823
+ if (!input.summary)
824
+ return { model, tokensIn: "—", tokensOut: "—" };
740
825
  const { inputTokens, cacheCreationInputTokens, cacheReadInputTokens, outputTokens } = input.summary.components;
741
826
  const tokensIn = inputTokens + cacheCreationInputTokens + cacheReadInputTokens;
742
- return `Tokens: ${input.phase} ${input.role} ${model} ${tokensIn}/${outputTokens}/—`;
827
+ return { model, tokensIn: String(tokensIn), tokensOut: String(outputTokens) };
828
+ }
829
+ function formatTokensLine(input) {
830
+ const { model, tokensIn, tokensOut } = renderCells(input);
831
+ const phase = sanitizeForTokensLine(input.phase);
832
+ const role = sanitizeForTokensLine(input.role);
833
+ const safeModel = model === null ? "—" : sanitizeForTokensLine(model);
834
+ if (!input.summary) {
835
+ return `Tokens: ${phase} — ${role} — ${safeModel} — —`;
836
+ }
837
+ return `Tokens: ${phase} — ${role} — ${safeModel} — ${tokensIn}/${tokensOut}/—`;
838
+ }
839
+ function formatTokenReportRow(input) {
840
+ const { model, tokensIn, tokensOut } = renderCells(input);
841
+ const phase = sanitizeForTableCell(input.phase);
842
+ const role = sanitizeForTableCell(input.role);
843
+ const safeModel = model === null ? "—" : sanitizeForTableCell(model);
844
+ const date = sanitizeForTableCell(input.date);
845
+ return `| ${phase} | ${role} | ${safeModel} | ${tokensIn} | ${tokensOut} | — | ${date} |`;
743
846
  }
744
847
  function formatBreakdown(summary) {
745
848
  const { inputTokens, cacheCreationInputTokens, cacheReadInputTokens, outputTokens } = summary.components;
@@ -807,43 +910,65 @@ function transcriptPointerPath(projectDir, tmpDir) {
807
910
  }
808
911
  function resolvePointer(explicitTranscriptPath, deps) {
809
912
  if (explicitTranscriptPath)
810
- return { path: explicitTranscriptPath };
913
+ return { path: explicitTranscriptPath, corroborated: true };
811
914
  const projectDir = deps.env.CLAUDE_PROJECT_DIR ?? deps.cwd;
812
915
  const tmpDir = deps.env.TMPDIR ?? "/tmp";
813
916
  const pointerPath = transcriptPointerPath(projectDir, tmpDir);
917
+ const currentSessionId = deps.env.CLAUDE_CODE_SESSION_ID;
814
918
  if (!deps.exists(pointerPath)) {
815
919
  return {
920
+ pointerExisted: false,
921
+ corroborated: false,
816
922
  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."
817
923
  };
818
924
  }
819
925
  let contents;
820
926
  try {
821
- contents = deps.readFile(pointerPath).trim();
927
+ contents = deps.readFile(pointerPath).replace(/\r?\n+$/, "");
822
928
  } catch (err) {
823
- return { error: `Transcript pointer at ${pointerPath} could not be read: ${err.message}` };
929
+ return {
930
+ pointerExisted: true,
931
+ oursByLocation: true,
932
+ 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."
933
+ };
824
934
  }
825
935
  const [pointerSessionId, transcriptPath] = contents.split("\t");
826
936
  if (!transcriptPath) {
827
- return { error: `Transcript pointer file ${pointerPath} is malformed: "${contents}"` };
937
+ return {
938
+ pointerExisted: true,
939
+ oursByLocation: true,
940
+ 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."
941
+ };
828
942
  }
829
- const currentSessionId = deps.env.CLAUDE_CODE_SESSION_ID;
830
943
  if (currentSessionId && pointerSessionId && currentSessionId !== pointerSessionId) {
831
944
  return {
945
+ pointerExisted: true,
946
+ corroborated: false,
832
947
  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
948
  };
834
949
  }
835
- return { path: transcriptPath };
950
+ return { path: transcriptPath, corroborated: Boolean(currentSessionId && pointerSessionId) };
836
951
  }
837
952
  function resolveMeteringCapability(deps, explicitTranscriptPath) {
838
953
  const resolved = resolvePointer(explicitTranscriptPath, deps);
839
954
  if ("error" in resolved) {
840
- return { capable: false, reason: "no-transcript-resolved", detail: resolved.error };
955
+ const ours = resolved.pointerExisted && (resolved.corroborated || resolved.oursByLocation);
956
+ if (ours)
957
+ return { capable: false, reason: "pointer-unusable", detail: resolved.error };
958
+ return {
959
+ capable: false,
960
+ reason: "no-transcript-resolved",
961
+ 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
962
+ };
841
963
  }
964
+ const downstream = (r, detail) => resolved.corroborated ? { reason: r, detail } : {
965
+ reason: "no-transcript-resolved",
966
+ 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})`
967
+ };
842
968
  if (!deps.exists(resolved.path)) {
843
969
  return {
844
970
  capable: false,
845
- reason: "transcript-unreadable",
846
- detail: `Resolved transcript path ${resolved.path} does not exist.`
971
+ ...downstream("transcript-unreadable", `Resolved transcript path ${resolved.path} is not readable as a regular file owned by this user.`)
847
972
  };
848
973
  }
849
974
  let jsonl;
@@ -852,16 +977,14 @@ function resolveMeteringCapability(deps, explicitTranscriptPath) {
852
977
  } catch (err) {
853
978
  return {
854
979
  capable: false,
855
- reason: "transcript-unreadable",
856
- detail: `Transcript at ${resolved.path} could not be read: ${err.message}`
980
+ ...downstream("transcript-unreadable", `Transcript at ${resolved.path} could not be read: ${err.message}`)
857
981
  };
858
982
  }
859
983
  const summary = summarizeTranscript(jsonl);
860
984
  if (summary.messageCount === 0) {
861
985
  return {
862
986
  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."
987
+ ...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
988
  };
866
989
  }
867
990
  return { capable: true, transcriptPath: resolved.path, summary };
@@ -1453,6 +1576,8 @@ var EXEMPT_LITERALS = new Set([
1453
1576
  var NON_PATH_TOP_SEGMENTS = new Set(["origin", "refs", "HEAD", "vinaya", "fix"]);
1454
1577
  // ../../packages/aeg-core/src/reader-resolvable-prose.ts
1455
1578
  var SWEPT_CLASSES = new Set(["ships", "reader-facing"]);
1579
+ // ../../packages/aeg-core/src/quoted-command.ts
1580
+ var QUOTED_COMMAND_SWEPT_CLASSES = new Set(["ships", "reader-facing"]);
1456
1581
  // ../../packages/aeg-core/src/retired-vocabulary.ts
1457
1582
  var RETIRED_PATTERNS = [
1458
1583
  String.raw`\bD-[0-9]{3}\b`,
@@ -1545,15 +1670,15 @@ function releaseFieldFromText(text) {
1545
1670
  const raw = unwrapValue2(m[1] ?? "");
1546
1671
  return { declared: true, value: RELEASE_VALUE.test(raw) ? raw : null };
1547
1672
  }
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;
1673
+ var INTENTS_HEADING2 = /^#{1,6}\s*Tranche intents\s*$/im;
1674
+ var NEXT_HEADING2 = /^#{1,6}\s+\S/m;
1675
+ var INTENT_BULLET2 = /^-\s+([a-z0-9][a-z0-9-]*)\s*:\s*(.+)$/i;
1551
1676
  function intentsBlock(text) {
1552
- const start = text.match(INTENTS_HEADING);
1677
+ const start = text.match(INTENTS_HEADING2);
1553
1678
  if (!start || start.index === undefined)
1554
1679
  return null;
1555
1680
  const rest = text.slice(start.index + start[0].length);
1556
- const next = rest.match(NEXT_HEADING);
1681
+ const next = rest.match(NEXT_HEADING2);
1557
1682
  const sectionEnd = next && next.index !== undefined ? next.index : rest.length;
1558
1683
  return { start: start.index, end: start.index + start[0].length + sectionEnd, section: rest.slice(0, sectionEnd) };
1559
1684
  }
@@ -1570,7 +1695,7 @@ function parseIntents(text) {
1570
1695
  const trimmed = line.trim();
1571
1696
  if (trimmed.length === 0)
1572
1697
  continue;
1573
- const m = trimmed.match(INTENT_BULLET);
1698
+ const m = trimmed.match(INTENT_BULLET2);
1574
1699
  if (!m)
1575
1700
  return { intents: [], malformed: true };
1576
1701
  intents.push({ slug: (m[1] ?? "").toLowerCase(), goal: (m[2] ?? "").trim() });
@@ -2838,13 +2963,14 @@ function buildInitOps(ctx) {
2838
2963
  // src/lib/ops.ts
2839
2964
  import { execFileSync as execFileSync3 } from "node:child_process";
2840
2965
  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";
2966
+ import { dirname as dirname4, isAbsolute, join as join6, resolve as resolve2, sep as sep2 } from "node:path";
2842
2967
 
2843
2968
  // src/lib/config.ts
2844
2969
  import { execFileSync as execFileSync2 } from "node:child_process";
2845
- import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync4, writeFileSync } from "node:fs";
2970
+ import { createHash } from "node:crypto";
2971
+ import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync4, realpathSync, writeFileSync } from "node:fs";
2846
2972
  import { homedir } from "node:os";
2847
- import { dirname as dirname3, join as join5 } from "node:path";
2973
+ import { dirname as dirname3, join as join5, resolve } from "node:path";
2848
2974
  import { z } from "zod";
2849
2975
 
2850
2976
  // src/lib/agent-vendors.ts
@@ -2995,6 +3121,20 @@ function resolveAgentVendors(manifest) {
2995
3121
  return new Set(AGENT_VENDORS);
2996
3122
  return new Set(manifest.agents);
2997
3123
  }
3124
+ var ProjectEntrySchema = z.object({
3125
+ name: z.string().min(1),
3126
+ description: z.string().min(1).optional(),
3127
+ path: z.string().min(1).optional()
3128
+ });
3129
+ function parseTokensCollectDeclaration(value) {
3130
+ const m = value.trim().match(/^(\S+)\s+(\S+)$/);
3131
+ if (!m)
3132
+ return null;
3133
+ const [, interpreter, script] = m;
3134
+ if (!isSafeRepoRelPath(script))
3135
+ return null;
3136
+ return { interpreter, script };
3137
+ }
2998
3138
  var VinayaConfigSchema = z.object({
2999
3139
  rings: z.object({
3000
3140
  ring1_forgeWriteInterception: z.boolean(),
@@ -3007,13 +3147,19 @@ var VinayaConfigSchema = z.object({
3007
3147
  principals: z.array(z.string()).min(1).optional(),
3008
3148
  releaseActor: z.string().min(1).optional(),
3009
3149
  ci: z.object({ setup: z.string().min(1) }).optional(),
3150
+ tokens: z.object({
3151
+ collect: z.string().min(1).refine((v) => parseTokensCollectDeclaration(v) !== null, {
3152
+ message: 'tokens.collect must be exactly "<interpreter> <repo-relative-script-path>" — two whitespace-separated tokens, no flags, no shell syntax, no quoting'
3153
+ })
3154
+ }).optional(),
3010
3155
  blastRadius: z.object({ extraDomains: z.array(z.string()).optional() }).optional(),
3011
3156
  proseGates: z.object({
3012
3157
  doctrineRoot: z.string().min(1).optional(),
3013
3158
  readerFacingPrefix: z.string().min(1).optional(),
3014
3159
  readerFacingSuffix: z.string().min(1).optional(),
3015
3160
  legacySlugDir: z.string().min(1).optional()
3016
- }).optional()
3161
+ }).optional(),
3162
+ projects: z.array(ProjectEntrySchema).optional()
3017
3163
  });
3018
3164
  var GLOBAL_VINAYA_HOME = join5(homedir(), ".vinaya");
3019
3165
  var GLOBAL_CONFIG_PATH = join5(GLOBAL_VINAYA_HOME, "config.json");
@@ -3052,6 +3198,9 @@ function globalRolesIgnoredWarning(path) {
3052
3198
  function globalReleaseActorIgnoredWarning(path) {
3053
3199
  return `${path}: "releaseActor" in the global config is ignored — releaseActor may only be declared from a repo-local vinaya.config.json.`;
3054
3200
  }
3201
+ function globalTokensCollectIgnoredWarning(path) {
3202
+ return `${path}: "tokens" in the global config is ignored — tokens.collect may only be declared from a repo-local vinaya.config.json.`;
3203
+ }
3055
3204
  function stripGlobalOnlyKeys(config, path) {
3056
3205
  if (path !== GLOBAL_CONFIG_PATH)
3057
3206
  return config;
@@ -3072,6 +3221,10 @@ function stripGlobalOnlyKeys(config, path) {
3072
3221
  console.error(`⚠ ${globalReleaseActorIgnoredWarning(path)}`);
3073
3222
  result = { ...result, releaseActor: undefined };
3074
3223
  }
3224
+ if (result.tokens) {
3225
+ console.error(`⚠ ${globalTokensCollectIgnoredWarning(path)}`);
3226
+ result = { ...result, tokens: undefined };
3227
+ }
3075
3228
  return result;
3076
3229
  }
3077
3230
  function resolvePrincipalAllowlist(config) {
@@ -3181,6 +3334,69 @@ function loadConfigChecked() {
3181
3334
  }
3182
3335
  return { ok: true, config: stripGlobalOnlyKeys(parsed.data, path) };
3183
3336
  }
3337
+ var TOKENS_COLLECT_TRUST_PATH = join5(GLOBAL_VINAYA_HOME, "tokens-collect-trust.json");
3338
+ function readTokensCollectTrustStore(storePath) {
3339
+ try {
3340
+ if (!existsSync4(storePath))
3341
+ return {};
3342
+ const raw = JSON.parse(readFileSync4(storePath, "utf-8"));
3343
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
3344
+ } catch {
3345
+ return {};
3346
+ }
3347
+ }
3348
+ function writeTokensCollectTrustStore(storePath, store) {
3349
+ const dir = dirname3(storePath);
3350
+ if (!existsSync4(dir))
3351
+ mkdirSync(dir, { recursive: true });
3352
+ writeFileSync(storePath, JSON.stringify(store, null, 2), "utf-8");
3353
+ }
3354
+ var GIT_IDENTITY_TIMEOUT_MS = 5000;
3355
+ function gitCommonDir(cwd = process.cwd()) {
3356
+ try {
3357
+ const raw = execFileSync2("git", ["rev-parse", "--git-common-dir"], {
3358
+ cwd,
3359
+ encoding: "utf-8",
3360
+ stdio: ["ignore", "pipe", "pipe"],
3361
+ timeout: GIT_IDENTITY_TIMEOUT_MS
3362
+ }).trim();
3363
+ if (!raw)
3364
+ return null;
3365
+ return realpathSync(resolve(cwd, raw));
3366
+ } catch {
3367
+ return null;
3368
+ }
3369
+ }
3370
+ function repoLocalConfigDir() {
3371
+ const local = findLocalConfig();
3372
+ return local ? dirname3(local) : null;
3373
+ }
3374
+ function gitBlobHash(absolutePath, cwd) {
3375
+ try {
3376
+ const raw = execFileSync2("git", ["hash-object", absolutePath], {
3377
+ cwd,
3378
+ encoding: "utf-8",
3379
+ stdio: ["ignore", "pipe", "pipe"],
3380
+ timeout: GIT_IDENTITY_TIMEOUT_MS
3381
+ }).trim();
3382
+ return raw || null;
3383
+ } catch {
3384
+ return null;
3385
+ }
3386
+ }
3387
+ function tokensCollectTrustKey(repoGitCommonDir, interpreter, script) {
3388
+ return createHash("sha256").update(JSON.stringify([repoGitCommonDir, interpreter, script])).digest("hex");
3389
+ }
3390
+ function getTokensCollectTrust(repoGitCommonDir, interpreter, script, storePath = TOKENS_COLLECT_TRUST_PATH) {
3391
+ const key = tokensCollectTrustKey(repoGitCommonDir, interpreter, script);
3392
+ return readTokensCollectTrustStore(storePath)[key] ?? null;
3393
+ }
3394
+ function trustTokensCollectCommand(repoGitCommonDir, interpreter, script, scriptBlobHash, storePath = TOKENS_COLLECT_TRUST_PATH) {
3395
+ const store = readTokensCollectTrustStore(storePath);
3396
+ const key = tokensCollectTrustKey(repoGitCommonDir, interpreter, script);
3397
+ store[key] = { interpreter, script, scriptBlobHash, trustedAt: new Date().toISOString() };
3398
+ writeTokensCollectTrustStore(storePath, store);
3399
+ }
3184
3400
 
3185
3401
  // src/lib/ops.ts
3186
3402
  var MARKER_NS = "vinaya:managed";
@@ -3204,9 +3420,9 @@ var GIT_DIR_PREFIX = ".git/";
3204
3420
  function resolveManagedBlockPath(repoRoot, opPath) {
3205
3421
  if (!opPath.startsWith(GIT_DIR_PREFIX))
3206
3422
  return join6(repoRoot, opPath);
3207
- return join6(gitCommonDir(repoRoot), opPath.slice(GIT_DIR_PREFIX.length));
3423
+ return join6(gitCommonDir2(repoRoot), opPath.slice(GIT_DIR_PREFIX.length));
3208
3424
  }
3209
- function gitCommonDir(repoRoot) {
3425
+ function gitCommonDir2(repoRoot) {
3210
3426
  try {
3211
3427
  const commonDir = execFileSync3("git", ["-C", repoRoot, "rev-parse", "--git-common-dir"], {
3212
3428
  encoding: "utf8"
@@ -3219,8 +3435,8 @@ function gitCommonDir(repoRoot) {
3219
3435
  }
3220
3436
  }
3221
3437
  function containedAbs(repoRoot, relPath) {
3222
- const root = resolve(repoRoot);
3223
- const target = resolve(root, relPath);
3438
+ const root = resolve2(repoRoot);
3439
+ const target = resolve2(root, relPath);
3224
3440
  if (target === root)
3225
3441
  return null;
3226
3442
  return target === root || target.startsWith(root + sep2) ? target : null;
@@ -3228,9 +3444,9 @@ function containedAbs(repoRoot, relPath) {
3228
3444
  function containedManagedBlockAbs(repoRoot, relPath) {
3229
3445
  if (!relPath.startsWith(GIT_DIR_PREFIX))
3230
3446
  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)));
3447
+ const commonDir = gitCommonDir2(repoRoot);
3448
+ const hooksRoot = resolve2(join6(commonDir, "hooks"));
3449
+ const target = resolve2(join6(commonDir, relPath.slice(GIT_DIR_PREFIX.length)));
3234
3450
  if (target === hooksRoot)
3235
3451
  return null;
3236
3452
  return target.startsWith(hooksRoot + sep2) ? target : null;
@@ -3766,8 +3982,8 @@ function prompt(question) {
3766
3982
  setupStdinReader();
3767
3983
  if (stdinEnded && pendingResolvers.length === 0)
3768
3984
  return Promise.resolve("");
3769
- return new Promise((resolve2) => {
3770
- pendingResolvers.push(resolve2);
3985
+ return new Promise((resolve3) => {
3986
+ pendingResolvers.push(resolve3);
3771
3987
  if (stdinEnded)
3772
3988
  flushLines();
3773
3989
  });
@@ -3786,112 +4002,397 @@ function closeStdin() {
3786
4002
  }
3787
4003
  }
3788
4004
 
3789
- // src/commands/archive.ts
3790
- function ring2Accelerated() {
3791
- return loadConfig()?.rings?.ring2_asyncAudits === true;
3792
- }
3793
- function realDeps() {
3794
- return { detectRepo: detectGitRepo };
4005
+ // src/commands/tokens.ts
4006
+ import { execFileSync as execFileSync4 } from "node:child_process";
4007
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "node:fs";
4008
+ import { resolve as resolvePath } from "node:path";
4009
+ var USAGE = [
4010
+ 'Usage: vinaya tokens --phase "<task-id>: develop" --role Developer',
4011
+ " [--model <id>] [--transcript <path>] [--in <tokens-in> --out <tokens-out>]",
4012
+ "",
4013
+ " --transcript Read this transcript directly. Supported primary route — use it whenever you know",
4014
+ " which transcript is yours, and always in a repo with no track-transcript.sh hook.",
4015
+ " (omitted) Resolve via the Stop-hook pointer file, if this repo installs that hook.",
4016
+ " --in/--out Manual entry: the exact token figures, for a host whose usage arrives by some other",
4017
+ " means than a Claude Code transcript. Both required together; skips transcript",
4018
+ " resolution entirely.",
4019
+ "",
4020
+ "Usage: vinaya tokens --trust-collect",
4021
+ " Approves this repo's declared tokens.collect command, pinned to the script's exact current",
4022
+ " content, on this machine. Run this once before tokens.collect will ever execute — a changed",
4023
+ " interpreter, script path, or script CONTENT needs its own fresh approval. Does not take",
4024
+ " --phase/--role and never emits a Tokens: line."
4025
+ ].join(`
4026
+ `);
4027
+ function parseArgs(argv) {
4028
+ let phase;
4029
+ let role;
4030
+ let model;
4031
+ let transcriptPath;
4032
+ let tokensInRaw;
4033
+ let tokensOutRaw;
4034
+ for (let i = 0;i < argv.length; i++) {
4035
+ const arg = argv[i];
4036
+ if (arg === "--phase")
4037
+ phase = argv[++i];
4038
+ else if (arg === "--role")
4039
+ role = argv[++i];
4040
+ else if (arg === "--model")
4041
+ model = argv[++i];
4042
+ else if (arg === "--transcript")
4043
+ transcriptPath = argv[++i];
4044
+ else if (arg === "--in")
4045
+ tokensInRaw = argv[++i];
4046
+ else if (arg === "--out")
4047
+ tokensOutRaw = argv[++i];
4048
+ }
4049
+ if (!phase || !role) {
4050
+ throw new Error(USAGE);
4051
+ }
4052
+ if (tokensInRaw === undefined !== (tokensOutRaw === undefined)) {
4053
+ throw new Error("vinaya tokens: --in and --out must be given together, or not at all.");
4054
+ }
4055
+ const tokensIn = tokensInRaw === undefined ? undefined : Number(tokensInRaw);
4056
+ const tokensOut = tokensOutRaw === undefined ? undefined : Number(tokensOutRaw);
4057
+ if (tokensIn !== undefined && (!Number.isFinite(tokensIn) || tokensIn < 0)) {
4058
+ throw new Error(`vinaya tokens: --in must be a non-negative integer, got "${tokensInRaw}".`);
4059
+ }
4060
+ if (tokensOut !== undefined && (!Number.isFinite(tokensOut) || tokensOut < 0)) {
4061
+ throw new Error(`vinaya tokens: --out must be a non-negative integer, got "${tokensOutRaw}".`);
4062
+ }
4063
+ return { phase, role, model, transcriptPath, tokensIn, tokensOut };
3795
4064
  }
3796
- function sh(args, input) {
3797
- return execFileSync4(args[0], args.slice(1), { encoding: "utf8", input }).trim();
4065
+ function manualSummary(tokensIn, tokensOut) {
4066
+ return {
4067
+ components: {
4068
+ inputTokens: tokensIn,
4069
+ outputTokens: tokensOut,
4070
+ cacheCreationInputTokens: 0,
4071
+ cacheReadInputTokens: 0
4072
+ },
4073
+ model: null,
4074
+ messageCount: 1
4075
+ };
3798
4076
  }
3799
- function shJson(args) {
3800
- return JSON.parse(sh(args));
4077
+ function declaredSummary(components, model) {
4078
+ return { components, model, messageCount: 1 };
3801
4079
  }
3802
- function parseMergeSha(args) {
3803
- for (const a of args) {
3804
- const m = a.match(/^--merge-sha=(.+)$/);
3805
- if (m)
3806
- return m[1];
4080
+ var COLLECT_FIELDS = ["inputTokens", "outputTokens", "cacheCreationInputTokens", "cacheReadInputTokens"];
4081
+ function parseDeclaredCollectOutput(raw, command) {
4082
+ const trimmed = raw.trim();
4083
+ const fail = (why) => {
4084
+ throw new Error(`vinaya tokens: declared tokens.collect command "${command}" ${why}
4085
+ ` + "Expected stdout to be a JSON object: " + `{"inputTokens":N,"outputTokens":N,"cacheCreationInputTokens":N,"cacheReadInputTokens":N,"model":"…"|null}
4086
+ ` + `Got: ${trimmed.slice(0, 500)}`);
4087
+ };
4088
+ let parsed;
4089
+ try {
4090
+ parsed = JSON.parse(trimmed);
4091
+ } catch (err) {
4092
+ fail(`did not print valid JSON on stdout (${err.message}).`);
3807
4093
  }
3808
- return null;
4094
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
4095
+ fail("printed JSON that is not an object.");
4096
+ }
4097
+ const obj = parsed;
4098
+ const components = {};
4099
+ for (const field of COLLECT_FIELDS) {
4100
+ const value = obj[field];
4101
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
4102
+ fail(`printed an invalid "${field}" (must be a non-negative number), got ${JSON.stringify(value)}.`);
4103
+ }
4104
+ components[field] = value;
4105
+ }
4106
+ const model = obj.model;
4107
+ if (model !== undefined && model !== null && typeof model !== "string") {
4108
+ fail(`printed a non-string "model" field: ${JSON.stringify(model)}.`);
4109
+ }
4110
+ return declaredSummary(components, typeof model === "string" ? model : null);
3809
4111
  }
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;
4112
+ function resolveDeclaredCollect(command, deps) {
4113
+ const parsed = parseTokensCollectDeclaration(command);
4114
+ if (!parsed) {
4115
+ 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.");
3814
4116
  }
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;
4117
+ const repoConfigDir = deps.repoConfigDir();
4118
+ if (repoConfigDir === null) {
4119
+ 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.");
3819
4120
  }
3820
- if (!repo.owner || !repo.repo) {
3821
- console.error("Error: could not resolve a GitHub owner/repo from the `origin` remote.");
3822
- return 1;
4121
+ const repoGitCommonDir = deps.gitCommonDir();
4122
+ if (repoGitCommonDir === null) {
4123
+ 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.");
3823
4124
  }
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;
4125
+ const scriptAbsolutePath = resolvePath(repoConfigDir, parsed.script);
4126
+ if (!deps.exists(scriptAbsolutePath)) {
4127
+ throw new Error(`vinaya tokens: declared tokens.collect script "${parsed.script}" does not exist at ${scriptAbsolutePath}.`);
3831
4128
  }
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
- }
4129
+ return { interpreter: parsed.interpreter, script: parsed.script, scriptAbsolutePath, repoGitCommonDir };
4130
+ }
4131
+ function runDeclaredCollect(command, deps) {
4132
+ const { interpreter, script, scriptAbsolutePath, repoGitCommonDir } = resolveDeclaredCollect(command, deps);
4133
+ const currentHash = deps.scriptContentHash(scriptAbsolutePath, deps.cwd);
4134
+ if (currentHash === null) {
4135
+ 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.");
3859
4136
  }
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;
4137
+ const trust = deps.getTrust(repoGitCommonDir, interpreter, script);
4138
+ if (trust === null) {
4139
+ throw new Error(`vinaya tokens: declared tokens.collect is not yet trusted on this machine:
4140
+ ` + ` ${interpreter} ${script}
4141
+ ` + "Run `vinaya tokens --trust-collect` once to approve it. Refusing — no `Tokens:` line emitted, no zeros.");
3864
4142
  }
3865
- const comments = pr.comments.map((c) => c.body);
3866
- if (hasProvenance(comments)) {
3867
- process.stdout.write(`[vinaya archive] provenance already present on PR #${pr.number}skip (idempotent).
3868
- `);
3869
- return 0;
4143
+ if (trust.scriptBlobHash !== currentHash) {
4144
+ throw new Error(`vinaya tokens: declared tokens.collect script "${script}" no longer matches its trusted content ` + `(approved at ${trust.scriptBlobHash}, now ${currentHash}).
4145
+ ` + "Run `vinaya tokens --trust-collect` again to approve the new content. Refusing no `Tokens:` line emitted, no zeros.");
3870
4146
  }
3871
- const facts = {
3872
- number: pr.number,
3873
- headRefName: pr.headRefName,
3874
- body: pr.body,
3875
- mergedAt: pr.mergedAt,
3876
- mergeSha,
3877
- comments
3878
- };
3879
- const { block, issue, dangling } = buildProvenanceBlock(facts);
3880
- process.stdout.write(`[vinaya archive] posting provenance block to PR #${pr.number}...
3881
- `);
3882
- sh(["gh", "pr", "comment", String(pr.number), "-R", repoFlag, "--body-file", "-"], block);
3883
- process.stdout.write(`[vinaya archive] provenance block posted to PR #${pr.number}.
3884
- `);
3885
- if (dangling.length > 0) {
3886
- process.stdout.write(`[vinaya archive] DANGLING (${dangling.length}): ${dangling.join("; ")}
3887
- `);
4147
+ deps.warn(`⚠ vinaya tokens: running declared tokens.collect: ${interpreter} ${script}`);
4148
+ let raw;
4149
+ try {
4150
+ raw = deps.runScript(interpreter, scriptAbsolutePath, deps.cwd);
4151
+ } catch (err) {
4152
+ throw new Error(`vinaya tokens: declared tokens.collect "${interpreter} ${script}" failed: ${err.message}`);
3888
4153
  }
3889
- if (issue !== null) {
3890
- process.stdout.write(`[vinaya archive] closing Issue #${issue}...
3891
- `);
3892
- sh(["gh", "issue", "close", String(issue), "-R", repoFlag]);
3893
- const state = shJson([
3894
- "gh",
4154
+ return parseDeclaredCollectOutput(raw, command);
4155
+ }
4156
+ var COLLECT_COMMAND_TIMEOUT_MS = 30000;
4157
+ function realDeps() {
4158
+ return {
4159
+ env: process.env,
4160
+ cwd: process.cwd(),
4161
+ exists: existsSync7,
4162
+ readFile: (path) => readFileSync7(path, "utf8"),
4163
+ loadConfig,
4164
+ repoConfigDir: repoLocalConfigDir,
4165
+ runScript: (interpreter, scriptAbsolutePath, cwd) => execFileSync4(interpreter, [scriptAbsolutePath], { cwd, encoding: "utf-8", timeout: COLLECT_COMMAND_TIMEOUT_MS }),
4166
+ warn: (message) => console.error(message),
4167
+ gitCommonDir,
4168
+ scriptContentHash: gitBlobHash,
4169
+ getTrust: getTokensCollectTrust,
4170
+ trustCollect: trustTokensCollectCommand
4171
+ };
4172
+ }
4173
+ function buildTokensResult(parsed, deps) {
4174
+ if (parsed.tokensIn !== undefined && parsed.tokensOut !== undefined) {
4175
+ const summary = manualSummary(parsed.tokensIn, parsed.tokensOut);
4176
+ return {
4177
+ line: formatTokensLine({ phase: parsed.phase, role: parsed.role, summary, modelOverride: parsed.model }),
4178
+ breakdown: undefined
4179
+ };
4180
+ }
4181
+ const declaredCommand = deps.loadConfig()?.tokens?.collect;
4182
+ if (declaredCommand) {
4183
+ const summary = runDeclaredCollect(declaredCommand, deps);
4184
+ return {
4185
+ line: formatTokensLine({ phase: parsed.phase, role: parsed.role, summary, modelOverride: parsed.model }),
4186
+ breakdown: formatBreakdown(summary)
4187
+ };
4188
+ }
4189
+ const capability = resolveMeteringCapability(deps, parsed.transcriptPath);
4190
+ if (!capability.capable) {
4191
+ throw new Error(`vinaya tokens: refused — could not resolve real usage figures (${capability.reason}).
4192
+ ${capability.detail}
4193
+
4194
+ ${USAGE}`);
4195
+ }
4196
+ return {
4197
+ line: formatTokensLine({
4198
+ phase: parsed.phase,
4199
+ role: parsed.role,
4200
+ summary: capability.summary,
4201
+ modelOverride: parsed.model
4202
+ }),
4203
+ breakdown: formatBreakdown(capability.summary)
4204
+ };
4205
+ }
4206
+ function runTrustCollect(deps) {
4207
+ const declaredCommand = deps.loadConfig()?.tokens?.collect;
4208
+ if (!declaredCommand) {
4209
+ return {
4210
+ ok: false,
4211
+ message: "vinaya tokens --trust-collect: no tokens.collect declared in this repo's vinaya.config.json — nothing to trust."
4212
+ };
4213
+ }
4214
+ let resolved;
4215
+ try {
4216
+ resolved = resolveDeclaredCollect(declaredCommand, deps);
4217
+ } catch (err) {
4218
+ return { ok: false, message: err.message };
4219
+ }
4220
+ const { interpreter, script, scriptAbsolutePath, repoGitCommonDir } = resolved;
4221
+ const scriptBlobHash = deps.scriptContentHash(scriptAbsolutePath, deps.cwd);
4222
+ if (scriptBlobHash === null) {
4223
+ return {
4224
+ ok: false,
4225
+ message: `vinaya tokens --trust-collect: script "${script}" could not be hashed (git unavailable, or the file could not be read) — refusing to trust blindly.`
4226
+ };
4227
+ }
4228
+ deps.trustCollect(repoGitCommonDir, interpreter, script, scriptBlobHash);
4229
+ return { ok: true, interpreter, script, scriptBlobHash };
4230
+ }
4231
+ function tokensCommand(argv) {
4232
+ if (argv.includes("--trust-collect")) {
4233
+ const outcome = runTrustCollect(realDeps());
4234
+ if (!outcome.ok) {
4235
+ console.error(outcome.message);
4236
+ process.exit(1);
4237
+ }
4238
+ 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.
4239
+ ${outcome.interpreter} ${outcome.script}
4240
+ ` + ` content hash: ${outcome.scriptBlobHash}
4241
+ `);
4242
+ return;
4243
+ }
4244
+ let parsed;
4245
+ try {
4246
+ parsed = parseArgs(argv);
4247
+ } catch (err) {
4248
+ console.error(err.message);
4249
+ process.exit(2);
4250
+ }
4251
+ let result;
4252
+ try {
4253
+ result = buildTokensResult(parsed, realDeps());
4254
+ } catch (err) {
4255
+ console.error(err.message);
4256
+ process.exit(1);
4257
+ }
4258
+ if (result.breakdown)
4259
+ console.error(result.breakdown);
4260
+ process.stdout.write(`${result.line}
4261
+ `);
4262
+ }
4263
+
4264
+ // src/commands/archive.ts
4265
+ function ring2Accelerated() {
4266
+ return loadConfig()?.rings?.ring2_asyncAudits === true;
4267
+ }
4268
+ function realDeps2() {
4269
+ return { detectRepo: detectGitRepo, meteringCapability: () => resolveMeteringCapability(realDeps()) };
4270
+ }
4271
+ function isEmptySummary(summary) {
4272
+ const { inputTokens, outputTokens, cacheCreationInputTokens, cacheReadInputTokens } = summary.components;
4273
+ return inputTokens === 0 && outputTokens === 0 && cacheCreationInputTokens === 0 && cacheReadInputTokens === 0;
4274
+ }
4275
+ function renderArchiveTokensLine(capability, phase, role) {
4276
+ if (!capability.capable) {
4277
+ return { line: formatTokensLine({ phase, role, summary: null }), dangling: null };
4278
+ }
4279
+ if (isEmptySummary(capability.summary)) {
4280
+ return {
4281
+ line: null,
4282
+ 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."
4283
+ };
4284
+ }
4285
+ return { line: formatTokensLine({ phase, role, summary: capability.summary }), dangling: null };
4286
+ }
4287
+ function sh(args, input) {
4288
+ return execFileSync5(args[0], args.slice(1), { encoding: "utf8", input, env: process.env }).trim();
4289
+ }
4290
+ function shJson(args) {
4291
+ return JSON.parse(sh(args));
4292
+ }
4293
+ function parseMergeSha(args) {
4294
+ for (const a of args) {
4295
+ const m = a.match(/^--merge-sha=(.+)$/);
4296
+ if (m)
4297
+ return m[1];
4298
+ }
4299
+ return null;
4300
+ }
4301
+ async function runArchive(args, deps) {
4302
+ if (ring2Accelerated()) {
4303
+ process.stdout.write("[vinaya archive] rings.ring2_asyncAudits is `true` (opt-in accelerator) — skipping, nothing changed.\n");
4304
+ return 0;
4305
+ }
4306
+ const repo = await deps.detectRepo();
4307
+ if (!repo) {
4308
+ console.error("Error: not a git repository. Run `vinaya archive` from inside your repo.");
4309
+ return 1;
4310
+ }
4311
+ if (!repo.owner || !repo.repo) {
4312
+ console.error("Error: could not resolve a GitHub owner/repo from the `origin` remote.");
4313
+ return 1;
4314
+ }
4315
+ const repoFlag = `${repo.owner}/${repo.repo}`;
4316
+ const mergeSha = parseMergeSha(args) ?? sh(["git", "rev-parse", "HEAD"]);
4317
+ const associated = shJson(["gh", "api", `repos/${repoFlag}/commits/${mergeSha}/pulls`]);
4318
+ if (associated.length === 0) {
4319
+ process.stdout.write(`[vinaya archive] no associated PR for merge ${mergeSha} — skip.
4320
+ `);
4321
+ return 0;
4322
+ }
4323
+ const prNumber = associated[0].number;
4324
+ const pr = shJson([
4325
+ "gh",
4326
+ "pr",
4327
+ "view",
4328
+ String(prNumber),
4329
+ "-R",
4330
+ repoFlag,
4331
+ "--json",
4332
+ "number,headRefName,body,mergedAt,comments"
4333
+ ]);
4334
+ const ref = taskRefFromBranch(pr.headRefName);
4335
+ let issueLabels = [];
4336
+ if (ref === null) {
4337
+ const { issue: candidateIssue } = extractIssue(pr.body);
4338
+ if (candidateIssue !== null) {
4339
+ issueLabels = shJson([
4340
+ "gh",
4341
+ "issue",
4342
+ "view",
4343
+ String(candidateIssue),
4344
+ "-R",
4345
+ repoFlag,
4346
+ "--json",
4347
+ "labels"
4348
+ ]).labels.map((l) => l.name);
4349
+ }
4350
+ }
4351
+ if (!isEligibleForProvenance(ref, issueLabels)) {
4352
+ process.stdout.write(`[vinaya archive] non-task branch (${pr.headRefName}) and closing Issue carries no vinaya/tranche:* label — skip.
4353
+ `);
4354
+ return 0;
4355
+ }
4356
+ const comments = pr.comments.map((c) => c.body);
4357
+ if (hasProvenance(comments)) {
4358
+ process.stdout.write(`[vinaya archive] provenance already present on PR #${pr.number} — skip (idempotent).
4359
+ `);
4360
+ return 0;
4361
+ }
4362
+ const facts = {
4363
+ number: pr.number,
4364
+ headRefName: pr.headRefName,
4365
+ body: pr.body,
4366
+ mergedAt: pr.mergedAt,
4367
+ mergeSha,
4368
+ comments
4369
+ };
4370
+ const { block, issue, dangling } = buildProvenanceBlock(facts);
4371
+ const phase = `${ref ? ref.taskId : pr.headRefName}: archive`;
4372
+ const tokensResult = renderArchiveTokensLine(deps.meteringCapability(), phase, "Archivist");
4373
+ const tokensAddition = tokensResult.line !== null ? tokensResult.line : `DANGLING (tokens): ${tokensResult.dangling}`;
4374
+ const blockWithTokens = `${block}
4375
+
4376
+ ${tokensAddition}`;
4377
+ process.stdout.write(`[vinaya archive] posting provenance block to PR #${pr.number}...
4378
+ `);
4379
+ sh(["gh", "pr", "comment", String(pr.number), "-R", repoFlag, "--body-file", "-"], blockWithTokens);
4380
+ process.stdout.write(`[vinaya archive] provenance block posted to PR #${pr.number}.
4381
+ `);
4382
+ if (dangling.length > 0) {
4383
+ process.stdout.write(`[vinaya archive] DANGLING (${dangling.length}): ${dangling.join("; ")}
4384
+ `);
4385
+ }
4386
+ if (tokensResult.dangling !== null) {
4387
+ process.stdout.write(`[vinaya archive] DANGLING (tokens): ${tokensResult.dangling}
4388
+ `);
4389
+ }
4390
+ if (issue !== null) {
4391
+ process.stdout.write(`[vinaya archive] closing Issue #${issue}...
4392
+ `);
4393
+ sh(["gh", "issue", "close", String(issue), "-R", repoFlag]);
4394
+ const state = shJson([
4395
+ "gh",
3895
4396
  "issue",
3896
4397
  "view",
3897
4398
  String(issue),
@@ -3915,7 +4416,7 @@ async function runArchive(args, deps) {
3915
4416
  return 0;
3916
4417
  }
3917
4418
  async function archiveCommand(args) {
3918
- process.exit(await runArchive(args, realDeps()));
4419
+ process.exit(await runArchive(args, realDeps2()));
3919
4420
  }
3920
4421
  function parseTrancheArgs(args) {
3921
4422
  const yes = args.includes("--yes");
@@ -3994,19 +4495,19 @@ async function runArchiveTranche(args, deps) {
3994
4495
  return 0;
3995
4496
  }
3996
4497
  async function archiveTrancheCommand(args) {
3997
- process.exit(await runArchiveTranche(args, realDeps()));
4498
+ process.exit(await runArchiveTranche(args, realDeps2()));
3998
4499
  }
3999
4500
 
4000
4501
  // src/commands/audit.ts
4001
- import { execFileSync as execFileSync5 } from "node:child_process";
4502
+ import { execFileSync as execFileSync6 } from "node:child_process";
4002
4503
  function ring2Accelerated2() {
4003
4504
  return loadConfig()?.rings?.ring2_asyncAudits === true;
4004
4505
  }
4005
- function realDeps2() {
4506
+ function realDeps3() {
4006
4507
  return { detectRepo: detectGitRepo };
4007
4508
  }
4008
4509
  function defaultSleep(ms) {
4009
- return new Promise((resolve2) => setTimeout(resolve2, ms));
4510
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
4010
4511
  }
4011
4512
  var COMMAND_TIMEOUT_MS = 20000;
4012
4513
  var DEAD_BRANCH_LABEL = label("dead-branch-push");
@@ -4017,7 +4518,7 @@ var DIRECT_PUSH_MARKER = `<!-- ${DIRECT_PUSH_LABEL} -->`;
4017
4518
  var DIRECT_PUSH_LABEL_DESCRIPTION = "A commit landed on main with no associated merged PR (ring-2 detection)";
4018
4519
  function shSoft(args, input) {
4019
4520
  try {
4020
- return execFileSync5(args[0], args.slice(1), {
4521
+ return execFileSync6(args[0], args.slice(1), {
4021
4522
  encoding: "utf8",
4022
4523
  input,
4023
4524
  timeout: COMMAND_TIMEOUT_MS
@@ -4041,7 +4542,7 @@ function shSoftJson(args) {
4041
4542
  }
4042
4543
  }
4043
4544
  function sh2(args, input) {
4044
- return execFileSync5(args[0], args.slice(1), { encoding: "utf8", input }).trim();
4545
+ return execFileSync6(args[0], args.slice(1), { encoding: "utf8", input }).trim();
4045
4546
  }
4046
4547
  function listTaskBranches() {
4047
4548
  const out = shSoft(["git", "ls-remote", "--heads", "origin", "task/*"]);
@@ -4297,11 +4798,11 @@ async function runAudit(args, deps) {
4297
4798
  return failed ? 1 : 0;
4298
4799
  }
4299
4800
  async function auditCommand(args) {
4300
- process.exit(await runAudit(args, realDeps2()));
4801
+ process.exit(await runAudit(args, realDeps3()));
4301
4802
  }
4302
4803
 
4303
4804
  // src/commands/check.ts
4304
- import { execFileSync as execFileSync6 } from "node:child_process";
4805
+ import { execFileSync as execFileSync7 } from "node:child_process";
4305
4806
  import { dirname as dirname5 } from "node:path";
4306
4807
 
4307
4808
  // src/checks/contract.ts
@@ -4312,11 +4813,11 @@ function emitCheckError(error) {
4312
4813
  }
4313
4814
 
4314
4815
  // src/checks/registry.ts
4315
- import { existsSync as existsSync7 } from "node:fs";
4816
+ import { existsSync as existsSync8 } from "node:fs";
4316
4817
  import { join as join8 } from "node:path";
4317
4818
  var DIST_BIN_DIR = join8(packageRoot(import.meta.url), "dist", "checks", "bin");
4318
4819
  var SRC_BIN_DIR = join8(packageRoot(import.meta.url), "src", "checks", "bin");
4319
- var BIN_DIR = existsSync7(DIST_BIN_DIR) ? DIST_BIN_DIR : SRC_BIN_DIR;
4820
+ var BIN_DIR = existsSync8(DIST_BIN_DIR) ? DIST_BIN_DIR : SRC_BIN_DIR;
4320
4821
  var BIN_EXT = BIN_DIR === DIST_BIN_DIR ? ".js" : ".ts";
4321
4822
  function bin(name) {
4322
4823
  return join8(BIN_DIR, `${name}${BIN_EXT}`);
@@ -4627,6 +5128,26 @@ var REGISTRY = [
4627
5128
  },
4628
5129
  0
4629
5130
  ],
5131
+ [
5132
+ {
5133
+ name: "changeset-coverage",
5134
+ run: bin("check-changeset-coverage"),
5135
+ scope: "diff",
5136
+ timeoutMs: 15000,
5137
+ env: {}
5138
+ },
5139
+ 0
5140
+ ],
5141
+ [
5142
+ {
5143
+ name: "quoted-command",
5144
+ run: bin("check-quoted-command"),
5145
+ scope: "diff",
5146
+ timeoutMs: 15000,
5147
+ env: {}
5148
+ },
5149
+ 0
5150
+ ],
4630
5151
  [
4631
5152
  {
4632
5153
  name: "main-branch-refusal",
@@ -4636,6 +5157,19 @@ var REGISTRY = [
4636
5157
  env: {}
4637
5158
  },
4638
5159
  0
5160
+ ],
5161
+ [
5162
+ {
5163
+ name: "token-collection-wired",
5164
+ run: bin("check-token-collection-wired"),
5165
+ scope: "full",
5166
+ timeoutMs: 15000,
5167
+ env: {
5168
+ CLAUDE_PROJECT_DIR: { optional: true },
5169
+ CLAUDE_CODE_SESSION_ID: { optional: true }
5170
+ }
5171
+ },
5172
+ 0
4639
5173
  ]
4640
5174
  ];
4641
5175
  function coreCheckRegistry() {
@@ -4843,16 +5377,16 @@ async function runOne(spec, timeoutMs, callerEnv) {
4843
5377
  const timer = setTimeout(() => {
4844
5378
  timedOut = true;
4845
5379
  killTree("SIGTERM");
4846
- escalationDone = new Promise((resolve2) => {
5380
+ escalationDone = new Promise((resolve3) => {
4847
5381
  const graceTimer = setTimeout(() => {
4848
5382
  const pid = proc.pid;
4849
5383
  if (pid !== undefined && groupStillAlive(pid))
4850
5384
  killTree("SIGKILL");
4851
- resolve2();
5385
+ resolve3();
4852
5386
  }, KILL_GRACE_MS);
4853
5387
  cancelEscalation = () => {
4854
5388
  clearTimeout(graceTimer);
4855
- resolve2();
5389
+ resolve3();
4856
5390
  };
4857
5391
  });
4858
5392
  }, timeoutMs);
@@ -4863,11 +5397,11 @@ async function runOne(spec, timeoutMs, callerEnv) {
4863
5397
  });
4864
5398
  proc.stdout.resume();
4865
5399
  let spawnError;
4866
- const exitCode = await new Promise((resolve2) => {
4867
- proc.on("close", (code) => resolve2(code));
5400
+ const exitCode = await new Promise((resolve3) => {
5401
+ proc.on("close", (code) => resolve3(code));
4868
5402
  proc.on("error", (err) => {
4869
5403
  spawnError = err;
4870
- resolve2(null);
5404
+ resolve3(null);
4871
5405
  });
4872
5406
  });
4873
5407
  clearTimeout(timer);
@@ -4955,7 +5489,7 @@ async function runChecks(specs, opts) {
4955
5489
  }
4956
5490
 
4957
5491
  // src/roles/plan.ts
4958
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "node:fs";
5492
+ import { existsSync as existsSync9, readFileSync as readFileSync9 } from "node:fs";
4959
5493
  import { join as join10 } from "node:path";
4960
5494
 
4961
5495
  // ../../packages/sources/src/commands.ts
@@ -5110,6 +5644,18 @@ var COMMANDS = [
5110
5644
  ],
5111
5645
  status: "shipped"
5112
5646
  },
5647
+ {
5648
+ name: "pr verify-evidence",
5649
+ description: "Prove a pull request's AEG:EVIDENCE region was machine-generated — regenerate it and compare",
5650
+ flags: [],
5651
+ details: [
5652
+ "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.",
5653
+ "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.",
5654
+ "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.",
5655
+ "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."
5656
+ ],
5657
+ status: "shipped"
5658
+ },
5113
5659
  {
5114
5660
  name: "issue create",
5115
5661
  description: "Open an issue after full brief-schema validation",
@@ -5186,6 +5732,19 @@ var COMMANDS = [
5186
5732
  ],
5187
5733
  status: "shipped"
5188
5734
  },
5735
+ {
5736
+ name: "milestone close",
5737
+ description: "Close a tranche's Milestone after verifying every labeled Issue is actually attached",
5738
+ flags: [
5739
+ { flag: "--slug", description: "The tranche whose Milestone is being closed" },
5740
+ { flag: "--validate-only", description: "Run every gate and report PASS without closing the Milestone" },
5741
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
5742
+ ],
5743
+ details: [
5744
+ "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."
5745
+ ],
5746
+ status: "shipped"
5747
+ },
5189
5748
  {
5190
5749
  name: "review post",
5191
5750
  description: "Render, post, and self-verify a code-reviewer or security-review verdict comment on a PR",
@@ -5373,7 +5932,7 @@ var COMMANDS = [
5373
5932
  }
5374
5933
  ];
5375
5934
  // ../../packages/sources/src/doctrine-file-adapter.ts
5376
- import { readdirSync as readdirSync4, readFileSync as readFileSync7 } from "node:fs";
5935
+ import { readdirSync as readdirSync4, readFileSync as readFileSync8 } from "node:fs";
5377
5936
  import { join as join9 } from "node:path";
5378
5937
 
5379
5938
  // ../../packages/sources/src/file-adapter.ts
@@ -5384,14 +5943,14 @@ function readMarkdownDir(root, dir) {
5384
5943
  const dirPath = join9(root, dir);
5385
5944
  return readdirSync4(dirPath).filter((f) => f.endsWith(".md")).sort().map((file) => {
5386
5945
  const path = join9(dir, file);
5387
- return { path, content: readFileSync7(join9(root, path), "utf-8") };
5946
+ return { path, content: readFileSync8(join9(root, path), "utf-8") };
5388
5947
  });
5389
5948
  }
5390
5949
  function createFileDoctrineSource(config = {}) {
5391
5950
  const root = config.root ?? DEFAULT_GOVERNANCE_ROOT;
5392
5951
  return {
5393
5952
  async getDoctrine() {
5394
- const enforcement = readFileSync7(join9(root, "enforcement.md"), "utf-8");
5953
+ const enforcement = readFileSync8(join9(root, "enforcement.md"), "utf-8");
5395
5954
  const roles = readMarkdownDir(root, "roles");
5396
5955
  const contracts = readMarkdownDir(root, "contracts");
5397
5956
  return { enforcement, roles, contracts };
@@ -5566,11 +6125,11 @@ function resolveRoles(core, configRoles) {
5566
6125
  var NO_DOCTRINE_REASON = "no bundled doctrine found next to this CLI install — the doctrine ships inside the @attalabs/vinaya npm package. Reinstall it, or, in a repo that vendors the CLI, run the package's bundle-doctrine script first.";
5567
6126
  function readContract(configDir, contractPath) {
5568
6127
  const absolute = join10(configDir, contractPath);
5569
- if (!existsSync8(absolute)) {
6128
+ if (!existsSync9(absolute)) {
5570
6129
  return { ok: false, errors: [`contract file "${contractPath}" does not exist (resolved to "${absolute}")`] };
5571
6130
  }
5572
6131
  try {
5573
- return validateRoleContract(readFileSync8(absolute, "utf-8"));
6132
+ return validateRoleContract(readFileSync9(absolute, "utf-8"));
5574
6133
  } catch (err) {
5575
6134
  return { ok: false, errors: [`could not read "${contractPath}": ${err.message}`] };
5576
6135
  }
@@ -5603,7 +6162,7 @@ async function buildRolePlan(configDir, configRoles) {
5603
6162
  // src/commands/check.ts
5604
6163
  function git(args) {
5605
6164
  try {
5606
- return execFileSync6("git", args, { encoding: "utf8" }).trim();
6165
+ return execFileSync7("git", args, { encoding: "utf8" }).trim();
5607
6166
  } catch {
5608
6167
  return "";
5609
6168
  }
@@ -5841,7 +6400,7 @@ ${renderRolesTable(rolePlan)}
5841
6400
  }
5842
6401
 
5843
6402
  // src/commands/commit-msg.ts
5844
- import { readFileSync as readFileSync9 } from "node:fs";
6403
+ import { readFileSync as readFileSync10 } from "node:fs";
5845
6404
  function commitMsgCommand(args) {
5846
6405
  const [messageFile, source] = args;
5847
6406
  if (!messageFile) {
@@ -5852,7 +6411,7 @@ function commitMsgCommand(args) {
5852
6411
  }
5853
6412
  if (source === "merge")
5854
6413
  return;
5855
- const firstLine2 = readFileSync9(messageFile, "utf-8").split(`
6414
+ const firstLine2 = readFileSync10(messageFile, "utf-8").split(`
5856
6415
  `, 1)[0] ?? "";
5857
6416
  if (COMMIT_TYPE_STYLE.test(firstLine2))
5858
6417
  return;
@@ -5864,14 +6423,14 @@ function commitMsgCommand(args) {
5864
6423
  }
5865
6424
 
5866
6425
  // 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";
6426
+ import { execFileSync as execFileSync8, spawnSync } from "node:child_process";
6427
+ import { existsSync as existsSync10, readFileSync as readFileSync11, rmSync as rmSync2, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
5869
6428
  import { isAbsolute as isAbsolute2, join as join11 } from "node:path";
5870
6429
  var DEMO_BRANCH_PREFIX = "vinaya/demo-break-";
5871
6430
  var FIXTURE_PATH = ".vinaya-demo-brief.md";
5872
6431
  var STATE_FILENAME = "vinaya-demo-state.json";
5873
6432
  function gitCapture(repoRoot, args) {
5874
- return execFileSync7("git", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
6433
+ return execFileSync8("git", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
5875
6434
  }
5876
6435
  function currentBranchName(repoRoot) {
5877
6436
  try {
@@ -5882,7 +6441,7 @@ function currentBranchName(repoRoot) {
5882
6441
  }
5883
6442
  function branchExists(repoRoot, name) {
5884
6443
  try {
5885
- execFileSync7("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${name}`], {
6444
+ execFileSync8("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${name}`], {
5886
6445
  cwd: repoRoot,
5887
6446
  stdio: ["ignore", "pipe", "pipe"]
5888
6447
  });
@@ -5908,9 +6467,9 @@ function gitDirAbs(repoRoot) {
5908
6467
  function recoverFromCrash(repoRoot, statePath) {
5909
6468
  const currentBranch = currentBranchName(repoRoot);
5910
6469
  let state = null;
5911
- if (existsSync9(statePath)) {
6470
+ if (existsSync10(statePath)) {
5912
6471
  try {
5913
- state = JSON.parse(readFileSync10(statePath, "utf-8"));
6472
+ state = JSON.parse(readFileSync11(statePath, "utf-8"));
5914
6473
  } catch {
5915
6474
  state = null;
5916
6475
  }
@@ -5980,14 +6539,14 @@ function cleanup(repoRoot, originalBranch, demoBranch, statePath) {
5980
6539
  gitCapture(repoRoot, ["clean", "-fd"]);
5981
6540
  gitCapture(repoRoot, ["checkout", originalBranch]);
5982
6541
  gitCapture(repoRoot, ["branch", "-D", demoBranch]);
5983
- if (existsSync9(statePath))
6542
+ if (existsSync10(statePath))
5984
6543
  rmSync2(statePath, { force: true });
5985
6544
  }
5986
6545
  async function runDemoBreak(repoRoot, args) {
5987
6546
  const keep = args.includes("--keep");
5988
6547
  const hookDir = resolveHookDir(repoRoot);
5989
6548
  const hookPath = resolveManagedBlockPath(repoRoot, `${hookDir}/pre-commit`);
5990
- if (!existsSync9(hookPath)) {
6549
+ if (!existsSync10(hookPath)) {
5991
6550
  console.error("Vinaya hooks are not installed in this repo. Run `vinaya init` first.");
5992
6551
  return 1;
5993
6552
  }
@@ -6057,7 +6616,7 @@ Attempting to commit…
6057
6616
  process.stdout.write(`✓ Commit passed — the fix worked.
6058
6617
  `);
6059
6618
  if (keep) {
6060
- if (existsSync9(statePath))
6619
+ if (existsSync10(statePath))
6061
6620
  unlinkSync(statePath);
6062
6621
  process.stdout.write(`
6063
6622
  --keep: leaving \`${demoBranch}\` checked out for you to inspect. Clean up with:
@@ -6084,12 +6643,12 @@ async function demoBreakCommand(args) {
6084
6643
  }
6085
6644
 
6086
6645
  // 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";
6646
+ import { execFileSync as execFileSync9 } from "node:child_process";
6647
+ import { existsSync as existsSync13, readdirSync as readdirSync6, readFileSync as readFileSync15, statSync as statSync2 } from "node:fs";
6648
+ import { delimiter, join as join14 } from "node:path";
6090
6649
 
6091
6650
  // src/lib/self-host.ts
6092
- import { readdirSync as readdirSync5, readFileSync as readFileSync11, realpathSync } from "node:fs";
6651
+ import { readdirSync as readdirSync5, readFileSync as readFileSync12, realpathSync as realpathSync2 } from "node:fs";
6093
6652
  import { join as join12, sep as sep3 } from "node:path";
6094
6653
  var VINAYA_PACKAGE_NAME = "@attalabs/vinaya";
6095
6654
  var DEFAULT_BIN = "dist/index.js";
@@ -6108,7 +6667,7 @@ function isSafeRelPath(p) {
6108
6667
  }
6109
6668
  function readPackageJson(path) {
6110
6669
  try {
6111
- const parsed = JSON.parse(readFileSync11(path, "utf-8"));
6670
+ const parsed = JSON.parse(readFileSync12(path, "utf-8"));
6112
6671
  return parsed && typeof parsed === "object" ? parsed : null;
6113
6672
  } catch {
6114
6673
  return null;
@@ -6162,8 +6721,8 @@ function expandPattern(repoRoot, pattern) {
6162
6721
  }
6163
6722
  function resolvesInsideRepo(repoRoot, rel) {
6164
6723
  try {
6165
- const root = realpathSync(repoRoot);
6166
- const target = realpathSync(join12(repoRoot, rel));
6724
+ const root = realpathSync2(repoRoot);
6725
+ const target = realpathSync2(join12(repoRoot, rel));
6167
6726
  return target === root || target.startsWith(`${root}${sep3}`);
6168
6727
  } catch {
6169
6728
  return false;
@@ -6203,18 +6762,18 @@ function detectVendoredVinaya(repoRoot) {
6203
6762
  }
6204
6763
 
6205
6764
  // src/lib/env-lint.ts
6206
- import { existsSync as existsSync10, readFileSync as readFileSync12 } from "node:fs";
6765
+ import { existsSync as existsSync11, readFileSync as readFileSync13 } from "node:fs";
6207
6766
  var ENV_READ_PATTERN = /\b(?:process\.env|Bun\.env|Deno\.env)\b/;
6208
6767
  function checksMissingEnvDeclaration(specs) {
6209
6768
  const names = [];
6210
6769
  for (const spec of specs) {
6211
6770
  if (spec.env)
6212
6771
  continue;
6213
- if (!existsSync10(spec.run))
6772
+ if (!existsSync11(spec.run))
6214
6773
  continue;
6215
6774
  let source;
6216
6775
  try {
6217
- source = readFileSync12(spec.run, "utf8");
6776
+ source = readFileSync13(spec.run, "utf8");
6218
6777
  } catch {
6219
6778
  continue;
6220
6779
  }
@@ -6227,40 +6786,171 @@ function envDeclarationWarning(name) {
6227
6786
  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
6787
  }
6229
6788
 
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
- };
6789
+ // src/lib/registry-write.ts
6790
+ import { existsSync as existsSync12, mkdirSync as mkdirSync3, readFileSync as readFileSync14, writeFileSync as writeFileSync4 } from "node:fs";
6791
+ import { dirname as dirname6, join as join13 } from "node:path";
6792
+ var PROJECTS_REGISTRY_PATH = ".vinaya/projects.md";
6793
+ var CONFIG_FILE_PATH = "vinaya.config.json";
6794
+ function rowLine(name, path, specsPath) {
6795
+ return `| ${name} | \`${path}\` | \`${specsPath}\` | (state tracked globally) |`;
6252
6796
  }
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 });
6797
+ function freshProjectsRegistry(name, path, specsPath) {
6798
+ return `---
6799
+ sidebar_title: Projects
6800
+ ---
6801
+ # Projects in this repo
6802
+
6803
+ **The project registry.** Declares the projects in this repo and where each
6804
+ one's specs and per-project state live. The \`Project\` field on a task (a
6805
+ forge Issue) resolves against this file.
6806
+
6807
+ **Presence of this file means this is a multi-project repo** — the
6808
+ \`Project\` field is required on task Issues/PRs that touch a registered
6809
+ project's path. A project is a \`(name, path)\` pair you declared — nothing
6810
+ is derived from the folder tree.
6811
+
6812
+ ## Registry
6813
+
6814
+ | Project | Path | Specs | Per-project state |
6815
+ |---------|------|-------|---------------------|
6816
+ ${rowLine(name, path, specsPath)}
6817
+ `;
6818
+ }
6819
+ var REGISTRY_HEADING_RE = /^##\s+Registry\b/i;
6820
+ function appendRegistryRow(content, name, path, specsPath) {
6821
+ const lines = content.split(/\r?\n/);
6822
+ const headingIdx = lines.findIndex((l) => REGISTRY_HEADING_RE.test(l));
6823
+ const newRow = rowLine(name, path, specsPath);
6824
+ if (headingIdx === -1) {
6825
+ const sep4 = content.endsWith(`
6826
+ `) ? "" : `
6827
+ `;
6828
+ return `${content}${sep4}
6829
+ ## Registry
6830
+
6831
+ | Project | Path | Specs | Per-project state |
6832
+ |---------|------|-------|---------------------|
6833
+ ${newRow}
6834
+ `;
6835
+ }
6836
+ let i = headingIdx + 1;
6837
+ for (;i < lines.length; i++) {
6838
+ if ((lines[i] ?? "").trim().startsWith("|"))
6839
+ break;
6840
+ }
6841
+ if (i >= lines.length) {
6842
+ lines.splice(headingIdx + 1, 0, "", "| Project | Path | Specs | Per-project state |", "|---------|------|-------|---------------------|", newRow);
6843
+ return lines.join(`
6844
+ `);
6845
+ }
6846
+ let last = i + 1;
6847
+ for (let j = i + 2;j < lines.length; j++) {
6848
+ if (!(lines[j] ?? "").trim().startsWith("|"))
6849
+ break;
6850
+ last = j;
6851
+ }
6852
+ lines.splice(last + 1, 0, newRow);
6853
+ return lines.join(`
6854
+ `);
6855
+ }
6856
+ function planRegistryRow(repoRoot, name, path, specsPath) {
6857
+ const abs2 = join13(repoRoot, PROJECTS_REGISTRY_PATH);
6858
+ const line = rowLine(name, path, specsPath);
6859
+ if (!existsSync12(abs2)) {
6860
+ return { action: "create-host", rowLine: line, path: PROJECTS_REGISTRY_PATH };
6861
+ }
6862
+ const existing = readFileSync14(abs2, "utf-8");
6863
+ const already = parseRegistry(existing).some((p) => p.name === name);
6864
+ return { action: already ? "skip-present" : "append-row", rowLine: line, path: PROJECTS_REGISTRY_PATH };
6865
+ }
6866
+ function applyRegistryRow(repoRoot, plan, name, path, specsPath) {
6867
+ const abs2 = join13(repoRoot, PROJECTS_REGISTRY_PATH);
6868
+ if (plan.action === "create-host") {
6869
+ mkdirSync3(dirname6(abs2), { recursive: true });
6870
+ writeFileSync4(abs2, freshProjectsRegistry(name, path, specsPath), "utf-8");
6871
+ } else if (plan.action === "append-row") {
6872
+ const existing = readFileSync14(abs2, "utf-8");
6873
+ writeFileSync4(abs2, appendRegistryRow(existing, name, path, specsPath), "utf-8");
6874
+ }
6875
+ }
6876
+ function renderRegistryRowDiffLine(plan) {
6877
+ if (plan.action === "skip-present")
6878
+ return ` = keep ${plan.path} (project already registered)`;
6879
+ if (plan.action === "append-row")
6880
+ return ` ~ append row to ${plan.path}
6881
+ ${plan.rowLine}`;
6882
+ return ` + create ${plan.path} (with registry row)
6883
+ ${plan.rowLine}`;
6884
+ }
6885
+ function readProjectEntries(repoRoot) {
6886
+ const abs2 = join13(repoRoot, CONFIG_FILE_PATH);
6887
+ if (!existsSync12(abs2))
6888
+ return [];
6889
+ try {
6890
+ return VinayaConfigSchema.parse(JSON.parse(readFileSync14(abs2, "utf-8"))).projects ?? [];
6891
+ } catch {
6892
+ return [];
6893
+ }
6894
+ }
6895
+ function planConfigProjectEntry(repoRoot, entry2) {
6896
+ const already = readProjectEntries(repoRoot).some((p) => p.name === entry2.name);
6897
+ return { action: already ? "skip-present" : "add-entry", entry: entry2 };
6898
+ }
6899
+ function applyConfigProjectEntry(repoRoot, plan) {
6900
+ if (plan.action === "skip-present")
6901
+ return;
6902
+ const abs2 = join13(repoRoot, CONFIG_FILE_PATH);
6903
+ const seed = JSON.parse(readFileSync14(abs2, "utf-8"));
6904
+ const projects = Array.isArray(seed.projects) ? seed.projects : [];
6905
+ writeFileSync4(abs2, `${JSON.stringify({ ...seed, projects: [...projects, plan.entry] }, null, 2)}
6906
+ `, "utf-8");
6907
+ }
6908
+ function renderConfigProjectEntryDiffLine(plan) {
6909
+ if (plan.action === "skip-present")
6910
+ return ` = keep ${CONFIG_FILE_PATH} (project already declared under "projects")`;
6911
+ const bits = [`"name": ${JSON.stringify(plan.entry.name)}`];
6912
+ if (plan.entry.path)
6913
+ bits.push(`"path": ${JSON.stringify(plan.entry.path)}`);
6914
+ if (plan.entry.description)
6915
+ bits.push(`"description": ${JSON.stringify(plan.entry.description)}`);
6916
+ return ` ~ append entry to ${CONFIG_FILE_PATH}'s "projects"
6917
+ { ${bits.join(", ")} }`;
6918
+ }
6919
+
6920
+ // src/commands/doctor.ts
6921
+ function readVersion() {
6922
+ const pkg = JSON.parse(readFileSync15(join14(packageRoot(import.meta.url), "package.json"), "utf-8"));
6923
+ return pkg.version;
6924
+ }
6925
+ function realDeps4() {
6926
+ return {
6927
+ detectRepo: detectGitRepo,
6928
+ ghAuthStatus,
6929
+ branchProtectionConfigured,
6930
+ hookDirFor: resolveHookDir,
6931
+ readHooksPath: readCoreHooksPath,
6932
+ nodeVersion: () => process.version,
6933
+ bunVersion: () => typeof Bun === "undefined" ? null : Bun.version,
6934
+ packageVersion: readVersion,
6935
+ meteringCapability: () => resolveMeteringCapability({
6936
+ env: process.env,
6937
+ cwd: process.cwd(),
6938
+ exists: existsSync13,
6939
+ readFile: (path) => readFileSync15(path, "utf8")
6940
+ })
6941
+ };
6942
+ }
6943
+ var ok = (check, message) => ({ check, severity: "ok", message });
6944
+ var info = (check, message) => ({ check, severity: "info", message });
6945
+ var warn = (check, message) => ({ check, severity: "warn", message });
6946
+ var error = (check, message) => ({ check, severity: "error", message });
6257
6947
  function readConfig(repoRoot) {
6258
- const p = join13(repoRoot, CONFIG_PATH);
6259
- if (!existsSync11(p))
6948
+ const p = join14(repoRoot, CONFIG_PATH);
6949
+ if (!existsSync13(p))
6260
6950
  return { kind: "missing" };
6261
6951
  let raw;
6262
6952
  try {
6263
- raw = JSON.parse(readFileSync13(p, "utf-8"));
6953
+ raw = JSON.parse(readFileSync15(p, "utf-8"));
6264
6954
  } catch (err) {
6265
6955
  return { kind: "invalid", error: `invalid JSON: ${err.message}` };
6266
6956
  }
@@ -6299,14 +6989,14 @@ function diagnoseInstall(repoRoot, ctx, manifest) {
6299
6989
  for (const op of buildInitOps(ctx)) {
6300
6990
  if (op.kind === "create-file") {
6301
6991
  const check = labelForPath(op.path);
6302
- const abs2 = join13(repoRoot, op.path);
6303
- const exists = existsSync11(abs2);
6992
+ const abs2 = join14(repoRoot, op.path);
6993
+ const exists = existsSync13(abs2);
6304
6994
  const owned = ownedFiles.has(op.path) || isDefaultedAgentVendorPath(op.path, manifest);
6305
6995
  if (!exists) {
6306
6996
  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
6997
  continue;
6308
6998
  }
6309
- const content = readFileSync13(abs2, "utf-8");
6999
+ const content = readFileSync15(abs2, "utf-8");
6310
7000
  if (!owned) {
6311
7001
  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
7002
  continue;
@@ -6325,11 +7015,11 @@ function diagnoseInstall(repoRoot, ctx, manifest) {
6325
7015
  const check = "hooks";
6326
7016
  const abs2 = resolveManagedBlockPath(repoRoot, op.path);
6327
7017
  const owned = ownedBlocks.has(blockKey(op.path, op.marker));
6328
- if (!existsSync11(abs2)) {
7018
+ if (!existsSync13(abs2)) {
6329
7019
  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
7020
  continue;
6331
7021
  }
6332
- const content = readFileSync13(abs2, "utf-8");
7022
+ const content = readFileSync15(abs2, "utf-8");
6333
7023
  const { begin, end } = markerLines(op.marker, op.comment);
6334
7024
  const hasMarkers = content.includes(begin) && content.includes(end);
6335
7025
  if (!hasMarkers) {
@@ -6377,22 +7067,22 @@ async function diagnoseHookRouting(repoRoot, hookDir, readHooksPath) {
6377
7067
  function diagnoseCustomChecks(repoRoot, config) {
6378
7068
  const findings = [];
6379
7069
  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}`));
7070
+ const scriptAbs = join14(repoRoot, entry2.run);
7071
+ findings.push(existsSync13(scriptAbs) ? ok("checks", `custom check '${name}' → ${entry2.run}`) : error("checks", `custom check '${name}' points at a missing script: ${entry2.run}`));
6382
7072
  }
6383
7073
  return findings;
6384
7074
  }
6385
7075
  function listTrackedFiles(repoRoot) {
6386
7076
  try {
6387
- return execFileSync8("git", ["ls-files"], { cwd: repoRoot, encoding: "utf-8" }).split(`
7077
+ return execFileSync9("git", ["ls-files"], { cwd: repoRoot, encoding: "utf-8" }).split(`
6388
7078
  `).map((s) => s.trim()).filter(Boolean);
6389
7079
  } catch {
6390
7080
  return [];
6391
7081
  }
6392
7082
  }
6393
7083
  function diagnoseDocOwnersHealth(repoRoot) {
6394
- const path = join13(repoRoot, DOC_OWNERS_PATH);
6395
- const content = existsSync11(path) ? readFileSync13(path, "utf-8") : null;
7084
+ const path = join14(repoRoot, DOC_OWNERS_PATH);
7085
+ const content = existsSync13(path) ? readFileSync15(path, "utf-8") : null;
6396
7086
  const state = classifyDocOwnersManifest(content);
6397
7087
  if (state === "absent" || state === "empty")
6398
7088
  return [];
@@ -6409,8 +7099,8 @@ function diagnoseDocOwnersHealth(repoRoot) {
6409
7099
  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
7100
  }
6411
7101
  if (!isUrlPointer(b.pointer)) {
6412
- const pointerPath = join13(repoRoot, pointerToPath(b.pointer));
6413
- if (!existsSync11(pointerPath)) {
7102
+ const pointerPath = join14(repoRoot, pointerToPath(b.pointer));
7103
+ if (!existsSync13(pointerPath)) {
6414
7104
  flagged = true;
6415
7105
  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
7106
  }
@@ -6425,7 +7115,7 @@ var LEGACY_AEG_PACKAGES_PATH = ".aeg/packages";
6425
7115
  function readWorkspacesForDoctor(repoRoot) {
6426
7116
  const fromPackageJson = () => {
6427
7117
  try {
6428
- const pkg = JSON.parse(readFileSync13(join13(repoRoot, "package.json"), "utf-8"));
7118
+ const pkg = JSON.parse(readFileSync15(join14(repoRoot, "package.json"), "utf-8"));
6429
7119
  return Array.isArray(pkg.workspaces) ? pkg.workspaces.filter((w) => typeof w === "string") : [];
6430
7120
  } catch {
6431
7121
  return [];
@@ -6433,7 +7123,7 @@ function readWorkspacesForDoctor(repoRoot) {
6433
7123
  };
6434
7124
  const fromPnpmWorkspaceYaml = () => {
6435
7125
  try {
6436
- return parsePnpmWorkspaceYaml(readFileSync13(join13(repoRoot, "pnpm-workspace.yaml"), "utf-8"));
7126
+ return parsePnpmWorkspaceYaml(readFileSync15(join14(repoRoot, "pnpm-workspace.yaml"), "utf-8"));
6437
7127
  } catch {
6438
7128
  return [];
6439
7129
  }
@@ -6442,23 +7132,23 @@ function readWorkspacesForDoctor(repoRoot) {
6442
7132
  }
6443
7133
  function listChildDirsForDoctor(dir, repoRoot) {
6444
7134
  try {
6445
- return readdirSync6(join13(repoRoot, dir), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
7135
+ return readdirSync6(join14(repoRoot, dir), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
6446
7136
  } catch {
6447
7137
  return [];
6448
7138
  }
6449
7139
  }
6450
7140
  function diagnoseBlastRadiusDeprecation(repoRoot, config) {
6451
7141
  const derived = deriveWorkspacePackageDomains(readWorkspacesForDoctor(repoRoot), (dir) => listChildDirsForDoctor(dir, repoRoot));
6452
- const defaults = deriveBuiltinCrossCuttingDefaults((p) => existsSync11(join13(repoRoot, p)));
7142
+ const defaults = deriveBuiltinCrossCuttingDefaults((p) => existsSync13(join14(repoRoot, p)));
6453
7143
  const covered = new Set([...derived, ...defaults]);
6454
7144
  const configExtra = new Set(config?.blastRadius?.extraDomains ?? []);
6455
- const legacyPath = join13(repoRoot, LEGACY_AEG_PACKAGES_PATH);
6456
- if (!existsSync11(legacyPath)) {
7145
+ const legacyPath = join14(repoRoot, LEGACY_AEG_PACKAGES_PATH);
7146
+ if (!existsSync13(legacyPath)) {
6457
7147
  return [
6458
7148
  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
7149
  ];
6460
7150
  }
6461
- const legacyEntries = readFileSync13(legacyPath, "utf-8").split(`
7151
+ const legacyEntries = readFileSync15(legacyPath, "utf-8").split(`
6462
7152
  `).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
6463
7153
  const uncovered = legacyEntries.filter((e) => !covered.has(e) && !configExtra.has(e));
6464
7154
  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 +7183,7 @@ function diagnoseBriefSchemaDrift(config) {
6493
7183
  }
6494
7184
  function diagnoseEnvDeclarations(repoRoot, config) {
6495
7185
  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);
7186
+ const specs = resolved.map((entry2) => entry2.source === "config" ? { ...entry2.spec, run: join14(repoRoot, entry2.spec.run) } : entry2.spec);
6497
7187
  const missing = checksMissingEnvDeclaration(specs);
6498
7188
  const findings = missing.map((name) => info("env", envDeclarationWarning(name)));
6499
7189
  for (const message of lintEnvDeclarations(config?.checks)) {
@@ -6514,11 +7204,11 @@ function diagnoseCheckClassification(config) {
6514
7204
  return findings;
6515
7205
  }
6516
7206
  function diagnoseGlobalConfigChecks() {
6517
- if (!existsSync11(GLOBAL_CONFIG_PATH))
7207
+ if (!existsSync13(GLOBAL_CONFIG_PATH))
6518
7208
  return [];
6519
7209
  let raw;
6520
7210
  try {
6521
- raw = JSON.parse(readFileSync13(GLOBAL_CONFIG_PATH, "utf-8"));
7211
+ raw = JSON.parse(readFileSync15(GLOBAL_CONFIG_PATH, "utf-8"));
6522
7212
  } catch (err) {
6523
7213
  return [info("checks", `${GLOBAL_CONFIG_PATH} is invalid JSON — ${err.message}`)];
6524
7214
  }
@@ -6567,13 +7257,13 @@ function coversWorkflowsDir(pattern) {
6567
7257
  return bare === ".github/workflows" || bare === "/.github/workflows";
6568
7258
  }
6569
7259
  function diagnoseCodeowners(repoRoot) {
6570
- const path = join13(repoRoot, ".github", "CODEOWNERS");
6571
- if (!existsSync11(path)) {
7260
+ const path = join14(repoRoot, ".github", "CODEOWNERS");
7261
+ if (!existsSync13(path)) {
6572
7262
  return info("codeowners", "no .github/CODEOWNERS — vinaya's workflow files have no required-review protection; see `vinaya init`'s printed recommendation.");
6573
7263
  }
6574
7264
  let body;
6575
7265
  try {
6576
- body = readFileSync13(path, "utf-8");
7266
+ body = readFileSync15(path, "utf-8");
6577
7267
  } catch (err) {
6578
7268
  return warn("codeowners", `.github/CODEOWNERS exists but could not be read: ${err.message}`);
6579
7269
  }
@@ -6586,7 +7276,7 @@ function diagnoseVinayaOnPath(agents) {
6586
7276
  return [];
6587
7277
  const names = process.platform === "win32" ? ["vinaya.cmd", "vinaya.exe", "vinaya.bat"] : ["vinaya"];
6588
7278
  const dirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
6589
- const onPath = dirs.some((dir) => names.some((name) => existsSync11(join13(dir, name))));
7279
+ const onPath = dirs.some((dir) => names.some((name) => existsSync13(join14(dir, name))));
6590
7280
  return [
6591
7281
  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
7282
  ];
@@ -6597,21 +7287,38 @@ function diagnosePrincipals(config) {
6597
7287
  }
6598
7288
  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
7289
  }
7290
+ function diagnoseProjectsCoherence(repoRoot, config) {
7291
+ const registryAbs = join14(repoRoot, PROJECTS_REGISTRY_PATH);
7292
+ const registryNames = existsSync13(registryAbs) ? new Set(parseRegistry(readFileSync15(registryAbs, "utf-8")).map((p) => p.name)) : new Set;
7293
+ const configNames = new Set((config?.projects ?? []).map((p) => p.name));
7294
+ if (registryNames.size === 0 && configNames.size === 0)
7295
+ return [];
7296
+ const registryOnly = [...registryNames].filter((n) => !configNames.has(n)).sort();
7297
+ const configOnly = [...configNames].filter((n) => !registryNames.has(n)).sort();
7298
+ const findings = [];
7299
+ if (registryOnly.length > 0) {
7300
+ findings.push(info("projects", `${PROJECTS_REGISTRY_PATH} declares ${registryOnly.join(", ")} with no matching vinaya.config.json "projects" entry.`));
7301
+ }
7302
+ if (configOnly.length > 0) {
7303
+ findings.push(info("projects", `vinaya.config.json "projects" declares ${configOnly.join(", ")} with no matching ${PROJECTS_REGISTRY_PATH} row.`));
7304
+ }
7305
+ return findings;
7306
+ }
6600
7307
  var TEST_INVOCATION_SUBSTRINGS = ["npm test", "npm run test", "bun test", "bunx turbo test"];
6601
7308
  function diagnoseTestCi(repoRoot) {
6602
7309
  let pkg;
6603
7310
  try {
6604
- pkg = JSON.parse(readFileSync13(join13(repoRoot, "package.json"), "utf-8"));
7311
+ pkg = JSON.parse(readFileSync15(join14(repoRoot, "package.json"), "utf-8"));
6605
7312
  } catch {
6606
7313
  return [];
6607
7314
  }
6608
7315
  const testScript = pkg?.scripts?.test;
6609
7316
  if (typeof testScript !== "string" || testScript.trim() === "")
6610
7317
  return [];
6611
- const workflowsDir = join13(repoRoot, ".github/workflows");
6612
- const files = existsSync11(workflowsDir) ? readdirSync6(workflowsDir).filter((name) => statSync2(join13(workflowsDir, name)).isFile()) : [];
7318
+ const workflowsDir = join14(repoRoot, ".github/workflows");
7319
+ const files = existsSync13(workflowsDir) ? readdirSync6(workflowsDir).filter((name) => statSync2(join14(workflowsDir, name)).isFile()) : [];
6613
7320
  const invoked = files.some((name) => {
6614
- const content = readFileSync13(join13(workflowsDir, name), "utf-8");
7321
+ const content = readFileSync15(join14(workflowsDir, name), "utf-8");
6615
7322
  return TEST_INVOCATION_SUBSTRINGS.some((s) => content.includes(s));
6616
7323
  });
6617
7324
  if (invoked)
@@ -6678,6 +7385,7 @@ async function runDoctor(args, deps) {
6678
7385
  findings.push(diagnosePrincipals(configRead.config));
6679
7386
  findings.push(...diagnoseVinayaOnPath(ctx.agents));
6680
7387
  }
7388
+ findings.push(...diagnoseProjectsCoherence(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
6681
7389
  findings.push(...diagnoseBlastRadiusDeprecation(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
6682
7390
  findings.push(...diagnoseBriefSchemaDrift(configRead.kind === "ok" ? configRead.config : null));
6683
7391
  findings.push(...diagnoseEnvDeclarations(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
@@ -6697,13 +7405,13 @@ async function runDoctor(args, deps) {
6697
7405
  return healthy ? 0 : 1;
6698
7406
  }
6699
7407
  async function doctorCommand(args) {
6700
- process.exit(await runDoctor(args, realDeps3()));
7408
+ process.exit(await runDoctor(args, realDeps4()));
6701
7409
  }
6702
7410
 
6703
7411
  // 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() {
7412
+ import { existsSync as existsSync14, readFileSync as readFileSync16 } from "node:fs";
7413
+ import { join as join15 } from "node:path";
7414
+ function realDeps5() {
6707
7415
  return {
6708
7416
  detectRepo: detectGitRepo,
6709
7417
  readHooksPath: readCoreHooksPath,
@@ -6719,12 +7427,12 @@ function parse(args) {
6719
7427
  return { dryRun: args.includes("--dry-run"), yes: args.includes("--yes") };
6720
7428
  }
6721
7429
  function readManifest(repoRoot) {
6722
- const p = join14(repoRoot, CONFIG_PATH);
6723
- if (!existsSync12(p))
7430
+ const p = join15(repoRoot, CONFIG_PATH);
7431
+ if (!existsSync14(p))
6724
7432
  return { kind: "none" };
6725
7433
  let raw;
6726
7434
  try {
6727
- raw = JSON.parse(readFileSync14(p, "utf-8"));
7435
+ raw = JSON.parse(readFileSync16(p, "utf-8"));
6728
7436
  } catch (err) {
6729
7437
  return { kind: "orphan", reason: `vinaya.config.json is not valid JSON: ${err.message}` };
6730
7438
  }
@@ -6809,112 +7517,14 @@ Vinaya ejected.
6809
7517
  return 0;
6810
7518
  }
6811
7519
  async function ejectCommand(args) {
6812
- const code = await runEject(args, realDeps4());
7520
+ const code = await runEject(args, realDeps5());
6813
7521
  process.exit(code);
6814
7522
  }
6815
7523
 
6816
7524
  // src/commands/init.ts
6817
- import { existsSync as existsSync14, readFileSync as readFileSync16, writeFileSync as writeFileSync5 } from "node:fs";
7525
+ import { existsSync as existsSync15, readFileSync as readFileSync17, writeFileSync as writeFileSync5 } from "node:fs";
6818
7526
  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() {
7527
+ function realDeps6() {
6918
7528
  return {
6919
7529
  detectRepo: detectGitRepo,
6920
7530
  checkGhAuth,
@@ -6954,17 +7564,17 @@ function parseAgentsFlag(args) {
6954
7564
  var PRODUCT_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
6955
7565
  function readManifest2(repoRoot) {
6956
7566
  const p = join16(repoRoot, CONFIG_PATH);
6957
- if (!existsSync14(p))
7567
+ if (!existsSync15(p))
6958
7568
  return null;
6959
7569
  try {
6960
- return VinayaConfigSchema.parse(JSON.parse(readFileSync16(p, "utf-8"))).managed ?? null;
7570
+ return VinayaConfigSchema.parse(JSON.parse(readFileSync17(p, "utf-8"))).managed ?? null;
6961
7571
  } catch {
6962
7572
  return null;
6963
7573
  }
6964
7574
  }
6965
7575
  function writeManifest(repoRoot, manifest) {
6966
7576
  const configAbs = join16(repoRoot, CONFIG_PATH);
6967
- const seed = JSON.parse(readFileSync16(configAbs, "utf-8"));
7577
+ const seed = JSON.parse(readFileSync17(configAbs, "utf-8"));
6968
7578
  writeFileSync5(configAbs, `${JSON.stringify({ ...seed, managed: manifest }, null, 2)}
6969
7579
  `, "utf-8");
6970
7580
  }
@@ -7072,6 +7682,7 @@ async function runInitProduct(args, deps) {
7072
7682
  return 1;
7073
7683
  }
7074
7684
  const registryPlan = planRegistryRow(repo.repoRoot, name, productPath, specsPath);
7685
+ const configPlan = planConfigProjectEntry(repo.repoRoot, { name, path: productPath });
7075
7686
  process.stdout.write(`vinaya init product ${name} — the full diff:
7076
7687
 
7077
7688
  `);
@@ -7079,6 +7690,11 @@ async function runInitProduct(args, deps) {
7079
7690
  `);
7080
7691
  process.stdout.write(`${renderRegistryRowDiffLine(registryPlan)}
7081
7692
 
7693
+ `);
7694
+ process.stdout.write(`── Config ────────────────────────────────────────
7695
+ `);
7696
+ process.stdout.write(`${renderConfigProjectEntryDiffLine(configPlan)}
7697
+
7082
7698
  `);
7083
7699
  if (dryRun) {
7084
7700
  process.stdout.write(`--dry-run: nothing was written.
@@ -7094,25 +7710,26 @@ async function runInitProduct(args, deps) {
7094
7710
  }
7095
7711
  }
7096
7712
  applyRegistryRow(repo.repoRoot, registryPlan, name, productPath, specsPath);
7713
+ applyConfigProjectEntry(repo.repoRoot, configPlan);
7097
7714
  process.stdout.write(`
7098
7715
  Governed product area '${name}' scaffolded.
7099
7716
  `);
7100
7717
  return 0;
7101
7718
  }
7102
7719
  async function initCommand(args) {
7103
- process.exit(await runInit(args, realDeps5()));
7720
+ process.exit(await runInit(args, realDeps6()));
7104
7721
  }
7105
7722
  async function initProductCommand(args) {
7106
- process.exit(await runInitProduct(args, realDeps5()));
7723
+ process.exit(await runInitProduct(args, realDeps6()));
7107
7724
  }
7108
7725
 
7109
7726
  // src/commands/issue.ts
7110
- import { execFileSync as execFileSync10 } from "node:child_process";
7727
+ import { execFileSync as execFileSync11 } from "node:child_process";
7111
7728
 
7112
7729
  // src/lib/forge-write.ts
7113
- import { execFileSync as execFileSync9 } from "node:child_process";
7114
- 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";
7730
+ import { execFileSync as execFileSync10 } from "node:child_process";
7731
+ import { existsSync as existsSync16, mkdtempSync, readdirSync as readdirSync7, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
7732
+ import { readFileSync as readFileSync18 } from "node:fs";
7116
7733
  import { tmpdir } from "node:os";
7117
7734
  import { join as join17 } from "node:path";
7118
7735
  class ForgeArgError extends Error {
@@ -7124,13 +7741,13 @@ function locateBody(args) {
7124
7741
  const p = args[i + 1];
7125
7742
  if (!p)
7126
7743
  throw new ForgeArgError("`--body-file` was given with no path.");
7127
- return { body: readFileSync17(p, "utf8"), source: { kind: "file", argIndex: i + 1, inlineForm: false } };
7744
+ return { body: readFileSync18(p, "utf8"), source: { kind: "file", argIndex: i + 1, inlineForm: false } };
7128
7745
  }
7129
7746
  if (a.startsWith("--body-file=")) {
7130
7747
  const p = a.slice("--body-file=".length);
7131
7748
  if (!p)
7132
7749
  throw new ForgeArgError("`--body-file=` was given with no path.");
7133
- return { body: readFileSync17(p, "utf8"), source: { kind: "file", argIndex: i, inlineForm: true } };
7750
+ return { body: readFileSync18(p, "utf8"), source: { kind: "file", argIndex: i, inlineForm: true } };
7134
7751
  }
7135
7752
  if (a === "--body" || a === "-b") {
7136
7753
  const v = args[i + 1];
@@ -7204,7 +7821,7 @@ function ensureTrancheLabelExists(slug) {
7204
7821
  const name = trancheLabel(slug);
7205
7822
  let existing;
7206
7823
  try {
7207
- const out = execFileSync9("gh", ["label", "list", "--json", "name", "--limit", "200"], {
7824
+ const out = execFileSync10("gh", ["label", "list", "--json", "name", "--limit", "200"], {
7208
7825
  encoding: "utf8",
7209
7826
  stdio: ["ignore", "pipe", "pipe"]
7210
7827
  });
@@ -7217,7 +7834,7 @@ function ensureTrancheLabelExists(slug) {
7217
7834
  if (existing.some((l) => l.name === name))
7218
7835
  return;
7219
7836
  try {
7220
- execFileSync9("gh", ["label", "create", name, "--color", TRANCHE_LABEL_COLOR, "--description", `Tranche: ${slug}`], {
7837
+ execFileSync10("gh", ["label", "create", name, "--color", TRANCHE_LABEL_COLOR, "--description", `Tranche: ${slug}`], {
7221
7838
  encoding: "utf8",
7222
7839
  stdio: ["ignore", "pipe", "pipe"]
7223
7840
  });
@@ -7227,6 +7844,24 @@ function ensureTrancheLabelExists(slug) {
7227
7844
  ]);
7228
7845
  }
7229
7846
  }
7847
+ function resolveMilestoneAttachArgs(ghArgs, labels) {
7848
+ if (!isTaskIssueLabelSet(labels))
7849
+ return ghArgs;
7850
+ if (hasExplicitMilestoneFlag(ghArgs))
7851
+ return ghArgs;
7852
+ const slug = findTrancheSlug(labels);
7853
+ if (!slug)
7854
+ return ghArgs;
7855
+ let target;
7856
+ try {
7857
+ target = findMilestoneAttachTargetForSlug("{owner}", "{repo}", slug);
7858
+ } catch {
7859
+ process.stderr.write(`vinaya: milestone lookup for '${slug}' failed (\`gh api\`) — creating without --milestone.
7860
+ `);
7861
+ return ghArgs;
7862
+ }
7863
+ return target ? [...ghArgs, "--milestone", target.title] : ghArgs;
7864
+ }
7230
7865
  function resolveSections(kind, retryCommand) {
7231
7866
  const result = loadConfigChecked();
7232
7867
  if (!result.ok) {
@@ -7351,7 +7986,7 @@ function validateForgeWrite(input) {
7351
7986
  }
7352
7987
  function git2(args) {
7353
7988
  try {
7354
- return execFileSync9("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
7989
+ return execFileSync10("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
7355
7990
  } catch {
7356
7991
  return "";
7357
7992
  }
@@ -7362,7 +7997,7 @@ function repoRoot() {
7362
7997
  function readWorkspaceGlobs(root) {
7363
7998
  const fromPackageJson = () => {
7364
7999
  try {
7365
- const pkg = JSON.parse(readFileSync17(join17(root, "package.json"), "utf8"));
8000
+ const pkg = JSON.parse(readFileSync18(join17(root, "package.json"), "utf8"));
7366
8001
  return Array.isArray(pkg.workspaces) ? pkg.workspaces.filter((w) => typeof w === "string") : [];
7367
8002
  } catch {
7368
8003
  return [];
@@ -7370,7 +8005,7 @@ function readWorkspaceGlobs(root) {
7370
8005
  };
7371
8006
  const fromPnpmWorkspaceYaml = () => {
7372
8007
  try {
7373
- return parsePnpmWorkspaceYaml(readFileSync17(join17(root, "pnpm-workspace.yaml"), "utf8"));
8008
+ return parsePnpmWorkspaceYaml(readFileSync18(join17(root, "pnpm-workspace.yaml"), "utf8"));
7374
8009
  } catch {
7375
8010
  return [];
7376
8011
  }
@@ -7386,7 +8021,7 @@ function listWorkspaceChildDirs(dir, root) {
7386
8021
  }
7387
8022
  function readConfigExtraDomains(root) {
7388
8023
  try {
7389
- const parsed = VinayaConfigSchema.safeParse(JSON.parse(readFileSync17(join17(root, "vinaya.config.json"), "utf8")));
8024
+ const parsed = VinayaConfigSchema.safeParse(JSON.parse(readFileSync18(join17(root, "vinaya.config.json"), "utf8")));
7390
8025
  return parsed.success ? parsed.data.blastRadius?.extraDomains ?? [] : [];
7391
8026
  } catch {
7392
8027
  return [];
@@ -7396,7 +8031,7 @@ function readSharedPackages(root = repoRoot()) {
7396
8031
  if (!root)
7397
8032
  return [];
7398
8033
  const derived = deriveWorkspacePackageDomains(readWorkspaceGlobs(root), (dir) => listWorkspaceChildDirs(dir, root));
7399
- const defaults = deriveBuiltinCrossCuttingDefaults((p) => existsSync15(join17(root, p)));
8034
+ const defaults = deriveBuiltinCrossCuttingDefaults((p) => existsSync16(join17(root, p)));
7400
8035
  const configExtra = readConfigExtraDomains(root);
7401
8036
  return [...new Set([...derived, ...defaults, ...configExtra])];
7402
8037
  }
@@ -7404,7 +8039,7 @@ function readProjectPaths(root = repoRoot()) {
7404
8039
  if (!root)
7405
8040
  return [];
7406
8041
  try {
7407
- return parseRegistry(readFileSync17(join17(root, ".vinaya/projects.md"), "utf8"));
8042
+ return parseRegistry(readFileSync18(join17(root, ".vinaya/projects.md"), "utf8"));
7408
8043
  } catch {
7409
8044
  return [];
7410
8045
  }
@@ -7446,7 +8081,7 @@ function locateBodyOrRefuse(ghArgs, retryCommand) {
7446
8081
  function fetchForgeLabels(issueRef) {
7447
8082
  let out;
7448
8083
  try {
7449
- out = execFileSync10("gh", ["issue", "view", issueRef, "--json", "labels"], {
8084
+ out = execFileSync11("gh", ["issue", "view", issueRef, "--json", "labels"], {
7450
8085
  encoding: "utf8",
7451
8086
  stdio: ["ignore", "pipe", "pipe"]
7452
8087
  });
@@ -7474,7 +8109,7 @@ function reportPass(json, command) {
7474
8109
  function runGhWrite(ghCmd, ghArgs, bodyResult, json) {
7475
8110
  const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
7476
8111
  try {
7477
- const out = execFileSync10("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
8112
+ const out = execFileSync11("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
7478
8113
  const url = out.trim();
7479
8114
  if (json)
7480
8115
  printJson({ validated: true, written: true, url });
@@ -7529,7 +8164,7 @@ function issueCreateCommand(args) {
7529
8164
  const slugToEnsure = findTrancheSlug(labels);
7530
8165
  if (slugToEnsure)
7531
8166
  ensureTrancheLabelExists(slugToEnsure);
7532
- runGhWrite(["issue", "create"], ghArgs, bodyResult, json);
8167
+ runGhWrite(["issue", "create"], resolveMilestoneAttachArgs(ghArgs, labels), bodyResult, json);
7533
8168
  }
7534
8169
  function issueEditCommand(args) {
7535
8170
  const json = args.includes("--json");
@@ -7560,12 +8195,13 @@ function issueEditCommand(args) {
7560
8195
  }
7561
8196
 
7562
8197
  // src/commands/milestone.ts
7563
- import { execFileSync as execFileSync11 } from "node:child_process";
8198
+ import { execFileSync as execFileSync12 } from "node:child_process";
7564
8199
  var RETRY_CREATE2 = "vinaya milestone create --title <title> --body-file <path>";
7565
8200
  var RETRY_ADOPT = "vinaya milestone adopt --target <title> --slug <slug> [--slug <slug> ...]";
7566
8201
  var RETRY_EDIT2 = "vinaya milestone edit <n> --body-file <path>";
8202
+ var RETRY_CLOSE = "vinaya milestone close --slug <slug>";
7567
8203
  function sh3(args, input) {
7568
- return execFileSync11(args[0], args.slice(1), {
8204
+ return execFileSync12(args[0], args.slice(1), {
7569
8205
  encoding: "utf8",
7570
8206
  input,
7571
8207
  env: process.env,
@@ -7723,18 +8359,18 @@ function extractSlugs(args) {
7723
8359
  }
7724
8360
  return slugs;
7725
8361
  }
7726
- function ghJsonOrRefuse(args, what) {
8362
+ function ghJsonOrRefuse(args, what, retryCommand) {
7727
8363
  try {
7728
8364
  return shJson2(args);
7729
8365
  } catch (e) {
7730
8366
  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}\`.`)
8367
+ makeCheckError("forge-fetch", `could not fetch ${what} from the forge: ${ghErrorDetail(e)}`, `Check \`gh auth status\` and network, then re-run \`${retryCommand}\`.`)
7732
8368
  ]);
7733
8369
  }
7734
8370
  }
7735
8371
  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");
8372
+ const labels = ghJsonOrRefuse(["gh", "api", `repos/${repoFlag}/labels?per_page=100`], "this repo's labels", RETRY_ADOPT);
8373
+ const milestones = ghJsonOrRefuse(["gh", "api", `repos/${repoFlag}/milestones?state=all&per_page=100`], "this repo's Milestones", RETRY_ADOPT);
7738
8374
  const targetMilestone = milestones.find((m) => m.title === target);
7739
8375
  const targetFacts = {
7740
8376
  title: target,
@@ -7757,7 +8393,7 @@ function fetchAdoptFacts(repoFlag, target, slugs) {
7757
8393
  "number,state,milestone",
7758
8394
  "--limit",
7759
8395
  "200"
7760
- ], `Issues for \`${trancheLabel(slug)}\``) : [];
8396
+ ], `Issues for \`${trancheLabel(slug)}\``, RETRY_ADOPT) : [];
7761
8397
  const elsewhere = new Set;
7762
8398
  for (const issue of issues) {
7763
8399
  const title = issue.milestone?.title;
@@ -7818,41 +8454,134 @@ async function milestoneAdoptCommand(args) {
7818
8454
  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
8455
  }
7820
8456
  }
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
- }
8457
+ const legacy = milestones.find((m) => m.title === slugFact.slug && m.title !== target);
8458
+ let closedMilestone = null;
8459
+ if (legacy) {
8460
+ closedMilestone = legacy.number;
8461
+ if (legacy.state !== "closed") {
8462
+ try {
8463
+ sh3(["gh", "api", "-X", "PATCH", `repos/${repoFlag}/milestones/${legacy.number}`, "-f", "state=closed"]);
8464
+ } catch (e) {
8465
+ 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)"}.`);
8466
+ }
8467
+ }
8468
+ }
8469
+ adopted.push({ slug: slugFact.slug, issueCount: slugFact.issueNumbers.length, closedMilestone });
8470
+ }
8471
+ if (json)
8472
+ printJson({ validated: true, written: true, target, adopted });
8473
+ else {
8474
+ for (const a of adopted) {
8475
+ const closedNote = a.closedMilestone ? ` (old Milestone #${a.closedMilestone} closed)` : "";
8476
+ process.stdout.write(`${a.slug}: ${a.issueCount} Issue(s) → "${target}"${closedNote}
8477
+ `);
8478
+ }
8479
+ }
8480
+ }
8481
+ function extractSlug(args) {
8482
+ for (let i = 0;i < args.length; i++) {
8483
+ const a = args[i];
8484
+ if (a === "--slug")
8485
+ return args[i + 1] ?? null;
8486
+ if (a.startsWith("--slug="))
8487
+ return a.slice("--slug=".length);
8488
+ }
8489
+ return null;
8490
+ }
8491
+ async function milestoneCloseCommand(args) {
8492
+ const json = args.includes("--json");
8493
+ const validateOnly = args.includes("--validate-only");
8494
+ const rest = args.filter((a) => a !== "--json" && a !== "--validate-only");
8495
+ const slug = extractSlug(rest);
8496
+ if (!slug) {
8497
+ refuse([
8498
+ 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}\`.`)
8499
+ ]);
8500
+ }
8501
+ const repoFlag = await resolveRepoFlagOrRefuse(RETRY_CLOSE);
8502
+ const milestones = ghJsonOrRefuse(["gh", "api", `repos/${repoFlag}/milestones?state=all&per_page=100`], "this repo's Milestones", RETRY_CLOSE);
8503
+ const target = resolveMilestoneAttachTarget(milestones, slug);
8504
+ if (!target) {
8505
+ refuse([
8506
+ 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}\`.`)
8507
+ ]);
8508
+ }
8509
+ const labeledIssues = ghJsonOrRefuse([
8510
+ "gh",
8511
+ "issue",
8512
+ "list",
8513
+ "-R",
8514
+ repoFlag,
8515
+ "--label",
8516
+ trancheLabel(slug),
8517
+ "--state",
8518
+ "all",
8519
+ "--json",
8520
+ "number,state,milestone",
8521
+ "--limit",
8522
+ "200"
8523
+ ], `Issues for \`${trancheLabel(slug)}\``, RETRY_CLOSE);
8524
+ const attachedIssues = ghJsonOrRefuse([
8525
+ "gh",
8526
+ "issue",
8527
+ "list",
8528
+ "-R",
8529
+ repoFlag,
8530
+ "--milestone",
8531
+ target.title,
8532
+ "--state",
8533
+ "all",
8534
+ "--json",
8535
+ "number,state,milestone",
8536
+ "--limit",
8537
+ "200"
8538
+ ], `Issues attached to Milestone "${target.title}"`, RETRY_CLOSE);
8539
+ const report = checkMilestoneAttachment(labeledIssues.map((i) => i.number), attachedIssues.map((i) => i.number));
8540
+ if (report.status === "mismatch") {
8541
+ const errors = [];
8542
+ if (report.unattached.length > 0) {
8543
+ 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}\`.`));
8544
+ }
8545
+ if (report.foreign.length > 0) {
8546
+ 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}\`.`));
7832
8547
  }
7833
- adopted.push({ slug: slugFact.slug, issueCount: slugFact.issueNumbers.length, closedMilestone });
8548
+ refuse(errors);
7834
8549
  }
7835
- 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}
8550
+ if (validateOnly) {
8551
+ if (json)
8552
+ printJson({ validated: true, written: false, command: "milestone close", number: target.number });
8553
+ else {
8554
+ process.stdout.write(`✓ Milestone "${target.title}" (#${target.number}) attachment verified — nothing written (--validate-only).
7841
8555
  `);
7842
8556
  }
8557
+ return;
8558
+ }
8559
+ let out;
8560
+ try {
8561
+ out = sh3(["gh", "api", "-X", "PATCH", `repos/${repoFlag}/milestones/${target.number}`, "-f", "state=closed"]);
8562
+ } catch (e) {
8563
+ refuse([
8564
+ makeCheckError("forge-fetch", `\`gh api repos/${repoFlag}/milestones/${target.number}\` failed: ${ghErrorDetail(e)}`, `Check \`gh auth status\` and network, then re-run \`${RETRY_CLOSE}\`.`)
8565
+ ]);
7843
8566
  }
8567
+ const closed = JSON.parse(out);
8568
+ if (json)
8569
+ printJson({ validated: true, written: true, number: closed.number, url: closed.html_url });
8570
+ else
8571
+ process.stdout.write(`${closed.html_url}
8572
+ `);
7844
8573
  }
7845
8574
 
7846
8575
  // 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";
8576
+ import { chmodSync as chmodSync2, existsSync as existsSync17, mkdirSync as mkdirSync4, readFileSync as readFileSync19, writeFileSync as writeFileSync7 } from "node:fs";
7848
8577
  import { join as join18 } from "node:path";
7849
8578
  var TEMPLATE_PATH = join18(packageRoot(import.meta.url), "templates", "custom-check.template.ts");
7850
8579
  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)";
8580
+ var USAGE2 = "Usage: vinaya new check <yourname>/<id> (both segments: lowercase letters, digits, hyphens; e.g. myteam/vocab-check)";
7852
8581
  function newCheckCommand(args) {
7853
8582
  const name = args[0];
7854
8583
  if (!name) {
7855
- console.error(USAGE);
8584
+ console.error(USAGE2);
7856
8585
  process.exit(2);
7857
8586
  }
7858
8587
  if (coreCheckRegistry().some((spec) => spec.name === name)) {
@@ -7860,20 +8589,20 @@ function newCheckCommand(args) {
7860
8589
  process.exit(2);
7861
8590
  }
7862
8591
  if (!isValidNamespacedKey(name)) {
7863
- console.error(`Refusing: "${name}" is not a valid check id. ${USAGE.slice("Usage: ".length)}
8592
+ console.error(`Refusing: "${name}" is not a valid check id. ${USAGE2.slice("Usage: ".length)}
7864
8593
  \`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
8594
  process.exit(2);
7866
8595
  }
7867
8596
  const scriptsDir = join18(process.cwd(), CHECKS_DIR);
7868
- if (!existsSync16(scriptsDir))
8597
+ if (!existsSync17(scriptsDir))
7869
8598
  mkdirSync4(scriptsDir, { recursive: true });
7870
8599
  const fileStem = name.slice(name.indexOf("/") + 1);
7871
8600
  const targetPath = join18(scriptsDir, `${fileStem}.ts`);
7872
- if (existsSync16(targetPath)) {
8601
+ if (existsSync17(targetPath)) {
7873
8602
  console.error(`Error: ${targetPath} already exists.`);
7874
8603
  process.exit(1);
7875
8604
  }
7876
- const template = readFileSync18(TEMPLATE_PATH, "utf-8");
8605
+ const template = readFileSync19(TEMPLATE_PATH, "utf-8");
7877
8606
  const contents = template.split("{{CHECK_NAME}}").join(name);
7878
8607
  writeFileSync7(targetPath, contents, "utf-8");
7879
8608
  chmodSync2(targetPath, 493);
@@ -7887,32 +8616,32 @@ ${registration}
7887
8616
  }
7888
8617
 
7889
8618
  // 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";
8619
+ import { chmodSync as chmodSync3, existsSync as existsSync18, mkdirSync as mkdirSync5, readFileSync as readFileSync20, writeFileSync as writeFileSync8 } from "node:fs";
7891
8620
  import { join as join19 } from "node:path";
7892
8621
  var TEMPLATE_PATH2 = join19(packageRoot(import.meta.url), "templates", "noop-check.template.ts");
7893
8622
  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)";
8623
+ 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
8624
  function newNoopCheckCommand(args) {
7896
8625
  const name = args[0];
7897
8626
  if (!name) {
7898
- console.error(USAGE2);
8627
+ console.error(USAGE3);
7899
8628
  process.exit(2);
7900
8629
  }
7901
8630
  const coreSpec = coreCheckRegistry().find((spec) => spec.name === name);
7902
8631
  if (!coreSpec) {
7903
- console.error(`Refusing: "${name}" is not a core check id. ${USAGE2}
8632
+ console.error(`Refusing: "${name}" is not a core check id. ${USAGE3}
7904
8633
  \`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
8634
  process.exit(2);
7906
8635
  }
7907
8636
  const checksDir = join19(process.cwd(), CHECKS_DIR2);
7908
- if (!existsSync17(checksDir))
8637
+ if (!existsSync18(checksDir))
7909
8638
  mkdirSync5(checksDir, { recursive: true });
7910
8639
  const targetPath = join19(checksDir, `${name}.ts`);
7911
- if (existsSync17(targetPath)) {
8640
+ if (existsSync18(targetPath)) {
7912
8641
  console.error(`Error: ${targetPath} already exists.`);
7913
8642
  process.exit(1);
7914
8643
  }
7915
- const template = readFileSync19(TEMPLATE_PATH2, "utf-8");
8644
+ const template = readFileSync20(TEMPLATE_PATH2, "utf-8");
7916
8645
  const contents = template.split("{{CHECK_NAME}}").join(name);
7917
8646
  writeFileSync8(targetPath, contents, "utf-8");
7918
8647
  chmodSync3(targetPath, 493);
@@ -7934,32 +8663,32 @@ ${registration}
7934
8663
  }
7935
8664
 
7936
8665
  // src/commands/new-role.ts
7937
- import { existsSync as existsSync18, mkdirSync as mkdirSync6, readFileSync as readFileSync20, writeFileSync as writeFileSync9 } from "node:fs";
8666
+ import { existsSync as existsSync19, mkdirSync as mkdirSync6, readFileSync as readFileSync21, writeFileSync as writeFileSync9 } from "node:fs";
7938
8667
  import { join as join20 } from "node:path";
7939
8668
  var TEMPLATE_PATH3 = join20(packageRoot(import.meta.url), "templates", "role.template.md");
7940
8669
  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)";
8670
+ var USAGE4 = "Usage: vinaya new role <yourname>/<id> (both segments: lowercase letters, digits, hyphens; e.g. acme/qa-lead)";
7942
8671
  function newRoleCommand(args) {
7943
8672
  const name = args[0];
7944
8673
  if (!name) {
7945
- console.error(USAGE3);
8674
+ console.error(USAGE4);
7946
8675
  process.exit(2);
7947
8676
  }
7948
8677
  if (!isValidNamespacedKey(name)) {
7949
- console.error(`Refusing: "${name}" is not a valid role key. ${USAGE3.slice("Usage: ".length)}
8678
+ console.error(`Refusing: "${name}" is not a valid role key. ${USAGE4.slice("Usage: ".length)}
7950
8679
  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
8680
  process.exit(2);
7952
8681
  }
7953
8682
  const rolesDir = join20(process.cwd(), ROLES_DIR);
7954
- if (!existsSync18(rolesDir))
8683
+ if (!existsSync19(rolesDir))
7955
8684
  mkdirSync6(rolesDir, { recursive: true });
7956
8685
  const roleId = name.slice(name.indexOf("/") + 1);
7957
8686
  const targetPath = join20(rolesDir, `${roleId}.md`);
7958
- if (existsSync18(targetPath)) {
8687
+ if (existsSync19(targetPath)) {
7959
8688
  console.error(`Error: ${targetPath} already exists.`);
7960
8689
  process.exit(1);
7961
8690
  }
7962
- const template = readFileSync20(TEMPLATE_PATH3, "utf-8");
8691
+ const template = readFileSync21(TEMPLATE_PATH3, "utf-8");
7963
8692
  const contents = template.split("{{ROLE_ID}}").join(roleId);
7964
8693
  writeFileSync9(targetPath, contents, "utf-8");
7965
8694
  const relPath = join20(ROLES_DIR, `${roleId}.md`);
@@ -7972,7 +8701,7 @@ ${registration}
7972
8701
  }
7973
8702
 
7974
8703
  // src/commands/pr.ts
7975
- import { execFileSync as execFileSync12 } from "node:child_process";
8704
+ import { execFileSync as execFileSync13 } from "node:child_process";
7976
8705
 
7977
8706
  // src/lib/numstat.ts
7978
8707
  function summariseNumstat(numstat) {
@@ -8033,6 +8762,16 @@ class ScanContext {
8033
8762
  return rawHasPair === (anchoredRegionBounds(this.masked, field) !== null);
8034
8763
  }
8035
8764
  }
8765
+ function resolveAnchoredRegion(ctx, field) {
8766
+ const bounds = anchoredRegionBounds(ctx.masked, field);
8767
+ if (bounds === null) {
8768
+ return anchoredRegionBounds(ctx.normalised, field) !== null ? "hidden" : null;
8769
+ }
8770
+ return {
8771
+ region: ctx.normalised.slice(bounds.innerStart, bounds.innerEnd),
8772
+ maskedRegion: ctx.masked.slice(bounds.innerStart, bounds.innerEnd)
8773
+ };
8774
+ }
8036
8775
 
8037
8776
  // src/checks/body-bare-digits-logic.ts
8038
8777
  function blankRange(text, start, end) {
@@ -8254,7 +8993,7 @@ var RETRY_CREATE3 = "vinaya pr create --validate-only …";
8254
8993
  var RETRY_EDIT3 = "vinaya pr edit <n> --validate-only …";
8255
8994
  function git3(args) {
8256
8995
  try {
8257
- return execFileSync12("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
8996
+ return execFileSync13("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
8258
8997
  } catch {
8259
8998
  return "";
8260
8999
  }
@@ -8288,7 +9027,7 @@ function reportPass2(json, command) {
8288
9027
  function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
8289
9028
  const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
8290
9029
  try {
8291
- const out = execFileSync12("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
9030
+ const out = execFileSync13("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
8292
9031
  const url = out.trim();
8293
9032
  if (json)
8294
9033
  printJson({ validated: true, written: true, url });
@@ -8302,7 +9041,7 @@ function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
8302
9041
  function fetchPrForgeContext(prRef) {
8303
9042
  let viewOut;
8304
9043
  try {
8305
- viewOut = execFileSync12("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
9044
+ viewOut = execFileSync13("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
8306
9045
  encoding: "utf8",
8307
9046
  stdio: ["ignore", "pipe", "pipe"]
8308
9047
  });
@@ -8413,14 +9152,14 @@ function prEditCommand(args) {
8413
9152
  }
8414
9153
 
8415
9154
  // 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";
9155
+ import { execFileSync as execFileSync14, spawnSync as spawnSync2 } from "node:child_process";
9156
+ import { existsSync as existsSync20, readFileSync as readFileSync22, writeFileSync as writeFileSync10 } from "node:fs";
8418
9157
  import { join as join21 } from "node:path";
8419
9158
  var EVIDENCE_START = "<!-- AEG:EVIDENCE:START -->";
8420
9159
  var EVIDENCE_END = "<!-- AEG:EVIDENCE:END -->";
8421
9160
  function git4(args) {
8422
9161
  try {
8423
- return execFileSync13("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
9162
+ return execFileSync14("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
8424
9163
  } catch {
8425
9164
  return "";
8426
9165
  }
@@ -8438,7 +9177,7 @@ class GitCommandError extends Error {
8438
9177
  }
8439
9178
  function gitStrict(args) {
8440
9179
  try {
8441
- return execFileSync13("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
9180
+ return execFileSync14("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
8442
9181
  } catch (err) {
8443
9182
  const stderr = err.stderr;
8444
9183
  throw new GitCommandError(args, String(stderr ?? err.message).trim() || "non-zero exit");
@@ -8474,7 +9213,7 @@ function computeGroupA() {
8474
9213
  function resolveSelfEntry() {
8475
9214
  const root = packageRoot(import.meta.url);
8476
9215
  const dist = join21(root, "dist", "index.js");
8477
- return existsSync19(dist) ? dist : join21(root, "src", "index.ts");
9216
+ return existsSync20(dist) ? dist : join21(root, "src", "index.ts");
8478
9217
  }
8479
9218
  var FAILING_STATUSES = new Set(["fail", "error", "timeout"]);
8480
9219
  function anyGateFailed(outcomes) {
@@ -8567,6 +9306,84 @@ ${EVIDENCE_END}`;
8567
9306
  ${full}
8568
9307
  `;
8569
9308
  }
9309
+ var TOKENS_START = "<!-- AEG:TOKENS:START -->";
9310
+ var TOKENS_END = "<!-- AEG:TOKENS:END -->";
9311
+ var TOKEN_REPORT_HEADING = /^#{1,6}\s*token report\s*$/i;
9312
+ var HEADING_LINE = /^#{1,6}\s/;
9313
+ var TOKEN_TABLE_HEADER = "| Phase | Role | Agent/Model | Tokens in | Tokens out | Cost | Date |";
9314
+ var TOKEN_TABLE_SEPARATOR = "|---|---|---|---|---|---|---|";
9315
+ function tokensBlockLineBounds(maskedLines) {
9316
+ const startIdx = maskedLines.findIndex((l) => l.trim() === TOKENS_START);
9317
+ if (startIdx === -1)
9318
+ return null;
9319
+ for (let i = startIdx + 1;i < maskedLines.length; i++) {
9320
+ if (maskedLines[i].trim() === TOKENS_END)
9321
+ return { startIdx, endIdx: i };
9322
+ }
9323
+ return null;
9324
+ }
9325
+ function writeTokensBlock(body, addition) {
9326
+ const rawLines = body.split(`
9327
+ `);
9328
+ const maskedLines = maskCode(body).split(`
9329
+ `);
9330
+ const additionLines = addition.split(`
9331
+ `);
9332
+ const existing = tokensBlockLineBounds(maskedLines);
9333
+ if (existing) {
9334
+ const before = rawLines.slice(0, existing.endIdx);
9335
+ const after = rawLines.slice(existing.endIdx);
9336
+ return [...before, ...additionLines, ...after].join(`
9337
+ `);
9338
+ }
9339
+ const freshBlock = [TOKENS_START, TOKEN_TABLE_HEADER, TOKEN_TABLE_SEPARATOR, ...additionLines, TOKENS_END];
9340
+ const headingIdx = rawLines.findIndex((l) => TOKEN_REPORT_HEADING.test(l));
9341
+ if (headingIdx !== -1) {
9342
+ let sectionEnd = rawLines.length;
9343
+ for (let i = headingIdx + 1;i < rawLines.length; i++) {
9344
+ if (HEADING_LINE.test(rawLines[i])) {
9345
+ sectionEnd = i;
9346
+ break;
9347
+ }
9348
+ }
9349
+ const before = rawLines.slice(0, headingIdx + 1);
9350
+ const after = rawLines.slice(sectionEnd);
9351
+ return [...before, "", ...freshBlock, "", ...after].join(`
9352
+ `);
9353
+ }
9354
+ const sep4 = body.length === 0 || body.endsWith(`
9355
+ `) ? [] : [""];
9356
+ return [...rawLines, ...sep4, "", "## Token report", "", ...freshBlock, ""].join(`
9357
+ `);
9358
+ }
9359
+ function derivePhase() {
9360
+ const branch = git4(["rev-parse", "--abbrev-ref", "HEAD"]);
9361
+ const m = /^task\/[^/]+\/(.+)$/.exec(branch);
9362
+ const taskId = m ? m[1] : branch || "unknown";
9363
+ return `${taskId}: develop`;
9364
+ }
9365
+ function isoToday() {
9366
+ return new Date().toISOString().slice(0, 10);
9367
+ }
9368
+ function collectTokensAddition(opts) {
9369
+ const capability = resolveMeteringCapability(realDeps(), opts.transcriptPath);
9370
+ if (!capability.capable) {
9371
+ return formatTokenReportRow({
9372
+ phase: opts.phase,
9373
+ role: opts.role,
9374
+ summary: null,
9375
+ modelOverride: `— (${capability.reason})`,
9376
+ date: opts.date
9377
+ });
9378
+ }
9379
+ return formatTokenReportRow({
9380
+ phase: opts.phase,
9381
+ role: opts.role,
9382
+ summary: capability.summary,
9383
+ modelOverride: opts.modelOverride,
9384
+ date: opts.date
9385
+ });
9386
+ }
8570
9387
  async function buildReport(opts = {}) {
8571
9388
  const groupA = opts.groupA ?? computeGroupA();
8572
9389
  const gateRunner = opts.gateRunner ?? runRealGates;
@@ -8580,8 +9397,16 @@ ${EVIDENCE_END}`;
8580
9397
  async function prReportCommand(args) {
8581
9398
  const writeIdx = args.indexOf("--write");
8582
9399
  const writePath = writeIdx !== -1 ? args[writeIdx + 1] : undefined;
9400
+ const transcriptIdx = args.indexOf("--transcript");
9401
+ const transcriptPath = transcriptIdx !== -1 ? args[transcriptIdx + 1] : undefined;
9402
+ const phaseIdx = args.indexOf("--phase");
9403
+ const phaseOverride = phaseIdx !== -1 ? args[phaseIdx + 1] : undefined;
9404
+ const roleIdx = args.indexOf("--role");
9405
+ const roleOverride = roleIdx !== -1 ? args[roleIdx + 1] : undefined;
9406
+ const modelIdx = args.indexOf("--model");
9407
+ const modelOverride = modelIdx !== -1 ? args[modelIdx + 1] : undefined;
8583
9408
  if (writeIdx !== -1 && !writePath) {
8584
- console.error("Usage: vinaya pr report [--write <body-file>]");
9409
+ console.error("Usage: vinaya pr report [--write <body-file>] [--phase <phase>] [--role <role>] " + "[--model <id>] [--transcript <path>]");
8585
9410
  process.exit(2);
8586
9411
  }
8587
9412
  let result;
@@ -8595,10 +9420,18 @@ async function prReportCommand(args) {
8595
9420
  throw err;
8596
9421
  }
8597
9422
  if (writePath) {
8598
- const existing = existsSync19(writePath) ? readFileSync21(writePath, "utf8") : "";
8599
- const updated = replaceEvidenceBlock(existing, result.blockInner);
9423
+ const existing = existsSync20(writePath) ? readFileSync22(writePath, "utf8") : "";
9424
+ const withEvidence = replaceEvidenceBlock(existing, result.blockInner);
9425
+ const tokensAddition = collectTokensAddition({
9426
+ phase: phaseOverride ?? derivePhase(),
9427
+ role: roleOverride ?? "Developer",
9428
+ date: isoToday(),
9429
+ transcriptPath,
9430
+ modelOverride
9431
+ });
9432
+ const updated = writeTokensBlock(withEvidence, tokensAddition);
8600
9433
  writeFileSync10(writePath, updated);
8601
- process.stdout.write(`Wrote AEG:EVIDENCE block to ${writePath}
9434
+ process.stdout.write(`Wrote AEG:EVIDENCE and AEG:TOKENS blocks to ${writePath}
8602
9435
  `);
8603
9436
  } else {
8604
9437
  process.stdout.write(`${result.block}
@@ -8607,13 +9440,187 @@ async function prReportCommand(args) {
8607
9440
  process.exit(result.gatesFailed ? 1 : 0);
8608
9441
  }
8609
9442
 
9443
+ // src/commands/pr-verify-evidence.ts
9444
+ import { execFileSync as execFileSync15 } from "node:child_process";
9445
+
9446
+ // src/commands/pr-verify-evidence-logic.ts
9447
+ function publishedMergeBase(region) {
9448
+ const m = region.match(/^[ \t]*`git diff ([0-9a-f]{7,40})\.\.\.[0-9a-f]{7,40} --numstat`[ \t]*$/m);
9449
+ return m ? m[1] : null;
9450
+ }
9451
+ function normaliseLines(region, repoRoot2) {
9452
+ const root = repoRoot2.replace(/\/+$/, "");
9453
+ const out = [];
9454
+ for (const raw of region.split(`
9455
+ `)) {
9456
+ const localStripped = root ? raw.split(`${root}/`).join("").split(root).join("") : raw;
9457
+ const stripped = localStripped.replace(/(^|\s)\/\S*?\/(?=(?:apps|packages|aeg-root|scripts|\.changeset|\.vinaya|\.github)\/)/g, "$1");
9458
+ const line = stripped.replace(/[\u0000-\u001F\u007F-\u009F\u2028\u2029\u202A-\u202E\u2066-\u2069]/g, " ").trim();
9459
+ if (line)
9460
+ out.push(line);
9461
+ }
9462
+ return out;
9463
+ }
9464
+ function compareEvidence(published, freshRegion, repoRoot2) {
9465
+ if (published === null)
9466
+ return { status: "no-block" };
9467
+ if (published === "hidden")
9468
+ return { status: "hidden" };
9469
+ const publishedLines = normaliseLines(published.region, repoRoot2);
9470
+ const freshLines = normaliseLines(freshRegion, repoRoot2);
9471
+ const missing = subtractOnce(freshLines, publishedLines);
9472
+ const unexpected = subtractOnce(publishedLines, freshLines);
9473
+ if (missing.length === 0 && unexpected.length === 0)
9474
+ return { status: "match" };
9475
+ const publishedBase = publishedMergeBase(published.region);
9476
+ const currentBase = publishedMergeBase(freshRegion);
9477
+ const baseDrift = publishedBase && currentBase && publishedBase !== currentBase ? { publishedBase, currentBase } : undefined;
9478
+ return baseDrift ? { status: "differs", missing, unexpected, baseDrift } : { status: "differs", missing, unexpected };
9479
+ }
9480
+ function subtractOnce(a, b) {
9481
+ const counts = new Map;
9482
+ for (const line of b)
9483
+ counts.set(line, (counts.get(line) ?? 0) + 1);
9484
+ const out = [];
9485
+ for (const line of a) {
9486
+ const n = counts.get(line) ?? 0;
9487
+ if (n > 0)
9488
+ counts.set(line, n - 1);
9489
+ else
9490
+ out.push(line);
9491
+ }
9492
+ return out;
9493
+ }
9494
+ function renderVerdict(verdict) {
9495
+ if (verdict.status === "match") {
9496
+ 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).";
9497
+ }
9498
+ if (verdict.status === "no-block") {
9499
+ 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.";
9500
+ }
9501
+ if (verdict.status === "hidden") {
9502
+ return [
9503
+ "pr verify-evidence: HIDDEN — the AEG:EVIDENCE pair sits inside a collapsed `<details>` block.",
9504
+ "`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>`."
9505
+ ].join(`
9506
+ `);
9507
+ }
9508
+ const lines = ["pr verify-evidence: DIFFERS — the published AEG:EVIDENCE region does not reproduce at this head."];
9509
+ if (verdict.baseDrift) {
9510
+ 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.");
9511
+ } else {
9512
+ 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.");
9513
+ }
9514
+ lines.push("", "Regenerate with `vinaya pr report --write <body-file>` and push.");
9515
+ if (verdict.missing.length > 0) {
9516
+ lines.push("", `Present in a fresh run, absent from the published block (${verdict.missing.length}):`);
9517
+ for (const line of verdict.missing)
9518
+ lines.push(` + ${line}`);
9519
+ }
9520
+ if (verdict.unexpected.length > 0) {
9521
+ lines.push("", `Published but not reproducible (${verdict.unexpected.length}):`);
9522
+ for (const line of verdict.unexpected)
9523
+ lines.push(` - ${line}`);
9524
+ }
9525
+ return lines.join(`
9526
+ `);
9527
+ }
9528
+
9529
+ // src/commands/pr-verify-evidence.ts
9530
+ function gh(args) {
9531
+ return execFileSync15("gh", args, { encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
9532
+ }
9533
+ function assertCleanWorktree() {
9534
+ const dirty = execFileSync15("git", ["status", "--porcelain"], {
9535
+ encoding: "utf8",
9536
+ cwd: repoRoot2()
9537
+ }).trim();
9538
+ if (!dirty)
9539
+ return;
9540
+ process.stderr.write(`pr verify-evidence: REFUSED — the worktree has uncommitted or untracked changes.
9541
+ ` + `Group B is regenerated by diff-scoped checks, so local changes alter which files are scanned and can
9542
+ ` + `produce a MATCH that a clean checkout would not. Verify from a clean checkout at this head instead.
9543
+
9544
+ ` + `${dirty}
9545
+ `);
9546
+ process.exit(2);
9547
+ }
9548
+ function assertNoBaseOverride() {
9549
+ const override = process.env.BASE_SHA;
9550
+ if (!override)
9551
+ return;
9552
+ process.stderr.write(`pr verify-evidence: REFUSED — BASE_SHA is set (${override}).
9553
+ ` + `The regeneration resolves its merge-base from that variable, so an override changes what a "fresh run"
9554
+ ` + `means and can turn a drifted comparison into an apparently shared-base one. Unset it and re-run.
9555
+ `);
9556
+ process.exit(2);
9557
+ }
9558
+ function assertRepoRootCwd() {
9559
+ const root = repoRoot2();
9560
+ const cwd = process.cwd();
9561
+ if (cwd === root)
9562
+ return;
9563
+ process.stderr.write(`pr verify-evidence: REFUSED — run this from the repository root (${root}), not ${cwd}.
9564
+ ` + `The regeneration inherits this working directory, and several gates resolve their scan root from it,
9565
+ ` + `so a subdirectory silently narrows what the fresh run inspects and can report MATCH on a block that
9566
+ ` + `omits real findings.
9567
+ `);
9568
+ process.exit(2);
9569
+ }
9570
+ function repoRoot2() {
9571
+ return execFileSync15("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
9572
+ }
9573
+ async function prVerifyEvidenceCommand(args) {
9574
+ const prRef = args[0] && !args[0].startsWith("-") ? args[0] : undefined;
9575
+ if (!prRef) {
9576
+ process.stderr.write(`Usage: vinaya pr verify-evidence <pr-number>
9577
+ `);
9578
+ process.exit(2);
9579
+ }
9580
+ assertRepoRootCwd();
9581
+ assertCleanWorktree();
9582
+ assertNoBaseOverride();
9583
+ let body;
9584
+ let forgeHead;
9585
+ try {
9586
+ const view = JSON.parse(gh(["pr", "view", prRef, "--json", "body,headRefOid"]));
9587
+ body = view.body ?? "";
9588
+ forgeHead = view.headRefOid ?? "";
9589
+ } catch (err) {
9590
+ process.stderr.write(`pr verify-evidence: could not read pull request #${prRef} — ${err.message}
9591
+ `);
9592
+ process.exit(1);
9593
+ return;
9594
+ }
9595
+ const localHead = execFileSync15("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim();
9596
+ if (!forgeHead) {
9597
+ 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.
9598
+ `);
9599
+ process.exit(2);
9600
+ }
9601
+ if (forgeHead !== localHead) {
9602
+ process.stderr.write(`pr verify-evidence: REFUSED — this checkout is at ${localHead}, but #${prRef}'s head is ${forgeHead}.
9603
+ ` + `Comparing against a different tree produces a difference that is real and uninteresting. Check out the head first.
9604
+ `);
9605
+ process.exit(2);
9606
+ }
9607
+ const fresh = await buildReport();
9608
+ const resolved = resolveAnchoredRegion(ScanContext.from(body), "EVIDENCE");
9609
+ const verdict = compareEvidence(resolved === null || resolved === "hidden" ? resolved : { region: resolved.region }, fresh.blockInner, repoRoot2());
9610
+ process.stdout.write(`${renderVerdict(verdict)}
9611
+ `);
9612
+ process.stdout.write(` compared at head ${localHead}
9613
+ `);
9614
+ process.exit(verdict.status === "match" ? 0 : 1);
9615
+ }
9616
+
8610
9617
  // src/commands/quickstart.ts
8611
- import { execFileSync as execFileSync14 } from "node:child_process";
8612
- import { existsSync as existsSync21, readFileSync as readFileSync23 } from "node:fs";
9618
+ import { execFileSync as execFileSync16 } from "node:child_process";
9619
+ import { existsSync as existsSync22, readFileSync as readFileSync24 } from "node:fs";
8613
9620
  import { join as join23 } from "node:path";
8614
9621
 
8615
9622
  // src/lib/doc-owners-write.ts
8616
- import { existsSync as existsSync20, mkdirSync as mkdirSync7, readFileSync as readFileSync22, writeFileSync as writeFileSync11 } from "node:fs";
9623
+ import { existsSync as existsSync21, mkdirSync as mkdirSync7, readFileSync as readFileSync23, writeFileSync as writeFileSync11 } from "node:fs";
8617
9624
  import { dirname as dirname7, join as join22 } from "node:path";
8618
9625
  function bindingLine(glob, pointer) {
8619
9626
  return `${glob} ${pointer}`;
@@ -8629,23 +9636,23 @@ function appendDocOwnersBinding(content, glob, pointer) {
8629
9636
  return `${content}${sep4}${bindingLine(glob, pointer)}
8630
9637
  `;
8631
9638
  }
8632
- function planDocOwnersBinding(repoRoot2, glob, pointer) {
8633
- const abs2 = join22(repoRoot2, DOC_OWNERS_PATH);
9639
+ function planDocOwnersBinding(repoRoot3, glob, pointer) {
9640
+ const abs2 = join22(repoRoot3, DOC_OWNERS_PATH);
8634
9641
  const line = bindingLine(glob, pointer);
8635
- if (!existsSync20(abs2)) {
9642
+ if (!existsSync21(abs2)) {
8636
9643
  return { action: "create-host", line, path: DOC_OWNERS_PATH };
8637
9644
  }
8638
- const existing = readFileSync22(abs2, "utf-8");
9645
+ const existing = readFileSync23(abs2, "utf-8");
8639
9646
  const already = parseDocOwners(existing).bindings.some((b) => b.glob === glob);
8640
9647
  return { action: already ? "skip-present" : "append-row", line, path: DOC_OWNERS_PATH };
8641
9648
  }
8642
- function applyDocOwnersBinding(repoRoot2, plan, glob, pointer) {
8643
- const abs2 = join22(repoRoot2, DOC_OWNERS_PATH);
9649
+ function applyDocOwnersBinding(repoRoot3, plan, glob, pointer) {
9650
+ const abs2 = join22(repoRoot3, DOC_OWNERS_PATH);
8644
9651
  if (plan.action === "create-host") {
8645
9652
  mkdirSync7(dirname7(abs2), { recursive: true });
8646
9653
  writeFileSync11(abs2, freshDocOwners(glob, pointer), "utf-8");
8647
9654
  } else if (plan.action === "append-row") {
8648
- const existing = readFileSync22(abs2, "utf-8");
9655
+ const existing = readFileSync23(abs2, "utf-8");
8649
9656
  writeFileSync11(abs2, appendDocOwnersBinding(existing, glob, pointer), "utf-8");
8650
9657
  }
8651
9658
  }
@@ -8705,13 +9712,13 @@ var GLOB_INJECTION_RE = /[\s\r\n]/;
8705
9712
  var POINTER_INJECTION_RE = /[\r\n]/;
8706
9713
  var NAME_INJECTION_RE = /[\s|\r\n]/;
8707
9714
  var PATH_INJECTION_RE2 = /[|\r\n]/;
8708
- function pointerIsMissingLocalFile(repoRoot2, pointer) {
9715
+ function pointerIsMissingLocalFile(repoRoot3, pointer) {
8709
9716
  if (/^https?:\/\//i.test(pointer))
8710
9717
  return false;
8711
9718
  const path = pointer.split("#")[0] ?? pointer;
8712
- return !existsSync21(join23(repoRoot2, path));
9719
+ return !existsSync22(join23(repoRoot3, path));
8713
9720
  }
8714
- async function bindDocOwnerLoop(deps, repoRoot2) {
9721
+ async function bindDocOwnerLoop(deps, repoRoot3) {
8715
9722
  let any = false;
8716
9723
  for (;; ) {
8717
9724
  if (any && !await deps.confirm("Bind another doc→code pair?", false))
@@ -8726,11 +9733,11 @@ async function bindDocOwnerLoop(deps, repoRoot2) {
8726
9733
  }
8727
9734
  if (GLOB_INJECTION_RE.test(glob) || POINTER_INJECTION_RE.test(pointer)) {
8728
9735
  console.error(red("Error: glob must not contain whitespace, and the pointer must not contain a newline."));
8729
- } else if (pointerIsMissingLocalFile(repoRoot2, pointer)) {
9736
+ } else if (pointerIsMissingLocalFile(repoRoot3, pointer)) {
8730
9737
  console.error(red(`✗ "${pointer}" does not exist in this repo (and isn't a URL).`));
8731
9738
  } else {
8732
- const plan = planDocOwnersBinding(repoRoot2, glob, pointer);
8733
- applyDocOwnersBinding(repoRoot2, plan, glob, pointer);
9739
+ const plan = planDocOwnersBinding(repoRoot3, glob, pointer);
9740
+ applyDocOwnersBinding(repoRoot3, plan, glob, pointer);
8734
9741
  process.stdout.write(`${green(renderDocOwnersBindingDiffLine(plan))}
8735
9742
  `);
8736
9743
  any = true;
@@ -8776,10 +9783,10 @@ async function registerProjectLoop(deps) {
8776
9783
  }
8777
9784
  }
8778
9785
  function readPackageVersion() {
8779
- const pkg = JSON.parse(readFileSync23(join23(packageRoot(import.meta.url), "package.json"), "utf-8"));
9786
+ const pkg = JSON.parse(readFileSync24(join23(packageRoot(import.meta.url), "package.json"), "utf-8"));
8780
9787
  return pkg.version;
8781
9788
  }
8782
- function realDeps6() {
9789
+ function realDeps7() {
8783
9790
  return {
8784
9791
  detectRepo: detectGitRepo,
8785
9792
  initDeps: {
@@ -8803,8 +9810,8 @@ function realDeps6() {
8803
9810
  meteringCapability: () => resolveMeteringCapability({
8804
9811
  env: process.env,
8805
9812
  cwd: process.cwd(),
8806
- exists: existsSync21,
8807
- readFile: (path) => readFileSync23(path, "utf8")
9813
+ exists: existsSync22,
9814
+ readFile: (path) => readFileSync24(path, "utf8")
8808
9815
  })
8809
9816
  },
8810
9817
  confirm: async (q, defaultYes) => promptYesNo(q, defaultYes),
@@ -8812,8 +9819,8 @@ function realDeps6() {
8812
9819
  closeStdin
8813
9820
  };
8814
9821
  }
8815
- function execGit(repoRoot2, args) {
8816
- return execFileSync14("git", args, { cwd: repoRoot2, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
9822
+ function execGit(repoRoot3, args) {
9823
+ return execFileSync16("git", args, { cwd: repoRoot3, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
8817
9824
  }
8818
9825
  function errorDetail(err) {
8819
9826
  const stderr = err.stderr;
@@ -8821,37 +9828,37 @@ function errorDetail(err) {
8821
9828
  return stderr.toString().trim();
8822
9829
  return err instanceof Error ? err.message : String(err);
8823
9830
  }
8824
- function gitStatusShort(repoRoot2) {
8825
- return execGit(repoRoot2, ["status", "--short"]);
9831
+ function gitStatusShort(repoRoot3) {
9832
+ return execGit(repoRoot3, ["status", "--short"]);
8826
9833
  }
8827
- function commitInstall(repoRoot2) {
8828
- execGit(repoRoot2, ["add", "-A"]);
8829
- const staged = execGit(repoRoot2, ["diff", "--cached", "--stat"]);
9834
+ function commitInstall(repoRoot3) {
9835
+ execGit(repoRoot3, ["add", "-A"]);
9836
+ const staged = execGit(repoRoot3, ["diff", "--cached", "--stat"]);
8830
9837
  if (!staged)
8831
9838
  return { committed: false, message: "Nothing to commit — working tree already clean." };
8832
9839
  try {
8833
- execGit(repoRoot2, ["commit", "-m", "Chore: install Vinaya"]);
9840
+ execGit(repoRoot3, ["commit", "-m", "Chore: install Vinaya"]);
8834
9841
  return { committed: true, message: "✓ Committed `Chore: install Vinaya`." };
8835
9842
  } catch (err) {
8836
9843
  return { committed: false, message: `git commit failed: ${errorDetail(err)}` };
8837
9844
  }
8838
9845
  }
8839
- function pushInstall(repoRoot2) {
9846
+ function pushInstall(repoRoot3) {
8840
9847
  try {
8841
- const branch = execGit(repoRoot2, ["symbolic-ref", "--short", "HEAD"]);
9848
+ const branch = execGit(repoRoot3, ["symbolic-ref", "--short", "HEAD"]);
8842
9849
  let hasUpstream = true;
8843
9850
  try {
8844
- execGit(repoRoot2, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
9851
+ execGit(repoRoot3, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
8845
9852
  } catch {
8846
9853
  hasUpstream = false;
8847
9854
  }
8848
9855
  if (hasUpstream) {
8849
- const ahead = execGit(repoRoot2, ["rev-list", "--count", "@{u}..HEAD"]);
9856
+ const ahead = execGit(repoRoot3, ["rev-list", "--count", "@{u}..HEAD"]);
8850
9857
  if (ahead === "0") {
8851
9858
  return { status: "up-to-date", message: "Nothing to push — already up to date with the remote." };
8852
9859
  }
8853
9860
  }
8854
- execGit(repoRoot2, hasUpstream ? ["push"] : ["push", "-u", "origin", branch]);
9861
+ execGit(repoRoot3, hasUpstream ? ["push"] : ["push", "-u", "origin", branch]);
8855
9862
  return { status: "pushed", message: "✓ Pushed." };
8856
9863
  } catch (err) {
8857
9864
  return { status: "failed", message: `git push failed: ${errorDetail(err)}` };
@@ -8943,12 +9950,12 @@ ${bold("Next steps:")}
8943
9950
  }
8944
9951
  }
8945
9952
  async function quickstartCommand(args) {
8946
- process.exit(await runQuickstart(args, realDeps6()));
9953
+ process.exit(await runQuickstart(args, realDeps7()));
8947
9954
  }
8948
9955
 
8949
9956
  // 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";
9957
+ import { execFileSync as execFileSync17 } from "node:child_process";
9958
+ import { readFileSync as readFileSync25, rmSync as rmSync4, writeFileSync as writeFileSync12 } from "node:fs";
8952
9959
  import { tmpdir as tmpdir2 } from "node:os";
8953
9960
  import { join as join24 } from "node:path";
8954
9961
  class FindingsParseError extends Error {
@@ -9130,7 +10137,7 @@ function readFindingsFile(path, allowedSeverities) {
9130
10137
  return [];
9131
10138
  let content;
9132
10139
  try {
9133
- content = readFileSync24(path, "utf8");
10140
+ content = readFileSync25(path, "utf8");
9134
10141
  } catch {
9135
10142
  refuseCmd(`Could not read findings file at ${path}.`, "Check the path and re-run.");
9136
10143
  }
@@ -9159,9 +10166,9 @@ function normalizeSecurityVerdict(raw) {
9159
10166
  return "FAIL";
9160
10167
  refuseCmd(`\`--verdict ${raw}\` is not PASS or FAIL.`, "Pass `--verdict PASS` or `--verdict FAIL`.");
9161
10168
  }
9162
- function gh(args) {
10169
+ function gh2(args) {
9163
10170
  try {
9164
- return execFileSync15("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
10171
+ return execFileSync17("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
9165
10172
  } catch (err) {
9166
10173
  const stderr = err.stderr;
9167
10174
  throw new Error(String(stderr ?? err.message).trim() || "gh command failed");
@@ -9170,7 +10177,7 @@ function gh(args) {
9170
10177
  function resolveHeadSha(pr) {
9171
10178
  let out;
9172
10179
  try {
9173
- out = gh(["pr", "view", pr, "--json", "headRefOid", "-q", ".headRefOid"]);
10180
+ out = gh2(["pr", "view", pr, "--json", "headRefOid", "-q", ".headRefOid"]);
9174
10181
  } catch (err) {
9175
10182
  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
10183
  }
@@ -9183,7 +10190,7 @@ function postComment(pr, body) {
9183
10190
  const tmp = join24(tmpdir2(), `vinaya-review-post-${process.pid}-${Date.now()}.md`);
9184
10191
  writeFileSync12(tmp, body);
9185
10192
  try {
9186
- return gh(["pr", "comment", pr, "--body-file", tmp]).trim();
10193
+ return gh2(["pr", "comment", pr, "--body-file", tmp]).trim();
9187
10194
  } catch (err) {
9188
10195
  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
10196
  } finally {
@@ -9191,7 +10198,7 @@ function postComment(pr, body) {
9191
10198
  }
9192
10199
  }
9193
10200
  function fetchComments(pr) {
9194
- const out = gh(["pr", "view", pr, "--json", "comments"]);
10201
+ const out = gh2(["pr", "view", pr, "--json", "comments"]);
9195
10202
  const parsed = JSON.parse(out);
9196
10203
  return parsed.comments.map((c) => ({ body: c.body, author: c.author?.login ?? null }));
9197
10204
  }
@@ -9299,7 +10306,7 @@ async function reviewPostCommand(args) {
9299
10306
  let secretsEvidence = null;
9300
10307
  if (secretsEvidenceFile) {
9301
10308
  try {
9302
- secretsEvidence = readFileSync24(secretsEvidenceFile, "utf8");
10309
+ secretsEvidence = readFileSync25(secretsEvidenceFile, "utf8");
9303
10310
  } catch {
9304
10311
  refuseCmd(`Could not read secrets evidence file at ${secretsEvidenceFile}.`, "Check the path and re-run.");
9305
10312
  }
@@ -9340,7 +10347,7 @@ Self-verification: clean — re-parsed VERDICT is bound to head ${headSha}.
9340
10347
 
9341
10348
  // src/commands/studio.ts
9342
10349
  import { execFile as execFile5, spawn as spawn2 } from "node:child_process";
9343
- import { existsSync as existsSync22, readFileSync as readFileSync25, renameSync } from "node:fs";
10350
+ import { existsSync as existsSync23, readFileSync as readFileSync26, renameSync } from "node:fs";
9344
10351
  import net from "node:net";
9345
10352
  import { dirname as dirname8, join as join25 } from "node:path";
9346
10353
  import { promisify as promisify5 } from "node:util";
@@ -9397,15 +10404,15 @@ function resolveStudioTarget(cwd, moduleUrl = import.meta.url) {
9397
10404
  for (;; ) {
9398
10405
  const webDir = join25(dir, "apps", "vinaya-studio", "web");
9399
10406
  const pkgPath = join25(webDir, "package.json");
9400
- if (existsSync22(pkgPath)) {
10407
+ if (existsSync23(pkgPath)) {
9401
10408
  try {
9402
- const pkg = JSON.parse(readFileSync25(pkgPath, "utf-8"));
10409
+ const pkg = JSON.parse(readFileSync26(pkgPath, "utf-8"));
9403
10410
  if (pkg.name === "@atta/vinaya-studio-web") {
9404
10411
  return { kind: "workspace", webDir };
9405
10412
  }
9406
10413
  } catch {}
9407
10414
  }
9408
- if (existsSync22(join25(dir, ".git")))
10415
+ if (existsSync23(join25(dir, ".git")))
9409
10416
  break;
9410
10417
  const parent = dirname8(dir);
9411
10418
  if (parent === dir)
@@ -9413,29 +10420,29 @@ function resolveStudioTarget(cwd, moduleUrl = import.meta.url) {
9413
10420
  dir = parent;
9414
10421
  }
9415
10422
  const standaloneWebDir = join25(packageRoot(moduleUrl), "studio-standalone", "apps", "vinaya-studio", "web");
9416
- if (existsSync22(join25(standaloneWebDir, "server.js"))) {
10423
+ if (existsSync23(join25(standaloneWebDir, "server.js"))) {
9417
10424
  return { kind: "package", packageDir: standaloneWebDir };
9418
10425
  }
9419
10426
  return { kind: "missing" };
9420
10427
  }
9421
10428
  function spawnDev(webDir, args) {
9422
- return new Promise((resolve2) => {
10429
+ return new Promise((resolve3) => {
9423
10430
  const child = spawn2("bun", ["run", "dev", ...args], { cwd: webDir, stdio: "inherit" });
9424
- child.on("exit", (code) => resolve2(code ?? 0));
10431
+ child.on("exit", (code) => resolve3(code ?? 0));
9425
10432
  });
9426
10433
  }
9427
10434
  function isPortFree(port) {
9428
- return new Promise((resolve2) => {
10435
+ return new Promise((resolve3) => {
9429
10436
  const tester = net.createServer();
9430
- tester.once("error", () => resolve2(false));
9431
- tester.once("listening", () => tester.close(() => resolve2(true)));
10437
+ tester.once("error", () => resolve3(false));
10438
+ tester.once("listening", () => tester.close(() => resolve3(true)));
9432
10439
  tester.listen(port, "127.0.0.1");
9433
10440
  });
9434
10441
  }
9435
10442
  function ensureStudioNodeModules(bundleRoot) {
9436
10443
  const real = join25(bundleRoot, "node_modules");
9437
10444
  const packed = join25(bundleRoot, STUDIO_NODE_MODULES_PACKED_DIRNAME);
9438
- if (!existsSync22(real) && existsSync22(packed)) {
10445
+ if (!existsSync23(real) && existsSync23(packed)) {
9439
10446
  renameSync(packed, real);
9440
10447
  }
9441
10448
  }
@@ -9459,9 +10466,9 @@ async function spawnStandalone(cwd, serverPath, bundleRoot, explicitPort) {
9459
10466
  const env = { HOSTNAME: "127.0.0.1", ...process.env, PORT: String(port), VINAYA_REPO_ROOT: cwd };
9460
10467
  if (repo)
9461
10468
  env.AEG_REPO = `${repo.owner}/${repo.repo}`;
9462
- return new Promise((resolve2) => {
10469
+ return new Promise((resolve3) => {
9463
10470
  const child = spawn2("node", [serverPath], { cwd, stdio: "inherit", env });
9464
- child.on("exit", (code) => resolve2(code ?? 0));
10471
+ child.on("exit", (code) => resolve3(code ?? 0));
9465
10472
  });
9466
10473
  }
9467
10474
  async function runStudio(cwd, args, moduleUrl = import.meta.url) {
@@ -9493,124 +10500,6 @@ async function runStudio(cwd, args, moduleUrl = import.meta.url) {
9493
10500
  }
9494
10501
  }
9495
10502
 
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
10503
  // src/commands/upgrade.ts
9615
10504
  import { existsSync as existsSync24, readFileSync as readFileSync27, rmSync as rmSync5, writeFileSync as writeFileSync13 } from "node:fs";
9616
10505
  import { join as join26 } from "node:path";
@@ -9630,8 +10519,8 @@ function realDeps8() {
9630
10519
  function flags2(args) {
9631
10520
  return { dryRun: args.includes("--dry-run"), yes: args.includes("--yes") };
9632
10521
  }
9633
- function readManifest3(repoRoot2) {
9634
- const p = join26(repoRoot2, CONFIG_PATH);
10522
+ function readManifest3(repoRoot3) {
10523
+ const p = join26(repoRoot3, CONFIG_PATH);
9635
10524
  if (!existsSync24(p))
9636
10525
  return { kind: "missing" };
9637
10526
  let raw;
@@ -9651,15 +10540,15 @@ function readManifest3(repoRoot2) {
9651
10540
  return { kind: "not-initialized" };
9652
10541
  return { kind: "ok", manifest: parsed.data.managed };
9653
10542
  }
9654
- function writeManifestVersion(repoRoot2, manifest) {
9655
- const configAbs = join26(repoRoot2, CONFIG_PATH);
10543
+ function writeManifestVersion(repoRoot3, manifest) {
10544
+ const configAbs = join26(repoRoot3, CONFIG_PATH);
9656
10545
  const seed = JSON.parse(readFileSync27(configAbs, "utf-8"));
9657
10546
  const updated = { ...manifest, version: MANAGED_MANIFEST_VERSION };
9658
10547
  writeFileSync13(configAbs, `${JSON.stringify({ ...seed, managed: updated }, null, 2)}
9659
10548
  `, "utf-8");
9660
10549
  }
9661
- function stripFor(repoRoot2, path, marker, comment) {
9662
- const abs2 = resolveManagedBlockPath(repoRoot2, path);
10550
+ function stripFor(repoRoot3, path, marker, comment) {
10551
+ const abs2 = resolveManagedBlockPath(repoRoot3, path);
9663
10552
  if (!existsSync24(abs2))
9664
10553
  return { path, marker, comment, present: false, removesHost: false };
9665
10554
  const stripped = stripBlockFromContent(readFileSync27(abs2, "utf-8"), marker, comment);
@@ -9667,15 +10556,15 @@ function stripFor(repoRoot2, path, marker, comment) {
9667
10556
  return { path, marker, comment, present: false, removesHost: false };
9668
10557
  return { path, marker, comment, present: true, removesHost: blockStripLeavesEmpty(stripped) };
9669
10558
  }
9670
- function planHookRouting(repoRoot2, manifest, recorded, hooksPathValue) {
10559
+ function planHookRouting(repoRoot3, manifest, recorded, hooksPathValue) {
9671
10560
  const none = { target: recorded, strips: [], arm: false, migratesManifest: false, blockedReason: null };
9672
10561
  if (recorded === ".husky")
9673
10562
  return none;
9674
10563
  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);
10564
+ 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
10565
  if (hooksPathValue === TRACKED_HOOK_DIR)
9677
10566
  return { ...none, strips };
9678
- const foreign = foreignRawHooks(repoRoot2);
10567
+ const foreign = foreignRawHooks(repoRoot3);
9679
10568
  if (foreign.length > 0) {
9680
10569
  return {
9681
10570
  ...none,
@@ -9688,7 +10577,7 @@ function planHookRouting(repoRoot2, manifest, recorded, hooksPathValue) {
9688
10577
  const legacy = manifest.blocks.filter((b) => b.path.startsWith(".git/hooks/"));
9689
10578
  const blocked = [];
9690
10579
  for (const b of legacy) {
9691
- const abs2 = resolveManagedBlockPath(repoRoot2, b.path);
10580
+ const abs2 = resolveManagedBlockPath(repoRoot3, b.path);
9692
10581
  if (!existsSync24(abs2))
9693
10582
  continue;
9694
10583
  const stripped = stripBlockFromContent(readFileSync27(abs2, "utf-8"), b.marker, b.comment);
@@ -9698,7 +10587,7 @@ function planHookRouting(repoRoot2, manifest, recorded, hooksPathValue) {
9698
10587
  blocked.push(`${b.path} carries your own lines alongside the managed block`);
9699
10588
  }
9700
10589
  const legacyNames = new Set(legacy.map((b) => b.path.slice(".git/hooks/".length)));
9701
- for (const f of activeRawHooks(repoRoot2)) {
10590
+ for (const f of activeRawHooks(repoRoot3)) {
9702
10591
  if (!legacyNames.has(f))
9703
10592
  blocked.push(`.git/hooks/${f} is an active raw hook vinaya does not manage`);
9704
10593
  }
@@ -9706,7 +10595,7 @@ function planHookRouting(repoRoot2, manifest, recorded, hooksPathValue) {
9706
10595
  return { ...none, blockedReason: blocked.join("; ") };
9707
10596
  return {
9708
10597
  target: TRACKED_HOOK_DIR,
9709
- strips: legacy.map((b) => stripFor(repoRoot2, b.path, b.marker, b.comment)).filter((s) => s.present),
10598
+ strips: legacy.map((b) => stripFor(repoRoot3, b.path, b.marker, b.comment)).filter((s) => s.present),
9710
10599
  arm: hooksPathValue !== TRACKED_HOOK_DIR,
9711
10600
  migratesManifest: true,
9712
10601
  blockedReason: null
@@ -9722,7 +10611,7 @@ function extractWorkflowTrigger(content) {
9722
10611
  const match = content.match(/^on:\r?\n {2}([A-Za-z0-9_]+):/m);
9723
10612
  return match?.[1] ?? null;
9724
10613
  }
9725
- function planUpgrade(ops, repoRoot2, manifest, routing) {
10614
+ function planUpgrade(ops, repoRoot3, manifest, routing) {
9726
10615
  const entries = [];
9727
10616
  let hasChanges = routing.arm || routing.migratesManifest || routing.strips.some((s) => s.present);
9728
10617
  const ownedFiles = new Set(manifest.files);
@@ -9730,7 +10619,7 @@ function planUpgrade(ops, repoRoot2, manifest, routing) {
9730
10619
  const ownedBlocks = new Set(manifest.blocks.map((b) => blockKey(b.path, b.marker)));
9731
10620
  for (const op of ops) {
9732
10621
  if (op.kind === "create-file") {
9733
- const abs2 = join26(repoRoot2, op.path);
10622
+ const abs2 = join26(repoRoot3, op.path);
9734
10623
  const exists = existsSync24(abs2);
9735
10624
  const owned = ownedFiles.has(op.path) || isDefaultedAgentVendorPath(op.path, manifest);
9736
10625
  let action;
@@ -9759,7 +10648,7 @@ function planUpgrade(ops, repoRoot2, manifest, routing) {
9759
10648
  }
9760
10649
  entries.push({ kind: "create-file", op, action, ...triggerChange ? { triggerChange } : {} });
9761
10650
  } else if (op.kind === "managed-block") {
9762
- const abs2 = resolveManagedBlockPath(repoRoot2, op.path);
10651
+ const abs2 = resolveManagedBlockPath(repoRoot3, op.path);
9763
10652
  const owned = ownedBlocks.has(blockKey(op.path, op.marker));
9764
10653
  let action;
9765
10654
  if (!owned) {
@@ -9872,8 +10761,8 @@ function renderUpgradeDiff(plan) {
9872
10761
  return lines.join(`
9873
10762
  `);
9874
10763
  }
9875
- function regenerateBlock(repoRoot2, op) {
9876
- const abs2 = resolveManagedBlockPath(repoRoot2, op.path);
10764
+ function regenerateBlock(repoRoot3, op) {
10765
+ const abs2 = resolveManagedBlockPath(repoRoot3, op.path);
9877
10766
  const content = readFileSync27(abs2, "utf-8");
9878
10767
  const stripped = stripBlockFromContent(content, op.marker, op.comment);
9879
10768
  if (stripped !== null) {
@@ -9881,27 +10770,27 @@ function regenerateBlock(repoRoot2, op) {
9881
10770
  `) ? stripped : `${stripped}
9882
10771
  `, "utf-8");
9883
10772
  }
9884
- appendBlock(repoRoot2, op);
10773
+ appendBlock(repoRoot3, op);
9885
10774
  }
9886
- function applyUpgrade(plan, repoRoot2) {
10775
+ function applyUpgrade(plan, repoRoot3) {
9887
10776
  for (const e of plan.entries) {
9888
10777
  if (e.kind === "create-file") {
9889
10778
  if (e.action === "regenerate" || e.action === "recreate") {
9890
- writeFileWithDirs(join26(repoRoot2, e.op.path), e.op.content, e.op.mode);
10779
+ writeFileWithDirs(join26(repoRoot3, e.op.path), e.op.content, e.op.mode);
9891
10780
  }
9892
10781
  } else {
9893
10782
  if (e.action === "recreate-host")
9894
- createHost(repoRoot2, e.op);
10783
+ createHost(repoRoot3, e.op);
9895
10784
  else if (e.action === "recreate-append")
9896
- appendBlock(repoRoot2, e.op);
10785
+ appendBlock(repoRoot3, e.op);
9897
10786
  else if (e.action === "regenerate-block")
9898
- regenerateBlock(repoRoot2, e.op);
10787
+ regenerateBlock(repoRoot3, e.op);
9899
10788
  }
9900
10789
  }
9901
10790
  for (const s of plan.routing.strips) {
9902
10791
  if (!s.present)
9903
10792
  continue;
9904
- const abs2 = resolveManagedBlockPath(repoRoot2, s.path);
10793
+ const abs2 = resolveManagedBlockPath(repoRoot3, s.path);
9905
10794
  if (!existsSync24(abs2))
9906
10795
  continue;
9907
10796
  const stripped = stripBlockFromContent(readFileSync27(abs2, "utf-8"), s.marker, s.comment);
@@ -9993,16 +10882,16 @@ async function upgradeCommand(args) {
9993
10882
  }
9994
10883
 
9995
10884
  // src/commands/waiver.ts
9996
- import { execFileSync as execFileSync16 } from "node:child_process";
10885
+ import { execFileSync as execFileSync18 } from "node:child_process";
9997
10886
  function labelFor(kind) {
9998
10887
  return kind === "docs" ? WAIVER_LABEL : WAIVER_LABEL_REVIEW;
9999
10888
  }
10000
- function gh2(args) {
10001
- return execFileSync16("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], env: process.env }).trim();
10889
+ function gh3(args) {
10890
+ return execFileSync18("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], env: process.env }).trim();
10002
10891
  }
10003
10892
  function detectPrNumber() {
10004
10893
  try {
10005
- const out = gh2(["pr", "view", "--json", "number", "-q", ".number"]);
10894
+ const out = gh3(["pr", "view", "--json", "number", "-q", ".number"]);
10006
10895
  return /^\d+$/.test(out) ? out : null;
10007
10896
  } catch {
10008
10897
  return null;
@@ -10098,8 +10987,8 @@ Reason: ${reason}`];
10098
10987
  }
10099
10988
  process.stdout.write(`Applying \`${label2}\` to PR #${prNumber} under your authenticated \`gh\` identity — vinaya never fabricates an actor.
10100
10989
  `);
10101
- execFileSync16("gh", editArgs, { stdio: "inherit", env: process.env });
10102
- execFileSync16("gh", commentArgs, { stdio: "inherit", env: process.env });
10990
+ execFileSync18("gh", editArgs, { stdio: "inherit", env: process.env });
10991
+ execFileSync18("gh", commentArgs, { stdio: "inherit", env: process.env });
10103
10992
  process.stdout.write(`
10104
10993
  ✓ Label applied and reason posted as a PR comment.
10105
10994
  `);
@@ -10223,8 +11112,10 @@ try {
10223
11112
  prEditCommand(rest);
10224
11113
  } else if (subcommand === "report") {
10225
11114
  await prReportCommand(rest);
11115
+ } else if (subcommand === "verify-evidence") {
11116
+ await prVerifyEvidenceCommand(rest);
10226
11117
  } else {
10227
- console.error(`Unknown 'pr' subcommand: ${subcommand ?? "(none)"} (expected 'create', 'edit', or 'report')`);
11118
+ console.error(`Unknown 'pr' subcommand: ${subcommand ?? "(none)"} (expected 'create', 'edit', 'report', or 'verify-evidence')`);
10228
11119
  process.exit(2);
10229
11120
  }
10230
11121
  break;
@@ -10249,8 +11140,10 @@ try {
10249
11140
  await milestoneAdoptCommand(rest);
10250
11141
  } else if (subcommand === "edit") {
10251
11142
  await milestoneEditCommand(rest);
11143
+ } else if (subcommand === "close") {
11144
+ await milestoneCloseCommand(rest);
10252
11145
  } else {
10253
- console.error(`Unknown 'milestone' subcommand: ${subcommand ?? "(none)"} (expected create/adopt/edit)`);
11146
+ console.error(`Unknown 'milestone' subcommand: ${subcommand ?? "(none)"} (expected create/adopt/edit/close)`);
10254
11147
  process.exit(2);
10255
11148
  }
10256
11149
  break;