@attalabs/vinaya 0.19.3 → 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 (153) hide show
  1. package/README.md +81 -0
  2. package/aeg-root/contracts/archivist-tranche-archivist.md +2 -2
  3. package/aeg-root/contracts/tranche-archivist-planner.md +8 -8
  4. package/aeg-root/enforcement.md +18 -9
  5. package/aeg-root/milestone-model.md +3 -1
  6. package/aeg-root/process.md +5 -5
  7. package/aeg-root/roles/architect.md +2 -2
  8. package/aeg-root/roles/archivist.md +10 -11
  9. package/aeg-root/roles/brief-author.md +1 -1
  10. package/aeg-root/roles/developer.md +4 -3
  11. package/aeg-root/roles/planner.md +2 -2
  12. package/aeg-root/roles/principal.md +2 -1
  13. package/aeg-root/roles/tranche-archivist.md +5 -18
  14. package/aeg-root/skills/aeg/SKILL.md +30 -1
  15. package/aeg-root/skills/aeg-roles/SKILL.md +1 -1
  16. package/aeg-root/skills/brief-authoring/SKILL.md +2 -2
  17. package/aeg-root/state-machine.md +8 -6
  18. package/aeg-root/task-model.md +1 -1
  19. package/aeg-root/templates/issue-rationale-template.md +4 -4
  20. package/aeg-root/templates/pr-report-template.md +4 -0
  21. package/aeg-root/tranche-model.md +1 -1
  22. package/dist/checks/bin/check-body-bare-digits.js +473 -32
  23. package/dist/checks/bin/check-branch-topology.js +792 -11
  24. package/dist/checks/bin/check-brief-shape.js +353 -11
  25. package/dist/checks/bin/check-changeset-coverage.js +4156 -0
  26. package/dist/checks/bin/check-closes-n.js +792 -11
  27. package/dist/checks/bin/check-coherence.js +831 -15
  28. package/dist/checks/bin/check-dead-branch-push.js +353 -11
  29. package/dist/checks/bin/check-dispatch-readiness.js +996 -28
  30. package/dist/checks/bin/check-doc-coverage-push.js +449 -22
  31. package/dist/checks/bin/check-doc-coverage.js +449 -22
  32. package/dist/checks/bin/check-doctrine-portability.js +392 -15
  33. package/dist/checks/bin/check-evidence-fresh.js +462 -20
  34. package/dist/checks/bin/check-first-push-dispatch.js +898 -26
  35. package/dist/checks/bin/check-issue-assignment.js +792 -11
  36. package/dist/checks/bin/check-main-branch-refusal.js +3973 -0
  37. package/dist/checks/bin/check-no-disk-state.js +353 -11
  38. package/dist/checks/bin/check-quoted-command.js +4405 -0
  39. package/dist/checks/bin/check-reader-resolvable-prose.js +466 -20
  40. package/dist/checks/bin/check-registry-gates.js +467 -16
  41. package/dist/checks/bin/check-retired-vocabulary.js +465 -19
  42. package/dist/checks/bin/check-review-gate.js +392 -15
  43. package/dist/checks/bin/check-single-plan-pr.js +353 -11
  44. package/dist/checks/bin/check-test-plan.js +353 -11
  45. package/dist/checks/bin/check-token-collection-wired.js +3975 -0
  46. package/dist/checks/bin/check-workspace-escape.js +353 -11
  47. package/dist/index.js +1921 -420
  48. package/package.json +1 -1
  49. package/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/aeg-core/bin/verify-coherence.ts +1089 -0
  50. package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
  51. package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
  52. package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
  53. package/studio-standalone/apps/vinaya-studio/web/.next/required-server-files.json +5 -0
  54. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
  55. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
  56. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  57. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  58. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  59. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  60. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  61. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
  62. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js +2 -2
  63. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  64. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  65. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js +1 -1
  66. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
  67. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
  68. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js +2 -2
  69. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
  70. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
  71. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
  72. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js +2 -2
  73. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
  74. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
  75. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
  76. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js +3 -3
  77. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
  78. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
  79. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
  80. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js +3 -3
  81. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
  82. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
  83. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
  84. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js +3 -3
  85. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
  86. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
  87. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
  88. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js +3 -3
  89. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
  90. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
  91. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
  92. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js +3 -3
  93. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
  94. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
  95. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
  96. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js +2 -2
  97. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
  98. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
  99. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/{[externals]__0pvgf28._.js → [externals]__1agj1ww._.js} +2 -2
  100. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/[root-of-the-server]__02a-3g8._.js +1 -1
  101. 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
  102. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0053k9k._.js +1 -1
  103. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0112h-k._.js +1 -1
  104. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__040smoa._.js +1 -1
  105. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0cxtq46._.js +25 -18
  106. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0o771t1._.js +1 -1
  107. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__03jepya._.js → [root-of-the-server]__0ozbaq0._.js} +1 -1
  108. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0puovz5._.js +1 -1
  109. 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
  110. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__18kqy4q._.js +1 -1
  111. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__19jyp3m._.js +1 -1
  112. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1hs0dcu._.js +1 -1
  113. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1wc4-ip._.js +1 -1
  114. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1widio3._.js +1 -1
  115. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1yoa8tx._.js +3 -0
  116. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_03x_w6q._.js +1 -1
  117. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0gvm3og._.js +1 -1
  118. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0lwxg63._.js +1 -1
  119. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0wxycau._.js +1 -1
  120. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0x999pb._.js +1 -1
  121. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1-6rzip._.js +1 -1
  122. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +2 -2
  123. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_0zys35s._.js → _1mqrlbl._.js} +2 -2
  124. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_1c9b9j-._.js → _1mrtj0f._.js} +2 -2
  125. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1n0cnq-._.js +1 -1
  126. 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
  127. 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
  128. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
  129. package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
  130. package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
  131. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
  132. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
  133. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0r4i_ks9z02bw.js +1 -0
  134. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3noqvds-2bvrd.js → 1hfhxjy577l5d.js} +1 -1
  135. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1k916ih3p4nsg.js +1 -0
  136. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{29-foak16tjfu.js → 1t2kft7bxzuxn.js} +1 -1
  137. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{2iysedx-n06xc.js → 1ybrihl5_aocy.js} +1 -1
  138. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{30ojnsx3p4lp4.js → 1yqs2zgvwa4p1.js} +1 -1
  139. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0msr2a1i-x-br.js → 25kbt5k-xb2a5.js} +1 -1
  140. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3gehliafvmjo9.js → 2_k6j1o9q5nn4.js} +4 -4
  141. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/2hmannd4bjhzy.js +1 -0
  142. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/3mqu_uhluy19i.css +1 -0
  143. package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
  144. package/studio-standalone/apps/vinaya-studio/web/server.js +1 -1
  145. package/aeg-root/coordination.md +0 -292
  146. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1rku4gz._.js +0 -3
  147. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/25j-y57ng11hl.js +0 -1
  148. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/30lyvn0yzwdiz.js +0 -1
  149. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/35u8inxa732is.js +0 -1
  150. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/42wxpzjhw7oy4.css +0 -1
  151. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{5Lv83SnT--7YJDjSbxona → crNCHQ9uAgKOYC6DKkZma}/_buildManifest.js +0 -0
  152. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{5Lv83SnT--7YJDjSbxona → crNCHQ9uAgKOYC6DKkZma}/_clientMiddlewareManifest.js +0 -0
  153. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{5Lv83SnT--7YJDjSbxona → crNCHQ9uAgKOYC6DKkZma}/_ssgManifest.js +0 -0
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync27 } from "node:fs";
4
+ import { readFileSync as readFileSync28 } from "node:fs";
5
5
  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 = [
@@ -182,6 +198,13 @@ var LABELS = [
182
198
  form: "literal",
183
199
  carries: "Execution is halted pending an external unblock; wins over every other derived status."
184
200
  },
201
+ {
202
+ key: "backlog",
203
+ id: "vinaya/backlog",
204
+ category: "state",
205
+ form: "literal",
206
+ carries: "The Issue is deliberately unplanned — belongs to no tranche and is excluded from dispatch, until a Planner promotes it."
207
+ },
185
208
  {
186
209
  key: "tier-0",
187
210
  id: "vinaya/tier:0",
@@ -428,6 +451,47 @@ function parseFieldValue(value) {
428
451
  function projectsFromBody(body) {
429
452
  return projectFieldFromBody(body).names;
430
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
+ }
431
495
  // ../../packages/aeg-forge-state/src/github-token.ts
432
496
  import { execFile as execFile2 } from "node:child_process";
433
497
  import { promisify as promisify2 } from "node:util";
@@ -724,6 +788,207 @@ var DERIVED_STATUSES = [
724
788
  "incoherent"
725
789
  ];
726
790
  var DERIVABLE_STATUSES = DERIVED_STATUSES.filter((s) => s !== "backlog");
791
+ // ../../packages/aeg-core/src/report-tokens.ts
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;
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: "—" };
825
+ const { inputTokens, cacheCreationInputTokens, cacheReadInputTokens, outputTokens } = input.summary.components;
826
+ const tokensIn = inputTokens + cacheCreationInputTokens + cacheReadInputTokens;
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} |`;
846
+ }
847
+ function formatBreakdown(summary) {
848
+ const { inputTokens, cacheCreationInputTokens, cacheReadInputTokens, outputTokens } = summary.components;
849
+ return [
850
+ ` fresh input tokens: ${inputTokens}`,
851
+ ` cache creation tokens: ${cacheCreationInputTokens}`,
852
+ ` cache read tokens: ${cacheReadInputTokens}`,
853
+ ` output tokens: ${outputTokens}`,
854
+ ` model: ${summary.model ?? "—"}`,
855
+ ` messages summed: ${summary.messageCount}`
856
+ ].join(`
857
+ `);
858
+ }
859
+ // ../../packages/aeg-core/src/claude-code-transcript.ts
860
+ function summarizeTranscript(jsonl) {
861
+ const seen = new Set;
862
+ const components = {
863
+ inputTokens: 0,
864
+ outputTokens: 0,
865
+ cacheCreationInputTokens: 0,
866
+ cacheReadInputTokens: 0
867
+ };
868
+ let model = null;
869
+ let messageCount = 0;
870
+ for (const rawLine of jsonl.split(/\r?\n/)) {
871
+ const line = rawLine.trim();
872
+ if (!line)
873
+ continue;
874
+ let entry2;
875
+ try {
876
+ entry2 = JSON.parse(line);
877
+ } catch {
878
+ continue;
879
+ }
880
+ if (!entry2 || typeof entry2 !== "object")
881
+ continue;
882
+ const obj = entry2;
883
+ if (obj.type !== "assistant")
884
+ continue;
885
+ const message = obj.message;
886
+ const id = message?.id;
887
+ const usage = message?.usage;
888
+ if (typeof id !== "string" || !id || !usage || seen.has(id))
889
+ continue;
890
+ seen.add(id);
891
+ messageCount++;
892
+ components.inputTokens += numberOr(usage.input_tokens, 0);
893
+ components.outputTokens += numberOr(usage.output_tokens, 0);
894
+ components.cacheCreationInputTokens += numberOr(usage.cache_creation_input_tokens, 0);
895
+ components.cacheReadInputTokens += numberOr(usage.cache_read_input_tokens, 0);
896
+ const messageModel = message?.model;
897
+ if (typeof messageModel === "string" && messageModel)
898
+ model = messageModel;
899
+ }
900
+ return { components, model, messageCount };
901
+ }
902
+ function numberOr(value, fallback) {
903
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
904
+ }
905
+ function sanitizeKey(value) {
906
+ return value.replace(/[^A-Za-z0-9]+/g, "-");
907
+ }
908
+ function transcriptPointerPath(projectDir, tmpDir) {
909
+ return `${tmpDir}/claude-transcript-${sanitizeKey(projectDir)}.txt`;
910
+ }
911
+ function resolvePointer(explicitTranscriptPath, deps) {
912
+ if (explicitTranscriptPath)
913
+ return { path: explicitTranscriptPath, corroborated: true };
914
+ const projectDir = deps.env.CLAUDE_PROJECT_DIR ?? deps.cwd;
915
+ const tmpDir = deps.env.TMPDIR ?? "/tmp";
916
+ const pointerPath = transcriptPointerPath(projectDir, tmpDir);
917
+ const currentSessionId = deps.env.CLAUDE_CODE_SESSION_ID;
918
+ if (!deps.exists(pointerPath)) {
919
+ return {
920
+ pointerExisted: false,
921
+ corroborated: false,
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."
923
+ };
924
+ }
925
+ let contents;
926
+ try {
927
+ contents = deps.readFile(pointerPath).replace(/\r?\n+$/, "");
928
+ } catch (err) {
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
+ };
934
+ }
935
+ const [pointerSessionId, transcriptPath] = contents.split("\t");
936
+ if (!transcriptPath) {
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
+ };
942
+ }
943
+ if (currentSessionId && pointerSessionId && currentSessionId !== pointerSessionId) {
944
+ return {
945
+ pointerExisted: true,
946
+ corroborated: false,
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."
948
+ };
949
+ }
950
+ return { path: transcriptPath, corroborated: Boolean(currentSessionId && pointerSessionId) };
951
+ }
952
+ function resolveMeteringCapability(deps, explicitTranscriptPath) {
953
+ const resolved = resolvePointer(explicitTranscriptPath, deps);
954
+ if ("error" in resolved) {
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
+ };
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
+ };
968
+ if (!deps.exists(resolved.path)) {
969
+ return {
970
+ capable: false,
971
+ ...downstream("transcript-unreadable", `Resolved transcript path ${resolved.path} is not readable as a regular file owned by this user.`)
972
+ };
973
+ }
974
+ let jsonl;
975
+ try {
976
+ jsonl = deps.readFile(resolved.path);
977
+ } catch (err) {
978
+ return {
979
+ capable: false,
980
+ ...downstream("transcript-unreadable", `Transcript at ${resolved.path} could not be read: ${err.message}`)
981
+ };
982
+ }
983
+ const summary = summarizeTranscript(jsonl);
984
+ if (summary.messageCount === 0) {
985
+ return {
986
+ capable: false,
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.")
988
+ };
989
+ }
990
+ return { capable: true, transcriptPath: resolved.path, summary };
991
+ }
727
992
  // ../../packages/aeg-core/src/file-classify.ts
728
993
  function isCodeFile(p) {
729
994
  if (p.endsWith(".md"))
@@ -1311,6 +1576,8 @@ var EXEMPT_LITERALS = new Set([
1311
1576
  var NON_PATH_TOP_SEGMENTS = new Set(["origin", "refs", "HEAD", "vinaya", "fix"]);
1312
1577
  // ../../packages/aeg-core/src/reader-resolvable-prose.ts
1313
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"]);
1314
1581
  // ../../packages/aeg-core/src/retired-vocabulary.ts
1315
1582
  var RETIRED_PATTERNS = [
1316
1583
  String.raw`\bD-[0-9]{3}\b`,
@@ -1368,6 +1635,14 @@ function checkDirectMainPush(fact) {
1368
1635
  return { verdict: "legitimate", mergedPrNumber: first };
1369
1636
  return { verdict: "direct-push", sha: fact.sha };
1370
1637
  }
1638
+ // ../../packages/aeg-core/src/ensure-label.ts
1639
+ var LABEL_COLOR = "B60205";
1640
+ function ensureLabelExists(repoFlag, name, description, io) {
1641
+ const existing = io.listLabelNames(repoFlag);
1642
+ if (existing.includes(name))
1643
+ return;
1644
+ io.createLabel(repoFlag, name, description, LABEL_COLOR);
1645
+ }
1371
1646
  // ../../packages/aeg-core/src/dead-branch-push-audit.ts
1372
1647
  function findDeadBranchPushes(facts) {
1373
1648
  return facts.filter((f) => new Date(f.latestCommitAt).getTime() > new Date(f.resolvedAt).getTime()).map(({ branch, prNumber, prState, resolvedAt, latestCommitAt }) => ({
@@ -1395,15 +1670,15 @@ function releaseFieldFromText(text) {
1395
1670
  const raw = unwrapValue2(m[1] ?? "");
1396
1671
  return { declared: true, value: RELEASE_VALUE.test(raw) ? raw : null };
1397
1672
  }
1398
- var INTENTS_HEADING = /^#{1,6}\s*Tranche intents\s*$/im;
1399
- var NEXT_HEADING = /^#{1,6}\s+\S/m;
1400
- 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;
1401
1676
  function intentsBlock(text) {
1402
- const start = text.match(INTENTS_HEADING);
1677
+ const start = text.match(INTENTS_HEADING2);
1403
1678
  if (!start || start.index === undefined)
1404
1679
  return null;
1405
1680
  const rest = text.slice(start.index + start[0].length);
1406
- const next = rest.match(NEXT_HEADING);
1681
+ const next = rest.match(NEXT_HEADING2);
1407
1682
  const sectionEnd = next && next.index !== undefined ? next.index : rest.length;
1408
1683
  return { start: start.index, end: start.index + start[0].length + sectionEnd, section: rest.slice(0, sectionEnd) };
1409
1684
  }
@@ -1420,7 +1695,7 @@ function parseIntents(text) {
1420
1695
  const trimmed = line.trim();
1421
1696
  if (trimmed.length === 0)
1422
1697
  continue;
1423
- const m = trimmed.match(INTENT_BULLET);
1698
+ const m = trimmed.match(INTENT_BULLET2);
1424
1699
  if (!m)
1425
1700
  return { intents: [], malformed: true };
1426
1701
  intents.push({ slug: (m[1] ?? "").toLowerCase(), goal: (m[2] ?? "").trim() });
@@ -1634,6 +1909,91 @@ function buildClaudeCommandOps() {
1634
1909
  ];
1635
1910
  }
1636
1911
 
1912
+ // src/lib/claude-stop-hook-emitter.ts
1913
+ var CLAUDE_STOP_HOOK_GROUP = "Claude Code Stop hook (transcript pointer)";
1914
+ var CLAUDE_STOP_HOOK_MARKER = "track-transcript";
1915
+ var CLAUDE_STOP_HOOK_SCRIPT_PATH = ".claude/hooks/track-transcript.sh";
1916
+ var CLAUDE_SETTINGS_PATH = ".claude/settings.json";
1917
+ var SH_PREAMBLE = `#!/usr/bin/env sh
1918
+ `;
1919
+ function renderTrackTranscriptScriptBody() {
1920
+ return `# Vinaya-managed Stop hook. Records this session's transcript_path (and
1921
+ # session_id) so the token-report adapter can resolve it without scanning
1922
+ # ~/.claude/projects/ for the newest file, which silently grabs another
1923
+ # concurrent session's transcript when two worktrees are active at once.
1924
+ node -e '
1925
+ const fs = require("fs")
1926
+ let data = ""
1927
+ process.stdin.on("data", (c) => { data += c })
1928
+ process.stdin.on("end", () => {
1929
+ let hook
1930
+ try {
1931
+ hook = JSON.parse(data)
1932
+ } catch {
1933
+ return
1934
+ }
1935
+ if (!hook.transcript_path) return
1936
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd()
1937
+ const tmpDir = process.env.TMPDIR || "/tmp"
1938
+ const key = projectDir.replace(/[^A-Za-z0-9]+/g, "-")
1939
+ const pointerPath = tmpDir + "/claude-transcript-" + key + ".txt"
1940
+ const content = (hook.session_id || "") + "\\t" + hook.transcript_path + "\\n"
1941
+ const scratchPath = pointerPath + "." + process.pid + "." + Math.random().toString(36).slice(2) + ".tmp"
1942
+ try {
1943
+ fs.writeFileSync(scratchPath, content, { mode: 0o600, flag: "wx" })
1944
+ } catch {
1945
+ return
1946
+ }
1947
+ try {
1948
+ fs.renameSync(scratchPath, pointerPath)
1949
+ } catch {
1950
+ try { fs.unlinkSync(scratchPath) } catch {}
1951
+ }
1952
+ })
1953
+ '
1954
+ exit 0`;
1955
+ }
1956
+ function renderClaudeSettingsWithStopHook() {
1957
+ const settings = {
1958
+ $schema: "https://json.schemastore.org/claude-code-settings.json",
1959
+ hooks: {
1960
+ Stop: [
1961
+ {
1962
+ hooks: [
1963
+ {
1964
+ type: "command",
1965
+ command: `$CLAUDE_PROJECT_DIR/${CLAUDE_STOP_HOOK_SCRIPT_PATH}`,
1966
+ timeout: 5
1967
+ }
1968
+ ]
1969
+ }
1970
+ ]
1971
+ }
1972
+ };
1973
+ return `${JSON.stringify(settings, null, 2)}
1974
+ `;
1975
+ }
1976
+ function buildClaudeStopHookOps() {
1977
+ return [
1978
+ {
1979
+ kind: "managed-block",
1980
+ path: CLAUDE_STOP_HOOK_SCRIPT_PATH,
1981
+ marker: CLAUDE_STOP_HOOK_MARKER,
1982
+ body: renderTrackTranscriptScriptBody(),
1983
+ comment: "hash",
1984
+ hostPreamble: SH_PREAMBLE,
1985
+ mode: 493,
1986
+ group: CLAUDE_STOP_HOOK_GROUP
1987
+ },
1988
+ {
1989
+ kind: "create-file",
1990
+ path: CLAUDE_SETTINGS_PATH,
1991
+ content: renderClaudeSettingsWithStopHook(),
1992
+ group: CLAUDE_STOP_HOOK_GROUP
1993
+ }
1994
+ ];
1995
+ }
1996
+
1637
1997
  // src/lib/gemini-command-emitter.ts
1638
1998
  var GEMINI_COMMAND_GROUP = "Gemini CLI command (.gemini/commands/)";
1639
1999
  var GEMINI_COMMAND_PATH = ".gemini/commands/vinaya.toml";
@@ -2396,6 +2756,7 @@ function starterDocOwners() {
2396
2756
  }
2397
2757
  var FIXED_LABEL_METADATA = {
2398
2758
  blocked: { color: "e11d21", description: "Execution halted pending an external unblock" },
2759
+ backlog: { color: "ededed", description: "Deliberately unplanned — belongs to no tranche until promoted" },
2399
2760
  "tier-0": { color: "ededed", description: "Trivial / mechanical change" },
2400
2761
  "tier-1": { color: "c5def5", description: "Standard task — code + tests + docs" },
2401
2762
  "tier-3": { color: "d93f0b", description: "Records a decision; ratification-gated" },
@@ -2451,6 +2812,14 @@ var CODEOWNERS_NOTE = `Recommended: require review on vinaya's own workflow file
2451
2812
  Then add this flag to the branch-protection command above:
2452
2813
 
2453
2814
  -F required_pull_request_reviews.require_code_owner_reviews=true`;
2815
+ var PRINCIPALS_NOTE = `Recommended: without this, review-gate trusts nobody's verdicts on this repo.
2816
+
2817
+ Add to vinaya.config.json:
2818
+ "principals": ["<your-github-login>"]
2819
+
2820
+ Anyone reviewing a PR here (code-reviewer/security verdicts, the
2821
+ vinaya/waiver:review label) must be listed, or their comments are ignored
2822
+ and every PR reads as unreviewed. \`vinaya doctor\` reports when this is unset.`;
2454
2823
  function buildInitOps(ctx) {
2455
2824
  const ops = [];
2456
2825
  const hookMode = 493;
@@ -2563,26 +2932,45 @@ function buildInitOps(ctx) {
2563
2932
  }
2564
2933
  if (ctx.agents.has("claude")) {
2565
2934
  ops.push(...buildClaudeCommandOps());
2935
+ ops.push(...buildClaudeStopHookOps());
2566
2936
  }
2567
2937
  if (ctx.agents.has("gemini")) {
2568
2938
  ops.push(buildGeminiCommandOp());
2569
2939
  }
2940
+ if (ctx.agents.size > 0) {
2941
+ const affected = [];
2942
+ if (ctx.agents.has("claude"))
2943
+ affected.push("the `/vinaya <role>` command (.claude/commands/vinaya.md)");
2944
+ if (ctx.agents.has("gemini"))
2945
+ affected.push("the `/vinaya` command (.gemini/commands/vinaya.toml)");
2946
+ if (ctx.agents.has("skills"))
2947
+ affected.push("every .agents/skills/vinaya-*/SKILL.md file");
2948
+ ops.push({
2949
+ kind: "print",
2950
+ message: "The agent-native commands above (`vinaya doctrine`) need `vinaya` resolvable on PATH:\n\n" + ` npm install -g @attalabs/vinaya
2951
+
2952
+ ` + `Without a global install, ${affected.join(" and ")} fail "command not found" the first time an agent ` + "tries to use them.",
2953
+ group: "Agent-native commands (printed, never applied)"
2954
+ });
2955
+ }
2570
2956
  ops.push(...labelOps());
2571
2957
  ops.push({ kind: "print", message: BRANCH_PROTECTION_NOTE, group: "Branch protection (printed, never applied)" });
2572
2958
  ops.push({ kind: "print", message: CODEOWNERS_NOTE, group: "Branch protection (printed, never applied)" });
2959
+ ops.push({ kind: "print", message: PRINCIPALS_NOTE, group: "Review trust (printed, never applied)" });
2573
2960
  return ops;
2574
2961
  }
2575
2962
 
2576
2963
  // src/lib/ops.ts
2577
2964
  import { execFileSync as execFileSync3 } from "node:child_process";
2578
2965
  import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
2579
- 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";
2580
2967
 
2581
2968
  // src/lib/config.ts
2582
2969
  import { execFileSync as execFileSync2 } from "node:child_process";
2583
- 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";
2584
2972
  import { homedir } from "node:os";
2585
- import { dirname as dirname3, join as join5 } from "node:path";
2973
+ import { dirname as dirname3, join as join5, resolve } from "node:path";
2586
2974
  import { z } from "zod";
2587
2975
 
2588
2976
  // src/lib/agent-vendors.ts
@@ -2703,8 +3091,15 @@ function isSafeRepoRelPath(p) {
2703
3091
  var SafeRepoRelPath = z.string().refine(isSafeRepoRelPath, {
2704
3092
  message: "must be a repo-root-relative path with no `..` segment or absolute root"
2705
3093
  });
3094
+ var CANONICAL_HOOK_BLOCK_PREFIXES = [".git/", ".husky/", ".vinaya/hooks/", ".claude/hooks/"];
3095
+ function isCanonicalHookBlockPath(p) {
3096
+ return CANONICAL_HOOK_BLOCK_PREFIXES.some((prefix) => p.startsWith(prefix) && p.length > prefix.length);
3097
+ }
3098
+ var ManagedHookBlockPath = SafeRepoRelPath.refine(isCanonicalHookBlockPath, {
3099
+ message: `must start with one of ${CANONICAL_HOOK_BLOCK_PREFIXES.join(", ")} (byte-exact, case-sensitive)`
3100
+ });
2706
3101
  var ManagedBlockRecordSchema = z.object({
2707
- path: SafeRepoRelPath,
3102
+ path: ManagedHookBlockPath,
2708
3103
  marker: z.string(),
2709
3104
  comment: z.enum(["hash", "html"])
2710
3105
  });
@@ -2726,6 +3121,20 @@ function resolveAgentVendors(manifest) {
2726
3121
  return new Set(AGENT_VENDORS);
2727
3122
  return new Set(manifest.agents);
2728
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
+ }
2729
3138
  var VinayaConfigSchema = z.object({
2730
3139
  rings: z.object({
2731
3140
  ring1_forgeWriteInterception: z.boolean(),
@@ -2738,13 +3147,19 @@ var VinayaConfigSchema = z.object({
2738
3147
  principals: z.array(z.string()).min(1).optional(),
2739
3148
  releaseActor: z.string().min(1).optional(),
2740
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(),
2741
3155
  blastRadius: z.object({ extraDomains: z.array(z.string()).optional() }).optional(),
2742
3156
  proseGates: z.object({
2743
3157
  doctrineRoot: z.string().min(1).optional(),
2744
3158
  readerFacingPrefix: z.string().min(1).optional(),
2745
3159
  readerFacingSuffix: z.string().min(1).optional(),
2746
3160
  legacySlugDir: z.string().min(1).optional()
2747
- }).optional()
3161
+ }).optional(),
3162
+ projects: z.array(ProjectEntrySchema).optional()
2748
3163
  });
2749
3164
  var GLOBAL_VINAYA_HOME = join5(homedir(), ".vinaya");
2750
3165
  var GLOBAL_CONFIG_PATH = join5(GLOBAL_VINAYA_HOME, "config.json");
@@ -2783,6 +3198,9 @@ function globalRolesIgnoredWarning(path) {
2783
3198
  function globalReleaseActorIgnoredWarning(path) {
2784
3199
  return `${path}: "releaseActor" in the global config is ignored — releaseActor may only be declared from a repo-local vinaya.config.json.`;
2785
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
+ }
2786
3204
  function stripGlobalOnlyKeys(config, path) {
2787
3205
  if (path !== GLOBAL_CONFIG_PATH)
2788
3206
  return config;
@@ -2803,6 +3221,10 @@ function stripGlobalOnlyKeys(config, path) {
2803
3221
  console.error(`⚠ ${globalReleaseActorIgnoredWarning(path)}`);
2804
3222
  result = { ...result, releaseActor: undefined };
2805
3223
  }
3224
+ if (result.tokens) {
3225
+ console.error(`⚠ ${globalTokensCollectIgnoredWarning(path)}`);
3226
+ result = { ...result, tokens: undefined };
3227
+ }
2806
3228
  return result;
2807
3229
  }
2808
3230
  function resolvePrincipalAllowlist(config) {
@@ -2912,6 +3334,69 @@ function loadConfigChecked() {
2912
3334
  }
2913
3335
  return { ok: true, config: stripGlobalOnlyKeys(parsed.data, path) };
2914
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
+ }
2915
3400
 
2916
3401
  // src/lib/ops.ts
2917
3402
  var MARKER_NS = "vinaya:managed";
@@ -2935,9 +3420,9 @@ var GIT_DIR_PREFIX = ".git/";
2935
3420
  function resolveManagedBlockPath(repoRoot, opPath) {
2936
3421
  if (!opPath.startsWith(GIT_DIR_PREFIX))
2937
3422
  return join6(repoRoot, opPath);
2938
- return join6(gitCommonDir(repoRoot), opPath.slice(GIT_DIR_PREFIX.length));
3423
+ return join6(gitCommonDir2(repoRoot), opPath.slice(GIT_DIR_PREFIX.length));
2939
3424
  }
2940
- function gitCommonDir(repoRoot) {
3425
+ function gitCommonDir2(repoRoot) {
2941
3426
  try {
2942
3427
  const commonDir = execFileSync3("git", ["-C", repoRoot, "rev-parse", "--git-common-dir"], {
2943
3428
  encoding: "utf8"
@@ -2950,8 +3435,8 @@ function gitCommonDir(repoRoot) {
2950
3435
  }
2951
3436
  }
2952
3437
  function containedAbs(repoRoot, relPath) {
2953
- const root = resolve(repoRoot);
2954
- const target = resolve(root, relPath);
3438
+ const root = resolve2(repoRoot);
3439
+ const target = resolve2(root, relPath);
2955
3440
  if (target === root)
2956
3441
  return null;
2957
3442
  return target === root || target.startsWith(root + sep2) ? target : null;
@@ -2959,9 +3444,9 @@ function containedAbs(repoRoot, relPath) {
2959
3444
  function containedManagedBlockAbs(repoRoot, relPath) {
2960
3445
  if (!relPath.startsWith(GIT_DIR_PREFIX))
2961
3446
  return containedAbs(repoRoot, relPath);
2962
- const commonDir = gitCommonDir(repoRoot);
2963
- const hooksRoot = resolve(join6(commonDir, "hooks"));
2964
- 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)));
2965
3450
  if (target === hooksRoot)
2966
3451
  return null;
2967
3452
  return target.startsWith(hooksRoot + sep2) ? target : null;
@@ -3497,8 +3982,8 @@ function prompt(question) {
3497
3982
  setupStdinReader();
3498
3983
  if (stdinEnded && pendingResolvers.length === 0)
3499
3984
  return Promise.resolve("");
3500
- return new Promise((resolve2) => {
3501
- pendingResolvers.push(resolve2);
3985
+ return new Promise((resolve3) => {
3986
+ pendingResolvers.push(resolve3);
3502
3987
  if (stdinEnded)
3503
3988
  flushLines();
3504
3989
  });
@@ -3517,15 +4002,290 @@ function closeStdin() {
3517
4002
  }
3518
4003
  }
3519
4004
 
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 };
4064
+ }
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
+ };
4076
+ }
4077
+ function declaredSummary(components, model) {
4078
+ return { components, model, messageCount: 1 };
4079
+ }
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}).`);
4093
+ }
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);
4111
+ }
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.");
4116
+ }
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.");
4120
+ }
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.");
4124
+ }
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}.`);
4128
+ }
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.");
4136
+ }
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.");
4142
+ }
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.");
4146
+ }
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}`);
4153
+ }
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
+
3520
4264
  // src/commands/archive.ts
3521
4265
  function ring2Accelerated() {
3522
4266
  return loadConfig()?.rings?.ring2_asyncAudits === true;
3523
4267
  }
3524
- function realDeps() {
3525
- return { detectRepo: detectGitRepo };
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 };
3526
4286
  }
3527
4287
  function sh(args, input) {
3528
- return execFileSync4(args[0], args.slice(1), { encoding: "utf8", input }).trim();
4288
+ return execFileSync5(args[0], args.slice(1), { encoding: "utf8", input, env: process.env }).trim();
3529
4289
  }
3530
4290
  function shJson(args) {
3531
4291
  return JSON.parse(sh(args));
@@ -3608,13 +4368,23 @@ async function runArchive(args, deps) {
3608
4368
  comments
3609
4369
  };
3610
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}`;
3611
4377
  process.stdout.write(`[vinaya archive] posting provenance block to PR #${pr.number}...
3612
4378
  `);
