@attalabs/vinaya 0.12.0 → 0.16.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 (94) hide show
  1. package/README.md +26 -2
  2. package/aeg-root/contracts/planner-brief.md +1 -1
  3. package/aeg-root/enforcement.md +5 -5
  4. package/aeg-root/roles/planner.md +2 -2
  5. package/aeg-root/roles/reviewer.md +17 -4
  6. package/aeg-root/roles/security.md +16 -5
  7. package/aeg-root/skills/aeg/SKILL.md +1 -1
  8. package/aeg-root/state-machine.md +2 -2
  9. package/aeg-root/tranche-model.md +1 -1
  10. package/dist/checks/bin/check-branch-topology.js +28 -5
  11. package/dist/checks/bin/check-brief-shape.js +28 -5
  12. package/dist/checks/bin/check-closes-n.js +28 -5
  13. package/dist/checks/bin/check-coherence.js +30 -6
  14. package/dist/checks/bin/check-dead-branch-push.js +28 -5
  15. package/dist/checks/bin/check-dispatch-readiness.js +30 -6
  16. package/dist/checks/bin/check-doc-coverage-push.js +30 -6
  17. package/dist/checks/bin/check-doc-coverage.js +30 -6
  18. package/dist/checks/bin/check-evidence-fresh.js +28 -5
  19. package/dist/checks/bin/check-first-push-dispatch.js +30 -6
  20. package/dist/checks/bin/check-issue-assignment.js +28 -5
  21. package/dist/checks/bin/check-no-disk-state.js +28 -5
  22. package/dist/checks/bin/check-reader-resolvable-prose.js +28 -5
  23. package/dist/checks/bin/check-registry-gates.js +28 -5
  24. package/dist/checks/bin/check-review-gate.js +33 -8
  25. package/dist/checks/bin/check-single-plan-pr.js +28 -5
  26. package/dist/checks/bin/check-test-plan.js +28 -5
  27. package/dist/index.js +777 -73
  28. package/package.json +1 -1
  29. package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
  30. package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
  31. package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
  32. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
  33. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
  34. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  35. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  36. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  37. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
  38. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  39. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  40. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
  41. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
  42. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
  43. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
  44. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
  45. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
  46. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
  47. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
  48. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
  49. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
  50. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
  51. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
  52. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
  53. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
  54. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
  55. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
  56. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
  57. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
  58. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
  59. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
  60. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
  61. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
  62. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
  63. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
  64. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
  65. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/[root-of-the-server]__0e95dv5._.js +1 -1
  66. 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
  67. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__029-1ch._.js +1 -1
  68. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0q56ohl._.js +1 -1
  69. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0zfjc2o._.js +1 -1
  70. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1_ojzp1._.js +1 -1
  71. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1d_8nm4._.js +1 -1
  72. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__1wsl6ro._.js → [root-of-the-server]__1qpl4m_._.js} +1 -1
  73. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1zkwtx8._.js +1 -1
  74. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__210ehg9._.js +1 -1
  75. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_00yjcv1._.js +1 -1
  76. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0o54suy._.js +1 -1
  77. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0rix02i._.js +1 -1
  78. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_18-d2be._.js +1 -1
  79. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +1 -1
  80. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1gw6vc9._.js +1 -1
  81. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
  82. package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
  83. package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
  84. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
  85. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
  86. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{2ieu4oi-pp_bz.js → 0ek6qlqhokys0.js} +1 -1
  87. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1khy4oilrab6g.js → 0fm4p9os2lsom.js} +1 -1
  88. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1eo32v1tbjy7c.js → 0ywkq74falx-x.js} +1 -1
  89. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1t7yy_s34022p.js → 1s9ixp5nhnevp.js} +1 -1
  90. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/27sqffqm2wbxx.css +1 -0
  91. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0qu0xgvifbacu.css +0 -1
  92. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{ZHVE9Y9G8XKR91g27SgVX → R7xvB2iuNFp_jxENpL7xv}/_buildManifest.js +0 -0
  93. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{ZHVE9Y9G8XKR91g27SgVX → R7xvB2iuNFp_jxENpL7xv}/_clientMiddlewareManifest.js +0 -0
  94. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{ZHVE9Y9G8XKR91g27SgVX → R7xvB2iuNFp_jxENpL7xv}/_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 readFileSync19 } from "node:fs";
5
- import { dirname as dirname8, join as join21 } from "node:path";
4
+ import { readFileSync as readFileSync20 } from "node:fs";
5
+ import { dirname as dirname8, join as join22 } from "node:path";
6
6
  import { fileURLToPath as fileURLToPath2 } from "node:url";
7
7
 
8
8
  // src/commands/archive.ts
9
- import { execFileSync as execFileSync3 } from "node:child_process";
9
+ import { execFileSync as execFileSync4 } from "node:child_process";
10
10
  // ../../packages/aeg-forge-state/src/strip-code.ts
