@attalabs/vinaya 0.1.0 → 0.1.2

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 (47) hide show
  1. package/LICENSE +201 -661
  2. package/README.md +1 -1
  3. package/aeg-root/contracts/archivist-tranche-archivist.md +92 -0
  4. package/aeg-root/contracts/brief-developer.md +140 -0
  5. package/aeg-root/contracts/developer-reviewer.md +106 -0
  6. package/aeg-root/contracts/planner-brief.md +130 -0
  7. package/aeg-root/contracts/reviewer-archivist.md +96 -0
  8. package/aeg-root/contracts/tranche-archivist-planner.md +116 -0
  9. package/aeg-root/coordination.md +293 -0
  10. package/aeg-root/enforcement.md +159 -0
  11. package/aeg-root/glossary.md +32 -0
  12. package/aeg-root/process.md +371 -0
  13. package/aeg-root/roles/archivist.md +167 -0
  14. package/aeg-root/roles/brief-author.md +108 -0
  15. package/aeg-root/roles/developer.md +429 -0
  16. package/aeg-root/roles/planner.md +285 -0
  17. package/aeg-root/roles/principal.md +107 -0
  18. package/aeg-root/roles/reviewer.md +130 -0
  19. package/aeg-root/roles/security.md +121 -0
  20. package/aeg-root/roles/tranche-archivist.md +257 -0
  21. package/aeg-root/skills/aeg/SKILL.md +90 -0
  22. package/aeg-root/skills/aeg-roles/SKILL.md +58 -0
  23. package/aeg-root/skills/brief-authoring/SKILL.md +497 -0
  24. package/aeg-root/state-machine.md +642 -0
  25. package/aeg-root/templates/brief-template.md +103 -0
  26. package/aeg-root/templates/issue-rationale-template.md +35 -0
  27. package/aeg-root/templates/pr-report-template.md +68 -0
  28. package/aeg-root/tranche-model.md +304 -0
  29. package/dist/checks/bin/check-branch-topology.js +2840 -0
  30. package/dist/checks/bin/check-brief-shape.js +2775 -0
  31. package/dist/checks/bin/check-closes-n.js +2824 -0
  32. package/dist/checks/bin/check-coherence.js +2922 -0
  33. package/dist/checks/bin/check-dead-branch-push.js +2799 -0
  34. package/dist/checks/bin/check-dispatch-readiness.js +2923 -0
  35. package/dist/checks/bin/check-doc-coverage-push.js +2818 -0
  36. package/dist/checks/bin/check-doc-coverage.js +2816 -0
  37. package/dist/checks/bin/check-first-push-dispatch.js +2888 -0
  38. package/dist/checks/bin/check-issue-assignment.js +2863 -0
  39. package/dist/checks/bin/check-no-disk-state.js +2811 -0
  40. package/dist/checks/bin/check-reader-resolvable-prose.js +2820 -0
  41. package/dist/checks/bin/check-registry-gates.js +2923 -0
  42. package/dist/checks/bin/check-review-gate.js +2826 -0
  43. package/dist/checks/bin/check-single-plan-pr.js +2810 -0
  44. package/dist/checks/bin/check-test-plan.js +2775 -0
  45. package/dist/index.js +1563 -446
  46. package/package.json +7 -4
  47. package/templates/custom-check.template.ts +1 -1
package/dist/index.js CHANGED
@@ -1,63 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync10 } from "node:fs";
5
- import { dirname as dirname7, join as join12 } from "node:path";
6
- import { fileURLToPath as fileURLToPath4 } from "node:url";
4
+ import { readFileSync as readFileSync11 } from "node:fs";
5
+ import { dirname as dirname5, join as join15 } from "node:path";
6
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
7
7
 
8
- // src/commands/check.ts
8
+ // src/commands/archive.ts
9
9
  import { execFileSync as execFileSync2 } from "node:child_process";
10
-
11
- // src/checks/contract.ts
12
- var CHECK_SCHEMA_VERSION = 1;
13
- function emitCheckError(error) {
14
- process.stderr.write(`${JSON.stringify(error)}
15
- `);
16
- }
17
-
18
- // src/checks/registry.ts
19
- import { dirname, join } from "node:path";
20
- import { fileURLToPath } from "node:url";
21
- var CLI_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
22
- var BIN_DIR = join(CLI_ROOT, "src", "checks", "bin");
23
- function coreCheckRegistry() {
24
- return [
25
- {
26
- name: "brief-shape",
27
- run: join(BIN_DIR, "check-brief-shape.ts"),
28
- scope: "diff",
29
- timeoutMs: 15000
30
- },
31
- {
32
- name: "doc-coverage",
33
- run: join(BIN_DIR, "check-doc-coverage.ts"),
34
- scope: "diff",
35
- timeoutMs: 15000
36
- },
37
- {
38
- name: "coherence",
39
- run: join(BIN_DIR, "check-coherence.ts"),
40
- scope: "full",
41
- timeoutMs: 30000
42
- },
43
- {
44
- name: "dispatch-readiness",
45
- run: join(BIN_DIR, "check-dispatch-readiness.ts"),
46
- scope: "full",
47
- timeoutMs: 30000
48
- },
49
- {
50
- name: "reader-resolvable-prose",
51
- run: join(BIN_DIR, "check-reader-resolvable-prose.ts"),
52
- scope: "full",
53
- timeoutMs: 30000
54
- }
55
- ];
56
- }
57
-
58
- // src/checks/runner.ts
59
- import { spawn } from "node:child_process";
60
- import { cpus } from "node:os";
61
10
  // ../../../packages/aeg-core/src/anchored-region.ts