3613
- sh(["gh", "pr", "comment", String(pr.number), "-R", repoFlag, "--body-file", "-"], block);
4379
+ sh(["gh", "pr", "comment", String(pr.number), "-R", repoFlag, "--body-file", "-"], blockWithTokens);
3614
4380
  process.stdout.write(`[vinaya archive] provenance block posted to PR #${pr.number}.
3615
4381
  `);
3616
4382
  if (dangling.length > 0) {
3617
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}
3618
4388
  `);
3619
4389
  }
3620
4390
  if (issue !== null) {
@@ -3646,7 +4416,7 @@ async function runArchive(args, deps) {
3646
4416
  return 0;
3647
4417
  }
3648
4418
  async function archiveCommand(args) {
3649
- process.exit(await runArchive(args, realDeps()));
4419
+ process.exit(await runArchive(args, realDeps2()));
3650
4420
  }
3651
4421
  function parseTrancheArgs(args) {
3652
4422
  const yes = args.includes("--yes");
@@ -3725,19 +4495,19 @@ async function runArchiveTranche(args, deps) {
3725
4495
  return 0;
3726
4496
  }
3727
4497
  async function archiveTrancheCommand(args) {
3728
- process.exit(await runArchiveTranche(args, realDeps()));
4498
+ process.exit(await runArchiveTranche(args, realDeps2()));
3729
4499
  }
3730
4500
 
3731
4501
  // src/commands/audit.ts
3732
- import { execFileSync as execFileSync5 } from "node:child_process";
4502
+ import { execFileSync as execFileSync6 } from "node:child_process";
3733
4503
  function ring2Accelerated2() {
3734
4504
  return loadConfig()?.rings?.ring2_asyncAudits === true;
3735
4505
  }
3736
- function realDeps2() {
4506
+ function realDeps3() {
3737
4507
  return { detectRepo: detectGitRepo };
3738
4508
  }
3739
4509
  function defaultSleep(ms) {
3740
- return new Promise((resolve2) => setTimeout(resolve2, ms));
4510
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
3741
4511
  }
3742
4512
  var COMMAND_TIMEOUT_MS = 20000;
3743
4513
  var DEAD_BRANCH_LABEL = label("dead-branch-push");
@@ -3748,7 +4518,7 @@ var DIRECT_PUSH_MARKER = `<!-- ${DIRECT_PUSH_LABEL} -->`;
3748
4518
  var DIRECT_PUSH_LABEL_DESCRIPTION = "A commit landed on main with no associated merged PR (ring-2 detection)";
3749
4519
  function shSoft(args, input) {
3750
4520
  try {
3751
- return execFileSync5(args[0], args.slice(1), {
4521
+ return execFileSync6(args[0], args.slice(1), {
3752
4522
  encoding: "utf8",
3753
4523
  input,
3754
4524
  timeout: COMMAND_TIMEOUT_MS
@@ -3772,7 +4542,7 @@ function shSoftJson(args) {
3772
4542
  }
3773
4543
  }
3774
4544
  function sh2(args, input) {
3775
- return execFileSync5(args[0], args.slice(1), { encoding: "utf8", input }).trim();
4545
+ return execFileSync6(args[0], args.slice(1), { encoding: "utf8", input }).trim();
3776
4546
  }
3777
4547
  function listTaskBranches() {
3778
4548
  const out = shSoft(["git", "ls-remote", "--heads", "origin", "task/*"]);
@@ -3828,11 +4598,15 @@ function gatherDeadBranchFacts(repoFlag) {
3828
4598
  }
3829
4599
  return facts;
3830
4600
  }
3831
- function ensureLabelExists(repoFlag, name, description) {
4601
+ function listLabelNames(repoFlag) {
3832
4602
  const existing = shSoftJson(["gh", "label", "list", "-R", repoFlag, "--json", "name", "--limit", "200"]) ?? [];
3833
- if (existing.some((l) => l.name === name))
3834
- return;
3835
- shSoft(["gh", "label", "create", name, "-R", repoFlag, "--color", "B60205", "--description", description]);
4603
+ return existing.map((l) => l.name);
4604
+ }
4605
+ function createLabel(repoFlag, name, description, color) {
4606
+ shSoft(["gh", "label", "create", name, "-R", repoFlag, "--color", color, "--description", description]);
4607
+ }
4608
+ function ensureLabelExists2(repoFlag, name, description) {
4609
+ ensureLabelExists(repoFlag, name, description, { listLabelNames, createLabel });
3836
4610
  }
3837
4611
  function commentIdFromUrl(url) {
3838
4612
  const match = url.match(/issuecomment-(\d+)/);
@@ -3870,7 +4644,7 @@ function runDeadBranchAudit(repoFlag) {
3870
4644
  const facts = gatherDeadBranchFacts(repoFlag);
3871
4645
  const findings = findDeadBranchPushes(facts);
3872
4646
  if (findings.length > 0) {
3873
- ensureLabelExists(repoFlag, DEAD_BRANCH_LABEL, DEAD_BRANCH_LABEL_DESCRIPTION);
4647
+ ensureLabelExists2(repoFlag, DEAD_BRANCH_LABEL, DEAD_BRANCH_LABEL_DESCRIPTION);
3874
4648
  for (const finding of findings)
3875
4649
  flagDeadBranchPush(finding, repoFlag);
3876
4650
  }
@@ -3918,7 +4692,7 @@ function incidentAlreadyOpen(sha, repoFlag) {
3918
4692
  return issues.length > 0;
3919
4693
  }
3920
4694
  function openDirectPushIncident(sha, repoFlag) {
3921
- ensureLabelExists(repoFlag, DIRECT_PUSH_LABEL, DIRECT_PUSH_LABEL_DESCRIPTION);
4695
+ ensureLabelExists2(repoFlag, DIRECT_PUSH_LABEL, DIRECT_PUSH_LABEL_DESCRIPTION);
3922
4696
  if (incidentAlreadyOpen(sha, repoFlag)) {
3923
4697
  console.error(`[vinaya audit] incident already open for ${sha} — not duplicating.`);
3924
4698
  return;
@@ -4024,11 +4798,11 @@ async function runAudit(args, deps) {
4024
4798
  return failed ? 1 : 0;
4025
4799
  }
4026
4800
  async function auditCommand(args) {
4027
- process.exit(await runAudit(args, realDeps2()));
4801
+ process.exit(await runAudit(args, realDeps3()));
4028
4802
  }
4029
4803
 
4030
4804
  // src/commands/check.ts
4031
- import { execFileSync as execFileSync6 } from "node:child_process";
4805
+ import { execFileSync as execFileSync7 } from "node:child_process";
4032
4806
  import { dirname as dirname5 } from "node:path";
4033
4807
 
4034
4808
  // src/checks/contract.ts
@@ -4039,11 +4813,11 @@ function emitCheckError(error) {
4039
4813
  }
4040
4814
 
4041
4815
  // src/checks/registry.ts
4042
- import { existsSync as existsSync7 } from "node:fs";
4816
+ import { existsSync as existsSync8 } from "node:fs";
4043
4817
  import { join as join8 } from "node:path";
4044
4818
  var DIST_BIN_DIR = join8(packageRoot(import.meta.url), "dist", "checks", "bin");
4045
4819
  var SRC_BIN_DIR = join8(packageRoot(import.meta.url), "src", "checks", "bin");
4046
- 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;
4047
4821
  var BIN_EXT = BIN_DIR === DIST_BIN_DIR ? ".js" : ".ts";
4048
4822
  function bin(name) {
4049
4823
  return join8(BIN_DIR, `${name}${BIN_EXT}`);
@@ -4051,8 +4825,8 @@ function bin(name) {
4051
4825
  function runsUnderAll(spec) {
4052
4826
  return !spec.ownWorkflow;
4053
4827
  }
4054
- function coreCheckRegistry() {
4055
- return [
4828
+ var REGISTRY = [
4829
+ [
4056
4830
  {
4057
4831
  name: "brief-shape",
4058
4832
  run: bin("check-brief-shape"),
@@ -4063,6 +4837,9 @@ function coreCheckRegistry() {
4063
4837
  BRANCH: { optional: true }
4064
4838
  }
4065
4839
  },
4840
+ 0
4841
+ ],
4842
+ [
4066
4843
  {
4067
4844
  name: "doc-coverage",
4068
4845
  run: bin("check-doc-coverage"),
@@ -4078,6 +4855,9 @@ function coreCheckRegistry() {
4078
4855
  GH_TOKEN: { optional: true }
4079
4856
  }
4080
4857
  },
4858
+ 0
4859
+ ],
4860
+ [
4081
4861
  {
4082
4862
  name: "coherence",
4083
4863
  run: bin("check-coherence"),
@@ -4090,6 +4870,9 @@ function coreCheckRegistry() {
4090
4870
  GH_TOKEN: { optional: true }
4091
4871
  }
4092
4872
  },
4873
+ 0
4874
+ ],
4875
+ [
4093
4876
  {
4094
4877
  name: "dispatch-readiness",
4095
4878
  run: bin("check-dispatch-readiness"),
@@ -4099,9 +4882,13 @@ function coreCheckRegistry() {
4099
4882
  AEG_REPO: { optional: true },
4100
4883
  BRANCH: { optional: true },
4101
4884
  GITHUB_TOKEN: { optional: true },
4102
- GH_TOKEN: { optional: true }
4885
+ GH_TOKEN: { optional: true },
4886
+ PREMISE_FILE: { optional: true }
4103
4887
  }
4104
4888
  },
4889
+ 0
4890
+ ],
4891
+ [
4105
4892
  {
4106
4893
  name: "closes-n",
4107
4894
  run: bin("check-closes-n"),
@@ -4116,6 +4903,9 @@ function coreCheckRegistry() {
4116
4903
  GH_TOKEN: { optional: true }
4117
4904
  }
4118
4905
  },
4906
+ 1
4907
+ ],
4908
+ [
4119
4909
  {
4120
4910
  name: "single-plan-pr",
4121
4911
  run: bin("check-single-plan-pr"),
@@ -4128,6 +4918,9 @@ function coreCheckRegistry() {
4128
4918
  GH_TOKEN: { optional: true }
4129
4919
  }
4130
4920
  },
4921
+ 0
4922
+ ],
4923
+ [
4131
4924
  {
4132
4925
  name: "test-plan",
4133
4926
  run: bin("check-test-plan"),
@@ -4139,6 +4932,9 @@ function coreCheckRegistry() {
4139
4932
  BRANCH: { optional: true }
4140
4933
  }
4141
4934
  },
4935
+ 1
4936
+ ],
4937
+ [
4142
4938
  {
4143
4939
  name: "body-bare-digits",
4144
4940
  run: bin("check-body-bare-digits"),
@@ -4154,6 +4950,9 @@ function coreCheckRegistry() {
4154
4950
  GH_TOKEN: { optional: true }
4155
4951
  }
4156
4952
  },
4953
+ 1
4954
+ ],
4955
+ [
4157
4956
  {
4158
4957
  name: "no-disk-state",
4159
4958
  run: bin("check-no-disk-state"),
@@ -4161,6 +4960,9 @@ function coreCheckRegistry() {
4161
4960
  timeoutMs: 15000,
4162
4961
  env: { BASE_SHA: { optional: true } }
4163
4962
  },
4963
+ 0
4964
+ ],
4965
+ [
4164
4966
  {
4165
4967
  name: "registry-gates",
4166
4968
  run: bin("check-registry-gates"),
@@ -4172,6 +4974,9 @@ function coreCheckRegistry() {
4172
4974
  GH_TOKEN: { optional: true }
4173
4975
  }
4174
4976
  },
4977
+ 0
4978
+ ],
4979
+ [
4175
4980
  {
4176
4981
  name: "review-gate",
4177
4982
  run: bin("check-review-gate"),
@@ -4185,6 +4990,9 @@ function coreCheckRegistry() {
4185
4990
  GH_TOKEN: { optional: true }
4186
4991
  }
4187
4992
  },
4993
+ 1
4994
+ ],
4995
+ [
4188
4996
  {
4189
4997
  name: "branch-topology",
4190
4998
  run: bin("check-branch-topology"),
@@ -4197,6 +5005,9 @@ function coreCheckRegistry() {
4197
5005
  GH_TOKEN: { optional: true }
4198
5006
  }
4199
5007
  },
5008
+ 0
5009
+ ],
5010
+ [
4200
5011
  {
4201
5012
  name: "dead-branch-push",
4202
5013
  run: bin("check-dead-branch-push"),
@@ -4208,6 +5019,9 @@ function coreCheckRegistry() {
4208
5019
  GH_TOKEN: { optional: true }
4209
5020
  }
4210
5021
  },
5022
+ 0
5023
+ ],
5024
+ [
4211
5025
  {
4212
5026
  name: "first-push-dispatch",
4213
5027
  run: bin("check-first-push-dispatch"),
@@ -4220,6 +5034,9 @@ function coreCheckRegistry() {
4220
5034
  GH_TOKEN: { optional: true }
4221
5035
  }
4222
5036
  },
5037
+ 0
5038
+ ],
5039
+ [
4223
5040
  {
4224
5041
  name: "doc-coverage-push",
4225
5042
  run: bin("check-doc-coverage-push"),
@@ -4237,6 +5054,9 @@ function coreCheckRegistry() {
4237
5054
  GH_TOKEN: { optional: true }
4238
5055
  }
4239
5056
  },
5057
+ 0
5058
+ ],
5059
+ [
4240
5060
  {
4241
5061
  name: "issue-assignment",
4242
5062
  run: bin("check-issue-assignment"),
@@ -4249,6 +5069,9 @@ function coreCheckRegistry() {
4249
5069
  GH_TOKEN: { optional: true }
4250
5070
  }
4251
5071
  },
5072
+ 0
5073
+ ],
5074
+ [
4252
5075
  {
4253
5076
  name: "evidence-fresh",
4254
5077
  run: bin("check-evidence-fresh"),
@@ -4263,6 +5086,9 @@ function coreCheckRegistry() {
4263
5086
  GH_TOKEN: { optional: true }
4264
5087
  }
4265
5088
  },
5089
+ 1
5090
+ ],
5091
+ [
4266
5092
  {
4267
5093
  name: "reader-resolvable-prose",
4268
5094
  run: bin("check-reader-resolvable-prose"),
@@ -4270,6 +5096,9 @@ function coreCheckRegistry() {
4270
5096
  timeoutMs: 30000,
4271
5097
  env: {}
4272
5098
  },
5099
+ 0
5100
+ ],
5101
+ [
4273
5102
  {
4274
5103
  name: "retired-vocabulary",
4275
5104
  run: bin("check-retired-vocabulary"),
@@ -4277,6 +5106,9 @@ function coreCheckRegistry() {
4277
5106
  timeoutMs: 30000,
4278
5107
  env: {}
4279
5108
  },
5109
+ 0
5110
+ ],
5111
+ [
4280
5112
  {
4281
5113
  name: "doctrine-portability",
4282
5114
  run: bin("check-doctrine-portability"),
@@ -4284,15 +5116,66 @@ function coreCheckRegistry() {
4284
5116
  timeoutMs: 30000,
4285
5117
  env: { BASE_SHA: { optional: true } }
4286
5118
  },
5119
+ 0
5120
+ ],
5121
+ [
4287
5122
  {
4288
5123
  name: "workspace-escape",
4289
5124
  run: bin("check-workspace-escape"),
4290
5125
  scope: "full",
4291
5126
  timeoutMs: 30000,
4292
5127
  env: {}
4293
- }
4294
- ];
5128
+ },
5129
+ 0
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
+ ],
5151
+ [
5152
+ {
5153
+ name: "main-branch-refusal",
5154
+ run: bin("check-main-branch-refusal"),
5155
+ scope: "full",
5156
+ timeoutMs: 15000,
5157
+ env: {}
5158
+ },
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
5173
+ ]
5174
+ ];
5175
+ function coreCheckRegistry() {
5176
+ return REGISTRY.map(([spec]) => spec);
4295
5177
  }
5178
+ var CORE_CHECK_RING = Object.fromEntries(REGISTRY.map(([spec, ring]) => [spec.name, ring]));
4296
5179
 
4297
5180
  // src/checks/resolver.ts
4298
5181
  var NAMESPACE_SEGMENT = /^[a-z0-9][a-z0-9-]*$/;
@@ -4494,16 +5377,16 @@ async function runOne(spec, timeoutMs, callerEnv) {
4494
5377
  const timer = setTimeout(() => {
4495
5378
  timedOut = true;
4496
5379
  killTree("SIGTERM");
4497
- escalationDone = new Promise((resolve2) => {
5380
+ escalationDone = new Promise((resolve3) => {
4498
5381
  const graceTimer = setTimeout(() => {
4499
5382
  const pid = proc.pid;
4500
5383
  if (pid !== undefined && groupStillAlive(pid))
4501
5384
  killTree("SIGKILL");
4502
- resolve2();
5385
+ resolve3();
4503
5386
  }, KILL_GRACE_MS);
4504
5387
  cancelEscalation = () => {
4505
5388
  clearTimeout(graceTimer);
4506
- resolve2();
5389
+ resolve3();
4507
5390
  };
4508
5391
  });
4509
5392
  }, timeoutMs);
@@ -4514,11 +5397,11 @@ async function runOne(spec, timeoutMs, callerEnv) {
4514
5397
  });
4515
5398
  proc.stdout.resume();
4516
5399
  let spawnError;
4517
- const exitCode = await new Promise((resolve2) => {
4518
- proc.on("close", (code) => resolve2(code));
5400
+ const exitCode = await new Promise((resolve3) => {
5401
+ proc.on("close", (code) => resolve3(code));
4519
5402
  proc.on("error", (err) => {
4520
5403
  spawnError = err;
4521
- resolve2(null);
5404
+ resolve3(null);
4522
5405
  });
4523
5406
  });
4524
5407
  clearTimeout(timer);
@@ -4606,7 +5489,7 @@ async function runChecks(specs, opts) {
4606
5489
  }
4607
5490
 
4608
5491
  // src/roles/plan.ts
4609
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "node:fs";
5492
+ import { existsSync as existsSync9, readFileSync as readFileSync9 } from "node:fs";
4610
5493
  import { join as join10 } from "node:path";
4611
5494
 
4612
5495
  // ../../packages/sources/src/commands.ts
@@ -4761,6 +5644,18 @@ var COMMANDS = [
4761
5644
  ],
4762
5645
  status: "shipped"
4763
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
+ },
4764
5659
  {
4765
5660
  name: "issue create",
4766
5661
  description: "Open an issue after full brief-schema validation",
@@ -4837,6 +5732,19 @@ var COMMANDS = [
4837
5732
  ],
4838
5733
  status: "shipped"
4839
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
+ },
4840
5748
  {
4841
5749
  name: "review post",
4842
5750
  description: "Render, post, and self-verify a code-reviewer or security-review verdict comment on a PR",
@@ -4889,6 +5797,28 @@ var COMMANDS = [
4889
5797
  ],
4890
5798
  status: "shipped"
4891
5799
  },
5800
+ {
5801
+ name: "tokens",
5802
+ description: "Print a role's `Tokens: …` report line — the portable front door over the collection adapter",
5803
+ flags: [
5804
+ { flag: "--phase", description: 'e.g. `"<task-id>: develop"` — required' },
5805
+ { flag: "--role", description: "e.g. `Developer` — required" },
5806
+ { flag: "--model", description: "Overrides the model id the adapter derived, if given" },
5807
+ {
5808
+ flag: "--transcript",
5809
+ description: "Read this transcript directly. Supported primary route — use it whenever you know which transcript " + "is yours, and always in a repo with no track-transcript.sh hook. Omitted resolves via the Stop-hook " + "pointer file, if this repo installs that hook."
5810
+ },
5811
+ {
5812
+ flag: "--in / --out",
5813
+ description: "Manual entry: the exact token figures, for a host whose usage arrives by some other means than a " + "Claude Code transcript. Both required together; skips transcript resolution entirely."
5814
+ }
5815
+ ],
5816
+ details: [
5817
+ "Resolves the Claude Code collection adapter (`resolveMeteringCapability`, `summarizeTranscript`, `formatTokensLine`) from the INSTALLED `@attalabs/aeg-core` package, never by a repo-relative path — the fix for `packages/aeg-core/bin/report-tokens.ts` not existing in an adopter repo with no local `packages/`.",
5818
+ "Refuses — never emits a `0/0/—` line — when no transcript resolves, a resolved transcript can't be read, or it summarizes to zero usage records (empty, unparseable, or not yet flushed to disk). Capability is probed by actually attempting resolution, never declared from the host being Claude Code."
5819
+ ],
5820
+ status: "shipped"
5821
+ },
4892
5822
  {
4893
5823
  name: "doctrine",
4894
5824
  description: "Print the absolute path of the bundled doctrine's front door (aeg-root/skills/aeg/SKILL.md) on this machine",
@@ -5002,7 +5932,7 @@ var COMMANDS = [
5002
5932
  }
5003
5933
  ];
5004
5934
  // ../../packages/sources/src/doctrine-file-adapter.ts
5005
- import { readdirSync as readdirSync4, readFileSync as readFileSync7 } from "node:fs";
5935
+ import { readdirSync as readdirSync4, readFileSync as readFileSync8 } from "node:fs";
5006
5936
  import { join as join9 } from "node:path";
5007
5937
 
5008
5938
  // ../../packages/sources/src/file-adapter.ts
@@ -5013,14 +5943,14 @@ function readMarkdownDir(root, dir) {
5013
5943
  const dirPath = join9(root, dir);
5014
5944
  return readdirSync4(dirPath).filter((f) => f.endsWith(".md")).sort().map((file) => {
5015
5945
  const path = join9(dir, file);
5016
- return { path, content: readFileSync7(join9(root, path), "utf-8") };
5946
+ return { path, content: readFileSync8(join9(root, path), "utf-8") };
5017
5947
  });
5018
5948
  }
5019
5949
  function createFileDoctrineSource(config = {}) {
5020
5950
  const root = config.root ?? DEFAULT_GOVERNANCE_ROOT;
5021
5951
  return {
5022
5952
  async getDoctrine() {
5023
- const enforcement = readFileSync7(join9(root, "enforcement.md"), "utf-8");
5953
+ const enforcement = readFileSync8(join9(root, "enforcement.md"), "utf-8");
5024
5954
  const roles = readMarkdownDir(root, "roles");
5025
5955
  const contracts = readMarkdownDir(root, "contracts");
5026
5956
  return { enforcement, roles, contracts };
@@ -5195,11 +6125,11 @@ function resolveRoles(core, configRoles) {
5195
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.";
5196
6126
  function readContract(configDir, contractPath) {
5197
6127
  const absolute = join10(configDir, contractPath);
5198
- if (!existsSync8(absolute)) {
6128
+ if (!existsSync9(absolute)) {
5199
6129
  return { ok: false, errors: [`contract file "${contractPath}" does not exist (resolved to "${absolute}")`] };
5200
6130
  }
5201
6131
  try {
5202
- return validateRoleContract(readFileSync8(absolute, "utf-8"));
6132
+ return validateRoleContract(readFileSync9(absolute, "utf-8"));
5203
6133
  } catch (err) {
5204
6134
  return { ok: false, errors: [`could not read "${contractPath}": ${err.message}`] };
5205
6135
  }
@@ -5232,7 +6162,7 @@ async function buildRolePlan(configDir, configRoles) {
5232
6162
  // src/commands/check.ts
5233
6163
  function git(args) {
5234
6164
  try {
5235
- return execFileSync6("git", args, { encoding: "utf8" }).trim();
6165
+ return execFileSync7("git", args, { encoding: "utf8" }).trim();
5236
6166
  } catch {
5237
6167
  return "";
5238
6168
  }
@@ -5470,7 +6400,7 @@ ${renderRolesTable(rolePlan)}
5470
6400
  }
5471
6401
 
5472
6402
  // src/commands/commit-msg.ts
5473
- import { readFileSync as readFileSync9 } from "node:fs";
6403
+ import { readFileSync as readFileSync10 } from "node:fs";
5474
6404
  function commitMsgCommand(args) {
5475
6405
  const [messageFile, source] = args;
5476
6406
  if (!messageFile) {
@@ -5481,7 +6411,7 @@ function commitMsgCommand(args) {
5481
6411
  }
5482
6412
  if (source === "merge")
5483
6413
  return;
5484
- const firstLine2 = readFileSync9(messageFile, "utf-8").split(`
6414
+ const firstLine2 = readFileSync10(messageFile, "utf-8").split(`
5485
6415
  `, 1)[0] ?? "";