11
11
  function maskCode(body) {
12
12
  const fill = (line) => " ".repeat(line.length);
@@ -392,6 +392,85 @@ function splitTableRow(row) {
392
392
  parts.pop();
393
393
  return parts.map((p) => p.split(PLACEHOLDER).join("|"));
394
394
  }
395
+ // ../../packages/aeg-core/src/blast-radius-domains.ts
396
+ function resolveWorkspaceEntry(entry2, listDirs) {
397
+ if (entry2.endsWith("/*")) {
398
+ const baseDir = entry2.slice(0, -2);
399
+ return listDirs(baseDir).map((name) => `${baseDir}/${name}`);
400
+ }
401
+ if (entry2.includes("*"))
402
+ return [];
403
+ return [entry2];
404
+ }
405
+ function deriveWorkspacePackageDomains(workspaces, listDirs) {
406
+ const positive = workspaces.filter((entry2) => !entry2.startsWith("!"));
407
+ const negative = workspaces.filter((entry2) => entry2.startsWith("!")).map((entry2) => entry2.slice(1));
408
+ const excluded = new Set;
409
+ for (const entry2 of negative) {
410
+ for (const member of resolveWorkspaceEntry(entry2, listDirs))
411
+ excluded.add(member);
412
+ }
413
+ const domains = new Set;
414
+ for (const entry2 of positive) {
415
+ for (const member of resolveWorkspaceEntry(entry2, listDirs)) {
416
+ if (excluded.has(member))
417
+ continue;
418
+ const segments = member.split("/");
419
+ if (segments[0] !== "packages" || segments.length < 2)
420
+ continue;
421
+ domains.add(`packages/${segments[1]}`);
422
+ }
423
+ }
424
+ return [...domains].sort();
425
+ }
426
+ function parsePnpmWorkspaceYaml(content) {
427
+ const unquote = (s) => {
428
+ const t = s.trim();
429
+ if (t.length >= 2 && (t[0] === "'" && t.at(-1) === "'" || t[0] === '"' && t.at(-1) === '"')) {
430
+ return t.slice(1, -1);
431
+ }
432
+ return t;
433
+ };
434
+ const inlineMatch = content.match(/^packages:\s*\[(.*)\]\s*$/m);
435
+ if (inlineMatch) {
436
+ return (inlineMatch[1] ?? "").split(",").map(unquote).filter((s) => s.length > 0);
437
+ }
438
+ const entries = [];
439
+ let inBlock = false;
440
+ for (const rawLine of content.split(`
441
+ `)) {
442
+ const line = rawLine.replace(/#.*$/, "");
443
+ if (/^packages:\s*$/.test(line)) {
444
+ inBlock = true;
445
+ continue;
446
+ }
447
+ if (!inBlock)
448
+ continue;
449
+ const itemMatch = line.match(/^\s+-\s*(.+?)\s*$/);
450
+ if (itemMatch?.[1]) {
451
+ entries.push(unquote(itemMatch[1]));
452
+ continue;
453
+ }
454
+ if (line.trim().length === 0)
455
+ continue;
456
+ break;
457
+ }
458
+ return entries;
459
+ }
460
+ var CROSS_CUTTING_CANDIDATES = [
461
+ "bun.lock",
462
+ "package-lock.json",
463
+ "pnpm-lock.yaml",
464
+ "yarn.lock",
465
+ "turbo.json",
466
+ "biome.json",
467
+ "tsconfig.json",
468
+ ".github/workflows",
469
+ ".husky"
470
+ ];
471
+ function deriveBuiltinCrossCuttingDefaults(exists) {
472
+ return CROSS_CUTTING_CANDIDATES.filter((path) => exists(path));
473
+ }
395
474
  // ../../packages/aeg-core/src/state-machine-model.ts
396
475
  var FORGE_FACT_INPUTS = [
397
476
  {
@@ -457,12 +536,30 @@ var DERIVED_STATUSES = [
457
536
  "incoherent"
458
537
  ];
459
538
  var DERIVABLE_STATUSES = DERIVED_STATUSES.filter((s) => s !== "backlog");
539
+ // ../../packages/aeg-core/src/file-classify.ts
540
+ function isCodeFile(p) {
541
+ if (p.endsWith(".md"))
542
+ return false;
543
+ return /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|sql|css)$/.test(p);
544
+ }
460
545
  // ../../packages/aeg-core/src/waiver-label.ts
461
546
  var WAIVER_LABEL = label("waiver-docs");
462
547
  var WAIVER_LABEL_REVIEW = label("waiver-review");
548
+ var PRINCIPAL_ALLOWLIST = ["daniboomerang"];
549
+ function isPrincipal(login, principalAllowlist) {
550
+ if (login === null)
551
+ return false;
552
+ const normalized = login.toLowerCase();
553
+ return principalAllowlist.some((p) => p.toLowerCase() === normalized);
554
+ }
463
555
 
464
556
  // ../../packages/aeg-core/src/doc-owners.ts
465
557
  var DOC_OWNERS_PATH = ".vinaya/doc-owners";
558
+ function classifyDocOwnersManifest(content) {
559
+ if (content === null)
560
+ return "absent";
561
+ return content.trim() === "" ? "empty" : "present";
562
+ }
466
563
  function parseDocOwners(content) {
467
564
  const bindings = [];
468
565
  const errors = [];
@@ -507,6 +604,13 @@ function globToRegex(pat) {
507
604
  re += "$";
508
605
  return new RegExp(re);
509
606
  }
607
+ function isUrlPointer(p) {
608
+ return /^https?:\/\//.test(p);
609
+ }
610
+ function pointerToPath(p) {
611
+ const idx = p.indexOf("#");
612
+ return idx === -1 ? p : p.slice(0, idx);
613
+ }
510
614
  var SEPARATOR = /(?:[ \t]*[—–][ \t]*|[ \t]+-[ \t]+)/.source;
511
615
  // ../../packages/aeg-core/src/pr-tier.ts
512
616
  function readTierFromPrBody(prBody) {
@@ -793,15 +897,25 @@ function isTaskIssueLabelSet(labels) {
793
897
  // ../../packages/aeg-core/src/coherence-checks.ts
794
898
  var R1_GRANDFATHERED_ISSUES = new Set([279, 280, 281, 282]);
795
899
  // ../../packages/aeg-core/src/verdict-extraction.ts
900
+ var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
901
+ function extractHeadSha(comment) {
902
+ const m = comment.match(HEAD_SHA_PATTERN);
903
+ return m ? m[1].toLowerCase() : null;
904
+ }
796
905
  function extractVerdict(comments, valuePattern, missingLabel) {
797
906
  const clearHits = comments.filter((c) => valuePattern.test(c));
798
907
  if (clearHits.length > 0) {
799
908
  const latest = clearHits[clearHits.length - 1];
800
909
  const m = latest.match(valuePattern);
801
- return { value: m[1].toUpperCase().replace(/[_-]/g, " "), danglingNote: null };
910
+ return {
911
+ value: m[1].toUpperCase().replace(/[_-]/g, " "),
912
+ headSha: extractHeadSha(latest),
913
+ danglingNote: null
914
+ };
802
915
  }
803
916
  return {
804
917
  value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
918
+ headSha: null,
805
919
  danglingNote: `no ${missingLabel} verdict comment found on this PR`
806
920
  };
807
921
  }
@@ -1536,6 +1650,14 @@ function starterDocOwners() {
1536
1650
  # This file absent, OR no glob matches any changed code file → silent no-op.
1537
1651
  # The gate has no opinion until you teach it one.
1538
1652
  #
1653
+ # \`vinaya doctor\` (never \`vinaya check\`) separately reports — it does not
1654
+ # fail the check — any binding whose code glob matches zero tracked files
1655
+ # anywhere in the repo, or whose in-repo pointer doesn't exist on disk. This
1656
+ # catches what the dormancy rule above structurally cannot: a glob matching
1657
+ # nothing trivially satisfies "no changed file matched" on every diff,
1658
+ # forever, so a binding naming deleted or renamed code reads as healthy
1659
+ # indefinitely unless something walks the whole repo, not just one diff.
1660
+ #
1539
1661
  # ── No-doc allow-list ───────────────────────────────────────────────────────
1540
1662
  # Surfaces that legitimately need no bound doc are listed below. Format:
1541
1663
  # # no-doc: <glob> — <reason>
@@ -1668,11 +1790,12 @@ function buildInitProductOps(name) {
1668
1790
  }
1669
1791
 
1670
1792
  // src/lib/ops.ts
1671
- import { execFileSync as execFileSync2 } from "node:child_process";
1793
+ import { execFileSync as execFileSync3 } from "node:child_process";
1672
1794
  import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
1673
1795
  import { dirname as dirname3, isAbsolute, join as join4, resolve, sep } from "node:path";
1674
1796
 
1675
1797
  // src/lib/config.ts
1798
+ import { execFileSync as execFileSync2 } from "node:child_process";
1676
1799
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
1677
1800
  import { homedir } from "node:os";
1678
1801
  import { dirname as dirname2, join as join3 } from "node:path";
@@ -1800,7 +1923,8 @@ var VinayaConfigSchema = z.object({
1800
1923
  briefSchema: BriefSchemaSchema.optional(),
1801
1924
  managed: ManagedManifestSchema.optional(),
1802
1925
  principals: z.array(z.string()).min(1).optional(),
1803
- ci: z.object({ setup: z.string().min(1) }).optional()
1926
+ ci: z.object({ setup: z.string().min(1) }).optional(),
1927
+ blastRadius: z.object({ extraDomains: z.array(z.string()).optional() }).optional()
1804
1928
  });
1805
1929
  var GLOBAL_VINAYA_HOME = join3(homedir(), ".vinaya");
1806
1930
  var GLOBAL_CONFIG_PATH = join3(GLOBAL_VINAYA_HOME, "config.json");
@@ -1847,6 +1971,75 @@ function stripGlobalOnlyKeys(config, path) {
1847
1971
  }
1848
1972
  return result;
1849
1973
  }
1974
+ function resolvePrincipalAllowlist(config) {
1975
+ return config?.principals ?? PRINCIPAL_ALLOWLIST;
1976
+ }
1977
+ function trustAnchorRepo() {
1978
+ const wellFormed = (slug) => /^[^/\s]+\/[^/\s]+$/.test(slug) ? slug : null;
1979
+ const fromRunner = process.env.GITHUB_REPOSITORY?.trim();
1980
+ if (fromRunner) {
1981
+ const validated = wellFormed(fromRunner);
1982
+ if (validated)
1983
+ return validated;
1984
+ }
1985
+ try {
1986
+ const url = execFileSync2("git", ["remote", "get-url", "origin"], {
1987
+ encoding: "utf-8",
1988
+ stdio: ["ignore", "pipe", "pipe"]
1989
+ }).trim();
1990
+ const m = url.match(/github\.com[:/]([^/]+)\/(.+?)(?:\.git)?\/?$/);
1991
+ return m?.[1] && m[2] ? wellFormed(`${m[1]}/${m[2]}`) : null;
1992
+ } catch {
1993
+ return null;
1994
+ }
1995
+ }
1996
+ function ghFetchTrustAnchorConfig() {
1997
+ const repo = trustAnchorRepo();
1998
+ if (!repo)
1999
+ throw new Error("could not resolve repo identity for the trust-anchor read");
2000
+ return execFileSync2("gh", ["api", `repos/${repo}/contents/${LOCAL_CONFIG_FILENAME}`, "--jq", ".content"], {
2001
+ encoding: "utf-8",
2002
+ stdio: ["ignore", "pipe", "pipe"]
2003
+ });
2004
+ }
2005
+ function firstLine(err) {
2006
+ return err?.message?.split(`
2007
+ `)[0] ?? "unknown error";
2008
+ }
2009
+ function isMissingFileError(err) {
2010
+ const stderr = err?.stderr;
2011
+ const haystack = [
2012
+ err?.message ?? "",
2013
+ typeof stderr === "string" ? stderr : stderr?.toString() ?? ""
2014
+ ].join(`
2015
+ `);
2016
+ return /\b404\b|not found/i.test(haystack);
2017
+ }
2018
+ function loadTrustAnchorConfig(fetcher = ghFetchTrustAnchorConfig) {
2019
+ const warn = (why) => process.stdout.write(`⚠ could not read \`principals\` from the default branch (${why}) — falling back to vinaya's built-in principal allowlist.
2020
+ `);
2021
+ let base64;
2022
+ try {
2023
+ base64 = fetcher().trim();
2024
+ } catch (err) {
2025
+ if (!isMissingFileError(err))
2026
+ warn(firstLine(err));
2027
+ return null;
2028
+ }
2029
+ if (!base64)
2030
+ return null;
2031
+ try {
2032
+ const raw = Buffer.from(base64, "base64").toString("utf-8");
2033
+ const parsed = VinayaConfigSchema.safeParse(JSON.parse(raw));
2034
+ if (parsed.success)
2035
+ return parsed.data;
2036
+ warn("it did not satisfy the config schema");
2037
+ return null;
2038
+ } catch {
2039
+ warn("it was not readable as JSON");
2040
+ return null;
2041
+ }
2042
+ }
1850
2043
  function readRepoCiSetup(repoRoot) {
1851
2044
  const p = join3(repoRoot, "vinaya.config.json");
1852
2045
  if (!existsSync2(p))
@@ -1897,7 +2090,7 @@ function resolveManagedBlockPath(repoRoot, opPath) {
1897
2090
  if (!opPath.startsWith(".git/"))
1898
2091
  return join4(repoRoot, opPath);
1899
2092
  try {
1900
- const commonDir = execFileSync2("git", ["-C", repoRoot, "rev-parse", "--git-common-dir"], {
2093
+ const commonDir = execFileSync3("git", ["-C", repoRoot, "rev-parse", "--git-common-dir"], {
1901
2094
  encoding: "utf8"
1902
2095
  }).trim();
1903
2096
  const gitDir = isAbsolute(commonDir) ? commonDir : join4(repoRoot, commonDir);
@@ -2463,7 +2656,7 @@ function realDeps() {
2463
2656
  return { detectRepo: detectGitRepo };
2464
2657
  }
2465
2658
  function sh(args, input) {
2466
- return execFileSync3(args[0], args.slice(1), { encoding: "utf8", input }).trim();
2659
+ return execFileSync4(args[0], args.slice(1), { encoding: "utf8", input }).trim();
2467
2660
  }
2468
2661
  function shJson(args) {
2469
2662
  return JSON.parse(sh(args));
@@ -2639,7 +2832,7 @@ async function archiveTrancheCommand(args) {
2639
2832
  }
2640
2833
 
2641
2834
  // src/commands/audit.ts
2642
- import { execFileSync as execFileSync4 } from "node:child_process";
2835
+ import { execFileSync as execFileSync5 } from "node:child_process";
2643
2836
 
2644
2837
  // src/lib/envelope.ts
2645
2838
  var ENVELOPE_SCHEMA_VERSION = 1;
@@ -2667,7 +2860,7 @@ var DIRECT_PUSH_MARKER = `<!-- ${DIRECT_PUSH_LABEL} -->`;
2667
2860
  var DIRECT_PUSH_LABEL_DESCRIPTION = "A commit landed on main with no associated merged PR (ring-2 detection)";
2668
2861
  function shSoft(args, input) {
2669
2862
  try {
2670
- return execFileSync4(args[0], args.slice(1), {
2863
+ return execFileSync5(args[0], args.slice(1), {
2671
2864
  encoding: "utf8",
2672
2865
  input,
2673
2866
  timeout: COMMAND_TIMEOUT_MS
@@ -2691,7 +2884,7 @@ function shSoftJson(args) {
2691
2884
  }
2692
2885
  }
2693
2886
  function sh2(args, input) {
2694
- return execFileSync4(args[0], args.slice(1), { encoding: "utf8", input }).trim();
2887
+ return execFileSync5(args[0], args.slice(1), { encoding: "utf8", input }).trim();
2695
2888
  }
2696
2889
  function listTaskBranches() {
2697
2890
  const out = shSoft(["git", "ls-remote", "--heads", "origin", "task/*"]);
@@ -2941,7 +3134,7 @@ async function auditCommand(args) {
2941
3134
  }
2942
3135
 
2943
3136
  // src/commands/check.ts
2944
- import { execFileSync as execFileSync5 } from "node:child_process";
3137
+ import { execFileSync as execFileSync6 } from "node:child_process";
2945
3138
 
2946
3139
  // src/checks/contract.ts
2947
3140
  var CHECK_SCHEMA_VERSION = 1;
@@ -3478,7 +3671,7 @@ async function runChecks(specs, opts) {
3478
3671
  // src/commands/check.ts
3479
3672
  function git(args) {
3480
3673
  try {
3481
- return execFileSync5("git", args, { encoding: "utf8" }).trim();
3674
+ return execFileSync6("git", args, { encoding: "utf8" }).trim();
3482
3675
  } catch {
3483
3676
  return "";
3484
3677
  }
@@ -3611,11 +3804,13 @@ async function checkCommand(args) {
3611
3804
  process.stdout.write(`${renderPlanTable(result)}
3612
3805
  `);
3613
3806
  }
3614
- process.exit(result.failures.length > 0 ? 1 : 0);
3807
+ process.exitCode = result.failures.length > 0 ? 1 : 0;
3808
+ return;
3615
3809
  }
3616
3810
  if (!allRequested && !requestedName) {
3617
3811
  console.error("Usage: vinaya check <name> | --all | --plan [--json] [--diff-only] [--local] [--parallel[=n]]");
3618
- process.exit(2);
3812
+ process.exitCode = 2;
3813
+ return;
3619
3814
  }
3620
3815
  if (refusals.length > 0) {
3621
3816
  const outcome = {
@@ -3636,7 +3831,8 @@ async function checkCommand(args) {
3636
3831
  process.stdout.write(` ${e.severity}: ${e.message}
3637
3832
  `);
3638
3833
  }
3639
- process.exit(1);
3834
+ process.exitCode = 1;
3835
+ return;
3640
3836
  }
3641
3837
  const allSpecs = result.resolved.map((entry2) => entry2.spec);
3642
3838
  const notices = substitutionNotices(result.resolved);
@@ -3650,7 +3846,8 @@ async function checkCommand(args) {
3650
3846
  const specsToRun = allRequested ? allSpecs.filter(runsUnderAll) : allSpecs.filter((s) => s.name === requestedName);
3651
3847
  if (!allRequested && specsToRun.length === 0) {
3652
3848
  console.error(`Unknown check: ${requestedName}`);
3653
- process.exit(2);
3849
+ process.exitCode = 2;
3850
+ return;
3654
3851
  }
3655
3852
  const changed = diffOnly ? changedFiles() : null;
3656
3853
  const outcomes = specsToRun.length > 0 ? await runChecks(specsToRun, {
@@ -3677,18 +3874,18 @@ async function checkCommand(args) {
3677
3874
  }
3678
3875
  }
3679
3876
  const failed = outcomes.some((o) => o.status === "fail" || o.status === "error" || o.status === "timeout");
3680
- process.exit(failed ? 1 : 0);
3877
+ process.exitCode = failed ? 1 : 0;
3681
3878
  }
3682
3879
 
3683
3880
  // src/commands/demo.ts
3684
- import { execFileSync as execFileSync6, spawnSync } from "node:child_process";
3881
+ import { execFileSync as execFileSync7, spawnSync } from "node:child_process";
3685
3882
  import { existsSync as existsSync6, readFileSync as readFileSync5, rmSync as rmSync2, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
3686
3883
  import { isAbsolute as isAbsolute2, join as join7 } from "node:path";
3687
3884
  var DEMO_BRANCH_PREFIX = "vinaya/demo-break-";
3688
3885
  var FIXTURE_PATH = ".vinaya-demo-brief.md";
3689
3886
  var STATE_FILENAME = "vinaya-demo-state.json";
3690
3887
  function gitCapture(repoRoot, args) {
3691
- return execFileSync6("git", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
3888
+ return execFileSync7("git", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
3692
3889
  }
3693
3890
  function currentBranchName(repoRoot) {
3694
3891
  try {
@@ -3699,7 +3896,7 @@ function currentBranchName(repoRoot) {
3699
3896
  }
3700
3897
  function branchExists(repoRoot, name) {
3701
3898
  try {
3702
- execFileSync6("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${name}`], {
3899
+ execFileSync7("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${name}`], {
3703
3900
  cwd: repoRoot,
3704
3901
  stdio: ["ignore", "pipe", "pipe"]
3705
3902
  });
@@ -3901,7 +4098,8 @@ async function demoBreakCommand(args) {
3901
4098
  }
3902
4099
 
3903
4100
  // src/commands/doctor.ts
3904
- import { existsSync as existsSync8, readFileSync as readFileSync8, statSync as statSync2 } from "node:fs";
4101
+ import { execFileSync as execFileSync8 } from "node:child_process";
4102
+ import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync2 } from "node:fs";
3905
4103
  import { join as join9 } from "node:path";
3906
4104
 
3907
4105
  // src/lib/self-host.ts
@@ -4182,6 +4380,90 @@ function diagnoseCustomChecks(repoRoot, config) {
4182
4380
  }
4183
4381
  return findings;
4184
4382
  }
4383
+ function listTrackedFiles(repoRoot) {
4384
+ try {
4385
+ return execFileSync8("git", ["ls-files"], { cwd: repoRoot, encoding: "utf-8" }).split(`
4386
+ `).map((s) => s.trim()).filter(Boolean);
4387
+ } catch {
4388
+ return [];
4389
+ }
4390
+ }
4391
+ function diagnoseDocOwnersHealth(repoRoot) {
4392
+ const path = join9(repoRoot, DOC_OWNERS_PATH);
4393
+ const content = existsSync8(path) ? readFileSync8(path, "utf-8") : null;
4394
+ const state = classifyDocOwnersManifest(content);
4395
+ if (state === "absent" || state === "empty")
4396
+ return [];
4397
+ const { bindings } = parseDocOwners(content);
4398
+ if (bindings.length === 0)
4399
+ return [];
4400
+ const codeFiles = listTrackedFiles(repoRoot).filter(isCodeFile);
4401
+ const findings = [];
4402
+ for (const b of bindings) {
4403
+ let flagged = false;
4404
+ const re = globToRegex(b.glob);
4405
+ if (!codeFiles.some((f) => re.test(f))) {
4406
+ flagged = true;
4407
+ 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."));
4408
+ }
4409
+ if (!isUrlPointer(b.pointer)) {
4410
+ const pointerPath = join9(repoRoot, pointerToPath(b.pointer));
4411
+ if (!existsSync8(pointerPath)) {
4412
+ flagged = true;
4413
+ 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."));
4414
+ }
4415
+ }
4416
+ if (!flagged) {
4417
+ findings.push(ok("doc-owners", `${DOC_OWNERS_PATH}:${b.lineNum} — '${b.glob}' → ${b.pointer} is live.`));
4418
+ }
4419
+ }
4420
+ return findings;
4421
+ }
4422
+ var LEGACY_AEG_PACKAGES_PATH = ".aeg/packages";
4423
+ function readWorkspacesForDoctor(repoRoot) {
4424
+ const fromPackageJson = () => {
4425
+ try {
4426
+ const pkg = JSON.parse(readFileSync8(join9(repoRoot, "package.json"), "utf-8"));
4427
+ return Array.isArray(pkg.workspaces) ? pkg.workspaces.filter((w) => typeof w === "string") : [];
4428
+ } catch {
4429
+ return [];
4430
+ }
4431
+ };
4432
+ const fromPnpmWorkspaceYaml = () => {
4433
+ try {
4434
+ return parsePnpmWorkspaceYaml(readFileSync8(join9(repoRoot, "pnpm-workspace.yaml"), "utf-8"));
4435
+ } catch {
4436
+ return [];
4437
+ }
4438
+ };
4439
+ return [...fromPackageJson(), ...fromPnpmWorkspaceYaml()];
4440
+ }
4441
+ function listChildDirsForDoctor(dir, repoRoot) {
4442
+ try {
4443
+ return readdirSync3(join9(repoRoot, dir), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
4444
+ } catch {
4445
+ return [];
4446
+ }
4447
+ }
4448
+ function diagnoseBlastRadiusDeprecation(repoRoot, config) {
4449
+ const derived = deriveWorkspacePackageDomains(readWorkspacesForDoctor(repoRoot), (dir) => listChildDirsForDoctor(dir, repoRoot));
4450
+ const defaults = deriveBuiltinCrossCuttingDefaults((p) => existsSync8(join9(repoRoot, p)));
4451
+ const covered = new Set([...derived, ...defaults]);
4452
+ const configExtra = new Set(config?.blastRadius?.extraDomains ?? []);
4453
+ const legacyPath = join9(repoRoot, LEGACY_AEG_PACKAGES_PATH);
4454
+ if (!existsSync8(legacyPath)) {
4455
+ return [
4456
+ 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.`)
4457
+ ];
4458
+ }
4459
+ const legacyEntries = readFileSync8(legacyPath, "utf-8").split(`
4460
+ `).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
4461
+ const uncovered = legacyEntries.filter((e) => !covered.has(e) && !configExtra.has(e));
4462
+ 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(", ")}.`;
4463
+ return [
4464
+ warn("blast-radius", `${LEGACY_AEG_PACKAGES_PATH} is deprecated — checkBlastRadiusScope now derives packages/* domains live and ships built-in cross-cutting defaults. Declare anything beyond those in vinaya.config.json's blastRadius.extraDomains instead. ${migrationNote}`)
4465
+ ];
4466
+ }
4185
4467
  function diagnoseEnvDeclarations(repoRoot, config) {
4186
4468
  const resolved = resolveChecks(coreCheckRegistry(), config?.checks).resolved;
4187
4469
  const specs = resolved.map((entry2) => entry2.source === "config" ? { ...entry2.spec, run: join9(repoRoot, entry2.spec.run) } : entry2.spec);
@@ -4242,6 +4524,29 @@ async function diagnoseBranchProtection(deps, owner, repo) {
4242
4524
  }
4243
4525
  return info("branch-protection", "main branch protection could not be determined (no gh auth, no remote, or a permission gap).");
4244
4526
  }
4527
+ var TEST_INVOCATION_SUBSTRINGS = ["npm test", "npm run test", "bun test", "bunx turbo test"];
4528
+ function diagnoseTestCi(repoRoot) {
4529
+ let pkg;
4530
+ try {
4531
+ pkg = JSON.parse(readFileSync8(join9(repoRoot, "package.json"), "utf-8"));
4532
+ } catch {
4533
+ return [];
4534
+ }
4535
+ const testScript = pkg?.scripts?.test;
4536
+ if (typeof testScript !== "string" || testScript.trim() === "")
4537
+ return [];
4538
+ const workflowsDir = join9(repoRoot, ".github/workflows");
4539
+ const files = existsSync8(workflowsDir) ? readdirSync3(workflowsDir).filter((name) => statSync2(join9(workflowsDir, name)).isFile()) : [];
4540
+ const invoked = files.some((name) => {
4541
+ const content = readFileSync8(join9(workflowsDir, name), "utf-8");
4542
+ return TEST_INVOCATION_SUBSTRINGS.some((s) => content.includes(s));
4543
+ });
4544
+ if (invoked)
4545
+ return [];
4546
+ return [
4547
+ warn("test-ci", "no workflow under .github/workflows/ appears to invoke this repo's `package.json` test script — " + "vinaya requires a Test Plan on every PR and enforces it as a blocking gate, but nothing here verifies that tests actually run.")
4548
+ ];
4549
+ }
4245
4550
  function symbolFor(severity) {
4246
4551
  switch (severity) {
4247
4552
  case "ok":
@@ -4295,12 +4600,15 @@ async function runDoctor(args, deps) {
4295
4600
  hasDrift = install.hasDrift;
4296
4601
  findings.push(...await diagnoseHookRouting(repo.repoRoot, hookDir, deps.readHooksPath));
4297
4602
  findings.push(...diagnoseCustomChecks(repo.repoRoot, configRead.config));
4603
+ findings.push(...diagnoseDocOwnersHealth(repo.repoRoot));
4298
4604
  }
4605
+ findings.push(...diagnoseBlastRadiusDeprecation(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
4299
4606
  findings.push(...diagnoseEnvDeclarations(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
4300
4607
  findings.push(...diagnoseCheckClassification(configRead.kind === "ok" ? configRead.config : null));
4301
4608
  findings.push(...diagnoseGlobalConfigChecks());
4302
4609
  findings.push(...await diagnoseEnvironment(deps, hasDrift));
4303
4610
  findings.push(await diagnoseBranchProtection(deps, repo.owner, repo.repo));
4611
+ findings.push(...diagnoseTestCi(repo.repoRoot));
4304
4612
  const healthy = findings.every((f) => f.severity === "ok" || f.severity === "info");
4305
4613
  if (jsonOutput) {
4306
4614
  printJson({ healthy, findings });
@@ -4314,7 +4622,7 @@ async function doctorCommand(args) {
4314
4622
  }
4315
4623
 
4316
4624
  // src/commands/doctrine.ts
4317
- import { existsSync as existsSync9 } from "node:fs";
4625
+ import { existsSync as existsSync9, readdirSync as readdirSync4 } from "node:fs";
4318
4626
  import { dirname as dirname4, join as join10, sep as sep3 } from "node:path";
4319
4627
  var ENTRY_SEGMENTS = ["skills", "aeg", "SKILL.md"];
4320
4628
  function resolveDoctrineRoot(pkg = packageRoot(import.meta.url)) {
@@ -4327,6 +4635,12 @@ function resolveDoctrineRoot(pkg = packageRoot(import.meta.url)) {
4327
4635
  }
4328
4636
  return null;
4329
4637
  }
4638
+ function listRoleNames(root) {
4639
+ const rolesDir = join10(root, "roles");
4640
+ if (!existsSync9(rolesDir))
4641
+ return [];
4642
+ return readdirSync4(rolesDir).filter((name) => name.endsWith(".md")).map((name) => name.slice(0, -".md".length)).sort();
4643
+ }
4330
4644
  function doctrineCommand(args) {
4331
4645
  const root = resolveDoctrineRoot();
4332
4646
  if (root === null) {
@@ -4334,6 +4648,24 @@ function doctrineCommand(args) {
4334
4648
  `);
4335
4649
  process.exit(1);
4336
4650
  }
4651
+ const roleFlagIndex = args.indexOf("--role");
4652
+ if (roleFlagIndex !== -1) {
4653
+ const roleName = args[roleFlagIndex + 1];
4654
+ const validRoleNames = listRoleNames(root);
4655
+ if (roleName === undefined || roleName.startsWith("--") || !validRoleNames.includes(roleName)) {
4656
+ process.stderr.write(`vinaya doctrine --role: '${roleName ?? ""}' is not a known role. ` + `Valid role names: ${validRoleNames.join(", ")}
4657
+ `);
4658
+ process.exit(1);
4659
+ }
4660
+ const entry3 = join10(root, "roles", `${roleName}.md`);
4661
+ if (args.includes("--json")) {
4662
+ printJson({ root, entry: entry3 });
4663
+ return;
4664
+ }
4665
+ process.stdout.write(`${entry3}
4666
+ `);
4667
+ return;
4668
+ }
4337
4669
  const entry2 = join10(root, ...ENTRY_SEGMENTS);
4338
4670
  if (args.includes("--json")) {
4339
4671
  printJson({ root, entry: entry2 });
@@ -4750,7 +5082,7 @@ async function initProductCommand(args) {
4750
5082
  }
4751
5083
 
4752
5084
  // src/commands/issue.ts
4753
- import { execFileSync as execFileSync7 } from "node:child_process";
5085
+ import { execFileSync as execFileSync9 } from "node:child_process";
4754
5086
 
4755
5087
  // src/lib/forge-write.ts
4756
5088
  import { mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
@@ -4973,7 +5305,7 @@ function locateBodyOrRefuse(ghArgs, retryCommand) {
4973
5305
  function fetchForgeLabels(issueRef) {
4974
5306
  let out;
4975
5307
  try {
4976
- out = execFileSync7("gh", ["issue", "view", issueRef, "--json", "labels"], {
5308
+ out = execFileSync9("gh", ["issue", "view", issueRef, "--json", "labels"], {
4977
5309
  encoding: "utf8",
4978
5310
  stdio: ["ignore", "pipe", "pipe"]
4979
5311
  });
@@ -5001,7 +5333,7 @@ function reportPass(json, command) {
5001
5333
  function runGhWrite(ghCmd, ghArgs, bodyResult, json) {
5002
5334
  const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
5003
5335
  try {
5004
- const out = execFileSync7("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
5336
+ const out = execFileSync9("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
5005
5337
  const url = out.trim();
5006
5338
  if (json)
5007
5339
  printJson({ validated: true, written: true, url });
@@ -5115,12 +5447,12 @@ ${registration}
5115
5447
  }
5116
5448
 
5117
5449
  // src/commands/pr.ts
5118
- import { execFileSync as execFileSync8 } from "node:child_process";
5450
+ import { execFileSync as execFileSync10 } from "node:child_process";
5119
5451
  var RETRY_CREATE2 = "vinaya pr create --validate-only …";
5120
5452
  var RETRY_EDIT2 = "vinaya pr edit <n> --validate-only …";
5121
5453
  function git2(args) {
5122
5454
  try {
5123
- return execFileSync8("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
5455
+ return execFileSync10("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
5124
5456
  } catch {
5125
5457
  return "";
5126
5458
  }
@@ -5154,7 +5486,7 @@ function reportPass2(json, command) {
5154
5486
  function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
5155
5487
  const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
5156
5488
  try {
5157
- const out = execFileSync8("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
5489
+ const out = execFileSync10("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
5158
5490
  const url = out.trim();
5159
5491
  if (json)
5160
5492
  printJson({ validated: true, written: true, url });
@@ -5168,7 +5500,7 @@ function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
5168
5500
  function fetchPrForgeContext(prRef) {
5169
5501
  let viewOut;
5170
5502
  try {
5171
- viewOut = execFileSync8("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
5503
+ viewOut = execFileSync10("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
5172
5504
  encoding: "utf8",
5173
5505
  stdio: ["ignore", "pipe", "pipe"]
5174
5506
  });
@@ -5270,14 +5602,14 @@ function prEditCommand(args) {
5270
5602
  }
5271
5603
 
5272
5604
  // src/commands/pr-report.ts
5273
- import { execFileSync as execFileSync9, spawnSync as spawnSync2 } from "node:child_process";
5605
+ import { execFileSync as execFileSync11, spawnSync as spawnSync2 } from "node:child_process";
5274
5606
  import { existsSync as existsSync14, readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "node:fs";
5275
5607
  import { join as join16 } from "node:path";
5276
5608
  var EVIDENCE_START = "<!-- AEG:EVIDENCE:START -->";
5277
5609
  var EVIDENCE_END = "<!-- AEG:EVIDENCE:END -->";
5278
5610
  function git3(args) {
5279
5611
  try {
5280
- return execFileSync9("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
5612
+ return execFileSync11("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
5281
5613
  } catch {
5282
5614
  return "";
5283
5615
  }
@@ -5295,7 +5627,7 @@ class GitCommandError extends Error {
5295
5627
  }
5296
5628
  function gitStrict(args) {
5297
5629
  try {
5298
- return execFileSync9("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
5630
+ return execFileSync11("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
5299
5631
  } catch (err) {
5300
5632
  const stderr = err.stderr;
5301
5633
  throw new GitCommandError(args, String(stderr ?? err.message).trim() || "non-zero exit");
@@ -5449,7 +5781,7 @@ async function prReportCommand(args) {
5449
5781
  }
5450
5782
 
5451
5783
  // src/commands/quickstart.ts
5452
- import { execFileSync as execFileSync10 } from "node:child_process";
5784
+ import { execFileSync as execFileSync12 } from "node:child_process";
5453
5785
  import { existsSync as existsSync16, readFileSync as readFileSync16 } from "node:fs";
5454
5786
  import { join as join18 } from "node:path";
5455
5787
 
@@ -5645,7 +5977,7 @@ function realDeps6() {
5645
5977
  };
5646
5978
  }
5647
5979
  function execGit(repoRoot, args) {
5648
- return execFileSync10("git", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
5980
+ return execFileSync12("git", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
5649
5981
  }
5650
5982
  function errorDetail(err) {
5651
5983
  const stderr = err.stderr;
@@ -5761,11 +6093,331 @@ async function quickstartCommand(args) {
5761
6093
  process.exit(await runQuickstart(args, realDeps6()));
5762
6094
  }
5763
6095
 
6096
+ // src/commands/review-post.ts
6097
+ import { execFileSync as execFileSync13 } from "node:child_process";
6098
+ import { readFileSync as readFileSync17, rmSync as rmSync4, writeFileSync as writeFileSync10 } from "node:fs";
6099
+ import { tmpdir as tmpdir2 } from "node:os";
6100
+ import { join as join19 } from "node:path";
6101
+ class FindingsParseError extends Error {
6102
+ }
6103
+ var CODE_REVIEW_SEVERITIES = ["BLOCKER", "MAJOR", "MINOR"];
6104
+ var SECURITY_SEVERITIES = ["CRITICAL", "HIGH", "MEDIUM", "LOW"];
6105
+ function parseFindingsFile(content, allowedSeverities) {
6106
+ const lines = content.split(`
6107
+ `).map((l) => l.trim()).filter((l) => l.length > 0);
6108
+ return lines.map((line, idx) => {
6109
+ const parts = line.split("|");
6110
+ if (parts.length !== 3) {
6111
+ throw new FindingsParseError(`findings file line ${idx + 1}: expected exactly 3 \`|\`-delimited fields (SEVERITY|file:line|description), found ${parts.length}: ${line}`);
6112
+ }
6113
+ const severity = parts[0].trim().toUpperCase();
6114
+ const location = parts[1].trim();
6115
+ const description = parts[2].trim();
6116
+ if (!allowedSeverities.includes(severity)) {
6117
+ throw new FindingsParseError(`findings file line ${idx + 1}: severity "${parts[0]}" is not one of ${allowedSeverities.join("|")}: ${line}`);
6118
+ }
6119
+ if (!location || !description) {
6120
+ throw new FindingsParseError(`findings file line ${idx + 1}: file:line and description must both be non-empty: ${line}`);
6121
+ }
6122
+ return { severity, location, description };
6123
+ });
6124
+ }
6125
+ function sortBySeverity(findings, order) {
6126
+ return findings.map((f, i) => ({ f, i })).sort((a, b) => order.indexOf(a.f.severity) - order.indexOf(b.f.severity) || a.i - b.i).map(({ f }) => f);
6127
+ }
6128
+ function renderFindingsSection(findings) {
6129
+ if (findings.length === 0)
6130
+ return "None.";
6131
+ return findings.map((f, i) => `${i + 1}. [${f.severity}] ${f.location} — ${f.description}`).join(`
6132
+ `);
6133
+ }
6134
+ function renderTokensLine(role, roleLabel, input) {
6135
+ return `Tokens: ${input.taskId}: ${role} — ${roleLabel} — ${input.model} — ${input.tokensIn}/${input.tokensOut}/${input.cost}`;
6136
+ }
6137
+ var CODE_REVIEW_VERDICT_TEXT = {
6138
+ APPROVE: "APPROVE",
6139
+ REQUEST_CHANGES: "REQUEST CHANGES"
6140
+ };
6141
+ function renderCodeReviewComment(input) {
6142
+ const sorted = sortBySeverity(input.findings, CODE_REVIEW_SEVERITIES);
6143
+ return [
6144
+ `VERDICT: ${CODE_REVIEW_VERDICT_TEXT[input.verdict]}`,
6145
+ "",
6146
+ `Judged head: ${input.headSha}`,
6147
+ "",
6148
+ `BRIEF CONFORMANCE: ${input.briefConformance}`,
6149
+ `SPEC CONFORMANCE: ${input.specConformance}`,
6150
+ "",
6151
+ "FINDINGS (ordered by severity):",
6152
+ renderFindingsSection(sorted),
6153
+ "",
6154
+ `SCOPE: ${input.scope}`,
6155
+ `TESTS: ${input.tests}`,
6156
+ `DOCS: ${input.docs}`,
6157
+ "",
6158
+ renderTokensLine("review", "Reviewer", input)
6159
+ ].join(`
6160
+ `);
6161
+ }
6162
+ function isNoneFoundClaim(value) {
6163
+ return value.trim().toLowerCase().replace(/[-_]+/g, " ").replace(/\s+/g, " ") === "none found";
6164
+ }
6165
+ function renderSecurityComment(input) {
6166
+ const sorted = sortBySeverity(input.findings, SECURITY_SEVERITIES);
6167
+ const lines = [
6168
+ `VERDICT: ${input.verdict}`,
6169
+ "",
6170
+ `Judged head: ${input.headSha}`,
6171
+ "",
6172
+ "FINDINGS (ordered by severity):",
6173
+ renderFindingsSection(sorted),
6174
+ "",
6175
+ `CONFIG SCAN: ${input.configScan}`,
6176
+ ""
6177
+ ];
6178
+ if (input.secretsEvidence !== null) {
6179
+ lines.push("```", input.secretsEvidence, "```", "");
6180
+ }
6181
+ lines.push(`SECRETS: ${input.secrets}`, "", renderTokensLine("security", "Security", input));
6182
+ return lines.join(`
6183
+ `);
6184
+ }
6185
+ function isBoundToHead(extraction, headSha) {
6186
+ if (!extraction.headSha)
6187
+ return false;
6188
+ return headSha.toLowerCase().startsWith(extraction.headSha.toLowerCase());
6189
+ }
6190
+ function checkExtraction(extraction, expectedValue, headSha) {
6191
+ if (extraction.danglingNote) {
6192
+ return {
6193
+ ok: false,
6194
+ reason: `no clean VERDICT was found on re-fetch (${extraction.danglingNote}) — the posted comment does not match the gate's line-anchored \`VERDICT:\` pattern.`
6195
+ };
6196
+ }
6197
+ if (extraction.value !== expectedValue) {
6198
+ return {
6199
+ ok: false,
6200
+ reason: `re-extraction found VERDICT "${extraction.value}", expected "${expectedValue}" — the posted comment's VERDICT line does not match what this command rendered.`
6201
+ };
6202
+ }
6203
+ if (!extraction.headSha) {
6204
+ return { ok: false, reason: "the winning VERDICT comment carries no `Judged head:` line on re-fetch." };
6205
+ }
6206
+ if (!isBoundToHead(extraction, headSha)) {
6207
+ return {
6208
+ ok: false,
6209
+ reason: `re-extraction found \`Judged head: ${extraction.headSha}\`, which does not cover the resolved head ${headSha}.`
6210
+ };
6211
+ }
6212
+ return { ok: true, reason: "clean" };
6213
+ }
6214
+ function principalBodies(comments, principalAllowlist) {
6215
+ return comments.filter((c) => isPrincipal(c.author, principalAllowlist)).map((c) => c.body);
6216
+ }
6217
+ function verifyPostedCodeReview(comments, verdict, headSha, principalAllowlist) {
6218
+ return checkExtraction(extractCodeReviewVerdict(principalBodies(comments, principalAllowlist)), CODE_REVIEW_VERDICT_TEXT[verdict], headSha);
6219
+ }
6220
+ function verifyPostedSecurity(comments, verdict, headSha, principalAllowlist) {
6221
+ return checkExtraction(extractSecurityReviewVerdict(principalBodies(comments, principalAllowlist)), verdict, headSha);
6222
+ }
6223
+ function parseFlags(args) {
6224
+ const map = new Map;
6225
+ for (let i = 0;i < args.length; i++) {
6226
+ const a = args[i];
6227
+ if (a.startsWith("--")) {
6228
+ const value = args[i + 1];
6229
+ if (value === undefined || value.startsWith("--")) {
6230
+ map.set(a, "");
6231
+ continue;
6232
+ }
6233
+ map.set(a, value);
6234
+ i++;
6235
+ }
6236
+ }
6237
+ return map;
6238
+ }
6239
+ function refuseCmd(message, recovery) {
6240
+ refuse([makeCheckError("review-post", message, recovery)]);
6241
+ }
6242
+ function requireFlag(flags2, name) {
6243
+ const v = flags2.get(name);
6244
+ if (v === undefined || v === "") {
6245
+ refuseCmd(`Missing required \`${name}\` flag.`, `Pass \`${name} <value>\`, then re-run \`vinaya review post ...\`.`);
6246
+ }
6247
+ return v;
6248
+ }
6249
+ function requireTokenField(flags2, name) {
6250
+ const v = requireFlag(flags2, name);
6251
+ if (v !== "-" && !/^\d+$/.test(v)) {
6252
+ refuseCmd(`\`${name} ${v}\` is neither a non-negative integer nor \`-\` (unknown).`, `Pass a whole number or \`-\` for ${name}.`);
6253
+ }
6254
+ return v;
6255
+ }
6256
+ function readFindingsFile(path, allowedSeverities) {
6257
+ if (!path)
6258
+ return [];
6259
+ let content;
6260
+ try {
6261
+ content = readFileSync17(path, "utf8");
6262
+ } catch {
6263
+ refuseCmd(`Could not read findings file at ${path}.`, "Check the path and re-run.");
6264
+ }
6265
+ try {
6266
+ return parseFindingsFile(content, allowedSeverities);
6267
+ } catch (err) {
6268
+ if (err instanceof FindingsParseError) {
6269
+ refuseCmd(err.message, "Fix the malformed line in the findings file, then re-run.");
6270
+ }
6271
+ throw err;
6272
+ }
6273
+ }
6274
+ function normalizeCodeReviewVerdict(raw) {
6275
+ const v = raw.trim().toUpperCase().replace(/[-\s]+/g, "_");
6276
+ if (v === "APPROVE")
6277
+ return "APPROVE";
6278
+ if (v === "REQUEST_CHANGES")
6279
+ return "REQUEST_CHANGES";
6280
+ refuseCmd(`\`--verdict ${raw}\` is not APPROVE or REQUEST_CHANGES.`, "Pass `--verdict APPROVE` or `--verdict REQUEST_CHANGES`.");
6281
+ }
6282
+ function normalizeSecurityVerdict(raw) {
6283
+ const v = raw.trim().toUpperCase();
6284
+ if (v === "PASS")
6285
+ return "PASS";
6286
+ if (v === "FAIL")
6287
+ return "FAIL";
6288
+ refuseCmd(`\`--verdict ${raw}\` is not PASS or FAIL.`, "Pass `--verdict PASS` or `--verdict FAIL`.");
6289
+ }
6290
+ function gh(args) {
6291
+ try {
6292
+ return execFileSync13("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
6293
+ } catch (err) {
6294
+ const stderr = err.stderr;
6295
+ throw new Error(String(stderr ?? err.message).trim() || "gh command failed");
6296
+ }
6297
+ }
6298
+ function resolveHeadSha(pr) {
6299
+ let out;
6300
+ try {
6301
+ out = gh(["pr", "view", pr, "--json", "headRefOid", "-q", ".headRefOid"]);
6302
+ } catch (err) {
6303
+ 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.");
6304
+ }
6305
+ if (!out) {
6306
+ refuseCmd(`\`gh pr view ${pr} --json headRefOid\` returned no head sha.`, "Confirm PR exists, then re-run.");
6307
+ }
6308
+ return out;
6309
+ }
6310
+ function postComment(pr, body) {
6311
+ const tmp = join19(tmpdir2(), `vinaya-review-post-${process.pid}-${Date.now()}.md`);
6312
+ writeFileSync10(tmp, body);
6313
+ try {
6314
+ return gh(["pr", "comment", pr, "--body-file", tmp]).trim();
6315
+ } catch (err) {
6316
+ 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.");
6317
+ } finally {
6318
+ rmSync4(tmp, { force: true });
6319
+ }
6320
+ }
6321
+ function fetchComments(pr) {
6322
+ const out = gh(["pr", "view", pr, "--json", "comments"]);
6323
+ const parsed = JSON.parse(out);
6324
+ return parsed.comments.map((c) => ({ body: c.body, author: c.author?.login ?? null }));
6325
+ }
6326
+ async function reviewPostCommand(args) {
6327
+ const json = args.includes("--json");
6328
+ const flags2 = parseFlags(args.filter((a) => a !== "--json"));
6329
+ const role = flags2.get("--role");
6330
+ if (role !== "code-reviewer" && role !== "security") {
6331
+ refuseCmd(`\`--role ${role ?? "(missing)"}\` is not \`code-reviewer\` or \`security\`.`, "Pass `--role code-reviewer` or `--role security`.");
6332
+ }
6333
+ const pr = requireFlag(flags2, "--pr");
6334
+ const taskId = requireFlag(flags2, "--task-id");
6335
+ const model = requireFlag(flags2, "--model");
6336
+ const tokensIn = requireTokenField(flags2, "--tokens-in");
6337
+ const tokensOut = requireTokenField(flags2, "--tokens-out");
6338
+ const cost = requireFlag(flags2, "--cost");
6339
+ const tokens = { taskId, model, tokensIn, tokensOut, cost };
6340
+ const headSha = resolveHeadSha(pr);
6341
+ const principalAllowlist = resolvePrincipalAllowlist(loadTrustAnchorConfig());
6342
+ let body;
6343
+ let verify;
6344
+ if (role === "code-reviewer") {
6345
+ const verdict = normalizeCodeReviewVerdict(requireFlag(flags2, "--verdict"));
6346
+ const findings = readFindingsFile(flags2.get("--findings-file"), CODE_REVIEW_SEVERITIES);
6347
+ if (findings.some((f) => f.severity === "BLOCKER") && verdict === "APPROVE") {
6348
+ refuseCmd("Findings include a BLOCKER but `--verdict APPROVE` — reviewer.md requires REQUEST CHANGES whenever any BLOCKER finding exists.", "Pass `--verdict REQUEST_CHANGES`, or fix the finding severity if BLOCKER was a miscategorization.");
6349
+ }
6350
+ const input = {
6351
+ ...tokens,
6352
+ headSha,
6353
+ verdict,
6354
+ briefConformance: requireFlag(flags2, "--brief-conformance"),
6355
+ specConformance: requireFlag(flags2, "--spec-conformance"),
6356
+ findings,
6357
+ scope: requireFlag(flags2, "--scope"),
6358
+ tests: requireFlag(flags2, "--tests"),
6359
+ docs: requireFlag(flags2, "--docs")
6360
+ };
6361
+ body = renderCodeReviewComment(input);
6362
+ verify = (comments2) => verifyPostedCodeReview(comments2, verdict, headSha, principalAllowlist);
6363
+ } else {
6364
+ const verdict = normalizeSecurityVerdict(requireFlag(flags2, "--verdict"));
6365
+ const findings = readFindingsFile(flags2.get("--findings-file"), SECURITY_SEVERITIES);
6366
+ if (findings.some((f) => f.severity === "CRITICAL" || f.severity === "HIGH") && verdict === "PASS") {
6367
+ refuseCmd("Findings include a CRITICAL/HIGH finding but `--verdict PASS` — security.md requires FAIL whenever any CRITICAL or HIGH finding exists.", "Pass `--verdict FAIL`, or fix the finding severity if it was a miscategorization.");
6368
+ }
6369
+ const secrets = requireFlag(flags2, "--secrets");
6370
+ const secretsEvidenceFile = flags2.get("--secrets-evidence-file");
6371
+ if (isNoneFoundClaim(secrets) && !secretsEvidenceFile) {
6372
+ refuseCmd('`--secrets` normalizes to "none found" but no `--secrets-evidence-file` was given — security.md: "SECRETS: none found" with no scan output pasted is an unbacked self-attestation.', "Pass `--secrets-evidence-file <path>` containing the actual scanner output, or change `--secrets` to describe what was found instead.");
6373
+ }
6374
+ let secretsEvidence = null;
6375
+ if (secretsEvidenceFile) {
6376
+ try {
6377
+ secretsEvidence = readFileSync17(secretsEvidenceFile, "utf8");
6378
+ } catch {
6379
+ refuseCmd(`Could not read secrets evidence file at ${secretsEvidenceFile}.`, "Check the path and re-run.");
6380
+ }
6381
+ }
6382
+ const input = {
6383
+ ...tokens,
6384
+ headSha,
6385
+ verdict,
6386
+ findings,
6387
+ configScan: requireFlag(flags2, "--config-scan"),
6388
+ secrets,
6389
+ secretsEvidence
6390
+ };
6391
+ body = renderSecurityComment(input);
6392
+ verify = (comments2) => verifyPostedSecurity(comments2, verdict, headSha, principalAllowlist);
6393
+ }
6394
+ const url = postComment(pr, body);
6395
+ let comments;
6396
+ try {
6397
+ comments = fetchComments(pr);
6398
+ } catch (err) {
6399
+ refuseCmd(`Posted the comment (${url}) but could not re-fetch PR ${pr}'s comments to self-verify: ${err instanceof Error ? err.message : String(err)}`, "Check `gh auth status`/network and manually confirm the posted comment parses cleanly — this command could not verify it.");
6400
+ }
6401
+ const result = verify(comments);
6402
+ if (!result.ok) {
6403
+ refuseCmd(`Posted comment ${url}, but self-verification FAILED on re-parse: ${result.reason}`, "The posted comment does not re-parse clean through the same extractCodeReviewVerdict/extractSecurityReviewVerdict functions the merge gate calls. Do not treat the post as valid — inspect the comment and this command for drift, fix, and re-run.");
6404
+ }
6405
+ if (json) {
6406
+ printJson({ posted: true, url, role, headSha, selfVerified: true });
6407
+ } else {
6408
+ process.stdout.write(`${body}
6409
+
6410
+ Posted: ${url}
6411
+ Self-verification: clean — re-parsed VERDICT is bound to head ${headSha}.
6412
+ `);
6413
+ }
6414
+ }
6415
+
5764
6416
  // src/commands/studio.ts
5765
6417
  import { execFile as execFile5, spawn as spawn2 } from "node:child_process";
5766
- import { existsSync as existsSync17, readFileSync as readFileSync17, renameSync } from "node:fs";
6418
+ import { existsSync as existsSync17, readFileSync as readFileSync18, renameSync } from "node:fs";
5767
6419
  import net from "node:net";
5768
- import { dirname as dirname7, join as join19 } from "node:path";
6420
+ import { dirname as dirname7, join as join20 } from "node:path";
5769
6421
  import { promisify as promisify5 } from "node:util";
5770
6422
 
5771
6423
  // src/lib/studio-bundle.ts
@@ -5781,25 +6433,25 @@ var FALLBACK_PORT = 3108;
5781
6433
  function resolveStudioTarget(cwd, moduleUrl = import.meta.url) {
5782
6434
  let dir = cwd;
5783
6435
  for (;; ) {
5784
- const webDir = join19(dir, "apps", "vinaya-studio", "web");
5785
- const pkgPath = join19(webDir, "package.json");
6436
+ const webDir = join20(dir, "apps", "vinaya-studio", "web");
6437
+ const pkgPath = join20(webDir, "package.json");
5786
6438
  if (existsSync17(pkgPath)) {
5787
6439
  try {
5788
- const pkg = JSON.parse(readFileSync17(pkgPath, "utf-8"));
6440
+ const pkg = JSON.parse(readFileSync18(pkgPath, "utf-8"));
5789
6441
  if (pkg.name === "@atta/vinaya-studio-web") {
5790
6442
  return { kind: "workspace", webDir };
5791
6443
  }
5792
6444
  } catch {}
5793
6445
  }
5794
- if (existsSync17(join19(dir, ".git")))
6446
+ if (existsSync17(join20(dir, ".git")))
5795
6447
  break;
5796
6448
  const parent = dirname7(dir);
5797
6449
  if (parent === dir)
5798
6450
  break;
5799
6451
  dir = parent;
5800
6452
  }
5801
- const standaloneWebDir = join19(packageRoot(moduleUrl), "studio-standalone", "apps", "vinaya-studio", "web");
5802
- if (existsSync17(join19(standaloneWebDir, "server.js"))) {
6453
+ const standaloneWebDir = join20(packageRoot(moduleUrl), "studio-standalone", "apps", "vinaya-studio", "web");
6454
+ if (existsSync17(join20(standaloneWebDir, "server.js"))) {
5803
6455
  return { kind: "package", packageDir: standaloneWebDir };
5804
6456
  }
5805
6457
  return { kind: "missing" };
@@ -5819,8 +6471,8 @@ function isPortFree(port) {
5819
6471
  });
5820
6472
  }
5821
6473
  function ensureStudioNodeModules(bundleRoot) {
5822
- const real = join19(bundleRoot, "node_modules");
5823
- const packed = join19(bundleRoot, STUDIO_NODE_MODULES_PACKED_DIRNAME);
6474
+ const real = join20(bundleRoot, "node_modules");
6475
+ const packed = join20(bundleRoot, STUDIO_NODE_MODULES_PACKED_DIRNAME);
5824
6476
  if (!existsSync17(real) && existsSync17(packed)) {
5825
6477
  renameSync(packed, real);
5826
6478
  }
@@ -5847,8 +6499,8 @@ async function runStudio(cwd, args, moduleUrl = import.meta.url) {
5847
6499
  case "workspace":
5848
6500
  return spawnDev(target.webDir, args);
5849
6501
  case "package": {
5850
- const bundleRoot = join19(target.packageDir, "..", "..", "..");
5851
- return spawnStandalone(cwd, join19(target.packageDir, "server.js"), bundleRoot);
6502
+ const bundleRoot = join20(target.packageDir, "..", "..", "..");
6503
+ return spawnStandalone(cwd, join20(target.packageDir, "server.js"), bundleRoot);
5852
6504
  }
5853
6505
  case "missing":
5854
6506
  console.error("Vinaya Studio isn't available in this install — no published @attalabs/vinaya build bundles the Studio app yet. Inside a checkout that contains Studio's source (apps/vinaya-studio/web), `vinaya studio` runs it directly.");
@@ -5857,8 +6509,8 @@ async function runStudio(cwd, args, moduleUrl = import.meta.url) {
5857
6509
  }
5858
6510
 
5859
6511
  // src/commands/upgrade.ts
5860
- import { existsSync as existsSync18, readFileSync as readFileSync18, rmSync as rmSync4, writeFileSync as writeFileSync10 } from "node:fs";
5861
- import { join as join20 } from "node:path";
6512
+ import { existsSync as existsSync18, readFileSync as readFileSync19, rmSync as rmSync5, writeFileSync as writeFileSync11 } from "node:fs";
6513
+ import { join as join21 } from "node:path";
5862
6514
  function realDeps7() {
5863
6515
  return {
5864
6516
  detectRepo: detectGitRepo,
@@ -5876,12 +6528,12 @@ function flags2(args) {
5876
6528
  return { dryRun: args.includes("--dry-run"), yes: args.includes("--yes") };
5877
6529
  }
5878
6530
  function readManifest3(repoRoot) {
5879
- const p = join20(repoRoot, CONFIG_PATH);
6531
+ const p = join21(repoRoot, CONFIG_PATH);
5880
6532
  if (!existsSync18(p))
5881
6533
  return { kind: "missing" };
5882
6534
  let raw;
5883
6535
  try {
5884
- raw = JSON.parse(readFileSync18(p, "utf-8"));
6536
+ raw = JSON.parse(readFileSync19(p, "utf-8"));
5885
6537
  } catch (err) {
5886
6538
  return { kind: "invalid", error: `invalid JSON: ${err.message}` };
5887
6539
  }
@@ -5897,17 +6549,17 @@ function readManifest3(repoRoot) {
5897
6549
  return { kind: "ok", manifest: parsed.data.managed };
5898
6550
  }
5899
6551
  function writeManifestVersion(repoRoot, manifest) {
5900
- const configAbs = join20(repoRoot, CONFIG_PATH);
5901
- const seed = JSON.parse(readFileSync18(configAbs, "utf-8"));
6552
+ const configAbs = join21(repoRoot, CONFIG_PATH);
6553
+ const seed = JSON.parse(readFileSync19(configAbs, "utf-8"));
5902
6554
  const updated = { ...manifest, version: MANAGED_MANIFEST_VERSION };
5903
- writeFileSync10(configAbs, `${JSON.stringify({ ...seed, managed: updated }, null, 2)}
6555
+ writeFileSync11(configAbs, `${JSON.stringify({ ...seed, managed: updated }, null, 2)}
5904
6556
  `, "utf-8");
5905
6557
  }
5906
6558
  function stripFor(repoRoot, path, marker, comment) {
5907
6559
  const abs2 = resolveManagedBlockPath(repoRoot, path);
5908
6560
  if (!existsSync18(abs2))
5909
6561
  return { path, marker, comment, present: false, removesHost: false };
5910
- const stripped = stripBlockFromContent(readFileSync18(abs2, "utf-8"), marker, comment);
6562
+ const stripped = stripBlockFromContent(readFileSync19(abs2, "utf-8"), marker, comment);
5911
6563
  if (stripped === null)
5912
6564
  return { path, marker, comment, present: false, removesHost: false };
5913
6565
  return { path, marker, comment, present: true, removesHost: blockStripLeavesEmpty(stripped) };
@@ -5936,7 +6588,7 @@ function planHookRouting(repoRoot, manifest, recorded, hooksPathValue) {
5936
6588
  const abs2 = resolveManagedBlockPath(repoRoot, b.path);
5937
6589
  if (!existsSync18(abs2))
5938
6590
  continue;
5939
- const stripped = stripBlockFromContent(readFileSync18(abs2, "utf-8"), b.marker, b.comment);
6591
+ const stripped = stripBlockFromContent(readFileSync19(abs2, "utf-8"), b.marker, b.comment);
5940
6592
  if (stripped === null)
5941
6593
  blocked.push(`${b.path} exists without vinaya's managed block`);
5942
6594
  else if (!blockStripLeavesEmpty(stripped))
@@ -5971,7 +6623,7 @@ function planUpgrade(ops, repoRoot, manifest, routing) {
5971
6623
  const ownedBlocks = new Set(manifest.blocks.map((b) => blockKey(b.path, b.marker)));
5972
6624
  for (const op of ops) {
5973
6625
  if (op.kind === "create-file") {
5974
- const abs2 = join20(repoRoot, op.path);
6626
+ const abs2 = join21(repoRoot, op.path);
5975
6627
  const exists = existsSync18(abs2);
5976
6628
  const owned = ownedFiles.has(op.path);
5977
6629
  let action;
@@ -5982,7 +6634,7 @@ function planUpgrade(ops, repoRoot, manifest, routing) {
5982
6634
  } else if (!exists) {
5983
6635
  action = "recreate";
5984
6636
  hasChanges = true;
5985
- } else if (readFileSync18(abs2, "utf-8") !== op.content) {
6637
+ } else if (readFileSync19(abs2, "utf-8") !== op.content) {
5986
6638
  action = "regenerate";
5987
6639
  hasChanges = true;
5988
6640
  } else {
@@ -5999,7 +6651,7 @@ function planUpgrade(ops, repoRoot, manifest, routing) {
5999
6651
  action = "recreate-host";
6000
6652
  hasChanges = true;
6001
6653
  } else {
6002
- const content = readFileSync18(abs2, "utf-8");
6654
+ const content = readFileSync19(abs2, "utf-8");
6003
6655
  const { begin, end } = markerLines(op.marker, op.comment);
6004
6656
  if (!(content.includes(begin) && content.includes(end))) {
6005
6657
  action = "recreate-append";
@@ -6099,10 +6751,10 @@ function renderUpgradeDiff(plan) {
6099
6751
  }
6100
6752
  function regenerateBlock(repoRoot, op) {
6101
6753
  const abs2 = resolveManagedBlockPath(repoRoot, op.path);
6102
- const content = readFileSync18(abs2, "utf-8");
6754
+ const content = readFileSync19(abs2, "utf-8");
6103
6755
  const stripped = stripBlockFromContent(content, op.marker, op.comment);
6104
6756
  if (stripped !== null) {
6105
- writeFileSync10(abs2, stripped.endsWith(`
6757
+ writeFileSync11(abs2, stripped.endsWith(`
6106
6758
  `) ? stripped : `${stripped}
6107
6759
  `, "utf-8");
6108
6760
  }
@@ -6112,7 +6764,7 @@ function applyUpgrade(plan, repoRoot) {
6112
6764
  for (const e of plan.entries) {
6113
6765
  if (e.kind === "create-file") {
6114
6766
  if (e.action === "regenerate" || e.action === "recreate") {
6115
- writeFileWithDirs(join20(repoRoot, e.op.path), e.op.content, e.op.mode);
6767
+ writeFileWithDirs(join21(repoRoot, e.op.path), e.op.content, e.op.mode);
6116
6768
  }
6117
6769
  } else {
6118
6770
  if (e.action === "recreate-host")
@@ -6129,13 +6781,13 @@ function applyUpgrade(plan, repoRoot) {
6129
6781
  const abs2 = resolveManagedBlockPath(repoRoot, s.path);
6130
6782
  if (!existsSync18(abs2))
6131
6783
  continue;
6132
- const stripped = stripBlockFromContent(readFileSync18(abs2, "utf-8"), s.marker, s.comment);
6784
+ const stripped = stripBlockFromContent(readFileSync19(abs2, "utf-8"), s.marker, s.comment);
6133
6785
  if (stripped === null)
6134
6786
  continue;
6135
6787
  if (blockStripLeavesEmpty(stripped))
6136
- rmSync4(abs2, { force: true });
6788
+ rmSync5(abs2, { force: true });
6137
6789
  else
6138
- writeFileSync10(abs2, stripped.endsWith(`
6790
+ writeFileSync11(abs2, stripped.endsWith(`
6139
6791
  `) ? stripped : `${stripped}
6140
6792
  `, "utf-8");
6141
6793
  }
@@ -6217,16 +6869,16 @@ async function upgradeCommand(args) {
6217
6869
  }
6218
6870
 
6219
6871
  // src/commands/waiver.ts
6220
- import { execFileSync as execFileSync11 } from "node:child_process";
6872
+ import { execFileSync as execFileSync14 } from "node:child_process";
6221
6873
  function labelFor(kind) {
6222
6874
  return kind === "docs" ? WAIVER_LABEL : WAIVER_LABEL_REVIEW;
6223
6875
  }
6224
- function gh(args) {
6225
- return execFileSync11("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], env: process.env }).trim();
6876
+ function gh2(args) {
6877
+ return execFileSync14("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], env: process.env }).trim();
6226
6878
  }
6227
6879
  function detectPrNumber() {
6228
6880
  try {
6229
- const out = gh(["pr", "view", "--json", "number", "-q", ".number"]);
6881
+ const out = gh2(["pr", "view", "--json", "number", "-q", ".number"]);
6230
6882
  return /^\d+$/.test(out) ? out : null;
6231
6883
  } catch {
6232
6884
  return null;
@@ -6322,8 +6974,8 @@ Reason: ${reason}`];
6322
6974
  }
6323
6975
  process.stdout.write(`Applying \`${label2}\` to PR #${prNumber} under your authenticated \`gh\` identity — vinaya never fabricates an actor.
6324
6976
  `);
6325
- execFileSync11("gh", editArgs, { stdio: "inherit", env: process.env });
6326
- execFileSync11("gh", commentArgs, { stdio: "inherit", env: process.env });
6977
+ execFileSync14("gh", editArgs, { stdio: "inherit", env: process.env });
6978
+ execFileSync14("gh", commentArgs, { stdio: "inherit", env: process.env });
6327
6979
  process.stdout.write(`
6328
6980
  ✓ Label applied and reason posted as a PR comment.
6329
6981
  `);
@@ -6483,6 +7135,48 @@ var COMMANDS = [
6483
7135
  ],
6484
7136
  status: "shipped"
6485
7137
  },
7138
+ {
7139
+ name: "review post",
7140
+ description: "Render, post, and self-verify a code-reviewer or security-review verdict comment on a PR",
7141
+ flags: [
7142
+ { flag: "--role", description: "`code-reviewer` or `security` — selects which role template is rendered" },
7143
+ {
7144
+ flag: "--pr",
7145
+ description: "Target PR number — its real head is resolved via `gh pr view`, never caller-supplied"
7146
+ },
7147
+ {
7148
+ flag: "--verdict",
7149
+ description: "code-reviewer: `APPROVE` | `REQUEST_CHANGES`. security: `PASS` | `FAIL`"
7150
+ },
7151
+ {
7152
+ flag: "--findings-file",
7153
+ description: "One finding per line: `SEVERITY|file:line|description` (`|`-delimited). Omit for zero findings."
7154
+ },
7155
+ { flag: "--brief-conformance", description: "code-reviewer only: the BRIEF CONFORMANCE line" },
7156
+ { flag: "--spec-conformance", description: "code-reviewer only: the SPEC CONFORMANCE line" },
7157
+ { flag: "--scope", description: "code-reviewer only: the SCOPE line" },
7158
+ { flag: "--tests", description: "code-reviewer only: the TESTS line" },
7159
+ { flag: "--docs", description: "code-reviewer only: the DOCS line" },
7160
+ { flag: "--config-scan", description: "security only: the CONFIG SCAN line" },
7161
+ { flag: "--secrets", description: "security only: the SECRETS line" },
7162
+ {
7163
+ flag: "--secrets-evidence-file",
7164
+ description: 'security only: required whenever `--secrets` claims "none found" — the pasted scanner output backing that claim'
7165
+ },
7166
+ { flag: "--task-id", description: "the closing `Tokens:` line's task id" },
7167
+ { flag: "--model", description: "the closing `Tokens:` line's model name" },
7168
+ { flag: "--tokens-in", description: "a non-negative integer, or `-` if unknown" },
7169
+ { flag: "--tokens-out", description: "a non-negative integer, or `-` if unknown" },
7170
+ { flag: "--cost", description: "free text, or `-` if unknown" },
7171
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
7172
+ ],
7173
+ details: [
7174
+ "Every structural line (`VERDICT:`, `Judged head:`) is rendered from this command's own validated enum/sha inputs — never from a caller-supplied string — so a Reviewer's free-typed prose can no longer produce a shape the merge gate's line-anchored regex fails to see.",
7175
+ "Refuses a contradictory verdict before posting anything: a BLOCKER finding with `--verdict APPROVE`, or a CRITICAL/HIGH finding with `--verdict PASS`, is rejected outright, mirroring each role doc's own consistency rule.",
7176
+ "After posting, re-fetches the PR's comments and runs them through the exact `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` functions `checkReviewGate` calls — the same functions, not a second implementation — and exits non-zero naming precisely what failed to re-parse if the post does not come back clean and bound to the resolved head. There is no `--skip-verify` escape."
7177
+ ],
7178
+ status: "shipped"
7179
+ },
6486
7180
  {
6487
7181
  name: "doctor",
6488
7182
  description: "Diagnose hook, workflow, and config health — report only, never mutates",
@@ -6628,9 +7322,9 @@ function printHelp() {
6628
7322
  }
6629
7323
 
6630
7324
  // src/index.ts
6631
- var PACKAGE_ROOT = join21(dirname8(fileURLToPath2(import.meta.url)), "..");
7325
+ var PACKAGE_ROOT = join22(dirname8(fileURLToPath2(import.meta.url)), "..");
6632
7326
  function readVersion2() {
6633
- const pkg = JSON.parse(readFileSync19(join21(PACKAGE_ROOT, "package.json"), "utf-8"));
7327
+ const pkg = JSON.parse(readFileSync20(join22(PACKAGE_ROOT, "package.json"), "utf-8"));
6634
7328
  return pkg.version;
6635
7329
  }
6636
7330
  var [, , command, ...args] = process.argv;
@@ -6733,6 +7427,16 @@ try {
6733
7427
  }
6734
7428
  break;
6735
7429
  }
7430
+ case "review": {
7431
+ const [subcommand, ...rest] = args;
7432
+ if (subcommand === "post") {
7433
+ await reviewPostCommand(rest);
7434
+ } else {
7435
+ console.error(`Unknown 'review' subcommand: ${subcommand ?? "(none)"} (expected 'post')`);
7436
+ process.exit(2);
7437
+ }
7438
+ break;
7439
+ }
6736
7440
  case "demo": {
6737
7441
  const [subcommand, ...rest] = args;
6738
7442
  if (subcommand === "break") {