62
11
  function maskCode(body) {
63
12
  const fill = (line) => " ".repeat(line.length);
@@ -151,6 +100,8 @@ function anchoredRegion(body, field) {
151
100
  return null;
152
101
  return body.slice(afterStart, afterStart + end.index);
153
102
  }
103
+ // ../../../packages/aeg-forge-state/src/parse-rationale-deps.ts
104
+ var SECTION_HEADER = /\*\*Dependency rationale\*\*/i;
154
105
  // ../../../packages/aeg-forge-state/src/gh.ts
155
106
  import { execFile, execFileSync } from "node:child_process";
156
107
  import { promisify } from "node:util";
@@ -370,17 +321,12 @@ var DERIVED_STATUSES = [
370
321
  "incoherent"
371
322
  ];
372
323
  var DERIVABLE_STATUSES = DERIVED_STATUSES.filter((s) => s !== "backlog");
373
- // ../../../packages/aeg-core/src/file-classify.ts
374
- var FROZEN_ARCHIVES = new Set([
375
- "docs/decisions-legacy.md",
376
- "apps/herald-ai/docs/herald-decisions-legacy.md",
377
- "apps/vada-ai/docs/vada-decisions-legacy.md"
378
- ]);
379
324
  // ../../../packages/aeg-core/src/waiver-label.ts
380
325
  var WAIVER_LABEL = label("waiver-docs");
381
326
  var WAIVER_LABEL_REVIEW = label("waiver-review");
382
327
 
383
328
  // ../../../packages/aeg-core/src/doc-owners.ts
329
+ var DOC_OWNERS_PATH = ".vinaya/doc-owners";
384
330
  function globToRegex(pat) {
385
331
  let re = "^";
386
332
  let i = 0;
@@ -620,7 +566,7 @@ function checkForgeTitle(title) {
620
566
  }
621
567
  // ../../../packages/aeg-core/src/issue-validation.ts
622
568
  function hasRationaleField(body, labelPattern) {
623
- const re = new RegExp(`(?:\\*\\*|^#{1,4}\\s+)\\s*${labelPattern}`, "im");
569
+ const re = new RegExp(`(?:\\*\\*|^#{1,4}\\s+)\\s*(?:${labelPattern})`, "im");
624
570
  return re.test(body);
625
571
  }
626
572
  var RATIONALE_FIELDS = [
@@ -633,8 +579,18 @@ var RATIONALE_FIELDS = [
633
579
  { name: "Stop-and-escalate", pattern: "Stop-and-escalate" },
634
580
  { name: "Docs to keep coherent", pattern: "Docs to keep coherent|§7" }
635
581
  ];
582
+ var DEPENDENCY_RATIONALE_FIELD_NAME = "Dependency rationale";
636
583
  function checkIssueRationale(body) {
637
- const errors = RATIONALE_FIELDS.filter((f) => !hasRationaleField(body, f.pattern)).map((f) => `issue-validation ${f.name}: rationale field not found in the Issue body — every task Issue carries the full Planner's rationale (aeg-root/contracts/planner-brief.md).`);
584
+ const errors = [];
585
+ for (const f of RATIONALE_FIELDS) {
586
+ if (!hasRationaleField(body, f.pattern)) {
587
+ errors.push(`issue-validation ${f.name}: rationale field not found in the Issue body — every task Issue carries the full Planner's rationale (aeg-root/contracts/planner-brief.md).`);
588
+ continue;
589
+ }
590
+ if (f.name === DEPENDENCY_RATIONALE_FIELD_NAME && !SECTION_HEADER.test(body)) {
591
+ errors.push(`issue-validation ${f.name}: rationale field found, but not in the form amend-deps requires — ` + "write `**Dependency rationale** — …`, not `**Dependency rationale:** …`. " + "`amendRationaleDeps` (the only sanctioned way to edit Depends-on/Conflicts-with) locates this " + "section by the exact anchor `**Dependency rationale**`; a colon inside the bold breaks that match " + "and the Issue becomes unamendable.");
592
+ }
593
+ }
638
594
  return { status: errors.length > 0 ? "fail" : "pass", errors };
639
595
  }
640
596
  function isTaskIssueLabelSet(labels) {
@@ -643,116 +599,880 @@ function isTaskIssueLabelSet(labels) {
643
599
 
644
600
  // ../../../packages/aeg-core/src/coherence-checks.ts
645
601
  var R1_GRANDFATHERED_ISSUES = new Set([279, 280, 281, 282]);
602
+ // ../../../packages/aeg-core/src/verdict-extraction.ts
603
+ function extractVerdict(comments, valuePattern, missingLabel) {
604
+ const clearHits = comments.filter((c) => valuePattern.test(c));
605
+ if (clearHits.length > 0) {
606
+ const latest = clearHits[clearHits.length - 1];
607
+ const m = latest.match(valuePattern);
608
+ return { value: m[1].toUpperCase().replace(/[_-]/g, " "), danglingNote: null };
609
+ }
610
+ return {
611
+ value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
612
+ danglingNote: `no ${missingLabel} verdict comment found on this PR`
613
+ };
614
+ }
615
+ function extractCodeReviewVerdict(comments) {
616
+ return extractVerdict(comments, /^[ \t]*(?:\*{1,3}|_{1,3})?VERDICT:\s*(APPROVE|REQUEST[ _-]?CHANGES|LGTM)(?![A-Za-z0-9])/im, "code-reviewer");
617
+ }
618
+ function extractSecurityReviewVerdict(comments) {
619
+ return extractVerdict(comments, /^[ \t]*(?:\*{1,3}|_{1,3})?VERDICT:\s*(PASS|FAIL)(?![A-Za-z0-9])/im, "security-review");
620
+ }
621
+
622
+ // ../../../packages/aeg-core/src/archive-task.ts
623
+ var TASK_BRANCH_PATTERN = /^task\/([^/]+)\/([^/]+)$/;
624
+ function taskRefFromBranch(branch) {
625
+ const m = branch.match(TASK_BRANCH_PATTERN);
626
+ if (!m)
627
+ return null;
628
+ return { tranche: m[1], taskId: m[2] };
629
+ }
630
+ function isEligibleForProvenance(ref, issueLabels) {
631
+ if (ref !== null)
632
+ return true;
633
+ return hasLabel("tranche", issueLabels);
634
+ }
635
+ var PROVENANCE_HEADING = "### AEG provenance";
636
+ function hasProvenance(comments) {
637
+ return comments.some((c) => c.includes(PROVENANCE_HEADING));
638
+ }
639
+ function extractField(body, labelPattern, anchor) {
640
+ const anchored = anchor !== undefined ? anchoredRegion(body, anchor) : null;
641
+ const region = anchored ?? headerRegion(body);
642
+ const re = new RegExp(`^(?:\\*\\*)?\\s*${labelPattern}\\s*(?:\\*\\*)?\\s*:\\s*(?:\\*\\*)?\\s*([^\\n·]+)`, "im");
643
+ const m = region.match(re);
644
+ if (!m)
645
+ return null;
646
+ const value = m[1].trim();
647
+ return value.length > 0 ? value : null;
648
+ }
649
+ function closesRefs(text) {
650
+ return [...new Set([...text.matchAll(/Closes #(\d+)/gi)].map((m) => Number(m[1])))];
651
+ }
652
+ function extractIssue(body) {
653
+ const stripped = stripCode(body);
654
+ const bodyNums = closesRefs(stripped);
655
+ const anchored = anchoredRegion(stripped, "CLOSES");
656
+ if (anchored !== null) {
657
+ const anchorNums = closesRefs(anchored);
658
+ if (anchorNums.length === 0)
659
+ return { issue: null, extraIssues: bodyNums, outsideHeader: false };
660
+ const issue2 = anchorNums[0];
661
+ return { issue: issue2, extraIssues: bodyNums.filter((n) => n !== issue2), outsideHeader: false };
662
+ }
663
+ const headerNums = closesRefs(headerRegion(stripped));
664
+ if (bodyNums.length === 0)
665
+ return { issue: null, extraIssues: [], outsideHeader: false };
666
+ const issue = headerNums.length > 0 ? headerNums[0] : bodyNums[0];
667
+ return { issue, extraIssues: bodyNums.filter((n) => n !== issue), outsideHeader: headerNums.length === 0 };
668
+ }
669
+ function buildProvenanceBlock(facts) {
670
+ const ref = taskRefFromBranch(facts.headRefName);
671
+ const dangling = [];
672
+ const { issue, extraIssues, outsideHeader } = extractIssue(facts.body);
673
+ if (issue === null) {
674
+ dangling.push("no `Closes #N` found in PR body — Issue field is DANGLING, no Issue will be closed");
675
+ }
676
+ if (outsideHeader) {
677
+ dangling.push(`\`Closes #${issue}\` was found outside the PR body's header block — the canonical form places it before the first \`##\` heading`);
678
+ }
679
+ if (extraIssues.length > 0) {
680
+ dangling.push(`PR body references additional Issues (${extraIssues.map((n) => `#${n}`).join(", ")}) beyond the first — closing only #${issue}, the rest are flagged, not closed`);
681
+ }
682
+ const tier = readTierFromPrBody(facts.body);
683
+ if (tier === null)
684
+ dangling.push("no `Tier:` field found in PR body — Tier field is DANGLING");
685
+ const project = extractField(facts.body, "Project(?:\\(s\\))?", "PROJECT");
686
+ if (!project)
687
+ dangling.push("no `Project:` field found in PR body — Project(s) field is DANGLING");
688
+ const forField = extractField(facts.body, "For");
689
+ if (!forField)
690
+ dangling.push("no `For:` field found in PR body — Model/agent field is DANGLING");
691
+ const codeReview = extractCodeReviewVerdict(facts.comments);
692
+ if (codeReview.danglingNote)
693
+ dangling.push(codeReview.danglingNote);
694
+ const security = extractSecurityReviewVerdict(facts.comments);
695
+ if (security.danglingNote)
696
+ dangling.push(security.danglingNote);
697
+ const ticket = extractField(facts.body, "Ticket") ?? "none";
698
+ const taskLabel = ref ? `task ${ref.taskId} (tranche ${ref.tranche})` : `task (branch ${facts.headRefName})`;
699
+ const lines = [
700
+ `${PROVENANCE_HEADING} — ${taskLabel}`,
701
+ `- Issue: ${issue !== null ? `#${issue} (closed by merge)` : "DANGLING — no Closes #N in PR body"}`,
702
+ `- Tier: ${tier !== null ? tier : "DANGLING — no Tier field in PR body"}`,
703
+ "- Brief: in this PR body (the frozen intent)",
704
+ `- Project(s): ${project ?? "DANGLING — no Project field in PR body"}`,
705
+ `- Model/agent: ${forField ?? "DANGLING — no For field in PR body"}`,
706
+ `- Code review: ${codeReview.value}`,
707
+ `- Security: ${security.value}`,
708
+ `- Ticket: ${ticket}`,
709
+ `- Merged: ${facts.mergeSha} at ${facts.mergedAt}`
710
+ ];
711
+ if (dangling.length > 0) {
712
+ lines.push("", `DANGLING: ${dangling.join("; ")}`);
713
+ }
714
+ return { block: lines.join(`
715
+ `), issue, dangling };
716
+ }
646
717
  // ../../../packages/aeg-core/src/diagram-model.ts
647
718
  import matter from "gray-matter";
648
719
  // ../../../packages/aeg-core/src/reader-resolvable-prose.ts
649
720
  var SWEPT_CLASSES = new Set(["ships", "reader-facing"]);
650
- // src/checks/runner.ts
651
- function defaultParallelism() {
652
- return Math.max(1, cpus().length);
721
+ // ../../../packages/aeg-core/src/direct-main-push.ts
722
+ function checkDirectMainPush(fact) {
723
+ const [first] = fact.associatedMergedPrNumbers;
724
+ if (first !== undefined)
725
+ return { verdict: "legitimate", mergedPrNumber: first };
726
+ return { verdict: "direct-push", sha: fact.sha };
727
+ }
728
+ // ../../../packages/aeg-core/src/dead-branch-push-audit.ts
729
+ function findDeadBranchPushes(facts) {
730
+ return facts.filter((f) => new Date(f.latestCommitAt).getTime() > new Date(f.resolvedAt).getTime()).map(({ branch, prNumber, prState, resolvedAt, latestCommitAt }) => ({
731
+ branch,
732
+ prNumber,
733
+ prState,
734
+ resolvedAt,
735
+ latestCommitAt
736
+ }));
653
737
  }
654
- function isCheckError(value) {
655
- if (typeof value !== "object" || value === null)
656
- return false;
657
- const v = value;
658
- return v.schema === CHECK_SCHEMA_VERSION && typeof v.check === "string" && (v.severity === "error" || v.severity === "warning") && typeof v.message === "string" && typeof v.agent_recovery_prompt === "string";
738
+ // src/lib/detect.ts
739
+ import { execFile as execFile4 } from "node:child_process";
740
+ import { existsSync } from "node:fs";
741
+ import { join } from "node:path";
742
+ import { promisify as promisify4 } from "node:util";
743
+ var execFileAsync4 = promisify4(execFile4);
744
+ async function detectGitRepo() {
745
+ try {
746
+ const { stdout: root } = await execFileAsync4("git", ["rev-parse", "--show-toplevel"]);
747
+ const repoRoot = root.trim();
748
+ let owner = "";
749
+ let repo = "";
750
+ try {
751
+ const { stdout: url } = await execFileAsync4("git", ["remote", "get-url", "origin"]);
752
+ const m = url.trim().match(/github\.com[/:]([^/]+)\/([^/]+?)(?:\.git)?$/);
753
+ if (m) {
754
+ owner = m[1] ?? "";
755
+ repo = m[2] ?? "";
756
+ }
757
+ } catch {}
758
+ return { repoRoot, owner, repo };
759
+ } catch {
760
+ return null;
761
+ }
659
762
  }
660
- function shouldSkip(spec, opts) {
661
- if (spec.scope !== "diff")
662
- return false;
663
- if (!opts.diffOnly)
664
- return false;
665
- if (opts.changedFiles === null)
666
- return false;
667
- if (!spec.include || spec.include.length === 0)
763
+ async function checkGhAuth() {
764
+ try {
765
+ await execFileAsync4("gh", ["auth", "status"]);
766
+ return true;
767
+ } catch {
668
768
  return false;
669
- const regexes = spec.include.map(globToRegex);
670
- return !opts.changedFiles.some((f) => regexes.some((re) => re.test(f)));
769
+ }
671
770
  }
672
- var KILL_GRACE_MS = 2000;
673
- async function runOne(spec, timeoutMs) {
674
- const start = performance.now();
675
- const proc = spawn(spec.run, spec.args ?? [], {
676
- stdio: ["ignore", "pipe", "pipe"]
677
- });
678
- let timedOut = false;
679
- let killTimer;
680
- const timer = setTimeout(() => {
681
- timedOut = true;
682
- proc.kill("SIGTERM");
683
- killTimer = setTimeout(() => proc.kill("SIGKILL"), KILL_GRACE_MS);
684
- }, timeoutMs);
685
- let stderrText = "";
686
- proc.stderr.setEncoding("utf-8");
687
- proc.stderr.on("data", (chunk) => {
688
- stderrText += chunk;
689
- });
690
- proc.stdout.resume();
691
- let spawnError;
692
- const exitCode = await new Promise((resolve) => {
693
- proc.on("close", (code) => resolve(code));
694
- proc.on("error", (err) => {
695
- spawnError = err;
696
- resolve(null);
697
- });
698
- });
699
- clearTimeout(timer);
700
- clearTimeout(killTimer);
701
- const durationMs = performance.now() - start;
702
- if (timedOut) {
703
- return { name: spec.name, status: "timeout", exitCode: null, errors: [], durationMs };
771
+ async function ghAuthStatus() {
772
+ try {
773
+ const { stderr, stdout } = await execFileAsync4("gh", ["auth", "status"]);
774
+ return { authenticated: true, detail: (stderr || stdout).trim() };
775
+ } catch (err) {
776
+ const stderr = err.stderr;
777
+ return { authenticated: false, detail: stderr?.trim() || "gh is not authenticated" };
704
778
  }
705
- if (spawnError) {
706
- const code = spawnError.code;
707
- return {
708
- name: spec.name,
709
- status: "error",
710
- exitCode: null,
711
- errors: [
712
- {
713
- schema: 1,
714
- check: spec.name,
715
- severity: "error",
716
- message: code === "ENOENT" ? `Could not run check "${spec.name}": executable \`${spec.run}\` was not found on PATH.` : `Could not run check "${spec.name}": ${spawnError.message}`,
717
- agent_recovery_prompt: code === "ENOENT" ? `Install \`${spec.run}\` or correct the \`run\` field for check "${spec.name}" in vinaya.config.json, then re-run.` : `Inspect the \`run\` and \`args\` fields for check "${spec.name}" in vinaya.config.json, then re-run.`
718
- }
719
- ],
720
- durationMs
721
- };
779
+ }
780
+ async function branchProtectionConfigured(owner, repo) {
781
+ if (!owner || !repo)
782
+ return null;
783
+ try {
784
+ await execFileAsync4("gh", ["api", `repos/${owner}/${repo}/branches/main/protection`]);
785
+ return true;
786
+ } catch (err) {
787
+ const stderr = err.stderr ?? "";
788
+ if (/\b404\b/.test(stderr))
789
+ return false;
790
+ return null;
722
791
  }
723
- const lines = stderrText.split(`
724
- `).map((l) => l.trim()).filter(Boolean);
725
- const errors = [];
726
- let malformed = false;
727
- for (const line of lines) {
728
- try {
729
- const parsed = JSON.parse(line);
730
- if (isCheckError(parsed)) {
731
- errors.push(parsed);
732
- } else {
733
- malformed = true;
792
+ }
793
+ function resolveHookDir(repoRoot) {
794
+ return existsSync(join(repoRoot, ".husky")) ? ".husky" : ".git/hooks";
795
+ }
796
+ function hookDirFromManifest(manifest, fallback) {
797
+ const block = manifest.blocks.find((b) => b.path.startsWith(".husky/") || b.path.startsWith(".git/hooks/"));
798
+ if (block?.path.startsWith(".husky/"))
799
+ return ".husky";
800
+ if (block?.path.startsWith(".git/hooks/"))
801
+ return ".git/hooks";
802
+ return fallback;
803
+ }
804
+ async function customHooksPath(repoRoot) {
805
+ try {
806
+ const { stdout } = await execFileAsync4("git", ["-C", repoRoot, "config", "--get", "core.hooksPath"]);
807
+ const v = stdout.trim();
808
+ if (!v)
809
+ return null;
810
+ if (v === ".husky" || v === ".husky/_")
811
+ return null;
812
+ return v;
813
+ } catch {
814
+ return null;
815
+ }
816
+ }
817
+ function ghLabelGateway(repoRoot) {
818
+ return {
819
+ async exists(name) {
820
+ try {
821
+ const { stdout } = await execFileAsync4("gh", ["label", "list", "--json", "name"], { cwd: repoRoot });
822
+ const names = JSON.parse(stdout);
823
+ return names.some((l) => l.name === name);
824
+ } catch {
825
+ return false;
734
826
  }
735
- } catch {
736
- malformed = true;
827
+ },
828
+ async create(name, color, description) {
829
+ await execFileAsync4("gh", ["label", "create", name, "--color", color, "--description", description], {
830
+ cwd: repoRoot
831
+ });
737
832
  }
738
- }
739
- let status;
740
- if (malformed) {
741
- status = "error";
742
- } else if (exitCode === 0) {
743
- status = "pass";
744
- } else if (exitCode === 1) {
745
- status = "fail";
746
- } else {
747
- status = "error";
748
- }
749
- return { name: spec.name, status, exitCode, errors, durationMs };
833
+ };
750
834
  }
751
- async function runChecks(specs, opts) {
752
- const results = new Array(specs.length);
753
- const toRun = [];
754
- for (let i = 0;i < specs.length; i++) {
755
- const spec = specs[i];
835
+
836
+ // src/commands/archive.ts
837
+ function realDeps() {
838
+ return { detectRepo: detectGitRepo };
839
+ }
840
+ function sh(args, input) {
841
+ return execFileSync2(args[0], args.slice(1), { encoding: "utf8", input }).trim();
842
+ }
843
+ function shJson(args) {
844
+ return JSON.parse(sh(args));
845
+ }
846
+ function parseMergeSha(args) {
847
+ for (const a of args) {
848
+ const m = a.match(/^--merge-sha=(.+)$/);
849
+ if (m)
850
+ return m[1];
851
+ }
852
+ return null;
853
+ }
854
+ async function runArchive(args, deps) {
855
+ const repo = await deps.detectRepo();
856
+ if (!repo) {
857
+ console.error("Error: not a git repository. Run `vinaya archive` from inside your repo.");
858
+ return 1;
859
+ }
860
+ if (!repo.owner || !repo.repo) {
861
+ console.error("Error: could not resolve a GitHub owner/repo from the `origin` remote.");
862
+ return 1;
863
+ }
864
+ const repoFlag = `${repo.owner}/${repo.repo}`;
865
+ const mergeSha = parseMergeSha(args) ?? sh(["git", "rev-parse", "HEAD"]);
866
+ const associated = shJson(["gh", "api", `repos/${repoFlag}/commits/${mergeSha}/pulls`]);
867
+ if (associated.length === 0) {
868
+ console.log(`[vinaya archive] no associated PR for merge ${mergeSha} — skip.`);
869
+ return 0;
870
+ }
871
+ const prNumber = associated[0].number;
872
+ const pr = shJson([
873
+ "gh",
874
+ "pr",
875
+ "view",
876
+ String(prNumber),
877
+ "-R",
878
+ repoFlag,
879
+ "--json",
880
+ "number,headRefName,body,mergedAt,comments"
881
+ ]);
882
+ const ref = taskRefFromBranch(pr.headRefName);
883
+ let issueLabels = [];
884
+ if (ref === null) {
885
+ const { issue: candidateIssue } = extractIssue(pr.body);
886
+ if (candidateIssue !== null) {
887
+ issueLabels = shJson([
888
+ "gh",
889
+ "issue",
890
+ "view",
891
+ String(candidateIssue),
892
+ "-R",
893
+ repoFlag,
894
+ "--json",
895
+ "labels"
896
+ ]).labels.map((l) => l.name);
897
+ }
898
+ }
899
+ if (!isEligibleForProvenance(ref, issueLabels)) {
900
+ console.log(`[vinaya archive] non-task branch (${pr.headRefName}) and closing Issue carries no vinaya/tranche:* label — skip.`);
901
+ return 0;
902
+ }
903
+ const comments = pr.comments.map((c) => c.body);
904
+ if (hasProvenance(comments)) {
905
+ console.log(`[vinaya archive] provenance already present on PR #${pr.number} — skip (idempotent).`);
906
+ return 0;
907
+ }
908
+ const facts = {
909
+ number: pr.number,
910
+ headRefName: pr.headRefName,
911
+ body: pr.body,
912
+ mergedAt: pr.mergedAt,
913
+ mergeSha,
914
+ comments
915
+ };
916
+ const { block, issue, dangling } = buildProvenanceBlock(facts);
917
+ console.log(`[vinaya archive] posting provenance block to PR #${pr.number}...`);
918
+ sh(["gh", "pr", "comment", String(pr.number), "-R", repoFlag, "--body-file", "-"], block);
919
+ console.log(`[vinaya archive] provenance block posted to PR #${pr.number}.`);
920
+ if (dangling.length > 0) {
921
+ console.log(`[vinaya archive] DANGLING (${dangling.length}): ${dangling.join("; ")}`);
922
+ }
923
+ if (issue !== null) {
924
+ console.log(`[vinaya archive] closing Issue #${issue}...`);
925
+ sh(["gh", "issue", "close", String(issue), "-R", repoFlag]);
926
+ const state = shJson([
927
+ "gh",
928
+ "issue",
929
+ "view",
930
+ String(issue),
931
+ "-R",
932
+ repoFlag,
933
+ "--json",
934
+ "state"
935
+ ]).state;
936
+ if (state !== "CLOSED") {
937
+ console.error(`[vinaya archive] FAILED — Issue #${issue} did not confirm CLOSED (state: ${state}).`);
938
+ return 1;
939
+ }
940
+ console.log(`[vinaya archive] Issue #${issue} confirmed CLOSED.`);
941
+ } else {
942
+ console.log("[vinaya archive] no Issue to close — Closes #N absent from PR body.");
943
+ }
944
+ console.log("[vinaya archive] PASS.");
945
+ return 0;
946
+ }
947
+ async function archiveCommand(args) {
948
+ process.exit(await runArchive(args, realDeps()));
949
+ }
950
+
951
+ // src/commands/audit.ts
952
+ import { execFileSync as execFileSync3 } from "node:child_process";
953
+
954
+ // src/lib/envelope.ts
955
+ var ENVELOPE_SCHEMA_VERSION = 1;
956
+ function toEnvelope(data) {
957
+ return { schema: ENVELOPE_SCHEMA_VERSION, data };
958
+ }
959
+ function printJson(data) {
960
+ process.stdout.write(`${JSON.stringify(toEnvelope(data), null, 2)}
961
+ `);
962
+ }
963
+
964
+ // src/commands/audit.ts
965
+ function realDeps2() {
966
+ return { detectRepo: detectGitRepo };
967
+ }
968
+ var COMMAND_TIMEOUT_MS = 20000;
969
+ var DEAD_BRANCH_LABEL = label("dead-branch-push");
970
+ var DEAD_BRANCH_MARKER = `<!-- ${DEAD_BRANCH_LABEL} -->`;
971
+ var DEAD_BRANCH_LABEL_DESCRIPTION = "daily-drift: commits landed on this branch after its PR already resolved";
972
+ var DIRECT_PUSH_LABEL = label("direct-main-push");
973
+ var DIRECT_PUSH_MARKER = `<!-- ${DIRECT_PUSH_LABEL} -->`;
974
+ var DIRECT_PUSH_LABEL_DESCRIPTION = "A commit landed on main with no associated merged PR (ring-2 detection)";
975
+ function shSoft(args, input) {
976
+ try {
977
+ return execFileSync3(args[0], args.slice(1), {
978
+ encoding: "utf8",
979
+ input,
980
+ timeout: COMMAND_TIMEOUT_MS
981
+ }).trim();
982
+ } catch (err) {
983
+ const stderr = err.stderr;
984
+ const message = stderr ? stderr.toString().trim() : err.message;
985
+ console.log(`[vinaya audit] non-fatal: command failed — ${message}`);
986
+ return "";
987
+ }
988
+ }
989
+ function shSoftJson(args) {
990
+ const out = shSoft(args);
991
+ if (!out)
992
+ return null;
993
+ try {
994
+ return JSON.parse(out);
995
+ } catch {
996
+ return null;
997
+ }
998
+ }
999
+ function sh2(args, input) {
1000
+ return execFileSync3(args[0], args.slice(1), { encoding: "utf8", input }).trim();
1001
+ }
1002
+ function listTaskBranches() {
1003
+ const out = shSoft(["git", "ls-remote", "--heads", "origin", "task/*"]);
1004
+ if (!out)
1005
+ return [];
1006
+ return out.split(`
1007
+ `).map((line) => line.trim()).filter(Boolean).map((line) => {
1008
+ const [sha, ref] = line.split("\t");
1009
+ return { name: ref.replace(/^refs\/heads\//, ""), sha };
1010
+ });
1011
+ }
1012
+ function mostRecentPr(branch, repoFlag) {
1013
+ const entries = shSoftJson([
1014
+ "gh",
1015
+ "pr",
1016
+ "list",
1017
+ "--head",
1018
+ branch,
1019
+ "-R",
1020
+ repoFlag,
1021
+ "--state",
1022
+ "all",
1023
+ "--json",
1024
+ "number,state,mergedAt,closedAt",
1025
+ "--limit",
1026
+ "1"
1027
+ ]);
1028
+ return entries?.[0] ?? null;
1029
+ }
1030
+ function commitDate(sha, repoFlag) {
1031
+ const json = shSoftJson([
1032
+ "gh",
1033
+ "api",
1034
+ `repos/${repoFlag}/commits/${sha}`
1035
+ ]);
1036
+ return json?.commit?.committer?.date ?? null;
1037
+ }
1038
+ function gatherDeadBranchFacts(repoFlag) {
1039
+ const facts = [];
1040
+ for (const { name, sha } of listTaskBranches()) {
1041
+ const pr = mostRecentPr(name, repoFlag);
1042
+ if (!pr)
1043
+ continue;
1044
+ if (pr.state !== "MERGED" && pr.state !== "CLOSED")
1045
+ continue;
1046
+ const resolvedAt = pr.state === "MERGED" ? pr.mergedAt : pr.closedAt;
1047
+ if (!resolvedAt)
1048
+ continue;
1049
+ const latestCommitAt = commitDate(sha, repoFlag);
1050
+ if (!latestCommitAt)
1051
+ continue;
1052
+ facts.push({ branch: name, prNumber: pr.number, prState: pr.state, resolvedAt, latestCommitAt });
1053
+ }
1054
+ return facts;
1055
+ }
1056
+ function ensureLabelExists(repoFlag, name, description) {
1057
+ const existing = shSoftJson(["gh", "label", "list", "-R", repoFlag, "--json", "name", "--limit", "200"]) ?? [];
1058
+ if (existing.some((l) => l.name === name))
1059
+ return;
1060
+ shSoft(["gh", "label", "create", name, "-R", repoFlag, "--color", "B60205", "--description", description]);
1061
+ }
1062
+ function commentIdFromUrl(url) {
1063
+ const match = url.match(/issuecomment-(\d+)/);
1064
+ return match ? match[1] : null;
1065
+ }
1066
+ function flagDeadBranchPush(finding, repoFlag) {
1067
+ shSoft(["gh", "pr", "edit", String(finding.prNumber), "-R", repoFlag, "--add-label", DEAD_BRANCH_LABEL]);
1068
+ const body = [
1069
+ DEAD_BRANCH_MARKER,
1070
+ "**Dead-branch push detected by `vinaya audit`.**",
1071
+ "",
1072
+ `Branch \`${finding.branch}\` has a ${finding.prState} PR (#${finding.prNumber}, resolved ${finding.resolvedAt}), ` + `but its remote tip commit is dated ${finding.latestCommitAt} — after that resolution. ` + "This is a notification, not a merge gate — nothing was reverted or mutated."
1073
+ ].join(`
1074
+ `);
1075
+ const prJson = shSoftJson([
1076
+ "gh",
1077
+ "pr",
1078
+ "view",
1079
+ String(finding.prNumber),
1080
+ "-R",
1081
+ repoFlag,
1082
+ "--json",
1083
+ "comments"
1084
+ ]);
1085
+ const existing = prJson?.comments.find((c) => c.body.includes(DEAD_BRANCH_MARKER));
1086
+ const existingId = existing ? commentIdFromUrl(existing.url) : null;
1087
+ if (existingId) {
1088
+ shSoft(["gh", "api", `repos/${repoFlag}/issues/comments/${existingId}`, "-X", "PATCH", "-F", "body=@-"], body);
1089
+ } else {
1090
+ shSoft(["gh", "pr", "comment", String(finding.prNumber), "-R", repoFlag, "--body-file", "-"], body);
1091
+ }
1092
+ }
1093
+ function runDeadBranchAudit(repoFlag) {
1094
+ try {
1095
+ const facts = gatherDeadBranchFacts(repoFlag);
1096
+ const findings = findDeadBranchPushes(facts);
1097
+ if (findings.length > 0) {
1098
+ ensureLabelExists(repoFlag, DEAD_BRANCH_LABEL, DEAD_BRANCH_LABEL_DESCRIPTION);
1099
+ for (const finding of findings)
1100
+ flagDeadBranchPush(finding, repoFlag);
1101
+ }
1102
+ return { scanned: facts.length, findings };
1103
+ } catch (err) {
1104
+ console.log(`[vinaya audit] dead-branch scan error (never-red, reporting only): ${err.message}`);
1105
+ return { scanned: 0, findings: [] };
1106
+ }
1107
+ }
1108
+ function fetchAssociatedMergedPrs(sha, repoFlag) {
1109
+ const out = sh2(["gh", "api", `repos/${repoFlag}/commits/${sha}/pulls`]);
1110
+ const prs = JSON.parse(out);
1111
+ return prs.filter((pr) => pr.merged_at !== null).map((pr) => pr.number);
1112
+ }
1113
+ function incidentAlreadyOpen(sha, repoFlag) {
1114
+ const out = sh2([
1115
+ "gh",
1116
+ "issue",
1117
+ "list",
1118
+ "-R",
1119
+ repoFlag,
1120
+ "--label",
1121
+ DIRECT_PUSH_LABEL,
1122
+ "--state",
1123
+ "open",
1124
+ "--search",
1125
+ sha,
1126
+ "--json",
1127
+ "number"
1128
+ ]);
1129
+ const issues = JSON.parse(out || "[]");
1130
+ return issues.length > 0;
1131
+ }
1132
+ function openDirectPushIncident(sha, repoFlag) {
1133
+ ensureLabelExists(repoFlag, DIRECT_PUSH_LABEL, DIRECT_PUSH_LABEL_DESCRIPTION);
1134
+ if (incidentAlreadyOpen(sha, repoFlag)) {
1135
+ console.error(`[vinaya audit] incident already open for ${sha} — not duplicating.`);
1136
+ return;
1137
+ }
1138
+ const body = [
1139
+ DIRECT_PUSH_MARKER,
1140
+ "**Direct push to `main` detected — no associated merged PR.**",
1141
+ "",
1142
+ `Commit \`${sha}\` landed on \`main\` but the commits→pulls association API reports no merged pull request that introduced it.`,
1143
+ "",
1144
+ "This is a detection-only finding — nothing was reverted or mutated."
1145
+ ].join(`
1146
+ `);
1147
+ sh2([
1148
+ "gh",
1149
+ "issue",
1150
+ "create",
1151
+ "-R",
1152
+ repoFlag,
1153
+ "--title",
1154
+ `Direct push to main detected: ${sha.slice(0, 12)}`,
1155
+ "--body",
1156
+ body,
1157
+ "--label",
1158
+ DIRECT_PUSH_LABEL
1159
+ ]);
1160
+ }
1161
+ function runDirectMainPushCheck(sha, repoFlag) {
1162
+ const associatedMergedPrNumbers = fetchAssociatedMergedPrs(sha, repoFlag);
1163
+ const result = checkDirectMainPush({ sha, associatedMergedPrNumbers });
1164
+ if (result.verdict === "direct-push") {
1165
+ openDirectPushIncident(sha, repoFlag);
1166
+ return { verdict: "direct-push" };
1167
+ }
1168
+ return { verdict: "legitimate", mergedPrNumber: result.mergedPrNumber };
1169
+ }
1170
+ function parseSha(args) {
1171
+ for (const a of args) {
1172
+ const m = a.match(/^--sha=(.+)$/);
1173
+ if (m)
1174
+ return m[1];
1175
+ }
1176
+ return null;
1177
+ }
1178
+ function parseOnly(args) {
1179
+ for (const a of args) {
1180
+ const m = a.match(/^--only=(dead-branches|direct-push)$/);
1181
+ if (m)
1182
+ return m[1];
1183
+ }
1184
+ return null;
1185
+ }
1186
+ async function runAudit(args, deps) {
1187
+ const jsonOutput = args.includes("--json");
1188
+ const only = parseOnly(args);
1189
+ const repo = await deps.detectRepo();
1190
+ if (!repo) {
1191
+ console.error("Error: not a git repository. Run `vinaya audit` from inside your repo.");
1192
+ return 1;
1193
+ }
1194
+ if (!repo.owner || !repo.repo) {
1195
+ console.error("Error: could not resolve a GitHub owner/repo from the `origin` remote.");
1196
+ return 1;
1197
+ }
1198
+ const repoFlag = `${repo.owner}/${repo.repo}`;
1199
+ const sha = parseSha(args) ?? sh2(["git", "rev-parse", "HEAD"]);
1200
+ const deadBranch = only === "direct-push" ? { scanned: 0, findings: [] } : runDeadBranchAudit(repoFlag);
1201
+ const directPush = only === "dead-branches" ? null : runDirectMainPushCheck(sha, repoFlag);
1202
+ const failed = directPush?.verdict === "direct-push";
1203
+ if (jsonOutput) {
1204
+ printJson({
1205
+ deadBranchAudit: only === "direct-push" ? null : { scanned: deadBranch.scanned, findings: deadBranch.findings },
1206
+ directMainPush: directPush ? { sha, ...directPush } : null
1207
+ });
1208
+ } else {
1209
+ process.stdout.write(`vinaya audit
1210
+
1211
+ `);
1212
+ if (only !== "direct-push") {
1213
+ process.stdout.write(`· [dead-branch-push] task branches scanned: ${deadBranch.scanned}, flagged: ${deadBranch.findings.length}
1214
+ `);
1215
+ for (const f of deadBranch.findings) {
1216
+ process.stdout.write(` ⚠ ${f.branch} — PR #${f.prNumber} (${f.prState}) resolved ${f.resolvedAt}, tip commit ${f.latestCommitAt}
1217
+ `);
1218
+ }
1219
+ }
1220
+ if (directPush) {
1221
+ if (directPush.verdict === "legitimate") {
1222
+ process.stdout.write(`✓ [direct-main-push] ${sha} is legitimate — introduced by merged PR #${directPush.mergedPrNumber}.
1223
+ `);
1224
+ } else {
1225
+ process.stdout.write(`✗ [direct-main-push] VIOLATION: ${sha} has no associated merged PR.
1226
+ `);
1227
+ }
1228
+ }
1229
+ }
1230
+ return failed ? 1 : 0;
1231
+ }
1232
+ async function auditCommand(args) {
1233
+ process.exit(await runAudit(args, realDeps2()));
1234
+ }
1235
+
1236
+ // src/commands/check.ts
1237
+ import { execFileSync as execFileSync4 } from "node:child_process";
1238
+
1239
+ // src/checks/contract.ts
1240
+ var CHECK_SCHEMA_VERSION = 1;
1241
+ function emitCheckError(error) {
1242
+ process.stderr.write(`${JSON.stringify(error)}
1243
+ `);
1244
+ }
1245
+
1246
+ // src/checks/registry.ts
1247
+ import { existsSync as existsSync3 } from "node:fs";
1248
+ import { join as join3 } from "node:path";
1249
+
1250
+ // src/lib/package-root.ts
1251
+ import { existsSync as existsSync2 } from "node:fs";
1252
+ import { dirname, join as join2 } from "node:path";
1253
+ import { fileURLToPath } from "node:url";
1254
+ function packageRoot(moduleUrl) {
1255
+ let dir = dirname(fileURLToPath(moduleUrl));
1256
+ while (!existsSync2(join2(dir, "package.json"))) {
1257
+ const parent = dirname(dir);
1258
+ if (parent === dir)
1259
+ break;
1260
+ dir = parent;
1261
+ }
1262
+ return dir;
1263
+ }
1264
+
1265
+ // src/checks/registry.ts
1266
+ var DIST_BIN_DIR = join3(packageRoot(import.meta.url), "dist", "checks", "bin");
1267
+ var SRC_BIN_DIR = join3(packageRoot(import.meta.url), "src", "checks", "bin");
1268
+ var BIN_DIR = existsSync3(DIST_BIN_DIR) ? DIST_BIN_DIR : SRC_BIN_DIR;
1269
+ var BIN_EXT = BIN_DIR === DIST_BIN_DIR ? ".js" : ".ts";
1270
+ function bin(name) {
1271
+ return join3(BIN_DIR, `${name}${BIN_EXT}`);
1272
+ }
1273
+ function coreCheckRegistry() {
1274
+ return [
1275
+ {
1276
+ name: "brief-shape",
1277
+ run: bin("check-brief-shape"),
1278
+ scope: "diff",
1279
+ timeoutMs: 15000
1280
+ },
1281
+ {
1282
+ name: "doc-coverage",
1283
+ run: bin("check-doc-coverage"),
1284
+ scope: "diff",
1285
+ timeoutMs: 15000
1286
+ },
1287
+ {
1288
+ name: "coherence",
1289
+ run: bin("check-coherence"),
1290
+ scope: "full",
1291
+ timeoutMs: 30000
1292
+ },
1293
+ {
1294
+ name: "dispatch-readiness",
1295
+ run: bin("check-dispatch-readiness"),
1296
+ scope: "full",
1297
+ timeoutMs: 30000
1298
+ },
1299
+ {
1300
+ name: "closes-n",
1301
+ run: bin("check-closes-n"),
1302
+ scope: "diff",
1303
+ timeoutMs: 15000
1304
+ },
1305
+ {
1306
+ name: "single-plan-pr",
1307
+ run: bin("check-single-plan-pr"),
1308
+ scope: "diff",
1309
+ timeoutMs: 15000
1310
+ },
1311
+ {
1312
+ name: "test-plan",
1313
+ run: bin("check-test-plan"),
1314
+ scope: "diff",
1315
+ timeoutMs: 15000
1316
+ },
1317
+ {
1318
+ name: "no-disk-state",
1319
+ run: bin("check-no-disk-state"),
1320
+ scope: "diff",
1321
+ timeoutMs: 15000
1322
+ },
1323
+ {
1324
+ name: "registry-gates",
1325
+ run: bin("check-registry-gates"),
1326
+ scope: "full",
1327
+ timeoutMs: 30000
1328
+ },
1329
+ {
1330
+ name: "review-gate",
1331
+ run: bin("check-review-gate"),
1332
+ scope: "full",
1333
+ timeoutMs: 30000
1334
+ },
1335
+ {
1336
+ name: "branch-topology",
1337
+ run: bin("check-branch-topology"),
1338
+ scope: "full",
1339
+ timeoutMs: 30000
1340
+ },
1341
+ {
1342
+ name: "dead-branch-push",
1343
+ run: bin("check-dead-branch-push"),
1344
+ scope: "full",
1345
+ timeoutMs: 30000
1346
+ },
1347
+ {
1348
+ name: "first-push-dispatch",
1349
+ run: bin("check-first-push-dispatch"),
1350
+ scope: "full",
1351
+ timeoutMs: 30000
1352
+ },
1353
+ {
1354
+ name: "doc-coverage-push",
1355
+ run: bin("check-doc-coverage-push"),
1356
+ scope: "diff",
1357
+ timeoutMs: 15000
1358
+ },
1359
+ {
1360
+ name: "issue-assignment",
1361
+ run: bin("check-issue-assignment"),
1362
+ scope: "full",
1363
+ timeoutMs: 30000
1364
+ }
1365
+ ];
1366
+ }
1367
+
1368
+ // src/checks/runner.ts
1369
+ import { spawn } from "node:child_process";
1370
+ import { cpus } from "node:os";
1371
+ function defaultParallelism() {
1372
+ return Math.max(1, cpus().length);
1373
+ }
1374
+ function isCheckError(value) {
1375
+ if (typeof value !== "object" || value === null)
1376
+ return false;
1377
+ const v = value;
1378
+ return v.schema === CHECK_SCHEMA_VERSION && typeof v.check === "string" && (v.severity === "error" || v.severity === "warning") && typeof v.message === "string" && typeof v.agent_recovery_prompt === "string";
1379
+ }
1380
+ function shouldSkip(spec, opts) {
1381
+ if (spec.scope !== "diff")
1382
+ return false;
1383
+ if (!opts.diffOnly)
1384
+ return false;
1385
+ if (opts.changedFiles === null)
1386
+ return false;
1387
+ if (!spec.include || spec.include.length === 0)
1388
+ return false;
1389
+ const regexes = spec.include.map(globToRegex);
1390
+ return !opts.changedFiles.some((f) => regexes.some((re) => re.test(f)));
1391
+ }
1392
+ var KILL_GRACE_MS = 2000;
1393
+ async function runOne(spec, timeoutMs) {
1394
+ const start = performance.now();
1395
+ const proc = spawn(spec.run, spec.args ?? [], {
1396
+ stdio: ["ignore", "pipe", "pipe"]
1397
+ });
1398
+ let timedOut = false;
1399
+ let killTimer;
1400
+ const timer = setTimeout(() => {
1401
+ timedOut = true;
1402
+ proc.kill("SIGTERM");
1403
+ killTimer = setTimeout(() => proc.kill("SIGKILL"), KILL_GRACE_MS);
1404
+ }, timeoutMs);
1405
+ let stderrText = "";
1406
+ proc.stderr.setEncoding("utf-8");
1407
+ proc.stderr.on("data", (chunk) => {
1408
+ stderrText += chunk;
1409
+ });
1410
+ proc.stdout.resume();
1411
+ let spawnError;
1412
+ const exitCode = await new Promise((resolve) => {
1413
+ proc.on("close", (code) => resolve(code));
1414
+ proc.on("error", (err) => {
1415
+ spawnError = err;
1416
+ resolve(null);
1417
+ });
1418
+ });
1419
+ clearTimeout(timer);
1420
+ clearTimeout(killTimer);
1421
+ const durationMs = performance.now() - start;
1422
+ if (timedOut) {
1423
+ return { name: spec.name, status: "timeout", exitCode: null, errors: [], durationMs };
1424
+ }
1425
+ if (spawnError) {
1426
+ const code = spawnError.code;
1427
+ return {
1428
+ name: spec.name,
1429
+ status: "error",
1430
+ exitCode: null,
1431
+ errors: [
1432
+ {
1433
+ schema: 1,
1434
+ check: spec.name,
1435
+ severity: "error",
1436
+ message: code === "ENOENT" ? `Could not run check "${spec.name}": executable \`${spec.run}\` was not found on PATH.` : `Could not run check "${spec.name}": ${spawnError.message}`,
1437
+ agent_recovery_prompt: code === "ENOENT" ? `Install \`${spec.run}\` or correct the \`run\` field for check "${spec.name}" in vinaya.config.json, then re-run.` : `Inspect the \`run\` and \`args\` fields for check "${spec.name}" in vinaya.config.json, then re-run.`
1438
+ }
1439
+ ],
1440
+ durationMs
1441
+ };
1442
+ }
1443
+ const lines = stderrText.split(`
1444
+ `).map((l) => l.trim()).filter(Boolean);
1445
+ const errors = [];
1446
+ let malformed = false;
1447
+ for (const line of lines) {
1448
+ try {
1449
+ const parsed = JSON.parse(line);
1450
+ if (isCheckError(parsed)) {
1451
+ errors.push(parsed);
1452
+ } else {
1453
+ malformed = true;
1454
+ }
1455
+ } catch {
1456
+ malformed = true;
1457
+ }
1458
+ }
1459
+ let status;
1460
+ if (malformed) {
1461
+ status = "error";
1462
+ } else if (exitCode === 0) {
1463
+ status = "pass";
1464
+ } else if (exitCode === 1) {
1465
+ status = "fail";
1466
+ } else {
1467
+ status = "error";
1468
+ }
1469
+ return { name: spec.name, status, exitCode, errors, durationMs };
1470
+ }
1471
+ async function runChecks(specs, opts) {
1472
+ const results = new Array(specs.length);
1473
+ const toRun = [];
1474
+ for (let i = 0;i < specs.length; i++) {
1475
+ const spec = specs[i];
756
1476
  if (shouldSkip(spec, opts)) {
757
1477
  results[i] = { name: spec.name, status: "skipped", exitCode: null, errors: [], durationMs: 0 };
758
1478
  } else {
@@ -774,9 +1494,9 @@ async function runChecks(specs, opts) {
774
1494
  }
775
1495
 
776
1496
  // src/lib/config.ts
777
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1497
+ import { existsSync as existsSync4, mkdirSync, readFileSync, writeFileSync } from "node:fs";
778
1498
  import { homedir } from "node:os";
779
- import { dirname as dirname2, join as join2 } from "node:path";
1499
+ import { dirname as dirname2, join as join4 } from "node:path";
780
1500
  import { z } from "zod";
781
1501
  var CheckEntrySchema = z.object({
782
1502
  run: z.string(),
@@ -842,14 +1562,14 @@ var VinayaConfigSchema = z.object({
842
1562
  briefSchema: BriefSchemaSchema.optional(),
843
1563
  managed: ManagedManifestSchema.optional()
844
1564
  });
845
- var GLOBAL_VINAYA_HOME = join2(homedir(), ".vinaya");
846
- var GLOBAL_CONFIG_PATH = join2(GLOBAL_VINAYA_HOME, "config.json");
1565
+ var GLOBAL_VINAYA_HOME = join4(homedir(), ".vinaya");
1566
+ var GLOBAL_CONFIG_PATH = join4(GLOBAL_VINAYA_HOME, "config.json");
847
1567
  var LOCAL_CONFIG_FILENAME = "vinaya.config.json";
848
1568
  function findLocalConfig() {
849
1569
  let dir = process.cwd();
850
1570
  while (true) {
851
- const candidate = join2(dir, LOCAL_CONFIG_FILENAME);
852
- if (existsSync(candidate))
1571
+ const candidate = join4(dir, LOCAL_CONFIG_FILENAME);
1572
+ if (existsSync4(candidate))
853
1573
  return candidate;
854
1574
  const parent = dirname2(dir);
855
1575
  if (parent === dir)
@@ -861,7 +1581,7 @@ function configPath() {
861
1581
  const local = findLocalConfig();
862
1582
  if (local)
863
1583
  return local;
864
- if (existsSync(GLOBAL_CONFIG_PATH))
1584
+ if (existsSync4(GLOBAL_CONFIG_PATH))
865
1585
  return GLOBAL_CONFIG_PATH;
866
1586
  return null;
867
1587
  }
@@ -883,20 +1603,10 @@ function loadConfigChecked() {
883
1603
  return { ok: true, config: parsed.data };
884
1604
  }
885
1605
 
886
- // src/lib/envelope.ts
887
- var ENVELOPE_SCHEMA_VERSION = 1;
888
- function toEnvelope(data) {
889
- return { schema: ENVELOPE_SCHEMA_VERSION, data };
890
- }
891
- function printJson(data) {
892
- process.stdout.write(`${JSON.stringify(toEnvelope(data), null, 2)}
893
- `);
894
- }
895
-
896
1606
  // src/commands/check.ts
897
1607
  function git(args) {
898
1608
  try {
899
- return execFileSync2("git", args, { encoding: "utf8" }).trim();
1609
+ return execFileSync4("git", args, { encoding: "utf8" }).trim();
900
1610
  } catch {
901
1611
  return "";
902
1612
  }
@@ -983,16 +1693,225 @@ async function checkCommand(args) {
983
1693
  process.exit(failed ? 1 : 0);
984
1694
  }
985
1695
 
1696
+ // src/commands/demo.ts
1697
+ import { execFileSync as execFileSync5, spawnSync } from "node:child_process";
1698
+ import { existsSync as existsSync5, readFileSync as readFileSync2, rmSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
1699
+ import { isAbsolute, join as join5 } from "node:path";
1700
+ var DEMO_BRANCH_PREFIX = "vinaya/demo-break-";
1701
+ var FIXTURE_PATH = ".vinaya-demo-brief.md";
1702
+ var STATE_FILENAME = "vinaya-demo-state.json";
1703
+ function gitCapture(repoRoot, args) {
1704
+ return execFileSync5("git", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
1705
+ }
1706
+ function currentBranchName(repoRoot) {
1707
+ try {
1708
+ return gitCapture(repoRoot, ["symbolic-ref", "--short", "HEAD"]);
1709
+ } catch {
1710
+ return "HEAD";
1711
+ }
1712
+ }
1713
+ function branchExists(repoRoot, name) {
1714
+ try {
1715
+ execFileSync5("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${name}`], {
1716
+ cwd: repoRoot,
1717
+ stdio: ["ignore", "pipe", "pipe"]
1718
+ });
1719
+ return true;
1720
+ } catch {
1721
+ return false;
1722
+ }
1723
+ }
1724
+ function createDemoBranch(repoRoot) {
1725
+ for (let attempt = 0;attempt < 5; attempt++) {
1726
+ const candidate = `${DEMO_BRANCH_PREFIX}${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1727
+ if (!branchExists(repoRoot, candidate)) {
1728
+ gitCapture(repoRoot, ["checkout", "-b", candidate]);
1729
+ return candidate;
1730
+ }
1731
+ }
1732
+ throw new Error("Could not generate a collision-free demo branch name after 5 attempts.");
1733
+ }
1734
+ function gitDirAbs(repoRoot) {
1735
+ const raw = gitCapture(repoRoot, ["rev-parse", "--git-dir"]);
1736
+ return isAbsolute(raw) ? raw : join5(repoRoot, raw);
1737
+ }
1738
+ function recoverFromCrash(repoRoot, statePath) {
1739
+ const currentBranch = currentBranchName(repoRoot);
1740
+ let state = null;
1741
+ if (existsSync5(statePath)) {
1742
+ try {
1743
+ state = JSON.parse(readFileSync2(statePath, "utf-8"));
1744
+ } catch {
1745
+ state = null;
1746
+ }
1747
+ }
1748
+ const onStrayBranch = currentBranch.startsWith(DEMO_BRANCH_PREFIX);
1749
+ if (!state) {
1750
+ if (onStrayBranch) {
1751
+ throw new Error(`On a leftover demo branch \`${currentBranch}\` with no recoverable state file. ` + "Checkout the branch you want manually, then re-run `vinaya demo break`.");
1752
+ }
1753
+ return;
1754
+ }
1755
+ process.stdout.write(`→ Found a leftover demo branch from a previous run — cleaning it up first.
1756
+ `);
1757
+ if (onStrayBranch) {
1758
+ gitCapture(repoRoot, ["reset", "--hard"]);
1759
+ gitCapture(repoRoot, ["clean", "-fd"]);
1760
+ if (!branchExists(repoRoot, state.originalBranch)) {
1761
+ throw new Error(`Recorded original branch \`${state.originalBranch}\` no longer exists. ` + "Checkout the branch you want manually, then re-run `vinaya demo break`.");
1762
+ }
1763
+ gitCapture(repoRoot, ["checkout", state.originalBranch]);
1764
+ }
1765
+ if (branchExists(repoRoot, state.demoBranch)) {
1766
+ gitCapture(repoRoot, ["branch", "-D", state.demoBranch]);
1767
+ }
1768
+ rmSync(statePath, { force: true });
1769
+ }
1770
+ function attemptCommit(repoRoot, demoBranch, prBody, message) {
1771
+ const result = spawnSync("git", ["commit", "-m", message], {
1772
+ cwd: repoRoot,
1773
+ encoding: "utf8",
1774
+ stdio: ["ignore", "pipe", "pipe"],
1775
+ env: { ...process.env, PR_BODY: prBody, BRANCH: demoBranch }
1776
+ });
1777
+ return { status: result.status ?? 1, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
1778
+ }
1779
+ function briefFixture(opts) {
1780
+ const tierLine = opts.includeTier ? `Tier: 1
1781
+ ` : "";
1782
+ return `For: vinaya demo
1783
+ Project: vinaya-demo-fixture
1784
+ ${tierLine}
1785
+ ## Summary
1786
+ Fixture brief staged only by \`vinaya demo break\` — discarded with the demo branch.
1787
+
1788
+ ## Technical surface map
1789
+ None — fixture content only; no real files change.
1790
+
1791
+ ## Documentation-update list
1792
+ None — fixture content only.
1793
+
1794
+ ## Test Plan
1795
+ Test Plan: unit-tests-only
1796
+
1797
+ ## Stop Conditions
1798
+ Autonomy: Do not stop to ask clarifying questions — this is a scripted demo; proceed and clean up automatically.
1799
+
1800
+ Illustrative Step 0 (never actually run by this demo):
1801
+ \`\`\`
1802
+ git worktree add .worktrees/demo-fixture -b demo/fixture origin/main
1803
+ \`\`\`
1804
+
1805
+ Closes #0 (fixture value — no real Issue; discarded with the demo branch)
1806
+ `;
1807
+ }
1808
+ function cleanup(repoRoot, originalBranch, demoBranch, statePath) {
1809
+ gitCapture(repoRoot, ["reset", "--hard"]);
1810
+ gitCapture(repoRoot, ["clean", "-fd"]);
1811
+ gitCapture(repoRoot, ["checkout", originalBranch]);
1812
+ gitCapture(repoRoot, ["branch", "-D", demoBranch]);
1813
+ if (existsSync5(statePath))
1814
+ rmSync(statePath, { force: true });
1815
+ }
1816
+ async function runDemoBreak(repoRoot, args) {
1817
+ const keep = args.includes("--keep");
1818
+ const hookDir = resolveHookDir(repoRoot);
1819
+ const hookPath = join5(repoRoot, hookDir, "pre-commit");
1820
+ if (!existsSync5(hookPath)) {
1821
+ console.error("Vinaya hooks are not installed in this repo. Run `vinaya init` first.");
1822
+ return 1;
1823
+ }
1824
+ const statePath = join5(gitDirAbs(repoRoot), STATE_FILENAME);
1825
+ recoverFromCrash(repoRoot, statePath);
1826
+ const dirty = gitCapture(repoRoot, ["status", "--porcelain"]);
1827
+ if (dirty.length > 0) {
1828
+ console.error("Working tree has uncommitted changes. Commit or stash them before running `vinaya demo break` — " + "the demo never touches a dirty tree.");
1829
+ return 1;
1830
+ }
1831
+ const originalBranch = currentBranchName(repoRoot);
1832
+ if (originalBranch === "HEAD") {
1833
+ console.error("Cannot run `vinaya demo break` from a detached HEAD — checkout a branch first.");
1834
+ return 1;
1835
+ }
1836
+ const demoBranch = createDemoBranch(repoRoot);
1837
+ writeFileSync2(statePath, JSON.stringify({ originalBranch, demoBranch }), "utf-8");
1838
+ process.stdout.write(`
1839
+ → Created isolated branch \`${demoBranch}\` off \`${originalBranch}\`.
1840
+
1841
+ `);
1842
+ const fixturePath = join5(repoRoot, FIXTURE_PATH);
1843
+ writeFileSync2(fixturePath, briefFixture({ includeTier: false }), "utf-8");
1844
+ gitCapture(repoRoot, ["add", FIXTURE_PATH]);
1845
+ process.stdout.write(`Staged \`${FIXTURE_PATH}\` — a brief missing its required \`Tier:\` field.
1846
+ Attempting to commit…
1847
+
1848
+ `);
1849
+ const badResult = attemptCommit(repoRoot, demoBranch, briefFixture({ includeTier: false }), "Chore(demo): stage a malformed brief");
1850
+ if (badResult.status === 0) {
1851
+ console.error("Expected the installed hook to refuse this commit, but it passed — the demo cannot show a real refusal here.");
1852
+ cleanup(repoRoot, originalBranch, demoBranch, statePath);
1853
+ return 1;
1854
+ }
1855
+ process.stdout.write(`✗ Commit refused — the real installed hook output:
1856
+
1857
+ `);
1858
+ process.stdout.write(badResult.stdout);
1859
+ process.stdout.write(badResult.stderr);
1860
+ process.stdout.write(`
1861
+ → Fixing the brief: adding the missing \`Tier:\` field…
1862
+
1863
+ `);
1864
+ writeFileSync2(fixturePath, briefFixture({ includeTier: true }), "utf-8");
1865
+ gitCapture(repoRoot, ["add", FIXTURE_PATH]);
1866
+ const goodResult = attemptCommit(repoRoot, demoBranch, briefFixture({ includeTier: true }), "Chore(demo): fix the brief and commit");
1867
+ if (goodResult.status !== 0) {
1868
+ console.error(`The fixed brief still failed the hook — this demo cannot proceed:
1869
+ `);
1870
+ console.error(goodResult.stdout);
1871
+ console.error(goodResult.stderr);
1872
+ cleanup(repoRoot, originalBranch, demoBranch, statePath);
1873
+ return 1;
1874
+ }
1875
+ process.stdout.write(`✓ Commit passed — the fix worked.
1876
+ `);
1877
+ if (keep) {
1878
+ if (existsSync5(statePath))
1879
+ unlinkSync(statePath);
1880
+ process.stdout.write(`
1881
+ --keep: leaving \`${demoBranch}\` checked out for you to inspect. Clean up with:
1882
+
1883
+ `);
1884
+ process.stdout.write(` git checkout ${originalBranch} && git branch -D ${demoBranch}
1885
+ `);
1886
+ return 0;
1887
+ }
1888
+ cleanup(repoRoot, originalBranch, demoBranch, statePath);
1889
+ process.stdout.write(`
1890
+ ✓ Cleaned up — back on \`${originalBranch}\`, \`${demoBranch}\` deleted.
1891
+ `);
1892
+ return 0;
1893
+ }
1894
+ async function demoBreakCommand(args) {
1895
+ const repo = await detectGitRepo();
1896
+ if (!repo) {
1897
+ console.error("Error: not a git repository. Run `vinaya demo break` from inside your repo.");
1898
+ process.exit(1);
1899
+ }
1900
+ const code = await runDemoBreak(repo.repoRoot, args);
1901
+ process.exit(code);
1902
+ }
1903
+
986
1904
  // src/commands/doctor.ts
987
- import { existsSync as existsSync4, readFileSync as readFileSync3, statSync } from "node:fs";
988
- import { dirname as dirname4, join as join5 } from "node:path";
989
- import { fileURLToPath as fileURLToPath2 } from "node:url";
1905
+ import { existsSync as existsSync7, readFileSync as readFileSync4, statSync } from "node:fs";
1906
+ import { join as join8 } from "node:path";
990
1907
 
991
1908
  // src/lib/artifacts.ts
1909
+ import { join as join6 } from "node:path";
992
1910
  var CONFIG_PATH = "vinaya.config.json";
993
1911
  var DOCTRINE_POINTER_PATH = "VINAYA.md";
994
1912
  var CHECKS_WORKFLOW_PATH = ".github/workflows/vinaya-checks.yml";
995
1913
  var REVIEW_WORKFLOW_PATH = ".github/workflows/vinaya-review.yml";
1914
+ var ARCHIVIST_WORKFLOW_PATH = ".github/workflows/vinaya-archivist.yml";
996
1915
  var MANAGED_NOTE = "Managed by Vinaya — created by `vinaya init`. `vinaya upgrade` regenerates it; `vinaya eject` removes it.";
997
1916
  function starterConfig() {
998
1917
  return {
@@ -1039,7 +1958,7 @@ jobs:
1039
1958
  with:
1040
1959
  node-version: 20
1041
1960
  - name: Run checks
1042
- run: npx --yes vinaya check --all --diff-only
1961
+ run: npx --yes @attalabs/vinaya check --all --diff-only
1043
1962
  `;
1044
1963
  }
1045
1964
  function reviewWorkflow() {
@@ -1058,44 +1977,123 @@ on:
1058
1977
  issue_comment:
1059
1978
  types: [created]
1060
1979
 
1061
- jobs:
1062
- vinaya-review:
1063
- name: vinaya review gate
1064
- if: >
1065
- github.event_name == 'pull_request' ||
1066
- (github.event.issue.pull_request != null && contains(github.event.comment.body, 'VERDICT'))
1980
+ jobs:
1981
+ vinaya-review:
1982
+ name: vinaya review gate
1983
+ if: >
1984
+ github.event_name == 'pull_request' ||
1985
+ (github.event.issue.pull_request != null && contains(github.event.comment.body, 'VERDICT'))
1986
+ runs-on: ubuntu-latest
1987
+ permissions:
1988
+ contents: read
1989
+ pull-requests: read
1990
+ issues: read
1991
+ steps:
1992
+ # issue_comment payloads carry no PR head SHA/branch — resolve them
1993
+ # before checkout, and check out that exact commit (the event's default
1994
+ # ref is the repo's default branch, not the PR head).
1995
+ - name: Resolve PR head
1996
+ id: pr
1997
+ env:
1998
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
1999
+ run: |
2000
+ if [ "\${{ github.event_name }}" = "pull_request" ]; then
2001
+ NUMBER="\${{ github.event.pull_request.number }}"
2002
+ else
2003
+ NUMBER="\${{ github.event.issue.number }}"
2004
+ fi
2005
+ SHA=$(gh pr view "$NUMBER" --repo "\${{ github.repository }}" --json headRefOid -q .headRefOid)
2006
+ echo "sha=$SHA" >> "$GITHUB_OUTPUT"
2007
+ - uses: actions/checkout@v4
2008
+ with:
2009
+ ref: \${{ steps.pr.outputs.sha }}
2010
+ fetch-depth: 0
2011
+ - uses: actions/setup-node@v4
2012
+ with:
2013
+ node-version: 20
2014
+ - name: Review gate
2015
+ env:
2016
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
2017
+ run: npx --yes @attalabs/vinaya check --all
2018
+ `;
2019
+ }
2020
+ function archivistWorkflow() {
2021
+ return `# ${MANAGED_NOTE}
2022
+ #
2023
+ # The ring-2 post-merge/scheduled mechanisms: per-task Archivist provenance
2024
+ # + close-out (post-merge), dead-branch-push drift (daily-drift, a
2025
+ # notification channel — never fails red), and direct-main-push detection
2026
+ # (direct-main-push-detection, a real pass/fail).
2027
+ name: Vinaya Archivist
2028
+
2029
+ on:
2030
+ push:
2031
+ branches: [main]
2032
+ schedule:
2033
+ - cron: "0 2 * * *" # daily at 02:00 UTC
2034
+ workflow_dispatch:
2035
+
2036
+ jobs:
2037
+ post-merge:
2038
+ name: Post-Merge Archivist
2039
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
2040
+ runs-on: ubuntu-latest
2041
+ permissions:
2042
+ contents: read
2043
+ issues: write
2044
+ pull-requests: write
2045
+ steps:
2046
+ - uses: actions/checkout@v4
2047
+ with:
2048
+ fetch-depth: 0
2049
+ - uses: actions/setup-node@v4
2050
+ with:
2051
+ node-version: 20
2052
+ - name: Run vinaya archive
2053
+ env:
2054
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
2055
+ run: npx --yes @attalabs/vinaya archive --merge-sha=\${{ github.sha }}
2056
+
2057
+ daily-drift:
2058
+ name: Daily Drift Check (dead-branch pushes)
2059
+ if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
1067
2060
  runs-on: ubuntu-latest
1068
2061
  permissions:
1069
2062
  contents: read
1070
- pull-requests: read
1071
- issues: read
2063
+ issues: write
2064
+ pull-requests: write
1072
2065
  steps:
1073
- # issue_comment payloads carry no PR head SHA/branch — resolve them
1074
- # before checkout, and check out that exact commit (the event's default
1075
- # ref is the repo's default branch, not the PR head).
1076
- - name: Resolve PR head
1077
- id: pr
2066
+ - uses: actions/checkout@v4
2067
+ with:
2068
+ fetch-depth: 0
2069
+ - uses: actions/setup-node@v4
2070
+ with:
2071
+ node-version: 20
2072
+ - name: Run vinaya audit --only=dead-branches
2073
+ continue-on-error: true # never-red — this job is a notification channel, not a gate
1078
2074
  env:
1079
2075
  GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
1080
- run: |
1081
- if [ "\${{ github.event_name }}" = "pull_request" ]; then
1082
- NUMBER="\${{ github.event.pull_request.number }}"
1083
- else
1084
- NUMBER="\${{ github.event.issue.number }}"
1085
- fi
1086
- SHA=$(gh pr view "$NUMBER" --repo "\${{ github.repository }}" --json headRefOid -q .headRefOid)
1087
- echo "sha=$SHA" >> "$GITHUB_OUTPUT"
2076
+ run: npx --yes @attalabs/vinaya audit --only=dead-branches
2077
+
2078
+ direct-main-push-detection:
2079
+ name: Direct-Main-Push Detection
2080
+ if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch'
2081
+ runs-on: ubuntu-latest
2082
+ permissions:
2083
+ contents: read
2084
+ issues: write
2085
+ pull-requests: read
2086
+ steps:
1088
2087
  - uses: actions/checkout@v4
1089
2088
  with:
1090
- ref: \${{ steps.pr.outputs.sha }}
1091
2089
  fetch-depth: 0
1092
2090
  - uses: actions/setup-node@v4
1093
2091
  with:
1094
2092
  node-version: 20
1095
- - name: Review gate
2093
+ - name: Run vinaya audit --only=direct-push
1096
2094
  env:
1097
2095
  GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
1098
- run: npx --yes vinaya check --all
2096
+ run: npx --yes @attalabs/vinaya audit --only=direct-push --sha=\${{ github.sha }}
1099
2097
  `;
1100
2098
  }
1101
2099
  var HOOK_PREAMBLE = `#!/usr/bin/env sh
@@ -1103,20 +2101,21 @@ var HOOK_PREAMBLE = `#!/usr/bin/env sh
1103
2101
  function preCommitBody() {
1104
2102
  return `# Vinaya commit-time gate. Runs the deterministic checks over your staged
1105
2103
  # diff before the commit lands.
1106
- npx --no-install vinaya check --all --diff-only || exit 1`;
2104
+ npx --no-install @attalabs/vinaya check --all --diff-only || exit 1`;
1107
2105
  }
1108
2106
  function prePushBody() {
1109
2107
  return `# Vinaya pre-push gate. Runs branch/dispatch checks before the push leaves.
1110
- npx --no-install vinaya check --all || exit 1`;
2108
+ npx --no-install @attalabs/vinaya check --all || exit 1`;
1111
2109
  }
1112
2110
  function doctrinePointer() {
2111
+ const doctrineRoot = join6(packageRoot(import.meta.url), "aeg-root");
1113
2112
  return `<!-- ${MANAGED_NOTE} -->
1114
2113
  # Vinaya doctrine — read this first
1115
2114
 
1116
2115
  This repo is governed by Vinaya. The full, canonical doctrine (roles,
1117
2116
  contracts, the state machine, the ring gates) ships inside the installed
1118
- \`vinaya\` npm package as versioned reference content and is updated cleanly by
1119
- \`vinaya upgrade\` there is no in-repo copy to drift.
2117
+ \`@attalabs/vinaya\` npm package itself no in-repo copy to drift, and
2118
+ \`vinaya upgrade\` regenerates this pointer if the install location changes.
1120
2119
 
1121
2120
  An agent working in this repo follows the governed flow by reading two things:
1122
2121
 
@@ -1128,8 +2127,61 @@ An agent working in this repo follows the governed flow by reading two things:
1128
2127
  Live task status is derived from the forge (Issues, labels, comments) via
1129
2128
  \`vinaya check\` — it is never written into a file here.
1130
2129
 
1131
- To view the doctrine text: \`vinaya doctor\` reports what is installed; the
1132
- package's own reference content is the source of truth.
2130
+ To read the doctrine text, it is bundled at this install's own resolved
2131
+ path:
2132
+
2133
+ ${doctrineRoot}
2134
+
2135
+ \`vinaya doctor\` reports what is installed in this repo.
2136
+ `;
2137
+ }
2138
+ function starterDocOwners() {
2139
+ return `# ${MANAGED_NOTE}
2140
+ #
2141
+ # ${DOC_OWNERS_PATH} — code → doc bindings for the coherence seam.
2142
+ #
2143
+ # Format: one binding per line, CODEOWNERS-shaped.
2144
+ # <code-glob> <doc-pointer>
2145
+ # Whitespace-separated. Lines starting with \`#\` and blank lines are ignored.
2146
+ #
2147
+ # Glob syntax (kept deliberately simple — no character classes):
2148
+ # \`**\` matches any sequence (including \`/\`)
2149
+ # \`*\` matches any sequence not containing \`/\`
2150
+ # every other character is literal — so dynamic-route segments like
2151
+ # \`[username]\` match literally, no escaping required.
2152
+ #
2153
+ # Pointer forms:
2154
+ # in-repo path e.g. \`docs/my-feature.md\`
2155
+ # in-repo path with anchor e.g. \`docs/my-feature.md#section-2\`
2156
+ # URL e.g. \`https://example.com/docs/x\`
2157
+ #
2158
+ # Coverage rule (enforced by C5, the doc-owners check bundled into \`vinaya check\`):
2159
+ # When a code file in the PR matches a glob:
2160
+ # - in-repo pointer → that path must appear in the PR diff, else FAIL
2161
+ # - URL pointer → require a \`Doc-ack: <pointer> — <note>\` PR-body line
2162
+ # - dangling pointer → in-repo pointer that does not exist on disk: FAIL
2163
+ # Escape:
2164
+ # A PR-wide \`vinaya/waiver:docs\` label whose labeling timeline event's
2165
+ # actor is a configured principal suppresses every fired binding for that
2166
+ # PR — a forge-authenticated human act, never a parseable string. There is
2167
+ # no PR-body waiver field; label presence alone is never sufficient.
2168
+ #
2169
+ # Dormancy:
2170
+ # This file absent, OR no glob matches any changed code file → silent no-op.
2171
+ # The gate has no opinion until you teach it one.
2172
+ #
2173
+ # ── No-doc allow-list ───────────────────────────────────────────────────────
2174
+ # Surfaces that legitimately need no bound doc are listed below. Format:
2175
+ # # no-doc: <glob> — <reason>
2176
+ # These lines exempt the matching directory from the completeness scoreboard
2177
+ # (\`vinaya check\`'s advisory output). They do NOT affect C5 enforcement — if a
2178
+ # binding exists for a surface, C5 still fires on changed files regardless of
2179
+ # any no-doc line. Use no-doc for scaffold-only, config-only, or stub-only
2180
+ # surfaces where adding a doc-owners binding would be meaningless busywork.
2181
+ # To un-exempt a surface, remove its no-doc line and add a real binding.
2182
+ #
2183
+ # This starter ships empty — no bindings, no no-doc entries. Add your own
2184
+ # below this line as your repo grows real code → doc coverage needs.
1133
2185
  `;
1134
2186
  }
1135
2187
  function labelOps() {
@@ -1162,6 +2214,7 @@ function buildInitOps(ctx) {
1162
2214
  const hookMode = 493;
1163
2215
  ops.push({ kind: "create-file", path: CHECKS_WORKFLOW_PATH, content: checksWorkflow(), group: "CI workflows" });
1164
2216
  ops.push({ kind: "create-file", path: REVIEW_WORKFLOW_PATH, content: reviewWorkflow(), group: "CI workflows" });
2217
+ ops.push({ kind: "create-file", path: ARCHIVIST_WORKFLOW_PATH, content: archivistWorkflow(), group: "CI workflows" });
1165
2218
  ops.push({
1166
2219
  kind: "managed-block",
1167
2220
  path: `${ctx.hookDir}/pre-commit`,
@@ -1195,6 +2248,12 @@ function buildInitOps(ctx) {
1195
2248
  content: doctrinePointer(),
1196
2249
  group: "Doctrine pointer"
1197
2250
  });
2251
+ ops.push({
2252
+ kind: "create-file",
2253
+ path: DOC_OWNERS_PATH,
2254
+ content: starterDocOwners(),
2255
+ group: "Doc-ownership manifest"
2256
+ });
1198
2257
  ops.push(...labelOps());
1199
2258
  ops.push({ kind: "print", message: BRANCH_PROTECTION_NOTE, group: "Branch protection (printed, never applied)" });
1200
2259
  return ops;
@@ -1212,107 +2271,9 @@ function buildInitProductOps(name) {
1212
2271
  ];
1213
2272
  }
1214
2273
 
1215
- // src/lib/detect.ts
1216
- import { execFile as execFile4 } from "node:child_process";
1217
- import { existsSync as existsSync2 } from "node:fs";
1218
- import { join as join3 } from "node:path";
1219
- import { promisify as promisify4 } from "node:util";
1220
- var execFileAsync4 = promisify4(execFile4);
1221
- async function detectGitRepo() {
1222
- try {
1223
- const { stdout: root } = await execFileAsync4("git", ["rev-parse", "--show-toplevel"]);
1224
- const repoRoot = root.trim();
1225
- let owner = "";
1226
- let repo = "";
1227
- try {
1228
- const { stdout: url } = await execFileAsync4("git", ["remote", "get-url", "origin"]);
1229
- const m = url.trim().match(/github\.com[/:]([^/]+)\/([^/]+?)(?:\.git)?$/);
1230
- if (m) {
1231
- owner = m[1] ?? "";
1232
- repo = m[2] ?? "";
1233
- }
1234
- } catch {}
1235
- return { repoRoot, owner, repo };
1236
- } catch {
1237
- return null;
1238
- }
1239
- }
1240
- async function checkGhAuth() {
1241
- try {
1242
- await execFileAsync4("gh", ["auth", "status"]);
1243
- return true;
1244
- } catch {
1245
- return false;
1246
- }
1247
- }
1248
- async function ghAuthStatus() {
1249
- try {
1250
- const { stderr, stdout } = await execFileAsync4("gh", ["auth", "status"]);
1251
- return { authenticated: true, detail: (stderr || stdout).trim() };
1252
- } catch (err) {
1253
- const stderr = err.stderr;
1254
- return { authenticated: false, detail: stderr?.trim() || "gh is not authenticated" };
1255
- }
1256
- }
1257
- async function branchProtectionConfigured(owner, repo) {
1258
- if (!owner || !repo)
1259
- return null;
1260
- try {
1261
- await execFileAsync4("gh", ["api", `repos/${owner}/${repo}/branches/main/protection`]);
1262
- return true;
1263
- } catch (err) {
1264
- const stderr = err.stderr ?? "";
1265
- if (/\b404\b/.test(stderr))
1266
- return false;
1267
- return null;
1268
- }
1269
- }
1270
- function resolveHookDir(repoRoot) {
1271
- return existsSync2(join3(repoRoot, ".husky")) ? ".husky" : ".git/hooks";
1272
- }
1273
- function hookDirFromManifest(manifest, fallback) {
1274
- const block = manifest.blocks.find((b) => b.path.startsWith(".husky/") || b.path.startsWith(".git/hooks/"));
1275
- if (block?.path.startsWith(".husky/"))
1276
- return ".husky";
1277
- if (block?.path.startsWith(".git/hooks/"))
1278
- return ".git/hooks";
1279
- return fallback;
1280
- }
1281
- async function customHooksPath(repoRoot) {
1282
- try {
1283
- const { stdout } = await execFileAsync4("git", ["-C", repoRoot, "config", "--get", "core.hooksPath"]);
1284
- const v = stdout.trim();
1285
- if (!v)
1286
- return null;
1287
- if (v === ".husky" || v === ".husky/_")
1288
- return null;
1289
- return v;
1290
- } catch {
1291
- return null;
1292
- }
1293
- }
1294
- function ghLabelGateway(repoRoot) {
1295
- return {
1296
- async exists(name) {
1297
- try {
1298
- const { stdout } = await execFileAsync4("gh", ["label", "list", "--json", "name"], { cwd: repoRoot });
1299
- const names = JSON.parse(stdout);
1300
- return names.some((l) => l.name === name);
1301
- } catch {
1302
- return false;
1303
- }
1304
- },
1305
- async create(name, color, description) {
1306
- await execFileAsync4("gh", ["label", "create", name, "--color", color, "--description", description], {
1307
- cwd: repoRoot
1308
- });
1309
- }
1310
- };
1311
- }
1312
-
1313
2274
  // src/lib/ops.ts
1314
- import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
1315
- import { dirname as dirname3, join as join4, resolve, sep } from "node:path";
2275
+ import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
2276
+ import { dirname as dirname3, join as join7, resolve, sep } from "node:path";
1316
2277
  var MARKER_NS = "vinaya:managed";
1317
2278
  function markerLines(marker, comment) {
1318
2279
  const [open, close] = comment === "hash" ? ["#", ""] : ["<!--", " -->"];
@@ -1328,7 +2289,7 @@ ${op.body}
1328
2289
  ${end}`;
1329
2290
  }
1330
2291
  function abs(repoRoot, relPath) {
1331
- return join4(repoRoot, relPath);
2292
+ return join7(repoRoot, relPath);
1332
2293
  }
1333
2294
  function containedAbs(repoRoot, relPath) {
1334
2295
  const root = resolve(repoRoot);
@@ -1339,9 +2300,9 @@ function containedAbs(repoRoot, relPath) {
1339
2300
  }
1340
2301
  function fileContains(repoRoot, relPath, needle) {
1341
2302
  const p = abs(repoRoot, relPath);
1342
- if (!existsSync3(p))
2303
+ if (!existsSync6(p))
1343
2304
  return false;
1344
- return readFileSync2(p, "utf-8").includes(needle);
2305
+ return readFileSync3(p, "utf-8").includes(needle);
1345
2306
  }
1346
2307
  function planInstall(ops, repoRoot, ownedFiles = new Set) {
1347
2308
  const entries = [];
@@ -1349,7 +2310,7 @@ function planInstall(ops, repoRoot, ownedFiles = new Set) {
1349
2310
  for (const op of ops) {
1350
2311
  switch (op.kind) {
1351
2312
  case "create-file": {
1352
- const exists = existsSync3(abs(repoRoot, op.path));
2313
+ const exists = existsSync6(abs(repoRoot, op.path));
1353
2314
  let action;
1354
2315
  if (!exists)
1355
2316
  action = "create";
@@ -1367,7 +2328,7 @@ function planInstall(ops, repoRoot, ownedFiles = new Set) {
1367
2328
  let action;
1368
2329
  if (fileContains(repoRoot, op.path, begin))
1369
2330
  action = "skip-present";
1370
- else if (existsSync3(abs(repoRoot, op.path)))
2331
+ else if (existsSync6(abs(repoRoot, op.path)))
1371
2332
  action = "append";
1372
2333
  else
1373
2334
  action = "create-host";
@@ -1445,19 +2406,19 @@ function renderInstallDiff(plan) {
1445
2406
  }
1446
2407
  function writeFileWithDirs(target, content, mode) {
1447
2408
  mkdirSync2(dirname3(target), { recursive: true });
1448
- writeFileSync2(target, content, "utf-8");
2409
+ writeFileSync3(target, content, "utf-8");
1449
2410
  if (mode !== undefined)
1450
2411
  chmodSync(target, mode);
1451
2412
  }
1452
2413
  function appendBlock(repoRoot, op) {
1453
2414
  const target = abs(repoRoot, op.path);
1454
- const existing = readFileSync2(target, "utf-8");
2415
+ const existing = readFileSync3(target, "utf-8");
1455
2416
  const sep2 = existing.endsWith(`
1456
2417
  `) ? `
1457
2418
  ` : `
1458
2419
 
1459
2420
  `;
1460
- writeFileSync2(target, `${existing}${sep2}${renderBlock(op)}
2421
+ writeFileSync3(target, `${existing}${sep2}${renderBlock(op)}
1461
2422
  `, "utf-8");
1462
2423
  if (op.mode !== undefined)
1463
2424
  chmodSync(target, op.mode);
@@ -1504,9 +2465,14 @@ async function applyInstall(plan, repoRoot, labels, onFilesRecorded) {
1504
2465
  for (const e of plan.entries) {
1505
2466
  if (e.kind !== "create-label")
1506
2467
  continue;
1507
- if (!await labels.exists(e.op.name)) {
1508
- await labels.create(e.op.name, e.op.color, e.op.description);
1509
- createdLabels.push(e.op.name);
2468
+ try {
2469
+ if (!await labels.exists(e.op.name)) {
2470
+ await labels.create(e.op.name, e.op.color, e.op.description);
2471
+ createdLabels.push(e.op.name);
2472
+ }
2473
+ } catch (err) {
2474
+ const reason = err.stderr?.trim() || err.message;
2475
+ console.warn(`Warning: could not create label '${e.op.name}': ${reason}`);
1510
2476
  }
1511
2477
  }
1512
2478
  return { ...base, labels: dedupe(createdLabels) };
@@ -1557,7 +2523,7 @@ function planEject(manifest, repoRoot) {
1557
2523
  escapes.push(b.path);
1558
2524
  continue;
1559
2525
  }
1560
- if (!existsSync3(p)) {
2526
+ if (!existsSync6(p)) {
1561
2527
  actions.push({
1562
2528
  kind: "strip-block",
1563
2529
  path: b.path,
@@ -1568,7 +2534,7 @@ function planEject(manifest, repoRoot) {
1568
2534
  });
1569
2535
  continue;
1570
2536
  }
1571
- const content = readFileSync2(p, "utf-8");
2537
+ const content = readFileSync3(p, "utf-8");
1572
2538
  const stripped = stripBlockFromContent(content, b.marker, b.comment);
1573
2539
  const removesHost = stripped !== null && blockStripLeavesEmpty(stripped);
1574
2540
  actions.push({
@@ -1586,7 +2552,7 @@ function planEject(manifest, repoRoot) {
1586
2552
  escapes.push(f);
1587
2553
  continue;
1588
2554
  }
1589
- actions.push({ kind: "delete-file", path: f, present: existsSync3(p) });
2555
+ actions.push({ kind: "delete-file", path: f, present: existsSync6(p) });
1590
2556
  }
1591
2557
  for (const name of manifest.labels) {
1592
2558
  actions.push({ kind: "report-label", name });
@@ -1620,20 +2586,20 @@ function applyEject(plan, repoRoot) {
1620
2586
  const p = containedAbs(repoRoot, a.path);
1621
2587
  if (p === null)
1622
2588
  continue;
1623
- const content = readFileSync2(p, "utf-8");
2589
+ const content = readFileSync3(p, "utf-8");
1624
2590
  const stripped = stripBlockFromContent(content, a.marker, a.comment);
1625
2591
  if (stripped === null)
1626
2592
  continue;
1627
2593
  if (blockStripLeavesEmpty(stripped))
1628
- rmSync(p, { force: true });
2594
+ rmSync2(p, { force: true });
1629
2595
  else
1630
- writeFileSync2(p, stripped.endsWith(`
2596
+ writeFileSync3(p, stripped.endsWith(`
1631
2597
  `) ? stripped : `${stripped}
1632
2598
  `, "utf-8");
1633
2599
  } else if (a.kind === "delete-file") {
1634
2600
  const p = containedAbs(repoRoot, a.path);
1635
2601
  if (a.present && p !== null)
1636
- rmSync(p, { force: true });
2602
+ rmSync2(p, { force: true });
1637
2603
  }
1638
2604
  }
1639
2605
  return {
@@ -1642,21 +2608,11 @@ function applyEject(plan, repoRoot) {
1642
2608
  }
1643
2609
 
1644
2610
  // src/commands/doctor.ts
1645
- function packageRoot() {
1646
- let dir = dirname4(fileURLToPath2(import.meta.url));
1647
- while (!existsSync4(join5(dir, "package.json"))) {
1648
- const parent = dirname4(dir);
1649
- if (parent === dir)
1650
- break;
1651
- dir = parent;
1652
- }
1653
- return dir;
1654
- }
1655
2611
  function readVersion() {
1656
- const pkg = JSON.parse(readFileSync3(join5(packageRoot(), "package.json"), "utf-8"));
2612
+ const pkg = JSON.parse(readFileSync4(join8(packageRoot(import.meta.url), "package.json"), "utf-8"));
1657
2613
  return pkg.version;
1658
2614
  }
1659
- function realDeps() {
2615
+ function realDeps3() {
1660
2616
  return {
1661
2617
  detectRepo: detectGitRepo,
1662
2618
  ghAuthStatus,
@@ -1672,12 +2628,12 @@ var info = (check, message) => ({ check, severity: "info", message });
1672
2628
  var warn = (check, message) => ({ check, severity: "warn", message });
1673
2629
  var error = (check, message) => ({ check, severity: "error", message });
1674
2630
  function readConfig(repoRoot) {
1675
- const p = join5(repoRoot, CONFIG_PATH);
1676
- if (!existsSync4(p))
2631
+ const p = join8(repoRoot, CONFIG_PATH);
2632
+ if (!existsSync7(p))
1677
2633
  return { kind: "missing" };
1678
2634
  let raw;
1679
2635
  try {
1680
- raw = JSON.parse(readFileSync3(p, "utf-8"));
2636
+ raw = JSON.parse(readFileSync4(p, "utf-8"));
1681
2637
  } catch (err) {
1682
2638
  return { kind: "invalid", error: `invalid JSON: ${err.message}` };
1683
2639
  }
@@ -1706,14 +2662,14 @@ function diagnoseInstall(repoRoot, ctx, manifest) {
1706
2662
  for (const op of buildInitOps(ctx)) {
1707
2663
  if (op.kind === "create-file") {
1708
2664
  const check = labelForPath(op.path);
1709
- const abs2 = join5(repoRoot, op.path);
1710
- const exists = existsSync4(abs2);
2665
+ const abs2 = join8(repoRoot, op.path);
2666
+ const exists = existsSync7(abs2);
1711
2667
  const owned = ownedFiles.has(op.path);
1712
2668
  if (!exists) {
1713
2669
  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\`.`));
1714
2670
  continue;
1715
2671
  }
1716
- const content = readFileSync3(abs2, "utf-8");
2672
+ const content = readFileSync4(abs2, "utf-8");
1717
2673
  if (!owned) {
1718
2674
  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.`));
1719
2675
  continue;
@@ -1730,13 +2686,13 @@ function diagnoseInstall(repoRoot, ctx, manifest) {
1730
2686
  }
1731
2687
  } else if (op.kind === "managed-block") {
1732
2688
  const check = "hooks";
1733
- const abs2 = join5(repoRoot, op.path);
2689
+ const abs2 = join8(repoRoot, op.path);
1734
2690
  const owned = ownedBlocks.has(blockKey(op.path, op.marker));
1735
- if (!existsSync4(abs2)) {
2691
+ if (!existsSync7(abs2)) {
1736
2692
  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.`));
1737
2693
  continue;
1738
2694
  }
1739
- const content = readFileSync3(abs2, "utf-8");
2695
+ const content = readFileSync4(abs2, "utf-8");
1740
2696
  const { begin, end } = markerLines(op.marker, op.comment);
1741
2697
  const hasMarkers = content.includes(begin) && content.includes(end);
1742
2698
  if (!hasMarkers) {
@@ -1761,8 +2717,8 @@ function diagnoseInstall(repoRoot, ctx, manifest) {
1761
2717
  function diagnoseCustomChecks(repoRoot, config) {
1762
2718
  const findings = [];
1763
2719
  for (const [name, entry2] of Object.entries(config.checks ?? {})) {
1764
- const scriptAbs = join5(repoRoot, entry2.run);
1765
- findings.push(existsSync4(scriptAbs) ? ok("checks", `custom check '${name}' → ${entry2.run}`) : error("checks", `custom check '${name}' points at a missing script: ${entry2.run}`));
2720
+ const scriptAbs = join8(repoRoot, entry2.run);
2721
+ findings.push(existsSync7(scriptAbs) ? ok("checks", `custom check '${name}' → ${entry2.run}`) : error("checks", `custom check '${name}' points at a missing script: ${entry2.run}`));
1766
2722
  }
1767
2723
  return findings;
1768
2724
  }
@@ -1846,12 +2802,12 @@ async function runDoctor(args, deps) {
1846
2802
  return healthy ? 0 : 1;
1847
2803
  }
1848
2804
  async function doctorCommand(args) {
1849
- process.exit(await runDoctor(args, realDeps()));
2805
+ process.exit(await runDoctor(args, realDeps3()));
1850
2806
  }
1851
2807
 
1852
2808
  // src/commands/eject.ts
1853
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
1854
- import { join as join6 } from "node:path";
2809
+ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "node:fs";
2810
+ import { join as join9 } from "node:path";
1855
2811
 
1856
2812
  // src/lib/prompt.ts
1857
2813
  var stdinBuffer = "";
@@ -1918,7 +2874,7 @@ function closeStdin() {
1918
2874
  }
1919
2875
 
1920
2876
  // src/commands/eject.ts
1921
- function realDeps2() {
2877
+ function realDeps4() {
1922
2878
  return {
1923
2879
  detectRepo: detectGitRepo,
1924
2880
  confirm: async (q) => {
@@ -1932,12 +2888,12 @@ function parse(args) {
1932
2888
  return { dryRun: args.includes("--dry-run"), yes: args.includes("--yes") };
1933
2889
  }
1934
2890
  function readManifest(repoRoot) {
1935
- const p = join6(repoRoot, CONFIG_PATH);
1936
- if (!existsSync5(p))
2891
+ const p = join9(repoRoot, CONFIG_PATH);
2892
+ if (!existsSync8(p))
1937
2893
  return { kind: "none" };
1938
2894
  let raw;
1939
2895
  try {
1940
- raw = JSON.parse(readFileSync4(p, "utf-8"));
2896
+ raw = JSON.parse(readFileSync5(p, "utf-8"));
1941
2897
  } catch (err) {
1942
2898
  return { kind: "orphan", reason: `vinaya.config.json is not valid JSON: ${err.message}` };
1943
2899
  }
@@ -2015,14 +2971,14 @@ Vinaya ejected.
2015
2971
  return 0;
2016
2972
  }
2017
2973
  async function ejectCommand(args) {
2018
- const code = await runEject(args, realDeps2());
2974
+ const code = await runEject(args, realDeps4());
2019
2975
  process.exit(code);
2020
2976
  }
2021
2977
 
2022
2978
  // src/commands/init.ts
2023
- import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
2024
- import { join as join7 } from "node:path";
2025
- function realDeps3() {
2979
+ import { existsSync as existsSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
2980
+ import { join as join10 } from "node:path";
2981
+ function realDeps5() {
2026
2982
  return {
2027
2983
  detectRepo: detectGitRepo,
2028
2984
  checkGhAuth,
@@ -2041,19 +2997,19 @@ function flags(args) {
2041
2997
  }
2042
2998
  var PRODUCT_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
2043
2999
  function readManifest2(repoRoot) {
2044
- const p = join7(repoRoot, CONFIG_PATH);
2045
- if (!existsSync6(p))
3000
+ const p = join10(repoRoot, CONFIG_PATH);
3001
+ if (!existsSync9(p))
2046
3002
  return null;
2047
3003
  try {
2048
- return VinayaConfigSchema.parse(JSON.parse(readFileSync5(p, "utf-8"))).managed ?? null;
3004
+ return VinayaConfigSchema.parse(JSON.parse(readFileSync6(p, "utf-8"))).managed ?? null;
2049
3005
  } catch {
2050
3006
  return null;
2051
3007
  }
2052
3008
  }
2053
3009
  function writeManifest(repoRoot, manifest) {
2054
- const configAbs = join7(repoRoot, CONFIG_PATH);
2055
- const seed = JSON.parse(readFileSync5(configAbs, "utf-8"));
2056
- writeFileSync3(configAbs, `${JSON.stringify({ ...seed, managed: manifest }, null, 2)}
3010
+ const configAbs = join10(repoRoot, CONFIG_PATH);
3011
+ const seed = JSON.parse(readFileSync6(configAbs, "utf-8"));
3012
+ writeFileSync4(configAbs, `${JSON.stringify({ ...seed, managed: manifest }, null, 2)}
2057
3013
  `, "utf-8");
2058
3014
  }
2059
3015
  async function runInit(args, deps) {
@@ -2181,20 +3137,20 @@ function dedupeBlocks2(bs) {
2181
3137
  });
2182
3138
  }
2183
3139
  async function initCommand(args) {
2184
- process.exit(await runInit(args, realDeps3()));
3140
+ process.exit(await runInit(args, realDeps5()));
2185
3141
  }
2186
3142
  async function initProductCommand(args) {
2187
- process.exit(await runInitProduct(args, realDeps3()));
3143
+ process.exit(await runInitProduct(args, realDeps5()));
2188
3144
  }
2189
3145
 
2190
3146
  // src/commands/issue.ts
2191
- import { execFileSync as execFileSync3 } from "node:child_process";
3147
+ import { execFileSync as execFileSync6 } from "node:child_process";
2192
3148
 
2193
3149
  // src/lib/forge-write.ts
2194
- import { mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "node:fs";
2195
- import { readFileSync as readFileSync6 } from "node:fs";
3150
+ import { mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "node:fs";
3151
+ import { readFileSync as readFileSync7 } from "node:fs";
2196
3152
  import { tmpdir } from "node:os";
2197
- import { join as join8 } from "node:path";
3153
+ import { join as join11 } from "node:path";
2198
3154
  class ForgeArgError extends Error {
2199
3155
  }
2200
3156
  function locateBody(args) {
@@ -2204,13 +3160,13 @@ function locateBody(args) {
2204
3160
  const p = args[i + 1];
2205
3161
  if (!p)
2206
3162
  throw new ForgeArgError("`--body-file` was given with no path.");
2207
- return { body: readFileSync6(p, "utf8"), source: { kind: "file", argIndex: i + 1, inlineForm: false } };
3163
+ return { body: readFileSync7(p, "utf8"), source: { kind: "file", argIndex: i + 1, inlineForm: false } };
2208
3164
  }
2209
3165
  if (a.startsWith("--body-file=")) {
2210
3166
  const p = a.slice("--body-file=".length);
2211
3167
  if (!p)
2212
3168
  throw new ForgeArgError("`--body-file=` was given with no path.");
2213
- return { body: readFileSync6(p, "utf8"), source: { kind: "file", argIndex: i, inlineForm: true } };
3169
+ return { body: readFileSync7(p, "utf8"), source: { kind: "file", argIndex: i, inlineForm: true } };
2214
3170
  }
2215
3171
  if (a === "--body" || a === "-b") {
2216
3172
  const v = args[i + 1];
@@ -2227,13 +3183,13 @@ function resolveShippableArgs(args, bodyResult) {
2227
3183
  if (bodyResult?.source.kind !== "file") {
2228
3184
  return { finalArgs: args, cleanup: () => {} };
2229
3185
  }
2230
- const dir = mkdtempSync(join8(tmpdir(), "vinaya-forge-body-"));
2231
- const tempPath = join8(dir, "body.md");
2232
- writeFileSync4(tempPath, bodyResult.body, "utf8");
3186
+ const dir = mkdtempSync(join11(tmpdir(), "vinaya-forge-body-"));
3187
+ const tempPath = join11(dir, "body.md");
3188
+ writeFileSync5(tempPath, bodyResult.body, "utf8");
2233
3189
  const finalArgs = [...args];
2234
3190
  const { argIndex, inlineForm } = bodyResult.source;
2235
3191
  finalArgs[argIndex] = inlineForm ? `--body-file=${tempPath}` : tempPath;
2236
- return { finalArgs, cleanup: () => rmSync2(dir, { recursive: true, force: true }) };
3192
+ return { finalArgs, cleanup: () => rmSync3(dir, { recursive: true, force: true }) };
2237
3193
  }
2238
3194
  function extractTitle(args) {
2239
3195
  for (let i = 0;i < args.length; i++) {
@@ -2401,7 +3357,7 @@ function locateBodyOrRefuse(ghArgs, retryCommand) {
2401
3357
  function fetchForgeLabels(issueRef) {
2402
3358
  let out;
2403
3359
  try {
2404
- out = execFileSync3("gh", ["issue", "view", issueRef, "--json", "labels"], {
3360
+ out = execFileSync6("gh", ["issue", "view", issueRef, "--json", "labels"], {
2405
3361
  encoding: "utf8",
2406
3362
  stdio: ["ignore", "pipe", "pipe"]
2407
3363
  });
@@ -2427,9 +3383,9 @@ function reportPass(json, command) {
2427
3383
  }
2428
3384
  }
2429
3385
  function runGhWrite(ghCmd, ghArgs, bodyResult, json) {
2430
- const { finalArgs, cleanup } = resolveShippableArgs(ghArgs, bodyResult);
3386
+ const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
2431
3387
  try {
2432
- const out = execFileSync3("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
3388
+ const out = execFileSync6("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
2433
3389
  const url = out.trim();
2434
3390
  if (json)
2435
3391
  printJson({ validated: true, written: true, url });
@@ -2437,7 +3393,7 @@ function runGhWrite(ghCmd, ghArgs, bodyResult, json) {
2437
3393
  process.stdout.write(`${url}
2438
3394
  `);
2439
3395
  } finally {
2440
- cleanup();
3396
+ cleanup2();
2441
3397
  }
2442
3398
  }
2443
3399
  function validateTaskIssue(body, title, retryCommand) {
@@ -2500,21 +3456,10 @@ function issueEditCommand(args) {
2500
3456
  }
2501
3457
 
2502
3458
  // src/commands/new-check.ts
2503
- import { chmodSync as chmodSync2, existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
2504
- import { dirname as dirname5, join as join9 } from "node:path";
2505
- import { fileURLToPath as fileURLToPath3 } from "node:url";
2506
- function packageRoot2() {
2507
- let dir = dirname5(fileURLToPath3(import.meta.url));
2508
- while (!existsSync7(join9(dir, "package.json"))) {
2509
- const parent = dirname5(dir);
2510
- if (parent === dir)
2511
- break;
2512
- dir = parent;
2513
- }
2514
- return dir;
2515
- }
2516
- var TEMPLATE_PATH = join9(packageRoot2(), "templates", "custom-check.template.ts");
2517
- var CHECKS_DIR = join9("scripts", "vinaya-checks");
3459
+ import { chmodSync as chmodSync2, existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
3460
+ import { join as join12 } from "node:path";
3461
+ var TEMPLATE_PATH = join12(packageRoot(import.meta.url), "templates", "custom-check.template.ts");
3462
+ var CHECKS_DIR = join12("scripts", "vinaya-checks");
2518
3463
  var VALID_NAME = /^[a-z0-9][a-z0-9-]*$/;
2519
3464
  function newCheckCommand(args) {
2520
3465
  const name = args[0];
@@ -2522,19 +3467,19 @@ function newCheckCommand(args) {
2522
3467
  console.error("Usage: vinaya new check <name> (name: lowercase letters, digits, hyphens)");
2523
3468
  process.exit(2);
2524
3469
  }
2525
- const scriptsDir = join9(process.cwd(), CHECKS_DIR);
2526
- if (!existsSync7(scriptsDir))
3470
+ const scriptsDir = join12(process.cwd(), CHECKS_DIR);
3471
+ if (!existsSync10(scriptsDir))
2527
3472
  mkdirSync3(scriptsDir, { recursive: true });
2528
- const targetPath = join9(scriptsDir, `${name}.ts`);
2529
- if (existsSync7(targetPath)) {
3473
+ const targetPath = join12(scriptsDir, `${name}.ts`);
3474
+ if (existsSync10(targetPath)) {
2530
3475
  console.error(`Error: ${targetPath} already exists.`);
2531
3476
  process.exit(1);
2532
3477
  }
2533
- const template = readFileSync7(TEMPLATE_PATH, "utf-8");
3478
+ const template = readFileSync8(TEMPLATE_PATH, "utf-8");
2534
3479
  const contents = template.split("{{CHECK_NAME}}").join(name);
2535
- writeFileSync5(targetPath, contents, "utf-8");
3480
+ writeFileSync6(targetPath, contents, "utf-8");
2536
3481
  chmodSync2(targetPath, 493);
2537
- const relPath = join9(CHECKS_DIR, `${name}.ts`);
3482
+ const relPath = join12(CHECKS_DIR, `${name}.ts`);
2538
3483
  const registration = JSON.stringify({ checks: { [name]: { run: `./${relPath}`, scope: "diff" } } }, null, 2);
2539
3484
  process.stdout.write(`Created ${relPath}
2540
3485
 
@@ -2544,12 +3489,12 @@ ${registration}
2544
3489
  }
2545
3490
 
2546
3491
  // src/commands/pr.ts
2547
- import { execFileSync as execFileSync4 } from "node:child_process";
3492
+ import { execFileSync as execFileSync7 } from "node:child_process";
2548
3493
  var RETRY_CREATE2 = "vinaya pr create --validate-only …";
2549
3494
  var RETRY_EDIT2 = "vinaya pr edit <n> --validate-only …";
2550
3495
  function git2(args) {
2551
3496
  try {
2552
- return execFileSync4("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
3497
+ return execFileSync7("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
2553
3498
  } catch {
2554
3499
  return "";
2555
3500
  }
@@ -2581,9 +3526,9 @@ function reportPass2(json, command) {
2581
3526
  }
2582
3527
  }
2583
3528
  function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
2584
- const { finalArgs, cleanup } = resolveShippableArgs(ghArgs, bodyResult);
3529
+ const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
2585
3530
  try {
2586
- const out = execFileSync4("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
3531
+ const out = execFileSync7("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
2587
3532
  const url = out.trim();
2588
3533
  if (json)
2589
3534
  printJson({ validated: true, written: true, url });
@@ -2591,13 +3536,13 @@ function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
2591
3536
  process.stdout.write(`${url}
2592
3537
  `);
2593
3538
  } finally {
2594
- cleanup();
3539
+ cleanup2();
2595
3540
  }
2596
3541
  }
2597
3542
  function fetchPrForgeContext(prRef) {
2598
3543
  let viewOut;
2599
3544
  try {
2600
- viewOut = execFileSync4("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
3545
+ viewOut = execFileSync7("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
2601
3546
  encoding: "utf8",
2602
3547
  stdio: ["ignore", "pipe", "pipe"]
2603
3548
  });
@@ -2694,22 +3639,22 @@ function prEditCommand(args) {
2694
3639
 
2695
3640
  // src/commands/studio.ts
2696
3641
  import { spawn as spawn2 } from "node:child_process";
2697
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "node:fs";
2698
- import { dirname as dirname6, join as join10 } from "node:path";
3642
+ import { existsSync as existsSync11, readFileSync as readFileSync9 } from "node:fs";
3643
+ import { dirname as dirname4, join as join13 } from "node:path";
2699
3644
  function resolveStudioTarget(cwd) {
2700
3645
  let dir = cwd;
2701
3646
  for (;; ) {
2702
- const webDir = join10(dir, "apps", "vinaya", "web");
2703
- const pkgPath = join10(webDir, "package.json");
2704
- if (existsSync8(pkgPath)) {
3647
+ const webDir = join13(dir, "apps", "vinaya", "web");
3648
+ const pkgPath = join13(webDir, "package.json");
3649
+ if (existsSync11(pkgPath)) {
2705
3650
  try {
2706
- const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
3651
+ const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
2707
3652
  if (pkg.name === "@atta/vinaya-web") {
2708
3653
  return { kind: "workspace", webDir };
2709
3654
  }
2710
3655
  } catch {}
2711
3656
  }
2712
- const parent = dirname6(dir);
3657
+ const parent = dirname4(dir);
2713
3658
  if (parent === dir)
2714
3659
  break;
2715
3660
  dir = parent;
@@ -2736,9 +3681,9 @@ async function runStudio(cwd, args) {
2736
3681
  }
2737
3682
 
2738
3683
  // src/commands/upgrade.ts
2739
- import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "node:fs";
2740
- import { join as join11 } from "node:path";
2741
- function realDeps4() {
3684
+ import { existsSync as existsSync12, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "node:fs";
3685
+ import { join as join14 } from "node:path";
3686
+ function realDeps6() {
2742
3687
  return {
2743
3688
  detectRepo: detectGitRepo,
2744
3689
  hookDirFor: resolveHookDir,
@@ -2753,12 +3698,12 @@ function flags2(args) {
2753
3698
  return { dryRun: args.includes("--dry-run"), yes: args.includes("--yes") };
2754
3699
  }
2755
3700
  function readManifest3(repoRoot) {
2756
- const p = join11(repoRoot, CONFIG_PATH);
2757
- if (!existsSync9(p))
3701
+ const p = join14(repoRoot, CONFIG_PATH);
3702
+ if (!existsSync12(p))
2758
3703
  return { kind: "missing" };
2759
3704
  let raw;
2760
3705
  try {
2761
- raw = JSON.parse(readFileSync9(p, "utf-8"));
3706
+ raw = JSON.parse(readFileSync10(p, "utf-8"));
2762
3707
  } catch (err) {
2763
3708
  return { kind: "invalid", error: `invalid JSON: ${err.message}` };
2764
3709
  }
@@ -2774,10 +3719,10 @@ function readManifest3(repoRoot) {
2774
3719
  return { kind: "ok", manifest: parsed.data.managed };
2775
3720
  }
2776
3721
  function writeManifestVersion(repoRoot, manifest) {
2777
- const configAbs = join11(repoRoot, CONFIG_PATH);
2778
- const seed = JSON.parse(readFileSync9(configAbs, "utf-8"));
3722
+ const configAbs = join14(repoRoot, CONFIG_PATH);
3723
+ const seed = JSON.parse(readFileSync10(configAbs, "utf-8"));
2779
3724
  const updated = { ...manifest, version: MANAGED_MANIFEST_VERSION };
2780
- writeFileSync6(configAbs, `${JSON.stringify({ ...seed, managed: updated }, null, 2)}
3725
+ writeFileSync7(configAbs, `${JSON.stringify({ ...seed, managed: updated }, null, 2)}
2781
3726
  `, "utf-8");
2782
3727
  }
2783
3728
  function planUpgrade(ops, repoRoot, manifest) {
@@ -2788,8 +3733,8 @@ function planUpgrade(ops, repoRoot, manifest) {
2788
3733
  const ownedBlocks = new Set(manifest.blocks.map((b) => blockKey(b.path, b.marker)));
2789
3734
  for (const op of ops) {
2790
3735
  if (op.kind === "create-file") {
2791
- const abs2 = join11(repoRoot, op.path);
2792
- const exists = existsSync9(abs2);
3736
+ const abs2 = join14(repoRoot, op.path);
3737
+ const exists = existsSync12(abs2);
2793
3738
  const owned = ownedFiles.has(op.path);
2794
3739
  let action;
2795
3740
  if (op.path === CONFIG_PATH) {
@@ -2799,7 +3744,7 @@ function planUpgrade(ops, repoRoot, manifest) {
2799
3744
  } else if (!exists) {
2800
3745
  action = "recreate";
2801
3746
  hasChanges = true;
2802
- } else if (readFileSync9(abs2, "utf-8") !== op.content) {
3747
+ } else if (readFileSync10(abs2, "utf-8") !== op.content) {
2803
3748
  action = "regenerate";
2804
3749
  hasChanges = true;
2805
3750
  } else {
@@ -2807,16 +3752,16 @@ function planUpgrade(ops, repoRoot, manifest) {
2807
3752
  }
2808
3753
  entries.push({ kind: "create-file", op, action });
2809
3754
  } else if (op.kind === "managed-block") {
2810
- const abs2 = join11(repoRoot, op.path);
3755
+ const abs2 = join14(repoRoot, op.path);
2811
3756
  const owned = ownedBlocks.has(blockKey(op.path, op.marker));
2812
3757
  let action;
2813
3758
  if (!owned) {
2814
3759
  action = "not-installed";
2815
- } else if (!existsSync9(abs2)) {
3760
+ } else if (!existsSync12(abs2)) {
2816
3761
  action = "recreate-host";
2817
3762
  hasChanges = true;
2818
3763
  } else {
2819
- const content = readFileSync9(abs2, "utf-8");
3764
+ const content = readFileSync10(abs2, "utf-8");
2820
3765
  const { begin, end } = markerLines(op.marker, op.comment);
2821
3766
  if (!(content.includes(begin) && content.includes(end))) {
2822
3767
  action = "recreate-append";
@@ -2892,11 +3837,11 @@ function renderUpgradeDiff(plan) {
2892
3837
  `);
2893
3838
  }
2894
3839
  function regenerateBlock(repoRoot, op) {
2895
- const abs2 = join11(repoRoot, op.path);
2896
- const content = readFileSync9(abs2, "utf-8");
3840
+ const abs2 = join14(repoRoot, op.path);
3841
+ const content = readFileSync10(abs2, "utf-8");
2897
3842
  const stripped = stripBlockFromContent(content, op.marker, op.comment);
2898
3843
  if (stripped !== null) {
2899
- writeFileSync6(abs2, stripped.endsWith(`
3844
+ writeFileSync7(abs2, stripped.endsWith(`
2900
3845
  `) ? stripped : `${stripped}
2901
3846
  `, "utf-8");
2902
3847
  }
@@ -2906,7 +3851,7 @@ function applyUpgrade(plan, repoRoot) {
2906
3851
  for (const e of plan.entries) {
2907
3852
  if (e.kind === "create-file") {
2908
3853
  if (e.action === "regenerate" || e.action === "recreate") {
2909
- writeFileWithDirs(join11(repoRoot, e.op.path), e.op.content, e.op.mode);
3854
+ writeFileWithDirs(join14(repoRoot, e.op.path), e.op.content, e.op.mode);
2910
3855
  }
2911
3856
  } else {
2912
3857
  if (e.action === "recreate-host")
@@ -2976,7 +3921,120 @@ Vinaya upgraded.
2976
3921
  return 0;
2977
3922
  }
2978
3923
  async function upgradeCommand(args) {
2979
- process.exit(await runUpgrade(args, realDeps4()));
3924
+ process.exit(await runUpgrade(args, realDeps6()));
3925
+ }
3926
+
3927
+ // src/commands/waiver.ts
3928
+ import { execFileSync as execFileSync8 } from "node:child_process";
3929
+ function labelFor(kind) {
3930
+ return kind === "docs" ? WAIVER_LABEL : WAIVER_LABEL_REVIEW;
3931
+ }
3932
+ function gh(args) {
3933
+ return execFileSync8("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], env: process.env }).trim();
3934
+ }
3935
+ function detectPrNumber() {
3936
+ try {
3937
+ const out = gh(["pr", "view", "--json", "number", "-q", ".number"]);
3938
+ return /^\d+$/.test(out) ? out : null;
3939
+ } catch {
3940
+ return null;
3941
+ }
3942
+ }
3943
+ var VALUE_FLAGS = ["--reason"];
3944
+ var BOOLEAN_FLAGS = ["--print-only"];
3945
+ function positionalArgs(args) {
3946
+ const result = [];
3947
+ for (let i = 0;i < args.length; i++) {
3948
+ const a = args[i];
3949
+ if (VALUE_FLAGS.some((f) => a.startsWith(`${f}=`)))
3950
+ continue;
3951
+ if (VALUE_FLAGS.includes(a)) {
3952
+ i += 1;
3953
+ continue;
3954
+ }
3955
+ if (BOOLEAN_FLAGS.includes(a))
3956
+ continue;
3957
+ result.push(a);
3958
+ }
3959
+ return result;
3960
+ }
3961
+ async function resolveKind(args) {
3962
+ const positional = positionalArgs(args).find((a) => a === "docs" || a === "review");
3963
+ if (positional === "docs" || positional === "review")
3964
+ return positional;
3965
+ const answer = (await prompt("Which waiver — `docs` or `review`? ")).trim().toLowerCase();
3966
+ return answer === "docs" || answer === "review" ? answer : null;
3967
+ }
3968
+ async function resolvePrNumber(args) {
3969
+ const positional = positionalArgs(args).find((a) => /^\d+$/.test(a));
3970
+ if (positional)
3971
+ return positional;
3972
+ const detected = detectPrNumber();
3973
+ if (detected)
3974
+ return detected;
3975
+ const answer = (await prompt("PR number (could not auto-detect one for the current branch): ")).trim();
3976
+ return /^\d+$/.test(answer) ? answer : null;
3977
+ }
3978
+ function flagValue(args, flag) {
3979
+ const eq = args.find((a) => a.startsWith(`${flag}=`));
3980
+ if (eq)
3981
+ return eq.slice(flag.length + 1);
3982
+ const idx = args.indexOf(flag);
3983
+ if (idx !== -1 && args[idx + 1] !== undefined)
3984
+ return args[idx + 1];
3985
+ return null;
3986
+ }
3987
+ async function resolveReason(args) {
3988
+ const flagged = flagValue(args, "--reason");
3989
+ if (flagged && flagged.trim().length > 0)
3990
+ return flagged.trim();
3991
+ const answer = await prompt("Reason for this waiver (one line — posted as a PR comment): ");
3992
+ return answer.trim();
3993
+ }
3994
+ async function waiverCommand(args) {
3995
+ const printOnly = args.includes("--print-only");
3996
+ const kind = await resolveKind(args);
3997
+ if (!kind) {
3998
+ console.error("Usage: vinaya waiver <docs|review> [<pr-number>] [--reason <text>] [--print-only]");
3999
+ closeStdin();
4000
+ process.exit(2);
4001
+ return;
4002
+ }
4003
+ const prNumber = await resolvePrNumber(args);
4004
+ if (!prNumber) {
4005
+ console.error("A PR number is required — none was given and none could be auto-detected.");
4006
+ closeStdin();
4007
+ process.exit(2);
4008
+ return;
4009
+ }
4010
+ const reason = await resolveReason(args);
4011
+ closeStdin();
4012
+ if (!reason) {
4013
+ console.error("A reason is required — the waiver label alone carries no rationale for the reviewer.");
4014
+ process.exit(2);
4015
+ return;
4016
+ }
4017
+ const label2 = labelFor(kind);
4018
+ const editArgs = ["pr", "edit", prNumber, "--add-label", label2];
4019
+ const commentArgs = ["pr", "comment", prNumber, "--body", `Waiver applied: \`${label2}\`
4020
+ Reason: ${reason}`];
4021
+ if (printOnly) {
4022
+ process.stdout.write(`--print-only: nothing below was executed. Run these yourself, as a human, to apply the waiver:
4023
+
4024
+ `);
4025
+ process.stdout.write(` gh ${editArgs.join(" ")}
4026
+ `);
4027
+ process.stdout.write(` gh ${commentArgs.join(" ")}
4028
+ `);
4029
+ return;
4030
+ }
4031
+ process.stdout.write(`Applying \`${label2}\` to PR #${prNumber} under your authenticated \`gh\` identity — vinaya never fabricates an actor.
4032
+ `);
4033
+ execFileSync8("gh", editArgs, { stdio: "inherit", env: process.env });
4034
+ execFileSync8("gh", commentArgs, { stdio: "inherit", env: process.env });
4035
+ process.stdout.write(`
4036
+ ✓ Label applied and reason posted as a PR comment.
4037
+ `);
2980
4038
  }
2981
4039
 
2982
4040
  // ../sources/src/commands.ts
@@ -3101,6 +4159,30 @@ var COMMANDS = [
3101
4159
  ],
3102
4160
  status: "shipped"
3103
4161
  },
4162
+ {
4163
+ name: "archive",
4164
+ description: "Run the post-merge Archivist directly: provenance + Issue close-out for a merged task PR",
4165
+ flags: [{ flag: "--merge-sha", description: "The merge commit to resolve (defaults to the current HEAD)" }],
4166
+ details: [
4167
+ "Resolves the merge commit's associated PR via `gh`, and — if it's a task PR without a provenance comment yet — posts the provenance block and closes the linked Issue. Idempotent: re-running against an already-archived PR is a no-op.",
4168
+ "The same logic the generated `vinaya-archivist.yml` workflow's `post-merge` job runs on every push to `main` — callable directly for a one-off run or local verification."
4169
+ ],
4170
+ status: "shipped"
4171
+ },
4172
+ {
4173
+ name: "audit",
4174
+ description: "Run the ring-2 dead-branch-push and direct-main-push detection checks directly",
4175
+ flags: [
4176
+ { flag: "--only", description: "Scope to one check: 'dead-branches' or 'direct-push'" },
4177
+ { flag: "--sha", description: "The commit to check for direct-main-push (defaults to the current HEAD)" },
4178
+ { flag: "--json", description: "Enveloped JSON output" }
4179
+ ],
4180
+ details: [
4181
+ "Dead-branch-push is never-red — a notification channel that flags (label + PR comment) any `task/*` branch whose tip commit lands after its own PR already resolved. Direct-main-push is a real pass/fail — it opens an incident Issue and exits 1 if a commit on `main` has no associated merged PR.",
4182
+ "The same logic the generated `vinaya-archivist.yml` workflow's `daily-drift` and `direct-main-push-detection` jobs run on schedule / on every push to `main` — callable directly for a one-off run or local verification."
4183
+ ],
4184
+ status: "shipped"
4185
+ },
3104
4186
  {
3105
4187
  name: "eject",
3106
4188
  description: "Remove every Vinaya-installed artifact, restoring the repo to stock",
@@ -3113,12 +4195,25 @@ var COMMANDS = [
3113
4195
  {
3114
4196
  name: "demo break",
3115
4197
  description: "Run a guided refusal-then-fix demo on an isolated, discardable branch",
3116
- status: "planned"
4198
+ flags: [{ flag: "--keep", description: "Skip cleanup and leave the demo branch checked out to inspect" }],
4199
+ details: [
4200
+ "Creates a collision-safe `vinaya/demo-break-<id>` branch off the current one, stages a deliberately incomplete draft brief, and attempts a real `git commit` — the repo's actually-installed pre-commit hook refuses it with its real output, not a scripted string. Applies the minimal fix, commits again, then switches back and deletes the demo branch.",
4201
+ "Safe to run twice: refuses on a dirty working tree, refuses from a detached HEAD, and recovers automatically from a prior crashed run before starting a fresh one — never leaves the original branch touched or a stray demo branch behind."
4202
+ ],
4203
+ status: "shipped"
3117
4204
  },
3118
4205
  {
3119
4206
  name: "waiver",
3120
- description: "Apply the principal-verified 'vinaya/waiver:docs' label after prompting for a reason",
3121
- status: "planned"
4207
+ description: "Apply the actor-verified 'vinaya/waiver:docs' or 'vinaya/waiver:review' label after prompting for a reason",
4208
+ flags: [
4209
+ { flag: "--reason", description: "The waiver rationale, posted as a PR comment (prompted for if omitted)" },
4210
+ { flag: "--print-only", description: "Print the exact `gh` commands instead of running them" }
4211
+ ],
4212
+ details: [
4213
+ "Applies the label via `gh pr edit --add-label`, under the invoking human's own authenticated `gh` identity — this command never fabricates an actor. A waiver is never an agent-emittable string: not a PR body field, not a commit trailer, not a comment — the label plus its own labeling-timeline actor is the only mechanism ring 1 honors.",
4214
+ "`--print-only` prints the exact `gh pr edit`/`gh pr comment` commands and runs nothing — for a human who wants to run them itself, or a CI/non-interactive context where an agent session should never be the one applying its own waiver."
4215
+ ],
4216
+ status: "shipped"
3122
4217
  },
3123
4218
  {
3124
4219
  name: "studio",
@@ -3155,9 +4250,9 @@ function printHelp() {
3155
4250
  }
3156
4251
 
3157
4252
  // src/index.ts
3158
- var PACKAGE_ROOT = join12(dirname7(fileURLToPath4(import.meta.url)), "..");
4253
+ var PACKAGE_ROOT = join15(dirname5(fileURLToPath2(import.meta.url)), "..");
3159
4254
  function readVersion2() {
3160
- const pkg = JSON.parse(readFileSync10(join12(PACKAGE_ROOT, "package.json"), "utf-8"));
4255
+ const pkg = JSON.parse(readFileSync11(join15(PACKAGE_ROOT, "package.json"), "utf-8"));
3161
4256
  return pkg.version;
3162
4257
  }
3163
4258
  var [, , command, ...args] = process.argv;
@@ -3207,6 +4302,14 @@ try {
3207
4302
  await checkCommand(args);
3208
4303
  break;
3209
4304
  }
4305
+ case "archive": {
4306
+ await archiveCommand(args);
4307
+ break;
4308
+ }
4309
+ case "audit": {
4310
+ await auditCommand(args);
4311
+ break;
4312
+ }
3210
4313
  case "new": {
3211
4314
  const [subcommand, ...rest] = args;
3212
4315
  if (subcommand === "check") {
@@ -3241,6 +4344,20 @@ try {
3241
4344
  }
3242
4345
  break;
3243
4346
  }
4347
+ case "demo": {
4348
+ const [subcommand, ...rest] = args;
4349
+ if (subcommand === "break") {
4350
+ await demoBreakCommand(rest);
4351
+ } else {
4352
+ console.error(`Unknown 'demo' subcommand: ${subcommand ?? "(none)"} (expected 'break')`);
4353
+ process.exit(2);
4354
+ }
4355
+ break;
4356
+ }
4357
+ case "waiver": {
4358
+ await waiverCommand(args);
4359
+ break;
4360
+ }
3244
4361
  default:
3245
4362
  console.error(`Unknown command: ${command}`);
3246
4363
  printHelp();