5486
6416
  if (COMMIT_TYPE_STYLE.test(firstLine2))
5487
6417
  return;
@@ -5493,14 +6423,14 @@ function commitMsgCommand(args) {
5493
6423
  }
5494
6424
 
5495
6425
  // src/commands/demo.ts
5496
- import { execFileSync as execFileSync7, spawnSync } from "node:child_process";
5497
- 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";
5498
6428
  import { isAbsolute as isAbsolute2, join as join11 } from "node:path";
5499
6429
  var DEMO_BRANCH_PREFIX = "vinaya/demo-break-";
5500
6430
  var FIXTURE_PATH = ".vinaya-demo-brief.md";
5501
6431
  var STATE_FILENAME = "vinaya-demo-state.json";
5502
6432
  function gitCapture(repoRoot, args) {
5503
- 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();
5504
6434
  }
5505
6435
  function currentBranchName(repoRoot) {
5506
6436
  try {
@@ -5511,7 +6441,7 @@ function currentBranchName(repoRoot) {
5511
6441
  }
5512
6442
  function branchExists(repoRoot, name) {
5513
6443
  try {
5514
- execFileSync7("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${name}`], {
6444
+ execFileSync8("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${name}`], {
5515
6445
  cwd: repoRoot,
5516
6446
  stdio: ["ignore", "pipe", "pipe"]
5517
6447
  });
@@ -5537,9 +6467,9 @@ function gitDirAbs(repoRoot) {
5537
6467
  function recoverFromCrash(repoRoot, statePath) {
5538
6468
  const currentBranch = currentBranchName(repoRoot);
5539
6469
  let state = null;
5540
- if (existsSync9(statePath)) {
6470
+ if (existsSync10(statePath)) {
5541
6471
  try {
5542
- state = JSON.parse(readFileSync10(statePath, "utf-8"));
6472
+ state = JSON.parse(readFileSync11(statePath, "utf-8"));
5543
6473
  } catch {
5544
6474
  state = null;
5545
6475
  }
@@ -5609,14 +6539,14 @@ function cleanup(repoRoot, originalBranch, demoBranch, statePath) {
5609
6539
  gitCapture(repoRoot, ["clean", "-fd"]);
5610
6540
  gitCapture(repoRoot, ["checkout", originalBranch]);
5611
6541
  gitCapture(repoRoot, ["branch", "-D", demoBranch]);
5612
- if (existsSync9(statePath))
6542
+ if (existsSync10(statePath))
5613
6543
  rmSync2(statePath, { force: true });
5614
6544
  }
5615
6545
  async function runDemoBreak(repoRoot, args) {
5616
6546
  const keep = args.includes("--keep");
5617
6547
  const hookDir = resolveHookDir(repoRoot);
5618
6548
  const hookPath = resolveManagedBlockPath(repoRoot, `${hookDir}/pre-commit`);
5619
- if (!existsSync9(hookPath)) {
6549
+ if (!existsSync10(hookPath)) {
5620
6550
  console.error("Vinaya hooks are not installed in this repo. Run `vinaya init` first.");
5621
6551
  return 1;
5622
6552
  }
@@ -5686,7 +6616,7 @@ Attempting to commit…
5686
6616
  process.stdout.write(`✓ Commit passed — the fix worked.
5687
6617
  `);
5688
6618
  if (keep) {
5689
- if (existsSync9(statePath))
6619
+ if (existsSync10(statePath))
5690
6620
  unlinkSync(statePath);
5691
6621
  process.stdout.write(`
5692
6622
  --keep: leaving \`${demoBranch}\` checked out for you to inspect. Clean up with:
@@ -5713,12 +6643,12 @@ async function demoBreakCommand(args) {
5713
6643
  }
5714
6644
 
5715
6645
  // src/commands/doctor.ts
5716
- import { execFileSync as execFileSync8 } from "node:child_process";
5717
- import { existsSync as existsSync11, readdirSync as readdirSync6, readFileSync as readFileSync13, statSync as statSync2 } from "node:fs";
5718
- import { 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";
5719
6649
 
5720
6650
  // src/lib/self-host.ts
5721
- 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";
5722
6652
  import { join as join12, sep as sep3 } from "node:path";
5723
6653
  var VINAYA_PACKAGE_NAME = "@attalabs/vinaya";
5724
6654
  var DEFAULT_BIN = "dist/index.js";
@@ -5737,7 +6667,7 @@ function isSafeRelPath(p) {
5737
6667
  }
5738
6668
  function readPackageJson(path) {
5739
6669
  try {
5740
- const parsed = JSON.parse(readFileSync11(path, "utf-8"));
6670
+ const parsed = JSON.parse(readFileSync12(path, "utf-8"));
5741
6671
  return parsed && typeof parsed === "object" ? parsed : null;
5742
6672
  } catch {
5743
6673
  return null;
@@ -5791,8 +6721,8 @@ function expandPattern(repoRoot, pattern) {
5791
6721
  }
5792
6722
  function resolvesInsideRepo(repoRoot, rel) {
5793
6723
  try {
5794
- const root = realpathSync(repoRoot);
5795
- const target = realpathSync(join12(repoRoot, rel));
6724
+ const root = realpathSync2(repoRoot);
6725
+ const target = realpathSync2(join12(repoRoot, rel));
5796
6726
  return target === root || target.startsWith(`${root}${sep3}`);
5797
6727
  } catch {
5798
6728
  return false;
@@ -5832,18 +6762,18 @@ function detectVendoredVinaya(repoRoot) {
5832
6762
  }
5833
6763
 
5834
6764
  // src/lib/env-lint.ts
5835
- import { existsSync as existsSync10, readFileSync as readFileSync12 } from "node:fs";
6765
+ import { existsSync as existsSync11, readFileSync as readFileSync13 } from "node:fs";
5836
6766
  var ENV_READ_PATTERN = /\b(?:process\.env|Bun\.env|Deno\.env)\b/;
5837
6767
  function checksMissingEnvDeclaration(specs) {
5838
6768
  const names = [];
5839
6769
  for (const spec of specs) {
5840
6770
  if (spec.env)
5841
6771
  continue;
5842
- if (!existsSync10(spec.run))
6772
+ if (!existsSync11(spec.run))
5843
6773
  continue;
5844
6774
  let source;
5845
6775
  try {
5846
- source = readFileSync12(spec.run, "utf8");
6776
+ source = readFileSync13(spec.run, "utf8");
5847
6777
  } catch {
5848
6778
  continue;
5849
6779
  }
@@ -5856,12 +6786,143 @@ function envDeclarationWarning(name) {
5856
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).`;
5857
6787
  }
5858
6788
 
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) |`;
6796
+ }
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
+
5859
6920
  // src/commands/doctor.ts
5860
6921
  function readVersion() {
5861
- const pkg = JSON.parse(readFileSync13(join13(packageRoot(import.meta.url), "package.json"), "utf-8"));
6922
+ const pkg = JSON.parse(readFileSync15(join14(packageRoot(import.meta.url), "package.json"), "utf-8"));
5862
6923
  return pkg.version;
5863
6924
  }
5864
- function realDeps3() {
6925
+ function realDeps4() {
5865
6926
  return {
5866
6927
  detectRepo: detectGitRepo,
5867
6928
  ghAuthStatus,
@@ -5870,7 +6931,13 @@ function realDeps3() {
5870
6931
  readHooksPath: readCoreHooksPath,
5871
6932
  nodeVersion: () => process.version,
5872
6933
  bunVersion: () => typeof Bun === "undefined" ? null : Bun.version,
5873
- packageVersion: readVersion
6934
+ packageVersion: readVersion,
6935
+ meteringCapability: () => resolveMeteringCapability({
6936
+ env: process.env,
6937
+ cwd: process.cwd(),
6938
+ exists: existsSync13,
6939
+ readFile: (path) => readFileSync15(path, "utf8")
6940
+ })
5874
6941
  };
5875
6942
  }
5876
6943
  var ok = (check, message) => ({ check, severity: "ok", message });
@@ -5878,12 +6945,12 @@ var info = (check, message) => ({ check, severity: "info", message });
5878
6945
  var warn = (check, message) => ({ check, severity: "warn", message });
5879
6946
  var error = (check, message) => ({ check, severity: "error", message });
5880
6947
  function readConfig(repoRoot) {
5881
- const p = join13(repoRoot, CONFIG_PATH);
5882
- if (!existsSync11(p))
6948
+ const p = join14(repoRoot, CONFIG_PATH);
6949
+ if (!existsSync13(p))
5883
6950
  return { kind: "missing" };
5884
6951
  let raw;
5885
6952
  try {
5886
- raw = JSON.parse(readFileSync13(p, "utf-8"));
6953
+ raw = JSON.parse(readFileSync15(p, "utf-8"));
5887
6954
  } catch (err) {
5888
6955
  return { kind: "invalid", error: `invalid JSON: ${err.message}` };
5889
6956
  }
@@ -5905,6 +6972,8 @@ function labelForPath(path) {
5905
6972
  return "agent-skills";
5906
6973
  if (path === CLAUDE_COMMAND_PATH)
5907
6974
  return "claude-command";
6975
+ if (path === CLAUDE_SETTINGS_PATH)
6976
+ return "claude-stop-hook";
5908
6977
  if (path === GEMINI_COMMAND_PATH)
5909
6978
  return "gemini-command";
5910
6979
  if (path === CHECKS_FOLDER_PLACEHOLDER_PATH || path === ROLES_FOLDER_PLACEHOLDER_PATH)
@@ -5920,14 +6989,14 @@ function diagnoseInstall(repoRoot, ctx, manifest) {
5920
6989
  for (const op of buildInitOps(ctx)) {
5921
6990
  if (op.kind === "create-file") {
5922
6991
  const check = labelForPath(op.path);
5923
- const abs2 = join13(repoRoot, op.path);
5924
- const exists = existsSync11(abs2);
6992
+ const abs2 = join14(repoRoot, op.path);
6993
+ const exists = existsSync13(abs2);
5925
6994
  const owned = ownedFiles.has(op.path) || isDefaultedAgentVendorPath(op.path, manifest);
5926
6995
  if (!exists) {
5927
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\`.`));
5928
6997
  continue;
5929
6998
  }
5930
- const content = readFileSync13(abs2, "utf-8");
6999
+ const content = readFileSync15(abs2, "utf-8");
5931
7000
  if (!owned) {
5932
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.`));
5933
7002
  continue;
@@ -5946,11 +7015,11 @@ function diagnoseInstall(repoRoot, ctx, manifest) {
5946
7015
  const check = "hooks";
5947
7016
  const abs2 = resolveManagedBlockPath(repoRoot, op.path);
5948
7017
  const owned = ownedBlocks.has(blockKey(op.path, op.marker));
5949
- if (!existsSync11(abs2)) {
7018
+ if (!existsSync13(abs2)) {
5950
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.`));
5951
7020
  continue;
5952
7021
  }
5953
- const content = readFileSync13(abs2, "utf-8");
7022
+ const content = readFileSync15(abs2, "utf-8");
5954
7023
  const { begin, end } = markerLines(op.marker, op.comment);
5955
7024
  const hasMarkers = content.includes(begin) && content.includes(end);
5956
7025
  if (!hasMarkers) {
@@ -5998,22 +7067,22 @@ async function diagnoseHookRouting(repoRoot, hookDir, readHooksPath) {
5998
7067
  function diagnoseCustomChecks(repoRoot, config) {
5999
7068
  const findings = [];
6000
7069
  for (const [name, entry2] of Object.entries(config.checks ?? {})) {
6001
- const scriptAbs = join13(repoRoot, entry2.run);
6002
- 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}`));
6003
7072
  }
6004
7073
  return findings;
6005
7074
  }
6006
7075
  function listTrackedFiles(repoRoot) {
6007
7076
  try {
6008
- return execFileSync8("git", ["ls-files"], { cwd: repoRoot, encoding: "utf-8" }).split(`
7077
+ return execFileSync9("git", ["ls-files"], { cwd: repoRoot, encoding: "utf-8" }).split(`
6009
7078
  `).map((s) => s.trim()).filter(Boolean);
6010
7079
  } catch {
6011
7080
  return [];
6012
7081
  }
6013
7082
  }
6014
7083
  function diagnoseDocOwnersHealth(repoRoot) {
6015
- const path = join13(repoRoot, DOC_OWNERS_PATH);
6016
- 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;
6017
7086
  const state = classifyDocOwnersManifest(content);
6018
7087
  if (state === "absent" || state === "empty")
6019
7088
  return [];
@@ -6030,8 +7099,8 @@ function diagnoseDocOwnersHealth(repoRoot) {
6030
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."));
6031
7100
  }
6032
7101
  if (!isUrlPointer(b.pointer)) {
6033
- const pointerPath = join13(repoRoot, pointerToPath(b.pointer));
6034
- if (!existsSync11(pointerPath)) {
7102
+ const pointerPath = join14(repoRoot, pointerToPath(b.pointer));
7103
+ if (!existsSync13(pointerPath)) {
6035
7104
  flagged = true;
6036
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."));
6037
7106
  }
@@ -6046,7 +7115,7 @@ var LEGACY_AEG_PACKAGES_PATH = ".aeg/packages";
6046
7115
  function readWorkspacesForDoctor(repoRoot) {
6047
7116
  const fromPackageJson = () => {
6048
7117
  try {
6049
- const pkg = JSON.parse(readFileSync13(join13(repoRoot, "package.json"), "utf-8"));
7118
+ const pkg = JSON.parse(readFileSync15(join14(repoRoot, "package.json"), "utf-8"));
6050
7119
  return Array.isArray(pkg.workspaces) ? pkg.workspaces.filter((w) => typeof w === "string") : [];
6051
7120
  } catch {
6052
7121
  return [];
@@ -6054,7 +7123,7 @@ function readWorkspacesForDoctor(repoRoot) {
6054
7123
  };
6055
7124
  const fromPnpmWorkspaceYaml = () => {
6056
7125
  try {
6057
- return parsePnpmWorkspaceYaml(readFileSync13(join13(repoRoot, "pnpm-workspace.yaml"), "utf-8"));
7126
+ return parsePnpmWorkspaceYaml(readFileSync15(join14(repoRoot, "pnpm-workspace.yaml"), "utf-8"));
6058
7127
  } catch {
6059
7128
  return [];
6060
7129
  }
@@ -6063,23 +7132,23 @@ function readWorkspacesForDoctor(repoRoot) {
6063
7132
  }
6064
7133
  function listChildDirsForDoctor(dir, repoRoot) {
6065
7134
  try {
6066
- 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);
6067
7136
  } catch {
6068
7137
  return [];
6069
7138
  }
6070
7139
  }
6071
7140
  function diagnoseBlastRadiusDeprecation(repoRoot, config) {
6072
7141
  const derived = deriveWorkspacePackageDomains(readWorkspacesForDoctor(repoRoot), (dir) => listChildDirsForDoctor(dir, repoRoot));
6073
- const defaults = deriveBuiltinCrossCuttingDefaults((p) => existsSync11(join13(repoRoot, p)));
7142
+ const defaults = deriveBuiltinCrossCuttingDefaults((p) => existsSync13(join14(repoRoot, p)));
6074
7143
  const covered = new Set([...derived, ...defaults]);
6075
7144
  const configExtra = new Set(config?.blastRadius?.extraDomains ?? []);
6076
- const legacyPath = join13(repoRoot, LEGACY_AEG_PACKAGES_PATH);
6077
- if (!existsSync11(legacyPath)) {
7145
+ const legacyPath = join14(repoRoot, LEGACY_AEG_PACKAGES_PATH);
7146
+ if (!existsSync13(legacyPath)) {
6078
7147
  return [
6079
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.`)
6080
7149
  ];
6081
7150
  }
6082
- const legacyEntries = readFileSync13(legacyPath, "utf-8").split(`
7151
+ const legacyEntries = readFileSync15(legacyPath, "utf-8").split(`
6083
7152
  `).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
6084
7153
  const uncovered = legacyEntries.filter((e) => !covered.has(e) && !configExtra.has(e));
6085
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(", ")}.`;
@@ -6114,7 +7183,7 @@ function diagnoseBriefSchemaDrift(config) {
6114
7183
  }
6115
7184
  function diagnoseEnvDeclarations(repoRoot, config) {
6116
7185
  const resolved = resolveChecks(coreCheckRegistry(), config?.checks).resolved;
6117
- 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);
6118
7187
  const missing = checksMissingEnvDeclaration(specs);
6119
7188
  const findings = missing.map((name) => info("env", envDeclarationWarning(name)));
6120
7189
  for (const message of lintEnvDeclarations(config?.checks)) {
@@ -6135,11 +7204,11 @@ function diagnoseCheckClassification(config) {
6135
7204
  return findings;
6136
7205
  }
6137
7206
  function diagnoseGlobalConfigChecks() {
6138
- if (!existsSync11(GLOBAL_CONFIG_PATH))
7207
+ if (!existsSync13(GLOBAL_CONFIG_PATH))
6139
7208
  return [];
6140
7209
  let raw;
6141
7210
  try {
6142
- raw = JSON.parse(readFileSync13(GLOBAL_CONFIG_PATH, "utf-8"));
7211
+ raw = JSON.parse(readFileSync15(GLOBAL_CONFIG_PATH, "utf-8"));
6143
7212
  } catch (err) {
6144
7213
  return [info("checks", `${GLOBAL_CONFIG_PATH} is invalid JSON — ${err.message}`)];
6145
7214
  }
@@ -6150,6 +7219,14 @@ function diagnoseGlobalConfigChecks() {
6150
7219
  return [];
6151
7220
  return [warn("checks", globalChecksIgnoredWarning(GLOBAL_CONFIG_PATH))];
6152
7221
  }
7222
+ function diagnoseTokenMetering(deps) {
7223
+ const capability = deps.meteringCapability();
7224
+ if (capability.capable)
7225
+ return [];
7226
+ return [
7227
+ info("tokens", `Token-metering capability probe: incapable (${capability.reason}) — ${capability.detail} Real per-turn ` + "token figures cannot currently be collected via `vinaya tokens`. Report them by whatever means your " + "host offers, or supply them directly with `vinaya tokens --in <n> --out <n>`.")
7228
+ ];
7229
+ }
6153
7230
  async function diagnoseEnvironment(deps, hasDrift) {
6154
7231
  const findings = [];
6155
7232
  const auth = await deps.ghAuthStatus();
@@ -6180,13 +7257,13 @@ function coversWorkflowsDir(pattern) {
6180
7257
  return bare === ".github/workflows" || bare === "/.github/workflows";
6181
7258
  }
6182
7259
  function diagnoseCodeowners(repoRoot) {
6183
- const path = join13(repoRoot, ".github", "CODEOWNERS");
6184
- if (!existsSync11(path)) {
7260
+ const path = join14(repoRoot, ".github", "CODEOWNERS");
7261
+ if (!existsSync13(path)) {
6185
7262
  return info("codeowners", "no .github/CODEOWNERS — vinaya's workflow files have no required-review protection; see `vinaya init`'s printed recommendation.");
6186
7263
  }
6187
7264
  let body;
6188
7265
  try {
6189
- body = readFileSync13(path, "utf-8");
7266
+ body = readFileSync15(path, "utf-8");
6190
7267
  } catch (err) {
6191
7268
  return warn("codeowners", `.github/CODEOWNERS exists but could not be read: ${err.message}`);
6192
7269
  }
@@ -6194,21 +7271,54 @@ function diagnoseCodeowners(repoRoot) {
6194
7271
  `).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).some((line) => coversWorkflowsDir(line.split(/\s+/)[0] ?? ""));
6195
7272
  return covered ? info("codeowners", ".github/CODEOWNERS covers .github/workflows/**.") : warn("codeowners", ".github/CODEOWNERS exists but has no entry covering .github/workflows/** — workflow file edits can merge unreviewed.");
6196
7273
  }
7274
+ function diagnoseVinayaOnPath(agents) {
7275
+ if (agents.size === 0)
7276
+ return [];
7277
+ const names = process.platform === "win32" ? ["vinaya.cmd", "vinaya.exe", "vinaya.bat"] : ["vinaya"];
7278
+ const dirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
7279
+ const onPath = dirs.some((dir) => names.some((name) => existsSync13(join14(dir, name))));
7280
+ return [
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`.')
7282
+ ];
7283
+ }
7284
+ function diagnosePrincipals(config) {
7285
+ if (config.principals && config.principals.length > 0) {
7286
+ return info("principals", `review-gate trusts ${config.principals.length} declared principal(s).`);
7287
+ }
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.');
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
+ }
6197
7307
  var TEST_INVOCATION_SUBSTRINGS = ["npm test", "npm run test", "bun test", "bunx turbo test"];
6198
7308
  function diagnoseTestCi(repoRoot) {
6199
7309
  let pkg;
6200
7310
  try {
6201
- pkg = JSON.parse(readFileSync13(join13(repoRoot, "package.json"), "utf-8"));
7311
+ pkg = JSON.parse(readFileSync15(join14(repoRoot, "package.json"), "utf-8"));
6202
7312
  } catch {
6203
7313
  return [];
6204
7314
  }
6205
7315
  const testScript = pkg?.scripts?.test;
6206
7316
  if (typeof testScript !== "string" || testScript.trim() === "")
6207
7317
  return [];
6208
- const workflowsDir = join13(repoRoot, ".github/workflows");
6209
- 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()) : [];
6210
7320
  const invoked = files.some((name) => {
6211
- const content = readFileSync13(join13(workflowsDir, name), "utf-8");
7321
+ const content = readFileSync15(join14(workflowsDir, name), "utf-8");
6212
7322
  return TEST_INVOCATION_SUBSTRINGS.some((s) => content.includes(s));
6213
7323
  });
6214
7324
  if (invoked)
@@ -6272,12 +7382,16 @@ async function runDoctor(args, deps) {
6272
7382
  findings.push(...await diagnoseHookRouting(repo.repoRoot, hookDir, deps.readHooksPath));
6273
7383
  findings.push(...diagnoseCustomChecks(repo.repoRoot, configRead.config));
6274
7384
  findings.push(...diagnoseDocOwnersHealth(repo.repoRoot));
7385
+ findings.push(diagnosePrincipals(configRead.config));
7386
+ findings.push(...diagnoseVinayaOnPath(ctx.agents));
6275
7387
  }
7388
+ findings.push(...diagnoseProjectsCoherence(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
6276
7389
  findings.push(...diagnoseBlastRadiusDeprecation(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
6277
7390
  findings.push(...diagnoseBriefSchemaDrift(configRead.kind === "ok" ? configRead.config : null));
6278
7391
  findings.push(...diagnoseEnvDeclarations(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
6279
7392
  findings.push(...diagnoseCheckClassification(configRead.kind === "ok" ? configRead.config : null));
6280
7393
  findings.push(...diagnoseGlobalConfigChecks());
7394
+ findings.push(...diagnoseTokenMetering(deps));
6281
7395
  findings.push(...await diagnoseEnvironment(deps, hasDrift));
6282
7396
  findings.push(await diagnoseBranchProtection(deps, repo.owner, repo.repo));
6283
7397
  findings.push(diagnoseCodeowners(repo.repoRoot));
@@ -6291,13 +7405,13 @@ async function runDoctor(args, deps) {
6291
7405
  return healthy ? 0 : 1;
6292
7406
  }
6293
7407
  async function doctorCommand(args) {
6294
- process.exit(await runDoctor(args, realDeps3()));
7408
+ process.exit(await runDoctor(args, realDeps4()));
6295
7409
  }
6296
7410
 
6297
7411
  // src/commands/eject.ts
6298
- import { existsSync as existsSync12, readFileSync as readFileSync14 } from "node:fs";
6299
- import { join as join14 } from "node:path";
6300
- function realDeps4() {
7412
+ import { existsSync as existsSync14, readFileSync as readFileSync16 } from "node:fs";
7413
+ import { join as join15 } from "node:path";
7414
+ function realDeps5() {
6301
7415
  return {
6302
7416
  detectRepo: detectGitRepo,
6303
7417
  readHooksPath: readCoreHooksPath,
@@ -6313,12 +7427,12 @@ function parse(args) {
6313
7427
  return { dryRun: args.includes("--dry-run"), yes: args.includes("--yes") };
6314
7428
  }
6315
7429
  function readManifest(repoRoot) {
6316
- const p = join14(repoRoot, CONFIG_PATH);
6317
- if (!existsSync12(p))
7430
+ const p = join15(repoRoot, CONFIG_PATH);
7431
+ if (!existsSync14(p))
6318
7432
  return { kind: "none" };
6319
7433
  let raw;
6320
7434
  try {
6321
- raw = JSON.parse(readFileSync14(p, "utf-8"));
7435
+ raw = JSON.parse(readFileSync16(p, "utf-8"));
6322
7436
  } catch (err) {
6323
7437
  return { kind: "orphan", reason: `vinaya.config.json is not valid JSON: ${err.message}` };
6324
7438
  }
@@ -6397,118 +7511,20 @@ Vinaya ejected.
6397
7511
  process.stdout.write(`These labels were created by vinaya and left in place — remove them manually if unused:
6398
7512
  `);
6399
7513
  for (const name of removedLabelsToReport)
6400
- process.stdout.write(` gh label delete ${name}
6401
- `);
6402
- }
6403
- return 0;
6404
- }
6405
- async function ejectCommand(args) {
6406
- const code = await runEject(args, realDeps4());
6407
- process.exit(code);
6408
- }
6409
-
6410
- // src/commands/init.ts
6411
- import { existsSync as existsSync14, readFileSync as readFileSync16, writeFileSync as writeFileSync5 } from "node:fs";
6412
- import { join as join16 } from "node:path";
6413
-
6414
- // src/lib/registry-write.ts
6415
- import { existsSync as existsSync13, mkdirSync as mkdirSync3, readFileSync as readFileSync15, writeFileSync as writeFileSync4 } from "node:fs";
6416
- import { dirname as dirname6, join as join15 } from "node:path";
6417
- var PROJECTS_REGISTRY_PATH = ".vinaya/projects.md";
6418
- function rowLine(name, path, specsPath) {
6419
- return `| ${name} | \`${path}\` | \`${specsPath}\` | (state tracked globally) |`;
6420
- }
6421
- function freshProjectsRegistry(name, path, specsPath) {
6422
- return `---
6423
- sidebar_title: Projects
6424
- ---
6425
- # Projects in this repo
6426
-
6427
- **The project registry.** Declares the projects in this repo and where each
6428
- one's specs and per-project state live. The \`Project\` field on a task (a
6429
- forge Issue) resolves against this file.
6430
-
6431
- **Presence of this file means this is a multi-project repo** — the
6432
- \`Project\` field is required on task Issues/PRs that touch a registered
6433
- project's path. A project is a \`(name, path)\` pair you declared — nothing
6434
- is derived from the folder tree.
6435
-
6436
- ## Registry
6437
-
6438
- | Project | Path | Specs | Per-project state |
6439
- |---------|------|-------|---------------------|
6440
- ${rowLine(name, path, specsPath)}
6441
- `;
6442
- }
6443
- var REGISTRY_HEADING_RE = /^##\s+Registry\b/i;
6444
- function appendRegistryRow(content, name, path, specsPath) {
6445
- const lines = content.split(/\r?\n/);
6446
- const headingIdx = lines.findIndex((l) => REGISTRY_HEADING_RE.test(l));
6447
- const newRow = rowLine(name, path, specsPath);
6448
- if (headingIdx === -1) {
6449
- const sep4 = content.endsWith(`
6450
- `) ? "" : `
6451
- `;
6452
- return `${content}${sep4}
6453
- ## Registry
6454
-
6455
- | Project | Path | Specs | Per-project state |
6456
- |---------|------|-------|---------------------|
6457
- ${newRow}
6458
- `;
6459
- }
6460
- let i = headingIdx + 1;
6461
- for (;i < lines.length; i++) {
6462
- if ((lines[i] ?? "").trim().startsWith("|"))
6463
- break;
6464
- }
6465
- if (i >= lines.length) {
6466
- lines.splice(headingIdx + 1, 0, "", "| Project | Path | Specs | Per-project state |", "|---------|------|-------|---------------------|", newRow);
6467
- return lines.join(`
6468
- `);
6469
- }
6470
- let last = i + 1;
6471
- for (let j = i + 2;j < lines.length; j++) {
6472
- if (!(lines[j] ?? "").trim().startsWith("|"))
6473
- break;
6474
- last = j;
6475
- }
6476
- lines.splice(last + 1, 0, newRow);
6477
- return lines.join(`
7514
+ process.stdout.write(` gh label delete ${name}
6478
7515
  `);
6479
- }
6480
- function planRegistryRow(repoRoot, name, path, specsPath) {
6481
- const abs2 = join15(repoRoot, PROJECTS_REGISTRY_PATH);
6482
- const line = rowLine(name, path, specsPath);
6483
- if (!existsSync13(abs2)) {
6484
- return { action: "create-host", rowLine: line, path: PROJECTS_REGISTRY_PATH };
6485
- }
6486
- const existing = readFileSync15(abs2, "utf-8");
6487
- const already = parseRegistry(existing).some((p) => p.name === name);
6488
- return { action: already ? "skip-present" : "append-row", rowLine: line, path: PROJECTS_REGISTRY_PATH };
6489
- }
6490
- function applyRegistryRow(repoRoot, plan, name, path, specsPath) {
6491
- const abs2 = join15(repoRoot, PROJECTS_REGISTRY_PATH);
6492
- if (plan.action === "create-host") {
6493
- mkdirSync3(dirname6(abs2), { recursive: true });
6494
- writeFileSync4(abs2, freshProjectsRegistry(name, path, specsPath), "utf-8");
6495
- } else if (plan.action === "append-row") {
6496
- const existing = readFileSync15(abs2, "utf-8");
6497
- writeFileSync4(abs2, appendRegistryRow(existing, name, path, specsPath), "utf-8");
6498
7516
  }
7517
+ return 0;
6499
7518
  }
6500
- function renderRegistryRowDiffLine(plan) {
6501
- if (plan.action === "skip-present")
6502
- return ` = keep ${plan.path} (project already registered)`;
6503
- if (plan.action === "append-row")
6504
- return ` ~ append row to ${plan.path}
6505
- ${plan.rowLine}`;
6506
- return ` + create ${plan.path} (with registry row)
6507
- ${plan.rowLine}`;
7519
+ async function ejectCommand(args) {
7520
+ const code = await runEject(args, realDeps5());
7521
+ process.exit(code);
6508
7522
  }
6509
7523
 
6510
7524
  // src/commands/init.ts
6511
- function realDeps5() {
7525
+ import { existsSync as existsSync15, readFileSync as readFileSync17, writeFileSync as writeFileSync5 } from "node:fs";
7526
+ import { join as join16 } from "node:path";
7527
+ function realDeps6() {
6512
7528
  return {
6513
7529
  detectRepo: detectGitRepo,
6514
7530
  checkGhAuth,
@@ -6548,17 +7564,17 @@ function parseAgentsFlag(args) {
6548
7564
  var PRODUCT_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
6549
7565
  function readManifest2(repoRoot) {
6550
7566
  const p = join16(repoRoot, CONFIG_PATH);
6551
- if (!existsSync14(p))
7567
+ if (!existsSync15(p))
6552
7568
  return null;
6553
7569
  try {
6554
- return VinayaConfigSchema.parse(JSON.parse(readFileSync16(p, "utf-8"))).managed ?? null;
7570
+ return VinayaConfigSchema.parse(JSON.parse(readFileSync17(p, "utf-8"))).managed ?? null;
6555
7571
  } catch {
6556
7572
  return null;
6557
7573
  }
6558
7574
  }
6559
7575
  function writeManifest(repoRoot, manifest) {
6560
7576
  const configAbs = join16(repoRoot, CONFIG_PATH);
6561
- const seed = JSON.parse(readFileSync16(configAbs, "utf-8"));
7577
+ const seed = JSON.parse(readFileSync17(configAbs, "utf-8"));
6562
7578
  writeFileSync5(configAbs, `${JSON.stringify({ ...seed, managed: manifest }, null, 2)}
6563
7579
  `, "utf-8");
6564
7580
  }
@@ -6634,7 +7650,7 @@ function pathFlag(args) {
6634
7650
  const i = args.indexOf("--path");
6635
7651
  return i !== -1 && args[i + 1] ? args[i + 1] : ".";
6636
7652
  }
6637
- var PATH_INJECTION_RE = /[|\r\n]/;
7653
+ var PATH_INJECTION_RE = /[|`\r\n]/;
6638
7654
  function validPathFlag(path) {
6639
7655
  return !PATH_INJECTION_RE.test(path);
6640
7656
  }
@@ -6647,7 +7663,7 @@ async function runInitProduct(args, deps) {
6647
7663
  }
6648
7664
  const productPath = pathFlag(args);
6649
7665
  if (!validPathFlag(productPath)) {
6650
- console.error("Error: --path must not contain '|' or a newline (it is written into a markdown table row).");
7666
+ console.error("Error: --path must not contain '|', a backtick, or a newline (it is written into a markdown table row and must round-trip verbatim).");
6651
7667
  return 2;
6652
7668
  }
6653
7669
  const specsPath = productPath === "." ? "specs/" : `${productPath}/specs/`;
@@ -6666,6 +7682,7 @@ async function runInitProduct(args, deps) {
6666
7682
  return 1;
6667
7683
  }
6668
7684
  const registryPlan = planRegistryRow(repo.repoRoot, name, productPath, specsPath);
7685
+ const configPlan = planConfigProjectEntry(repo.repoRoot, { name, path: productPath });
6669
7686
  process.stdout.write(`vinaya init product ${name} — the full diff:
6670
7687
 
6671
7688
  `);
@@ -6673,6 +7690,11 @@ async function runInitProduct(args, deps) {
6673
7690
  `);
6674
7691
  process.stdout.write(`${renderRegistryRowDiffLine(registryPlan)}
6675
7692
 
7693
+ `);
7694
+ process.stdout.write(`── Config ────────────────────────────────────────
7695
+ `);
7696
+ process.stdout.write(`${renderConfigProjectEntryDiffLine(configPlan)}
7697
+
6676
7698
  `);
6677
7699
  if (dryRun) {
6678
7700
  process.stdout.write(`--dry-run: nothing was written.
@@ -6688,25 +7710,26 @@ async function runInitProduct(args, deps) {
6688
7710
  }
6689
7711
  }
6690
7712
  applyRegistryRow(repo.repoRoot, registryPlan, name, productPath, specsPath);
7713
+ applyConfigProjectEntry(repo.repoRoot, configPlan);
6691
7714
  process.stdout.write(`
6692
7715
  Governed product area '${name}' scaffolded.
6693
7716
  `);
6694
7717
  return 0;
6695
7718
  }
6696
7719
  async function initCommand(args) {
6697
- process.exit(await runInit(args, realDeps5()));
7720
+ process.exit(await runInit(args, realDeps6()));
6698
7721
  }
6699
7722
  async function initProductCommand(args) {
6700
- process.exit(await runInitProduct(args, realDeps5()));
7723
+ process.exit(await runInitProduct(args, realDeps6()));
6701
7724
  }
6702
7725
 
6703
7726
  // src/commands/issue.ts
6704
- import { execFileSync as execFileSync10 } from "node:child_process";
7727
+ import { execFileSync as execFileSync11 } from "node:child_process";
6705
7728
 
6706
7729
  // src/lib/forge-write.ts
6707
- import { execFileSync as execFileSync9 } from "node:child_process";
6708
- import { existsSync as existsSync15, mkdtempSync, readdirSync as readdirSync7, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
6709
- 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";
6710
7733
  import { tmpdir } from "node:os";
6711
7734
  import { join as join17 } from "node:path";
6712
7735
  class ForgeArgError extends Error {
@@ -6718,13 +7741,13 @@ function locateBody(args) {
6718
7741
  const p = args[i + 1];
6719
7742
  if (!p)
6720
7743
  throw new ForgeArgError("`--body-file` was given with no path.");
6721
- 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 } };
6722
7745
  }
6723
7746
  if (a.startsWith("--body-file=")) {
6724
7747
  const p = a.slice("--body-file=".length);
6725
7748
  if (!p)
6726
7749
  throw new ForgeArgError("`--body-file=` was given with no path.");
6727
- 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 } };
6728
7751
  }
6729
7752
  if (a === "--body" || a === "-b") {
6730
7753
  const v = args[i + 1];
@@ -6798,7 +7821,7 @@ function ensureTrancheLabelExists(slug) {
6798
7821
  const name = trancheLabel(slug);
6799
7822
  let existing;
6800
7823
  try {
6801
- const out = execFileSync9("gh", ["label", "list", "--json", "name", "--limit", "200"], {
7824
+ const out = execFileSync10("gh", ["label", "list", "--json", "name", "--limit", "200"], {
6802
7825
  encoding: "utf8",
6803
7826
  stdio: ["ignore", "pipe", "pipe"]
6804
7827
  });
@@ -6811,7 +7834,7 @@ function ensureTrancheLabelExists(slug) {
6811
7834
  if (existing.some((l) => l.name === name))
6812
7835
  return;
6813
7836
  try {
6814
- 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}`], {
6815
7838
  encoding: "utf8",
6816
7839
  stdio: ["ignore", "pipe", "pipe"]
6817
7840
  });
@@ -6821,6 +7844,24 @@ function ensureTrancheLabelExists(slug) {
6821
7844
  ]);
6822
7845
  }
6823
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
+ }
6824
7865
  function resolveSections(kind, retryCommand) {
6825
7866
  const result = loadConfigChecked();
6826
7867
  if (!result.ok) {
@@ -6945,7 +7986,7 @@ function validateForgeWrite(input) {
6945
7986
  }
6946
7987
  function git2(args) {
6947
7988
  try {
6948
- return execFileSync9("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
7989
+ return execFileSync10("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
6949
7990
  } catch {
6950
7991
  return "";
6951
7992
  }
@@ -6956,7 +7997,7 @@ function repoRoot() {
6956
7997
  function readWorkspaceGlobs(root) {
6957
7998
  const fromPackageJson = () => {
6958
7999
  try {
6959
- const pkg = JSON.parse(readFileSync17(join17(root, "package.json"), "utf8"));
8000
+ const pkg = JSON.parse(readFileSync18(join17(root, "package.json"), "utf8"));
6960
8001
  return Array.isArray(pkg.workspaces) ? pkg.workspaces.filter((w) => typeof w === "string") : [];
6961
8002
  } catch {
6962
8003
  return [];
@@ -6964,7 +8005,7 @@ function readWorkspaceGlobs(root) {
6964
8005
  };
6965
8006
  const fromPnpmWorkspaceYaml = () => {
6966
8007
  try {
6967
- return parsePnpmWorkspaceYaml(readFileSync17(join17(root, "pnpm-workspace.yaml"), "utf8"));
8008
+ return parsePnpmWorkspaceYaml(readFileSync18(join17(root, "pnpm-workspace.yaml"), "utf8"));
6968
8009
  } catch {
6969
8010
  return [];
6970
8011
  }
@@ -6980,7 +8021,7 @@ function listWorkspaceChildDirs(dir, root) {
6980
8021
  }
6981
8022
  function readConfigExtraDomains(root) {
6982
8023
  try {
6983
- 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")));
6984
8025
  return parsed.success ? parsed.data.blastRadius?.extraDomains ?? [] : [];
6985
8026
  } catch {
6986
8027
  return [];
@@ -6990,7 +8031,7 @@ function readSharedPackages(root = repoRoot()) {
6990
8031
  if (!root)
6991
8032
  return [];
6992
8033
  const derived = deriveWorkspacePackageDomains(readWorkspaceGlobs(root), (dir) => listWorkspaceChildDirs(dir, root));
6993
- const defaults = deriveBuiltinCrossCuttingDefaults((p) => existsSync15(join17(root, p)));
8034
+ const defaults = deriveBuiltinCrossCuttingDefaults((p) => existsSync16(join17(root, p)));
6994
8035
  const configExtra = readConfigExtraDomains(root);
6995
8036
  return [...new Set([...derived, ...defaults, ...configExtra])];
6996
8037
  }
@@ -6998,7 +8039,7 @@ function readProjectPaths(root = repoRoot()) {
6998
8039
  if (!root)
6999
8040
  return [];
7000
8041
  try {
7001
- return parseRegistry(readFileSync17(join17(root, ".vinaya/projects.md"), "utf8"));
8042
+ return parseRegistry(readFileSync18(join17(root, ".vinaya/projects.md"), "utf8"));
7002
8043
  } catch {
7003
8044
  return [];
7004
8045
  }
@@ -7040,7 +8081,7 @@ function locateBodyOrRefuse(ghArgs, retryCommand) {
7040
8081
  function fetchForgeLabels(issueRef) {
7041
8082
  let out;
7042
8083
  try {
7043
- out = execFileSync10("gh", ["issue", "view", issueRef, "--json", "labels"], {
8084
+ out = execFileSync11("gh", ["issue", "view", issueRef, "--json", "labels"], {
7044
8085
  encoding: "utf8",
7045
8086
  stdio: ["ignore", "pipe", "pipe"]
7046
8087
  });
@@ -7068,7 +8109,7 @@ function reportPass(json, command) {
7068
8109
  function runGhWrite(ghCmd, ghArgs, bodyResult, json) {
7069
8110
  const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
7070
8111
  try {
7071
- 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"] });
7072
8113
  const url = out.trim();
7073
8114
  if (json)
7074
8115
  printJson({ validated: true, written: true, url });
@@ -7123,7 +8164,7 @@ function issueCreateCommand(args) {
7123
8164
  const slugToEnsure = findTrancheSlug(labels);
7124
8165
  if (slugToEnsure)
7125
8166
  ensureTrancheLabelExists(slugToEnsure);
7126
- runGhWrite(["issue", "create"], ghArgs, bodyResult, json);
8167
+ runGhWrite(["issue", "create"], resolveMilestoneAttachArgs(ghArgs, labels), bodyResult, json);
7127
8168
  }
7128
8169
  function issueEditCommand(args) {
7129
8170
  const json = args.includes("--json");
@@ -7154,12 +8195,13 @@ function issueEditCommand(args) {
7154
8195
  }
7155
8196
 
7156
8197
  // src/commands/milestone.ts
7157
- import { execFileSync as execFileSync11 } from "node:child_process";
8198
+ import { execFileSync as execFileSync12 } from "node:child_process";
7158
8199
  var RETRY_CREATE2 = "vinaya milestone create --title <title> --body-file <path>";
7159
8200
  var RETRY_ADOPT = "vinaya milestone adopt --target <title> --slug <slug> [--slug <slug> ...]";
7160
8201
  var RETRY_EDIT2 = "vinaya milestone edit <n> --body-file <path>";
8202
+ var RETRY_CLOSE = "vinaya milestone close --slug <slug>";
7161
8203
  function sh3(args, input) {
7162
- return execFileSync11(args[0], args.slice(1), {
8204
+ return execFileSync12(args[0], args.slice(1), {
7163
8205
  encoding: "utf8",
7164
8206
  input,
7165
8207
  env: process.env,
@@ -7317,18 +8359,18 @@ function extractSlugs(args) {
7317
8359
  }
7318
8360
  return slugs;
7319
8361
  }
7320
- function ghJsonOrRefuse(args, what) {
8362
+ function ghJsonOrRefuse(args, what, retryCommand) {
7321
8363
  try {
7322
8364
  return shJson2(args);
7323
8365
  } catch (e) {
7324
8366
  refuse([
7325
- 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}\`.`)
7326
8368
  ]);
7327
8369
  }
7328
8370
  }
7329
8371
  function fetchAdoptFacts(repoFlag, target, slugs) {
7330
- const labels = ghJsonOrRefuse(["gh", "api", `repos/${repoFlag}/labels?per_page=100`], "this repo's labels");
7331
- 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);
7332
8374
  const targetMilestone = milestones.find((m) => m.title === target);
7333
8375
  const targetFacts = {
7334
8376
  title: target,
@@ -7351,7 +8393,7 @@ function fetchAdoptFacts(repoFlag, target, slugs) {
7351
8393
  "number,state,milestone",
7352
8394
  "--limit",
7353
8395
  "200"
7354
- ], `Issues for \`${trancheLabel(slug)}\``) : [];
8396
+ ], `Issues for \`${trancheLabel(slug)}\``, RETRY_ADOPT) : [];
7355
8397
  const elsewhere = new Set;
7356
8398
  for (const issue of issues) {
7357
8399
  const title = issue.milestone?.title;
@@ -7436,17 +8478,110 @@ async function milestoneAdoptCommand(args) {
7436
8478
  }
7437
8479
  }
7438
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}\`.`));
8547
+ }
8548
+ refuse(errors);
8549
+ }
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).
8555
+ `);
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
+ ]);
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
+ `);
8573
+ }
7439
8574
 
7440
8575
  // src/commands/new-check.ts
7441
- 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";
7442
8577
  import { join as join18 } from "node:path";
7443
8578
  var TEMPLATE_PATH = join18(packageRoot(import.meta.url), "templates", "custom-check.template.ts");
7444
8579
  var CHECKS_DIR = join18("scripts", "vinaya-checks");
7445
- 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)";
7446
8581
  function newCheckCommand(args) {
7447
8582
  const name = args[0];
7448
8583
  if (!name) {
7449
- console.error(USAGE);
8584
+ console.error(USAGE2);
7450
8585
  process.exit(2);
7451
8586
  }
7452
8587
  if (coreCheckRegistry().some((spec) => spec.name === name)) {
@@ -7454,20 +8589,20 @@ function newCheckCommand(args) {
7454
8589
  process.exit(2);
7455
8590
  }
7456
8591
  if (!isValidNamespacedKey(name)) {
7457
- 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)}
7458
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.`);
7459
8594
  process.exit(2);
7460
8595
  }
7461
8596
  const scriptsDir = join18(process.cwd(), CHECKS_DIR);
7462
- if (!existsSync16(scriptsDir))
8597
+ if (!existsSync17(scriptsDir))
7463
8598
  mkdirSync4(scriptsDir, { recursive: true });
7464
8599
  const fileStem = name.slice(name.indexOf("/") + 1);
7465
8600
  const targetPath = join18(scriptsDir, `${fileStem}.ts`);
7466
- if (existsSync16(targetPath)) {
8601
+ if (existsSync17(targetPath)) {
7467
8602
  console.error(`Error: ${targetPath} already exists.`);
7468
8603
  process.exit(1);
7469
8604
  }
7470
- const template = readFileSync18(TEMPLATE_PATH, "utf-8");
8605
+ const template = readFileSync19(TEMPLATE_PATH, "utf-8");
7471
8606
  const contents = template.split("{{CHECK_NAME}}").join(name);
7472
8607
  writeFileSync7(targetPath, contents, "utf-8");
7473
8608
  chmodSync2(targetPath, 493);
@@ -7481,32 +8616,32 @@ ${registration}
7481
8616
  }
7482
8617
 
7483
8618
  // src/commands/new-noop-check.ts
7484
- 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";
7485
8620
  import { join as join19 } from "node:path";
7486
8621
  var TEMPLATE_PATH2 = join19(packageRoot(import.meta.url), "templates", "noop-check.template.ts");
7487
8622
  var CHECKS_DIR2 = join19("vinaya", "checks");
7488
- 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)";
7489
8624
  function newNoopCheckCommand(args) {
7490
8625
  const name = args[0];
7491
8626
  if (!name) {
7492
- console.error(USAGE2);
8627
+ console.error(USAGE3);
7493
8628
  process.exit(2);
7494
8629
  }
7495
8630
  const coreSpec = coreCheckRegistry().find((spec) => spec.name === name);
7496
8631
  if (!coreSpec) {
7497
- console.error(`Refusing: "${name}" is not a core check id. ${USAGE2}
8632
+ console.error(`Refusing: "${name}" is not a core check id. ${USAGE3}
7498
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.`);
7499
8634
  process.exit(2);
7500
8635
  }
7501
8636
  const checksDir = join19(process.cwd(), CHECKS_DIR2);
7502
- if (!existsSync17(checksDir))
8637
+ if (!existsSync18(checksDir))
7503
8638
  mkdirSync5(checksDir, { recursive: true });
7504
8639
  const targetPath = join19(checksDir, `${name}.ts`);
7505
- if (existsSync17(targetPath)) {
8640
+ if (existsSync18(targetPath)) {
7506
8641
  console.error(`Error: ${targetPath} already exists.`);
7507
8642
  process.exit(1);
7508
8643
  }
7509
- const template = readFileSync19(TEMPLATE_PATH2, "utf-8");
8644
+ const template = readFileSync20(TEMPLATE_PATH2, "utf-8");
7510
8645
  const contents = template.split("{{CHECK_NAME}}").join(name);
7511
8646
  writeFileSync8(targetPath, contents, "utf-8");
7512
8647
  chmodSync3(targetPath, 493);
@@ -7528,32 +8663,32 @@ ${registration}
7528
8663
  }
7529
8664
 
7530
8665
  // src/commands/new-role.ts
7531
- 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";
7532
8667
  import { join as join20 } from "node:path";
7533
8668
  var TEMPLATE_PATH3 = join20(packageRoot(import.meta.url), "templates", "role.template.md");
7534
8669
  var ROLES_DIR = join20("vinaya", "roles");
7535
- 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)";
7536
8671
  function newRoleCommand(args) {
7537
8672
  const name = args[0];
7538
8673
  if (!name) {
7539
- console.error(USAGE3);
8674
+ console.error(USAGE4);
7540
8675
  process.exit(2);
7541
8676
  }
7542
8677
  if (!isValidNamespacedKey(name)) {
7543
- 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)}
7544
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.`);
7545
8680
  process.exit(2);
7546
8681
  }
7547
8682
  const rolesDir = join20(process.cwd(), ROLES_DIR);
7548
- if (!existsSync18(rolesDir))
8683
+ if (!existsSync19(rolesDir))
7549
8684
  mkdirSync6(rolesDir, { recursive: true });
7550
8685
  const roleId = name.slice(name.indexOf("/") + 1);
7551
8686
  const targetPath = join20(rolesDir, `${roleId}.md`);
7552
- if (existsSync18(targetPath)) {
8687
+ if (existsSync19(targetPath)) {
7553
8688
  console.error(`Error: ${targetPath} already exists.`);
7554
8689
  process.exit(1);
7555
8690
  }
7556
- const template = readFileSync20(TEMPLATE_PATH3, "utf-8");
8691
+ const template = readFileSync21(TEMPLATE_PATH3, "utf-8");
7557
8692
  const contents = template.split("{{ROLE_ID}}").join(roleId);
7558
8693
  writeFileSync9(targetPath, contents, "utf-8");
7559
8694
  const relPath = join20(ROLES_DIR, `${roleId}.md`);
@@ -7566,7 +8701,77 @@ ${registration}
7566
8701
  }
7567
8702
 
7568
8703
  // src/commands/pr.ts
7569
- import { execFileSync as execFileSync12 } from "node:child_process";
8704
+ import { execFileSync as execFileSync13 } from "node:child_process";
8705
+
8706
+ // src/lib/numstat.ts
8707
+ function summariseNumstat(numstat) {
8708
+ const rows = numstat.split(`
8709
+ `).filter((l) => l.trim() !== "");
8710
+ let added = 0;
8711
+ let deleted = 0;
8712
+ let binary = 0;
8713
+ for (const row of rows) {
8714
+ const [a, d] = row.split("\t");
8715
+ if (a === "-" || d === "-") {
8716
+ binary++;
8717
+ continue;
8718
+ }
8719
+ added += Number(a) || 0;
8720
+ deleted += Number(d) || 0;
8721
+ }
8722
+ const files = `${rows.length} file${rows.length === 1 ? "" : "s"} changed`;
8723
+ const bin2 = binary > 0 ? `, ${binary} binary` : "";
8724
+ return `${files}, ${added} insertion${added === 1 ? "" : "s"}(+), ${deleted} deletion${deleted === 1 ? "" : "s"}(-)${bin2}`;
8725
+ }
8726
+ var EVIDENCE_SUMMARY_PREFIX = "Summary: ";
8727
+
8728
+ // src/checks/scan-context.ts
8729
+ var ZERO_WIDTH = /[\u200B-\u200D\u2060\uFEFF]/g;
8730
+ var HTML_ENTITIES = {
8731
+ "&quot;": '"',
8732
+ "&apos;": "'",
8733
+ "&lt;": "<",
8734
+ "&gt;": ">",
8735
+ "&amp;": "&"
8736
+ };
8737
+ function decodeNamedEntities(body) {
8738
+ return body.replace(/&(?:quot|apos|lt|gt|amp);/g, (m) => HTML_ENTITIES[m] ?? m);
8739
+ }
8740
+ function normalizeBody(rawBody) {
8741
+ return decodeNamedEntities(rawBody.replace(ZERO_WIDTH, ""));
8742
+ }
8743
+ function buildAnchorLookupMask(body) {
8744
+ return maskDetailsBlocks(maskCode(body));
8745
+ }
8746
+
8747
+ class ScanContext {
8748
+ raw;
8749
+ normalised;
8750
+ masked;
8751
+ constructor(raw, normalised, masked) {
8752
+ this.raw = raw;
8753
+ this.normalised = normalised;
8754
+ this.masked = masked;
8755
+ }
8756
+ static from(rawBody) {
8757
+ const normalised = normalizeBody(rawBody);
8758
+ return new ScanContext(rawBody, normalised, buildAnchorLookupMask(normalised));
8759
+ }
8760
+ rawResolutionAgrees(field) {
8761
+ const rawHasPair = anchoredRegionBounds(this.raw, field) !== null;
8762
+ return rawHasPair === (anchoredRegionBounds(this.masked, field) !== null);
8763
+ }
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
+ }
7570
8775
 
7571
8776
  // src/checks/body-bare-digits-logic.ts
7572
8777
  function blankRange(text, start, end) {
@@ -7743,8 +8948,8 @@ var BOUNDED_ANCHOR_BLANK = {
7743
8948
  PROJECT: blankProjectField,
7744
8949
  EVIDENCE: blankEvidenceField
7745
8950
  };
7746
- function buildScanMask(body) {
7747
- let masked = maskDetailsBlocks(maskCode(body));
8951
+ function buildScanMask(ctx) {
8952
+ let masked = ctx.masked;
7748
8953
  masked = blankAnchoredRegions(masked);
7749
8954
  masked = blankPremiseValues(masked);
7750
8955
  masked = blankTokenReportSection(masked);
@@ -7753,17 +8958,6 @@ function buildScanMask(body) {
7753
8958
  }
7754
8959
  var TOKEN_WITH_DIGIT = /\S*\p{Nd}\S*/gu;
7755
8960
  var LIST_MARKER_TOKEN = /^\p{Nd}{1,9}[.)]$/u;
7756
- var ZERO_WIDTH = /[\u200B-\u200D\u2060\uFEFF]/g;
7757
- var HTML_ENTITIES = {
7758
- "&quot;": '"',
7759
- "&apos;": "'",
7760
- "&lt;": "<",
7761
- "&gt;": ">",
7762
- "&amp;": "&"
7763
- };
7764
- function decodeNamedEntities(body) {
7765
- return body.replace(/&(?:quot|apos|lt|gt|amp);/g, (m) => HTML_ENTITIES[m] ?? m);
7766
- }
7767
8961
  function isLineLeadingListMarker(line, matchStart, rawToken) {
7768
8962
  if (!LIST_MARKER_TOKEN.test(rawToken))
7769
8963
  return false;
@@ -7771,8 +8965,9 @@ function isLineLeadingListMarker(line, matchStart, rawToken) {
7771
8965
  return /^ {0,3}$/.test(before);
7772
8966
  }
7773
8967
  function checkBareDigits(rawBody) {
7774
- const body = decodeNamedEntities(rawBody.replace(ZERO_WIDTH, ""));
7775
- const masked = buildScanMask(body);
8968
+ const ctx = ScanContext.from(rawBody);
8969
+ const body = ctx.normalised;
8970
+ const masked = buildScanMask(ctx);
7776
8971
  const maskedLines = masked.split(`
7777
8972
  `);
7778
8973
  const origLines = body.split(`
@@ -7798,7 +8993,7 @@ var RETRY_CREATE3 = "vinaya pr create --validate-only …";
7798
8993
  var RETRY_EDIT3 = "vinaya pr edit <n> --validate-only …";
7799
8994
  function git3(args) {
7800
8995
  try {
7801
- return execFileSync12("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
8996
+ return execFileSync13("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
7802
8997
  } catch {
7803
8998
  return "";
7804
8999
  }
@@ -7832,7 +9027,7 @@ function reportPass2(json, command) {
7832
9027
  function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
7833
9028
  const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
7834
9029
  try {
7835
- 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"] });
7836
9031
  const url = out.trim();
7837
9032
  if (json)
7838
9033
  printJson({ validated: true, written: true, url });
@@ -7846,7 +9041,7 @@ function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
7846
9041
  function fetchPrForgeContext(prRef) {
7847
9042
  let viewOut;
7848
9043
  try {
7849
- viewOut = execFileSync12("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
9044
+ viewOut = execFileSync13("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
7850
9045
  encoding: "utf8",
7851
9046
  stdio: ["ignore", "pipe", "pipe"]
7852
9047
  });
@@ -7957,14 +9152,14 @@ function prEditCommand(args) {
7957
9152
  }
7958
9153
 
7959
9154
  // src/commands/pr-report.ts
7960
- import { execFileSync as execFileSync13, spawnSync as spawnSync2 } from "node:child_process";
7961
- 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";
7962
9157
  import { join as join21 } from "node:path";
7963
9158
  var EVIDENCE_START = "<!-- AEG:EVIDENCE:START -->";
7964
9159
  var EVIDENCE_END = "<!-- AEG:EVIDENCE:END -->";
7965
9160
  function git4(args) {
7966
9161
  try {
7967
- return execFileSync13("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
9162
+ return execFileSync14("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
7968
9163
  } catch {
7969
9164
  return "";
7970
9165
  }
@@ -7982,7 +9177,7 @@ class GitCommandError extends Error {
7982
9177
  }
7983
9178
  function gitStrict(args) {
7984
9179
  try {
7985
- return execFileSync13("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
9180
+ return execFileSync14("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
7986
9181
  } catch (err) {
7987
9182
  const stderr = err.stderr;
7988
9183
  throw new GitCommandError(args, String(stderr ?? err.message).trim() || "non-zero exit");
@@ -8018,7 +9213,7 @@ function computeGroupA() {
8018
9213
  function resolveSelfEntry() {
8019
9214
  const root = packageRoot(import.meta.url);
8020
9215
  const dist = join21(root, "dist", "index.js");
8021
- return existsSync19(dist) ? dist : join21(root, "src", "index.ts");
9216
+ return existsSync20(dist) ? dist : join21(root, "src", "index.ts");
8022
9217
  }
8023
9218
  var FAILING_STATUSES = new Set(["fail", "error", "timeout"]);
8024
9219
  function anyGateFailed(outcomes) {
@@ -8077,10 +9272,26 @@ function renderGroupB(outcomes) {
8077
9272
  `);
8078
9273
  }
8079
9274
  function buildBlockInner(groupA, gateOutcomes) {
8080
- return [`Head: ${groupA.head}`, "", renderGroupA(groupA), "", renderGroupB(gateOutcomes)].join(`
9275
+ return [
9276
+ `Head: ${groupA.head}`,
9277
+ `${EVIDENCE_SUMMARY_PREFIX}\`${summariseNumstat(groupA.numstat)}\``,
9278
+ "",
9279
+ renderGroupA(groupA),
9280
+ "",
9281
+ renderGroupB(gateOutcomes)
9282
+ ].join(`
8081
9283
  `);
8082
9284
  }
9285
+
9286
+ class DivergentEvidenceAnchorError extends Error {
9287
+ constructor() {
9288
+ super("the AEG:EVIDENCE anchor resolves differently before and after normalisation — a zero-width character or an HTML entity inside a marker makes this writer and the checks that read the block disagree about which pair is real. Remove it and re-run.");
9289
+ this.name = "DivergentEvidenceAnchorError";
9290
+ }
9291
+ }
8083
9292
  function replaceEvidenceBlock(body, blockInner) {
9293
+ if (!ScanContext.from(body).rawResolutionAgrees("EVIDENCE"))
9294
+ throw new DivergentEvidenceAnchorError;
8084
9295
  const full = `${EVIDENCE_START}
8085
9296
  ${blockInner}
8086
9297
  ${EVIDENCE_END}`;
@@ -8095,6 +9306,84 @@ ${EVIDENCE_END}`;
8095
9306
  ${full}
8096
9307
  `;
8097
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
+ }
8098
9387
  async function buildReport(opts = {}) {
8099
9388
  const groupA = opts.groupA ?? computeGroupA();
8100
9389
  const gateRunner = opts.gateRunner ?? runRealGates;
@@ -8108,8 +9397,16 @@ ${EVIDENCE_END}`;
8108
9397
  async function prReportCommand(args) {
8109
9398
  const writeIdx = args.indexOf("--write");
8110
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;
8111
9408
  if (writeIdx !== -1 && !writePath) {
8112
- 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>]");
8113
9410
  process.exit(2);
8114
9411
  }
8115
9412
  let result;
@@ -8123,10 +9420,18 @@ async function prReportCommand(args) {
8123
9420
  throw err;
8124
9421
  }
8125
9422
  if (writePath) {
8126
- const existing = existsSync19(writePath) ? readFileSync21(writePath, "utf8") : "";
8127
- 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);
8128
9433
  writeFileSync10(writePath, updated);
8129
- process.stdout.write(`Wrote AEG:EVIDENCE block to ${writePath}
9434
+ process.stdout.write(`Wrote AEG:EVIDENCE and AEG:TOKENS blocks to ${writePath}
8130
9435
  `);
8131
9436
  } else {
8132
9437
  process.stdout.write(`${result.block}
@@ -8135,13 +9440,187 @@ async function prReportCommand(args) {
8135
9440
  process.exit(result.gatesFailed ? 1 : 0);
8136
9441
  }
8137
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
+
8138
9617
  // src/commands/quickstart.ts
8139
- import { execFileSync as execFileSync14 } from "node:child_process";
8140
- 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";
8141
9620
  import { join as join23 } from "node:path";
8142
9621
 
8143
9622
  // src/lib/doc-owners-write.ts
8144
- 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";
8145
9624
  import { dirname as dirname7, join as join22 } from "node:path";
8146
9625
  function bindingLine(glob, pointer) {
8147
9626
  return `${glob} ${pointer}`;
@@ -8157,23 +9636,23 @@ function appendDocOwnersBinding(content, glob, pointer) {
8157
9636
  return `${content}${sep4}${bindingLine(glob, pointer)}
8158
9637
  `;
8159
9638
  }
8160
- function planDocOwnersBinding(repoRoot2, glob, pointer) {
8161
- const abs2 = join22(repoRoot2, DOC_OWNERS_PATH);
9639
+ function planDocOwnersBinding(repoRoot3, glob, pointer) {
9640
+ const abs2 = join22(repoRoot3, DOC_OWNERS_PATH);
8162
9641
  const line = bindingLine(glob, pointer);
8163
- if (!existsSync20(abs2)) {
9642
+ if (!existsSync21(abs2)) {
8164
9643
  return { action: "create-host", line, path: DOC_OWNERS_PATH };
8165
9644
  }
8166
- const existing = readFileSync22(abs2, "utf-8");
9645
+ const existing = readFileSync23(abs2, "utf-8");
8167
9646
  const already = parseDocOwners(existing).bindings.some((b) => b.glob === glob);
8168
9647
  return { action: already ? "skip-present" : "append-row", line, path: DOC_OWNERS_PATH };
8169
9648
  }
8170
- function applyDocOwnersBinding(repoRoot2, plan, glob, pointer) {
8171
- const abs2 = join22(repoRoot2, DOC_OWNERS_PATH);
9649
+ function applyDocOwnersBinding(repoRoot3, plan, glob, pointer) {
9650
+ const abs2 = join22(repoRoot3, DOC_OWNERS_PATH);
8172
9651
  if (plan.action === "create-host") {
8173
9652
  mkdirSync7(dirname7(abs2), { recursive: true });
8174
9653
  writeFileSync11(abs2, freshDocOwners(glob, pointer), "utf-8");
8175
9654
  } else if (plan.action === "append-row") {
8176
- const existing = readFileSync22(abs2, "utf-8");
9655
+ const existing = readFileSync23(abs2, "utf-8");
8177
9656
  writeFileSync11(abs2, appendDocOwnersBinding(existing, glob, pointer), "utf-8");
8178
9657
  }
8179
9658
  }
@@ -8233,13 +9712,13 @@ var GLOB_INJECTION_RE = /[\s\r\n]/;
8233
9712
  var POINTER_INJECTION_RE = /[\r\n]/;
8234
9713
  var NAME_INJECTION_RE = /[\s|\r\n]/;
8235
9714
  var PATH_INJECTION_RE2 = /[|\r\n]/;
8236
- function pointerIsMissingLocalFile(repoRoot2, pointer) {
9715
+ function pointerIsMissingLocalFile(repoRoot3, pointer) {
8237
9716
  if (/^https?:\/\//i.test(pointer))
8238
9717
  return false;
8239
9718
  const path = pointer.split("#")[0] ?? pointer;
8240
- return !existsSync21(join23(repoRoot2, path));
9719
+ return !existsSync22(join23(repoRoot3, path));
8241
9720
  }
8242
- async function bindDocOwnerLoop(deps, repoRoot2) {
9721
+ async function bindDocOwnerLoop(deps, repoRoot3) {
8243
9722
  let any = false;
8244
9723
  for (;; ) {
8245
9724
  if (any && !await deps.confirm("Bind another doc→code pair?", false))
@@ -8254,11 +9733,11 @@ async function bindDocOwnerLoop(deps, repoRoot2) {
8254
9733
  }
8255
9734
  if (GLOB_INJECTION_RE.test(glob) || POINTER_INJECTION_RE.test(pointer)) {
8256
9735
  console.error(red("Error: glob must not contain whitespace, and the pointer must not contain a newline."));
8257
- } else if (pointerIsMissingLocalFile(repoRoot2, pointer)) {
9736
+ } else if (pointerIsMissingLocalFile(repoRoot3, pointer)) {
8258
9737
  console.error(red(`✗ "${pointer}" does not exist in this repo (and isn't a URL).`));
8259
9738
  } else {
8260
- const plan = planDocOwnersBinding(repoRoot2, glob, pointer);
8261
- applyDocOwnersBinding(repoRoot2, plan, glob, pointer);
9739
+ const plan = planDocOwnersBinding(repoRoot3, glob, pointer);
9740
+ applyDocOwnersBinding(repoRoot3, plan, glob, pointer);
8262
9741
  process.stdout.write(`${green(renderDocOwnersBindingDiffLine(plan))}
8263
9742
  `);
8264
9743
  any = true;
@@ -8304,10 +9783,10 @@ async function registerProjectLoop(deps) {
8304
9783
  }
8305
9784
  }
8306
9785
  function readPackageVersion() {
8307
- 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"));
8308
9787
  return pkg.version;
8309
9788
  }
8310
- function realDeps6() {
9789
+ function realDeps7() {
8311
9790
  return {
8312
9791
  detectRepo: detectGitRepo,
8313
9792
  initDeps: {
@@ -8327,15 +9806,21 @@ function realDeps6() {
8327
9806
  readHooksPath: readCoreHooksPath,
8328
9807
  nodeVersion: () => process.version,
8329
9808
  bunVersion: () => typeof Bun === "undefined" ? null : Bun.version,
8330
- packageVersion: readPackageVersion
9809
+ packageVersion: readPackageVersion,
9810
+ meteringCapability: () => resolveMeteringCapability({
9811
+ env: process.env,
9812
+ cwd: process.cwd(),
9813
+ exists: existsSync22,
9814
+ readFile: (path) => readFileSync24(path, "utf8")
9815
+ })
8331
9816
  },
8332
9817
  confirm: async (q, defaultYes) => promptYesNo(q, defaultYes),
8333
9818
  ask: async (q) => prompt(q),
8334
9819
  closeStdin
8335
9820
  };
8336
9821
  }
8337
- function execGit(repoRoot2, args) {
8338
- 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();
8339
9824
  }
8340
9825
  function errorDetail(err) {
8341
9826
  const stderr = err.stderr;
@@ -8343,37 +9828,37 @@ function errorDetail(err) {
8343
9828
  return stderr.toString().trim();
8344
9829
  return err instanceof Error ? err.message : String(err);
8345
9830
  }
8346
- function gitStatusShort(repoRoot2) {
8347
- return execGit(repoRoot2, ["status", "--short"]);
9831
+ function gitStatusShort(repoRoot3) {
9832
+ return execGit(repoRoot3, ["status", "--short"]);
8348
9833
  }
8349
- function commitInstall(repoRoot2) {
8350
- execGit(repoRoot2, ["add", "-A"]);
8351
- const staged = execGit(repoRoot2, ["diff", "--cached", "--stat"]);
9834
+ function commitInstall(repoRoot3) {
9835
+ execGit(repoRoot3, ["add", "-A"]);
9836
+ const staged = execGit(repoRoot3, ["diff", "--cached", "--stat"]);
8352
9837
  if (!staged)
8353
9838
  return { committed: false, message: "Nothing to commit — working tree already clean." };
8354
9839
  try {
8355
- execGit(repoRoot2, ["commit", "-m", "Chore: install Vinaya"]);
9840
+ execGit(repoRoot3, ["commit", "-m", "Chore: install Vinaya"]);
8356
9841
  return { committed: true, message: "✓ Committed `Chore: install Vinaya`." };
8357
9842
  } catch (err) {
8358
9843
  return { committed: false, message: `git commit failed: ${errorDetail(err)}` };
8359
9844
  }
8360
9845
  }
8361
- function pushInstall(repoRoot2) {
9846
+ function pushInstall(repoRoot3) {
8362
9847
  try {
8363
- const branch = execGit(repoRoot2, ["symbolic-ref", "--short", "HEAD"]);
9848
+ const branch = execGit(repoRoot3, ["symbolic-ref", "--short", "HEAD"]);
8364
9849
  let hasUpstream = true;
8365
9850
  try {
8366
- execGit(repoRoot2, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
9851
+ execGit(repoRoot3, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
8367
9852
  } catch {
8368
9853
  hasUpstream = false;
8369
9854
  }
8370
9855
  if (hasUpstream) {
8371
- const ahead = execGit(repoRoot2, ["rev-list", "--count", "@{u}..HEAD"]);
9856
+ const ahead = execGit(repoRoot3, ["rev-list", "--count", "@{u}..HEAD"]);
8372
9857
  if (ahead === "0") {
8373
9858
  return { status: "up-to-date", message: "Nothing to push — already up to date with the remote." };
8374
9859
  }
8375
9860
  }
8376
- execGit(repoRoot2, hasUpstream ? ["push"] : ["push", "-u", "origin", branch]);
9861
+ execGit(repoRoot3, hasUpstream ? ["push"] : ["push", "-u", "origin", branch]);
8377
9862
  return { status: "pushed", message: "✓ Pushed." };
8378
9863
  } catch (err) {
8379
9864
  return { status: "failed", message: `git push failed: ${errorDetail(err)}` };
@@ -8465,12 +9950,12 @@ ${bold("Next steps:")}
8465
9950
  }
8466
9951
  }
8467
9952
  async function quickstartCommand(args) {
8468
- process.exit(await runQuickstart(args, realDeps6()));
9953
+ process.exit(await runQuickstart(args, realDeps7()));
8469
9954
  }
8470
9955
 
8471
9956
  // src/commands/review-post.ts
8472
- import { execFileSync as execFileSync15 } from "node:child_process";
8473
- 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";
8474
9959
  import { tmpdir as tmpdir2 } from "node:os";
8475
9960
  import { join as join24 } from "node:path";
8476
9961
  class FindingsParseError extends Error {
@@ -8509,6 +9994,12 @@ function renderFindingsSection(findings) {
8509
9994
  function renderTokensLine(role, roleLabel, input) {
8510
9995
  return `Tokens: ${input.taskId}: ${role} — ${roleLabel} — ${input.model} — ${input.tokensIn}/${input.tokensOut}/${input.cost}`;
8511
9996
  }
9997
+ function renderCastByLine(roleLabel, sessionId) {
9998
+ return `Cast by: ${roleLabel} (session ${sessionId})`;
9999
+ }
10000
+ function resolveSessionId(env) {
10001
+ return env.CLAUDE_CODE_SESSION_ID ?? "(unknown)";
10002
+ }
8512
10003
  var CODE_REVIEW_VERDICT_TEXT = {
8513
10004
  APPROVE: "APPROVE",
8514
10005
  REQUEST_CHANGES: "REQUEST CHANGES"
@@ -8530,7 +10021,8 @@ function renderCodeReviewComment(input) {
8530
10021
  `TESTS: ${input.tests}`,
8531
10022
  `DOCS: ${input.docs}`,
8532
10023
  "",
8533
- renderTokensLine("review", "Reviewer", input)
10024
+ renderTokensLine("review", "Reviewer", input),
10025
+ renderCastByLine("Reviewer", input.sessionId)
8534
10026
  ].join(`
8535
10027
  `);
8536
10028
  }
@@ -8553,7 +10045,7 @@ function renderSecurityComment(input) {
8553
10045
  if (input.secretsEvidence !== null) {
8554
10046
  lines.push("```", input.secretsEvidence, "```", "");
8555
10047
  }
8556
- lines.push(`SECRETS: ${input.secrets}`, "", renderTokensLine("security", "Security", input));
10048
+ lines.push(`SECRETS: ${input.secrets}`, "", renderTokensLine("security", "Security", input), renderCastByLine("Security", input.sessionId));
8557
10049
  return lines.join(`
8558
10050
  `);
8559
10051
  }
@@ -8645,7 +10137,7 @@ function readFindingsFile(path, allowedSeverities) {
8645
10137
  return [];
8646
10138
  let content;
8647
10139
  try {
8648
- content = readFileSync24(path, "utf8");
10140
+ content = readFileSync25(path, "utf8");
8649
10141
  } catch {
8650
10142
  refuseCmd(`Could not read findings file at ${path}.`, "Check the path and re-run.");
8651
10143
  }
@@ -8674,9 +10166,9 @@ function normalizeSecurityVerdict(raw) {
8674
10166
  return "FAIL";
8675
10167
  refuseCmd(`\`--verdict ${raw}\` is not PASS or FAIL.`, "Pass `--verdict PASS` or `--verdict FAIL`.");
8676
10168
  }
8677
- function gh(args) {
10169
+ function gh2(args) {
8678
10170
  try {
8679
- return execFileSync15("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
10171
+ return execFileSync17("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
8680
10172
  } catch (err) {
8681
10173
  const stderr = err.stderr;
8682
10174
  throw new Error(String(stderr ?? err.message).trim() || "gh command failed");
@@ -8685,7 +10177,7 @@ function gh(args) {
8685
10177
  function resolveHeadSha(pr) {
8686
10178
  let out;
8687
10179
  try {
8688
- out = gh(["pr", "view", pr, "--json", "headRefOid", "-q", ".headRefOid"]);
10180
+ out = gh2(["pr", "view", pr, "--json", "headRefOid", "-q", ".headRefOid"]);
8689
10181
  } catch (err) {
8690
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.");
8691
10183
  }
@@ -8698,7 +10190,7 @@ function postComment(pr, body) {
8698
10190
  const tmp = join24(tmpdir2(), `vinaya-review-post-${process.pid}-${Date.now()}.md`);
8699
10191
  writeFileSync12(tmp, body);
8700
10192
  try {
8701
- return gh(["pr", "comment", pr, "--body-file", tmp]).trim();
10193
+ return gh2(["pr", "comment", pr, "--body-file", tmp]).trim();
8702
10194
  } catch (err) {
8703
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.");
8704
10196
  } finally {
@@ -8706,7 +10198,7 @@ function postComment(pr, body) {
8706
10198
  }
8707
10199
  }
8708
10200
  function fetchComments(pr) {
8709
- const out = gh(["pr", "view", pr, "--json", "comments"]);
10201
+ const out = gh2(["pr", "view", pr, "--json", "comments"]);
8710
10202
  const parsed = JSON.parse(out);
8711
10203
  return parsed.comments.map((c) => ({ body: c.body, author: c.author?.login ?? null }));
8712
10204
  }
@@ -8775,7 +10267,8 @@ async function reviewPostCommand(args) {
8775
10267
  const tokensIn = requireTokenField(flags2, "--tokens-in");
8776
10268
  const tokensOut = requireTokenField(flags2, "--tokens-out");
8777
10269
  const cost = requireFlag(flags2, "--cost");
8778
- const tokens = { taskId, model, tokensIn, tokensOut, cost };
10270
+ const sessionId = resolveSessionId(process.env);
10271
+ const tokens = { taskId, model, tokensIn, tokensOut, cost, sessionId };
8779
10272
  const headSha = resolveHeadSha(pr);
8780
10273
  const principalAllowlist = resolvePrincipalAllowlist(loadTrustAnchorConfig());
8781
10274
  let body;
@@ -8813,7 +10306,7 @@ async function reviewPostCommand(args) {
8813
10306
  let secretsEvidence = null;
8814
10307
  if (secretsEvidenceFile) {
8815
10308
  try {
8816
- secretsEvidence = readFileSync24(secretsEvidenceFile, "utf8");
10309
+ secretsEvidence = readFileSync25(secretsEvidenceFile, "utf8");
8817
10310
  } catch {
8818
10311
  refuseCmd(`Could not read secrets evidence file at ${secretsEvidenceFile}.`, "Check the path and re-run.");
8819
10312
  }
@@ -8854,7 +10347,7 @@ Self-verification: clean — re-parsed VERDICT is bound to head ${headSha}.
8854
10347
 
8855
10348
  // src/commands/studio.ts
8856
10349
  import { execFile as execFile5, spawn as spawn2 } from "node:child_process";
8857
- import { existsSync as existsSync22, readFileSync as readFileSync25, renameSync } from "node:fs";
10350
+ import { existsSync as existsSync23, readFileSync as readFileSync26, renameSync } from "node:fs";
8858
10351
  import net from "node:net";
8859
10352
  import { dirname as dirname8, join as join25 } from "node:path";
8860
10353
  import { promisify as promisify5 } from "node:util";
@@ -8911,15 +10404,15 @@ function resolveStudioTarget(cwd, moduleUrl = import.meta.url) {
8911
10404
  for (;; ) {
8912
10405
  const webDir = join25(dir, "apps", "vinaya-studio", "web");
8913
10406
  const pkgPath = join25(webDir, "package.json");
8914
- if (existsSync22(pkgPath)) {
10407
+ if (existsSync23(pkgPath)) {
8915
10408
  try {
8916
- const pkg = JSON.parse(readFileSync25(pkgPath, "utf-8"));
10409
+ const pkg = JSON.parse(readFileSync26(pkgPath, "utf-8"));
8917
10410
  if (pkg.name === "@atta/vinaya-studio-web") {
8918
10411
  return { kind: "workspace", webDir };
8919
10412
  }
8920
10413
  } catch {}
8921
10414
  }
8922
- if (existsSync22(join25(dir, ".git")))
10415
+ if (existsSync23(join25(dir, ".git")))
8923
10416
  break;
8924
10417
  const parent = dirname8(dir);
8925
10418
  if (parent === dir)
@@ -8927,29 +10420,29 @@ function resolveStudioTarget(cwd, moduleUrl = import.meta.url) {
8927
10420
  dir = parent;
8928
10421
  }
8929
10422
  const standaloneWebDir = join25(packageRoot(moduleUrl), "studio-standalone", "apps", "vinaya-studio", "web");
8930
- if (existsSync22(join25(standaloneWebDir, "server.js"))) {
10423
+ if (existsSync23(join25(standaloneWebDir, "server.js"))) {
8931
10424
  return { kind: "package", packageDir: standaloneWebDir };
8932
10425
  }
8933
10426
  return { kind: "missing" };
8934
10427
  }
8935
10428
  function spawnDev(webDir, args) {
8936
- return new Promise((resolve2) => {
10429
+ return new Promise((resolve3) => {
8937
10430
  const child = spawn2("bun", ["run", "dev", ...args], { cwd: webDir, stdio: "inherit" });
8938
- child.on("exit", (code) => resolve2(code ?? 0));
10431
+ child.on("exit", (code) => resolve3(code ?? 0));
8939
10432
  });
8940
10433
  }
8941
10434
  function isPortFree(port) {
8942
- return new Promise((resolve2) => {
10435
+ return new Promise((resolve3) => {
8943
10436
  const tester = net.createServer();
8944
- tester.once("error", () => resolve2(false));
8945
- tester.once("listening", () => tester.close(() => resolve2(true)));
10437
+ tester.once("error", () => resolve3(false));
10438
+ tester.once("listening", () => tester.close(() => resolve3(true)));
8946
10439
  tester.listen(port, "127.0.0.1");
8947
10440
  });
8948
10441
  }
8949
10442
  function ensureStudioNodeModules(bundleRoot) {
8950
10443
  const real = join25(bundleRoot, "node_modules");
8951
10444
  const packed = join25(bundleRoot, STUDIO_NODE_MODULES_PACKED_DIRNAME);
8952
- if (!existsSync22(real) && existsSync22(packed)) {
10445
+ if (!existsSync23(real) && existsSync23(packed)) {
8953
10446
  renameSync(packed, real);
8954
10447
  }
8955
10448
  }
@@ -8973,9 +10466,9 @@ async function spawnStandalone(cwd, serverPath, bundleRoot, explicitPort) {
8973
10466
  const env = { HOSTNAME: "127.0.0.1", ...process.env, PORT: String(port), VINAYA_REPO_ROOT: cwd };
8974
10467
  if (repo)
8975
10468
  env.AEG_REPO = `${repo.owner}/${repo.repo}`;
8976
- return new Promise((resolve2) => {
10469
+ return new Promise((resolve3) => {
8977
10470
  const child = spawn2("node", [serverPath], { cwd, stdio: "inherit", env });
8978
- child.on("exit", (code) => resolve2(code ?? 0));
10471
+ child.on("exit", (code) => resolve3(code ?? 0));
8979
10472
  });
8980
10473
  }
8981
10474
  async function runStudio(cwd, args, moduleUrl = import.meta.url) {
@@ -9008,9 +10501,9 @@ async function runStudio(cwd, args, moduleUrl = import.meta.url) {
9008
10501
  }
9009
10502
 
9010
10503
  // src/commands/upgrade.ts
9011
- import { existsSync as existsSync23, readFileSync as readFileSync26, rmSync as rmSync5, writeFileSync as writeFileSync13 } from "node:fs";
10504
+ import { existsSync as existsSync24, readFileSync as readFileSync27, rmSync as rmSync5, writeFileSync as writeFileSync13 } from "node:fs";
9012
10505
  import { join as join26 } from "node:path";
9013
- function realDeps7() {
10506
+ function realDeps8() {
9014
10507
  return {
9015
10508
  detectRepo: detectGitRepo,
9016
10509
  hookDirFor: resolveHookDir,
@@ -9026,13 +10519,13 @@ function realDeps7() {
9026
10519
  function flags2(args) {
9027
10520
  return { dryRun: args.includes("--dry-run"), yes: args.includes("--yes") };
9028
10521
  }
9029
- function readManifest3(repoRoot2) {
9030
- const p = join26(repoRoot2, CONFIG_PATH);
9031
- if (!existsSync23(p))
10522
+ function readManifest3(repoRoot3) {
10523
+ const p = join26(repoRoot3, CONFIG_PATH);
10524
+ if (!existsSync24(p))
9032
10525
  return { kind: "missing" };
9033
10526
  let raw;
9034
10527
  try {
9035
- raw = JSON.parse(readFileSync26(p, "utf-8"));
10528
+ raw = JSON.parse(readFileSync27(p, "utf-8"));
9036
10529
  } catch (err) {
9037
10530
  return { kind: "invalid", error: `invalid JSON: ${err.message}` };
9038
10531
  }
@@ -9047,31 +10540,31 @@ function readManifest3(repoRoot2) {
9047
10540
  return { kind: "not-initialized" };
9048
10541
  return { kind: "ok", manifest: parsed.data.managed };
9049
10542
  }
9050
- function writeManifestVersion(repoRoot2, manifest) {
9051
- const configAbs = join26(repoRoot2, CONFIG_PATH);
9052
- const seed = JSON.parse(readFileSync26(configAbs, "utf-8"));
10543
+ function writeManifestVersion(repoRoot3, manifest) {
10544
+ const configAbs = join26(repoRoot3, CONFIG_PATH);
10545
+ const seed = JSON.parse(readFileSync27(configAbs, "utf-8"));
9053
10546
  const updated = { ...manifest, version: MANAGED_MANIFEST_VERSION };
9054
10547
  writeFileSync13(configAbs, `${JSON.stringify({ ...seed, managed: updated }, null, 2)}
9055
10548
  `, "utf-8");
9056
10549
  }
9057
- function stripFor(repoRoot2, path, marker, comment) {
9058
- const abs2 = resolveManagedBlockPath(repoRoot2, path);
9059
- if (!existsSync23(abs2))
10550
+ function stripFor(repoRoot3, path, marker, comment) {
10551
+ const abs2 = resolveManagedBlockPath(repoRoot3, path);
10552
+ if (!existsSync24(abs2))
9060
10553
  return { path, marker, comment, present: false, removesHost: false };
9061
- const stripped = stripBlockFromContent(readFileSync26(abs2, "utf-8"), marker, comment);
10554
+ const stripped = stripBlockFromContent(readFileSync27(abs2, "utf-8"), marker, comment);
9062
10555
  if (stripped === null)
9063
10556
  return { path, marker, comment, present: false, removesHost: false };
9064
10557
  return { path, marker, comment, present: true, removesHost: blockStripLeavesEmpty(stripped) };
9065
10558
  }
9066
- function planHookRouting(repoRoot2, manifest, recorded, hooksPathValue) {
10559
+ function planHookRouting(repoRoot3, manifest, recorded, hooksPathValue) {
9067
10560
  const none = { target: recorded, strips: [], arm: false, migratesManifest: false, blockedReason: null };
9068
10561
  if (recorded === ".husky")
9069
10562
  return none;
9070
10563
  if (recorded === TRACKED_HOOK_DIR) {
9071
- 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);
9072
10565
  if (hooksPathValue === TRACKED_HOOK_DIR)
9073
10566
  return { ...none, strips };
9074
- const foreign = foreignRawHooks(repoRoot2);
10567
+ const foreign = foreignRawHooks(repoRoot3);
9075
10568
  if (foreign.length > 0) {
9076
10569
  return {
9077
10570
  ...none,
@@ -9084,17 +10577,17 @@ function planHookRouting(repoRoot2, manifest, recorded, hooksPathValue) {
9084
10577
  const legacy = manifest.blocks.filter((b) => b.path.startsWith(".git/hooks/"));
9085
10578
  const blocked = [];
9086
10579
  for (const b of legacy) {
9087
- const abs2 = resolveManagedBlockPath(repoRoot2, b.path);
9088
- if (!existsSync23(abs2))
10580
+ const abs2 = resolveManagedBlockPath(repoRoot3, b.path);
10581
+ if (!existsSync24(abs2))
9089
10582
  continue;
9090
- const stripped = stripBlockFromContent(readFileSync26(abs2, "utf-8"), b.marker, b.comment);
10583
+ const stripped = stripBlockFromContent(readFileSync27(abs2, "utf-8"), b.marker, b.comment);
9091
10584
  if (stripped === null)
9092
10585
  blocked.push(`${b.path} exists without vinaya's managed block`);
9093
10586
  else if (!blockStripLeavesEmpty(stripped))
9094
10587
  blocked.push(`${b.path} carries your own lines alongside the managed block`);
9095
10588
  }
9096
10589
  const legacyNames = new Set(legacy.map((b) => b.path.slice(".git/hooks/".length)));
9097
- for (const f of activeRawHooks(repoRoot2)) {
10590
+ for (const f of activeRawHooks(repoRoot3)) {
9098
10591
  if (!legacyNames.has(f))
9099
10592
  blocked.push(`.git/hooks/${f} is an active raw hook vinaya does not manage`);
9100
10593
  }
@@ -9102,7 +10595,7 @@ function planHookRouting(repoRoot2, manifest, recorded, hooksPathValue) {
9102
10595
  return { ...none, blockedReason: blocked.join("; ") };
9103
10596
  return {
9104
10597
  target: TRACKED_HOOK_DIR,
9105
- 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),
9106
10599
  arm: hooksPathValue !== TRACKED_HOOK_DIR,
9107
10600
  migratesManifest: true,
9108
10601
  blockedReason: null
@@ -9118,7 +10611,7 @@ function extractWorkflowTrigger(content) {
9118
10611
  const match = content.match(/^on:\r?\n {2}([A-Za-z0-9_]+):/m);
9119
10612
  return match?.[1] ?? null;
9120
10613
  }
9121
- function planUpgrade(ops, repoRoot2, manifest, routing) {
10614
+ function planUpgrade(ops, repoRoot3, manifest, routing) {
9122
10615
  const entries = [];
9123
10616
  let hasChanges = routing.arm || routing.migratesManifest || routing.strips.some((s) => s.present);
9124
10617
  const ownedFiles = new Set(manifest.files);
@@ -9126,12 +10619,12 @@ function planUpgrade(ops, repoRoot2, manifest, routing) {
9126
10619
  const ownedBlocks = new Set(manifest.blocks.map((b) => blockKey(b.path, b.marker)));
9127
10620
  for (const op of ops) {
9128
10621
  if (op.kind === "create-file") {
9129
- const abs2 = join26(repoRoot2, op.path);
9130
- const exists = existsSync23(abs2);
10622
+ const abs2 = join26(repoRoot3, op.path);
10623
+ const exists = existsSync24(abs2);
9131
10624
  const owned = ownedFiles.has(op.path) || isDefaultedAgentVendorPath(op.path, manifest);
9132
10625
  let action;
9133
10626
  let triggerChange;
9134
- if (op.path === CONFIG_PATH || op.path === DOC_OWNERS_PATH) {
10627
+ if ((op.path === CONFIG_PATH || op.path === DOC_OWNERS_PATH) && exists) {
9135
10628
  action = "keep";
9136
10629
  } else if (!owned) {
9137
10630
  action = exists ? "refuse-foreign" : "not-installed";
@@ -9139,7 +10632,7 @@ function planUpgrade(ops, repoRoot2, manifest, routing) {
9139
10632
  action = "recreate";
9140
10633
  hasChanges = true;
9141
10634
  } else {
9142
- const diskContent = readFileSync26(abs2, "utf-8");
10635
+ const diskContent = readFileSync27(abs2, "utf-8");
9143
10636
  if (diskContent !== op.content) {
9144
10637
  action = "regenerate";
9145
10638
  hasChanges = true;
@@ -9155,16 +10648,16 @@ function planUpgrade(ops, repoRoot2, manifest, routing) {
9155
10648
  }
9156
10649
  entries.push({ kind: "create-file", op, action, ...triggerChange ? { triggerChange } : {} });
9157
10650
  } else if (op.kind === "managed-block") {
9158
- const abs2 = resolveManagedBlockPath(repoRoot2, op.path);
10651
+ const abs2 = resolveManagedBlockPath(repoRoot3, op.path);
9159
10652
  const owned = ownedBlocks.has(blockKey(op.path, op.marker));
9160
10653
  let action;
9161
10654
  if (!owned) {
9162
10655
  action = "not-installed";
9163
- } else if (!existsSync23(abs2)) {
10656
+ } else if (!existsSync24(abs2)) {
9164
10657
  action = "recreate-host";
9165
10658
  hasChanges = true;
9166
10659
  } else {
9167
- const content = readFileSync26(abs2, "utf-8");
10660
+ const content = readFileSync27(abs2, "utf-8");
9168
10661
  const { begin, end } = markerLines(op.marker, op.comment);
9169
10662
  if (!(content.includes(begin) && content.includes(end))) {
9170
10663
  action = "recreate-append";
@@ -9268,39 +10761,39 @@ function renderUpgradeDiff(plan) {
9268
10761
  return lines.join(`
9269
10762
  `);
9270
10763
  }
9271
- function regenerateBlock(repoRoot2, op) {
9272
- const abs2 = resolveManagedBlockPath(repoRoot2, op.path);
9273
- const content = readFileSync26(abs2, "utf-8");
10764
+ function regenerateBlock(repoRoot3, op) {
10765
+ const abs2 = resolveManagedBlockPath(repoRoot3, op.path);
10766
+ const content = readFileSync27(abs2, "utf-8");
9274
10767
  const stripped = stripBlockFromContent(content, op.marker, op.comment);
9275
10768
  if (stripped !== null) {
9276
10769
  writeFileSync13(abs2, stripped.endsWith(`
9277
10770
  `) ? stripped : `${stripped}
9278
10771
  `, "utf-8");
9279
10772
  }
9280
- appendBlock(repoRoot2, op);
10773
+ appendBlock(repoRoot3, op);
9281
10774
  }
9282
- function applyUpgrade(plan, repoRoot2) {
10775
+ function applyUpgrade(plan, repoRoot3) {
9283
10776
  for (const e of plan.entries) {
9284
10777
  if (e.kind === "create-file") {
9285
10778
  if (e.action === "regenerate" || e.action === "recreate") {
9286
- 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);
9287
10780
  }
9288
10781
  } else {
9289
10782
  if (e.action === "recreate-host")
9290
- createHost(repoRoot2, e.op);
10783
+ createHost(repoRoot3, e.op);
9291
10784
  else if (e.action === "recreate-append")
9292
- appendBlock(repoRoot2, e.op);
10785
+ appendBlock(repoRoot3, e.op);
9293
10786
  else if (e.action === "regenerate-block")
9294
- regenerateBlock(repoRoot2, e.op);
10787
+ regenerateBlock(repoRoot3, e.op);
9295
10788
  }
9296
10789
  }
9297
10790
  for (const s of plan.routing.strips) {
9298
10791
  if (!s.present)
9299
10792
  continue;
9300
- const abs2 = resolveManagedBlockPath(repoRoot2, s.path);
9301
- if (!existsSync23(abs2))
10793
+ const abs2 = resolveManagedBlockPath(repoRoot3, s.path);
10794
+ if (!existsSync24(abs2))
9302
10795
  continue;
9303
- const stripped = stripBlockFromContent(readFileSync26(abs2, "utf-8"), s.marker, s.comment);
10796
+ const stripped = stripBlockFromContent(readFileSync27(abs2, "utf-8"), s.marker, s.comment);
9304
10797
  if (stripped === null)
9305
10798
  continue;
9306
10799
  if (blockStripLeavesEmpty(stripped))
@@ -9385,20 +10878,20 @@ Vinaya upgraded.
9385
10878
  return 0;
9386
10879
  }
9387
10880
  async function upgradeCommand(args) {
9388
- process.exit(await runUpgrade(args, realDeps7()));
10881
+ process.exit(await runUpgrade(args, realDeps8()));
9389
10882
  }
9390
10883
 
9391
10884
  // src/commands/waiver.ts
9392
- import { execFileSync as execFileSync16 } from "node:child_process";
10885
+ import { execFileSync as execFileSync18 } from "node:child_process";
9393
10886
  function labelFor(kind) {
9394
10887
  return kind === "docs" ? WAIVER_LABEL : WAIVER_LABEL_REVIEW;
9395
10888
  }
9396
- function gh2(args) {
9397
- 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();
9398
10891
  }
9399
10892
  function detectPrNumber() {
9400
10893
  try {
9401
- const out = gh2(["pr", "view", "--json", "number", "-q", ".number"]);
10894
+ const out = gh3(["pr", "view", "--json", "number", "-q", ".number"]);
9402
10895
  return /^\d+$/.test(out) ? out : null;
9403
10896
  } catch {
9404
10897
  return null;
@@ -9494,8 +10987,8 @@ Reason: ${reason}`];
9494
10987
  }
9495
10988
  process.stdout.write(`Applying \`${label2}\` to PR #${prNumber} under your authenticated \`gh\` identity — vinaya never fabricates an actor.
9496
10989
  `);
9497
- execFileSync16("gh", editArgs, { stdio: "inherit", env: process.env });
9498
- 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 });
9499
10992
  process.stdout.write(`
9500
10993
  ✓ Label applied and reason posted as a PR comment.
9501
10994
  `);
@@ -9526,7 +11019,7 @@ function printHelp() {
9526
11019
  // src/index.ts
9527
11020
  var PACKAGE_ROOT = join27(dirname9(fileURLToPath2(import.meta.url)), "..");
9528
11021
  function readVersion2() {
9529
- const pkg = JSON.parse(readFileSync27(join27(PACKAGE_ROOT, "package.json"), "utf-8"));
11022
+ const pkg = JSON.parse(readFileSync28(join27(PACKAGE_ROOT, "package.json"), "utf-8"));
9530
11023
  return pkg.version;
9531
11024
  }
9532
11025
  var [, , command, ...args] = process.argv;
@@ -9619,8 +11112,10 @@ try {
9619
11112
  prEditCommand(rest);
9620
11113
  } else if (subcommand === "report") {
9621
11114
  await prReportCommand(rest);
11115
+ } else if (subcommand === "verify-evidence") {
11116
+ await prVerifyEvidenceCommand(rest);
9622
11117
  } else {
9623
- 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')`);
9624
11119
  process.exit(2);
9625
11120
  }
9626
11121
  break;
@@ -9645,8 +11140,10 @@ try {
9645
11140
  await milestoneAdoptCommand(rest);
9646
11141
  } else if (subcommand === "edit") {
9647
11142
  await milestoneEditCommand(rest);
11143
+ } else if (subcommand === "close") {
11144
+ await milestoneCloseCommand(rest);
9648
11145
  } else {
9649
- console.error(`Unknown 'milestone' subcommand: ${subcommand ?? "(none)"} (expected create/adopt/edit)`);
11146
+ console.error(`Unknown 'milestone' subcommand: ${subcommand ?? "(none)"} (expected create/adopt/edit/close)`);
9650
11147
  process.exit(2);
9651
11148
  }
9652
11149
  break;
@@ -9679,6 +11176,10 @@ try {
9679
11176
  await quickstartCommand(args);
9680
11177
  break;
9681
11178
  }
11179
+ case "tokens": {
11180
+ tokensCommand(args);
11181
+ break;
11182
+ }
9682
11183
  default:
9683
11184
  console.error(`Unknown command: ${command}`);
9684
11185
  printHelp();