@attalabs/vinaya 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3253 @@
1
+ #!/usr/bin/env node
2
+
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";
7
+
8
+ // src/commands/check.ts
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
+ // ../../../packages/aeg-core/src/anchored-region.ts
62
+ function maskCode(body) {
63
+ const fill = (line) => " ".repeat(line.length);
64
+ return maskIndentedCode(maskFencedCode(body, fill), fill).replace(/(`+)[^\n]*?\1/g, (m) => " ".repeat(m.length));
65
+ }
66
+ function stripCode(body, options = {}) {
67
+ const normalised = body.replace(/\r\n?/g, `
68
+ `);
69
+ const blank = () => "";
70
+ const blocksStripped = maskIndentedCode(maskFencedCode(normalised, blank), blank);
71
+ if (options.inlineSpans === "keep")
72
+ return blocksStripped;
73
+ return blocksStripped.replace(/(`+)[^\n]*?\1/g, "");
74
+ }
75
+ var FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})([^\r\n]*)\r?$/;
76
+ var FENCE_CLOSE = /^ {0,3}(`+|~+)[ \t]*\r?$/;
77
+ function maskFencedCode(body, fill) {
78
+ let fenceChar = null;
79
+ let fenceLen = 0;
80
+ return body.split(`
81
+ `).map((line) => {
82
+ if (fenceChar === null) {
83
+ const open = line.match(FENCE_OPEN);
84
+ if (!open)
85
+ return line;
86
+ const marker = open[1];
87
+ if (marker[0] === "`" && open[2].includes("`"))
88
+ return line;
89
+ fenceChar = marker[0];
90
+ fenceLen = marker.length;
91
+ return fill(line);
92
+ }
93
+ const close = line.match(FENCE_CLOSE);
94
+ if (close && close[1][0] === fenceChar && close[1].length >= fenceLen) {
95
+ fenceChar = null;
96
+ fenceLen = 0;
97
+ }
98
+ return fill(line);
99
+ }).join(`
100
+ `);
101
+ }
102
+ var LIST_MARKER = /^ {0,3}(?:[-*+]|\d{1,9}[.)])(?:\s|$)/;
103
+ function indentWidth(line) {
104
+ let width = 0;
105
+ for (const ch of line) {
106
+ if (ch === " ")
107
+ width += 1;
108
+ else if (ch === "\t")
109
+ width += 4;
110
+ else
111
+ break;
112
+ }
113
+ return width;
114
+ }
115
+ function maskIndentedCode(body, fill) {
116
+ const lines = body.split(`
117
+ `);
118
+ let inList = false;
119
+ let inCode = false;
120
+ let prevBlank = true;
121
+ const out = lines.map((line) => {
122
+ if (line.trim() === "") {
123
+ prevBlank = true;
124
+ return line;
125
+ }
126
+ const indent = indentWidth(line);
127
+ if (indent >= 4 && (inCode || prevBlank && !inList)) {
128
+ inCode = true;
129
+ prevBlank = false;
130
+ return fill(line);
131
+ }
132
+ inCode = false;
133
+ if (indent < 4 && LIST_MARKER.test(line))
134
+ inList = true;
135
+ else if (indent === 0)
136
+ inList = false;
137
+ prevBlank = false;
138
+ return line;
139
+ });
140
+ return out.join(`
141
+ `);
142
+ }
143
+ function anchoredRegion(body, field) {
144
+ const masked = maskCode(body);
145
+ const start = new RegExp(`<!--\\s*AEG:${field}:START\\s*-->`).exec(masked);
146
+ if (!start)
147
+ return null;
148
+ const afterStart = start.index + start[0].length;
149
+ const end = new RegExp(`<!--\\s*AEG:${field}:END\\s*-->`).exec(masked.slice(afterStart));
150
+ if (!end)
151
+ return null;
152
+ return body.slice(afterStart, afterStart + end.index);
153
+ }
154
+ // ../../../packages/aeg-forge-state/src/gh.ts
155
+ import { execFile, execFileSync } from "node:child_process";
156
+ import { promisify } from "node:util";
157
+ var execFileAsync = promisify(execFile);
158
+ var systemEnv = {
159
+ ...process.env,
160
+ PATH: [process.env.PATH, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"].filter(Boolean).join(":")
161
+ };
162
+
163
+ // ../../../packages/aeg-forge-state/src/labels.ts
164
+ var LABELS = [
165
+ {
166
+ key: "blocked",
167
+ id: "vinaya/blocked",
168
+ category: "state",
169
+ form: "literal",
170
+ carries: "Execution is halted pending an external unblock; wins over every other derived status."
171
+ },
172
+ {
173
+ key: "tier-0",
174
+ id: "vinaya/tier:0",
175
+ category: "tier",
176
+ form: "literal",
177
+ carries: "Lowest governance weight — typecheck, lint, tests, and a conforming PR body."
178
+ },
179
+ {
180
+ key: "tier-1",
181
+ id: "vinaya/tier:1",
182
+ category: "tier",
183
+ form: "literal",
184
+ carries: "Tier 0 plus spec/skill coverage and a passing verify-docs run."
185
+ },
186
+ {
187
+ key: "tier-3",
188
+ id: "vinaya/tier:3",
189
+ category: "tier",
190
+ form: "literal",
191
+ carries: "Tier 1 plus the reasoning for the change, stated in the pull request that makes it."
192
+ },
193
+ {
194
+ key: "tranche",
195
+ id: "vinaya/tranche:",
196
+ category: "tranche",
197
+ form: "prefix",
198
+ carries: "The tranche slug this task Issue belongs to — the forge's grouping key, matched by prefix."
199
+ },
200
+ {
201
+ key: "needs-execution-input",
202
+ id: "vinaya/needs:execution-input",
203
+ category: "needs",
204
+ form: "literal",
205
+ carries: "Waiting on a missing execution detail — a flag, a dependency, a value the brief did not carry."
206
+ },
207
+ {
208
+ key: "needs-strategy-input",
209
+ id: "vinaya/needs:strategy-input",
210
+ category: "needs",
211
+ form: "literal",
212
+ carries: "Waiting on a strategy call — the brief assumes an approach the codebase has moved away from."
213
+ },
214
+ {
215
+ key: "needs-principal-input",
216
+ id: "vinaya/needs:principal-input",
217
+ category: "needs",
218
+ form: "literal",
219
+ carries: "Waiting on the Principal — a product-level call no agent may make."
220
+ },
221
+ {
222
+ key: "needs-brief-correction",
223
+ id: "vinaya/needs:brief-correction",
224
+ category: "needs",
225
+ form: "literal",
226
+ carries: "Waiting on the Brief Author — the brief contradicts the surface it describes."
227
+ },
228
+ {
229
+ key: "waiver-docs",
230
+ id: "vinaya/waiver:docs",
231
+ category: "waiver",
232
+ form: "literal",
233
+ carries: "Doc-coverage gate excused for this PR — honored only when a principal applied it."
234
+ },
235
+ {
236
+ key: "waiver-review",
237
+ id: "vinaya/waiver:review",
238
+ category: "waiver",
239
+ form: "literal",
240
+ carries: "Review gate excused for this PR — honored only when a principal applied it."
241
+ },
242
+ {
243
+ key: "override-docs",
244
+ id: "vinaya/override:docs",
245
+ category: "waiver",
246
+ form: "literal",
247
+ carries: "The whole verify-docs gate suppressed for this PR — a Principal-only blunt override, wider than a waiver."
248
+ },
249
+ {
250
+ key: "incoherent",
251
+ id: "vinaya/incoherent",
252
+ category: "flag",
253
+ form: "literal",
254
+ carries: "Closed COMPLETED with no merged-PR link — done-but-unprovable, surfaced for a human."
255
+ },
256
+ {
257
+ key: "direct-main-push",
258
+ id: "vinaya/direct-main-push",
259
+ category: "flag",
260
+ form: "literal",
261
+ carries: "A commit reached main with no associated merged PR — ring-2 detection, never a mutation."
262
+ },
263
+ {
264
+ key: "dead-branch-push",
265
+ id: "vinaya/dead-branch-push",
266
+ category: "flag",
267
+ form: "literal",
268
+ carries: "Commits landed on a branch after its PR had already resolved — daily-drift detection."
269
+ },
270
+ {
271
+ key: "state-object",
272
+ id: "vinaya/state-object",
273
+ category: "kind",
274
+ form: "literal",
275
+ carries: "A permanent forge-native storage object, never actionable work — excluded from every backlog."
276
+ }
277
+ ];
278
+ var BY_KEY = new Map(LABELS.map((l) => [l.key, l]));
279
+ function entry(key) {
280
+ const found = BY_KEY.get(key);
281
+ if (!found)
282
+ throw new Error(`labels.ts: no LABELS entry for key '${key}'`);
283
+ return found;
284
+ }
285
+ function label(key) {
286
+ return entry(key).id;
287
+ }
288
+ function matchesLabel(key, name) {
289
+ const l = entry(key);
290
+ if (l.form === "prefix")
291
+ return name.startsWith(l.id);
292
+ return name === l.id;
293
+ }
294
+ function hasLabel(key, names) {
295
+ return names.some((n) => matchesLabel(key, n));
296
+ }
297
+ // ../../../packages/aeg-forge-state/src/github-token.ts
298
+ import { execFile as execFile2 } from "node:child_process";
299
+ import { promisify as promisify2 } from "node:util";
300
+ var execFileAsync2 = promisify2(execFile2);
301
+ var systemEnv2 = {
302
+ ...process.env,
303
+ PATH: [process.env.PATH, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"].filter(Boolean).join(":")
304
+ };
305
+ // ../../../packages/aeg-forge-state/src/resolve-repo.ts
306
+ import { execFile as execFile3 } from "node:child_process";
307
+ import { promisify as promisify3 } from "node:util";
308
+ var execFileAsync3 = promisify3(execFile3);
309
+ var systemEnv3 = {
310
+ ...process.env,
311
+ PATH: [process.env.PATH, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"].filter(Boolean).join(":")
312
+ };
313
+ // ../../../packages/aeg-core/src/state-machine-model.ts
314
+ var FORGE_FACT_INPUTS = [
315
+ {
316
+ fact: "issueState",
317
+ readsFrom: "Issue.state (OPEN | CLOSED)",
318
+ meaning: "Whether the task Issue is still open — the anchor for the honest terminal statuses."
319
+ },
320
+ {
321
+ fact: "assigned",
322
+ readsFrom: "Issue.assignees (count > 0)",
323
+ meaning: "An assignee exists. No longer affects derivation: assigned and unassigned are both todo."
324
+ },
325
+ {
326
+ fact: "branchExists",
327
+ readsFrom: "Ref refs/heads/task/<tranche>/<id>",
328
+ meaning: "A task branch has been published — the todo → in-flight transition, written by git push, not by hand."
329
+ },
330
+ {
331
+ fact: "prState",
332
+ readsFrom: "PullRequest.state (OPEN | CLOSED | MERGED)",
333
+ meaning: "Whether work is proposed or landed. CLOSED-without-merge collapses to none — AEG models open/merged/none."
334
+ },
335
+ {
336
+ fact: "reviewDecision",
337
+ readsFrom: "PullRequest.reviewDecision",
338
+ meaning: "Only 'changes_requested' flips status; approved-but-unmerged stays in-review."
339
+ },
340
+ {
341
+ fact: "blockedLabel",
342
+ readsFrom: `Issue.labels contains '${label("blocked")}'`,
343
+ meaning: "Execution halted pending an external unblock. Wins over every other rule."
344
+ },
345
+ {
346
+ fact: "stateReason",
347
+ readsFrom: "Issue.stateReason (COMPLETED | NOT_PLANNED | REOPENED | null)",
348
+ meaning: "Separates a legitimate drop from an incoherent close on a closed, never-merged Issue."
349
+ },
350
+ {
351
+ fact: "closedAt",
352
+ readsFrom: "Issue.closedAt",
353
+ meaning: "Timestamp for the coherence oracle grandfather cutoff. No derivation rule reads it."
354
+ },
355
+ {
356
+ fact: "mergedAt",
357
+ readsFrom: "PullRequest.mergedAt",
358
+ meaning: "Timestamp for the coherence oracle grandfather cutoff. No derivation rule reads it."
359
+ }
360
+ ];
361
+ var DERIVED_STATUSES = [
362
+ "backlog",
363
+ "todo",
364
+ "in-flight",
365
+ "in-review",
366
+ "changes-requested",
367
+ "merged",
368
+ "blocked",
369
+ "dropped",
370
+ "incoherent"
371
+ ];
372
+ 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
+ // ../../../packages/aeg-core/src/waiver-label.ts
380
+ var WAIVER_LABEL = label("waiver-docs");
381
+ var WAIVER_LABEL_REVIEW = label("waiver-review");
382
+
383
+ // ../../../packages/aeg-core/src/doc-owners.ts
384
+ function globToRegex(pat) {
385
+ let re = "^";
386
+ let i = 0;
387
+ while (i < pat.length) {
388
+ const c = pat[i];
389
+ if (c === "*") {
390
+ if (pat[i + 1] === "*") {
391
+ re += ".*";
392
+ i += 2;
393
+ } else {
394
+ re += "[^/]*";
395
+ i += 1;
396
+ }
397
+ } else if (/[.+^$|(){}[\]\\?]/.test(c)) {
398
+ re += `\\${c}`;
399
+ i += 1;
400
+ } else {
401
+ re += c;
402
+ i += 1;
403
+ }
404
+ }
405
+ re += "$";
406
+ return new RegExp(re);
407
+ }
408
+ var SEPARATOR = /(?:[ \t]*[—–][ \t]*|[ \t]+-[ \t]+)/.source;
409
+ // ../../../packages/aeg-core/src/pr-tier.ts
410
+ function readTierFromPrBody(prBody) {
411
+ const searchIn = anchoredRegion(prBody, "TIER") ?? prBody;
412
+ const m = searchIn.match(/(\*\*)?\s*Tier\s*(\*\*)?\s*:\s*(\*\*)?\s*([013])\b/i);
413
+ if (!m)
414
+ return null;
415
+ const t = Number(m[4]);
416
+ return t === 0 || t === 1 || t === 3 ? t : null;
417
+ }
418
+ var OVERRIDE_BODY_TOKEN = `[${label("override-docs")}]`;
419
+ // ../../../packages/aeg-core/src/premise-check.ts
420
+ var ASSERTION_KINDS = new Set(["contains", "absent", "sha256"]);
421
+ function isPremiseHeader(line) {
422
+ const stripped = line.replace(/[*#]/g, "").trim();
423
+ return /^premise\s*:?$/i.test(stripped);
424
+ }
425
+ var PREMISE_LINE = /^[-*]\s*(\S+)\s+(contains|absent|sha256)\s*:\s*(.+)$/i;
426
+ function parsePremiseBlock(prBody) {
427
+ const searchIn = anchoredRegion(prBody, "PREMISE") ?? prBody;
428
+ const lines = searchIn.split(/\r?\n/);
429
+ const assertions = [];
430
+ let inBlock = false;
431
+ for (const raw of lines) {
432
+ const line = raw.trim();
433
+ if (!inBlock) {
434
+ if (isPremiseHeader(line))
435
+ inBlock = true;
436
+ continue;
437
+ }
438
+ if (line === "")
439
+ break;
440
+ const m = line.match(PREMISE_LINE);
441
+ if (!m)
442
+ break;
443
+ const [, path, kindRaw, value] = m;
444
+ const kind = kindRaw.toLowerCase();
445
+ if (!ASSERTION_KINDS.has(kind))
446
+ continue;
447
+ assertions.push({ kind, path, value: value.trim() });
448
+ }
449
+ return assertions;
450
+ }
451
+
452
+ // ../../../packages/aeg-core/src/brief-validation.ts
453
+ function headerRegion(prBody) {
454
+ const m = prBody.match(/^##\s/m);
455
+ return m?.index !== undefined ? prBody.slice(0, m.index) : prBody;
456
+ }
457
+ function headerField(prBody, labelPattern, anchor) {
458
+ const anchored = anchor !== undefined ? anchoredRegion(prBody, anchor) : null;
459
+ const region = anchored ?? headerRegion(prBody);
460
+ const re = new RegExp(`^(?:\\*\\*)?\\s*${labelPattern}\\s*(?:\\*\\*)?\\s*:\\s*(?:\\*\\*)?\\s*([^\\n·]+)`, "im");
461
+ const m = region.match(re);
462
+ if (!m)
463
+ return null;
464
+ const value = m[1].trim();
465
+ return value.length > 0 ? value : null;
466
+ }
467
+ function normalize(text) {
468
+ return text.replace(/[*_]/g, "").replace(/\s+/g, " ");
469
+ }
470
+ function headingCheck(prBody, keywordPattern, sectionName) {
471
+ const re = new RegExp(`^#{1,4}\\s*(?:\\*\\*)?(?:\\d+[a-z]?\\.\\s*)?[^\\n]*${keywordPattern}`, "im");
472
+ if (re.test(prBody))
473
+ return { status: "pass", errors: [] };
474
+ return {
475
+ status: "fail",
476
+ errors: [`brief-validation ${sectionName}: no "${sectionName}" section found in the PR body.`]
477
+ };
478
+ }
479
+ function checkTierField(prBody, readTier) {
480
+ if (readTier(prBody) !== null)
481
+ return { status: "pass", errors: [] };
482
+ return {
483
+ status: "fail",
484
+ errors: [
485
+ "brief-validation tier: no `Tier:` field found in the PR body (expected `Tier: 0|1|3` or `**Tier:** 0|1|3`)."
486
+ ]
487
+ };
488
+ }
489
+ var TEST_PLAN_UNIT_TESTS_ONLY_RE = /(?:\*\*)?Test Plan(?:\*\*)?\s*:\s*(?:\*\*)?\s*unit-tests-only/i;
490
+ function checkTestPlan(prBody) {
491
+ if (TEST_PLAN_UNIT_TESTS_ONLY_RE.test(prBody))
492
+ return { status: "pass", errors: [] };
493
+ if (/\*\*\[(?:agent|principal)\]\*\*/.test(prBody))
494
+ return { status: "pass", errors: [] };
495
+ return {
496
+ status: "fail",
497
+ errors: [
498
+ "brief-validation Test Plan: no Test Plan section found — expected `Test Plan: unit-tests-only`, or at least one `**[agent]**`/`**[principal]**`-tagged checklist item."
499
+ ]
500
+ };
501
+ }
502
+ function checkTestPlanExclusivity(prBody) {
503
+ if (!TEST_PLAN_UNIT_TESTS_ONLY_RE.test(prBody))
504
+ return { status: "pass", errors: [] };
505
+ if (/^-\s*\[[ xX]\]\s*\*{2}\[(?:agent|principal)\]\*{2}/im.test(prBody)) {
506
+ return {
507
+ status: "fail",
508
+ errors: [
509
+ "brief-validation Test Plan shape: `Test Plan: unit-tests-only` and a `- [ ]`/`- [x]` tagged checkbox item are mutually exclusive (brief-authoring §9) — declare one form, not both."
510
+ ]
511
+ };
512
+ }
513
+ return { status: "pass", errors: [] };
514
+ }
515
+ function checkPrincipalPlaceholder(prBody) {
516
+ const lineRe = /^-\s*\[[ xX]\]\s*\*{2}\[principal\]\*{2}(.*)$/gim;
517
+ for (const m of prBody.matchAll(lineRe)) {
518
+ const content = m[1] ?? "";
519
+ if (/^\s*None\b/i.test(content)) {
520
+ return {
521
+ status: "fail",
522
+ errors: [
523
+ 'brief-validation Test Plan shape: a `**[principal]**` checkbox item is a "None" placeholder — if there is no principal-runnable surface, omit the item entirely; an untickable placeholder box blocks the merge gate forever.'
524
+ ]
525
+ };
526
+ }
527
+ }
528
+ return { status: "pass", errors: [] };
529
+ }
530
+ function checkPremiseCoverage(prBody, surfaceFiles) {
531
+ if (surfaceFiles.length === 0)
532
+ return { status: "pass", errors: [] };
533
+ const assertions = parsePremiseBlock(prBody);
534
+ if (assertions.some((a) => surfaceFiles.includes(a.path)))
535
+ return { status: "pass", errors: [] };
536
+ return {
537
+ status: "fail",
538
+ errors: [
539
+ "brief-validation Premise: no `Premise:` assertion found whose path matches a file in the surface map — a brief with a real code surface must pin at least one premise (aeg-governance-hardening task 11)."
540
+ ]
541
+ };
542
+ }
543
+ function checkSurfaceMap(prBody) {
544
+ return headingCheck(prBody, "(?:technical\\s+)?surface map", "Technical surface map");
545
+ }
546
+ function checkDocUpdateList(prBody) {
547
+ return headingCheck(prBody, "(?:documentation|doc)[- ]update(?:\\s+list)?", "Documentation-update list");
548
+ }
549
+ function checkWorktreeStep0(prBody) {
550
+ if (/git worktree add/.test(prBody))
551
+ return { status: "pass", errors: [] };
552
+ return {
553
+ status: "fail",
554
+ errors: ["brief-validation worktree Step 0: no `git worktree add` command found in the PR body."]
555
+ };
556
+ }
557
+ function checkStopConditions(prBody) {
558
+ return headingCheck(prBody, "stop conditions", "Stop conditions");
559
+ }
560
+ function checkAutonomyClause(prBody) {
561
+ const normalized = normalize(prBody).toLowerCase();
562
+ if (/do not stop to ask clarifying questions/.test(normalized))
563
+ return { status: "pass", errors: [] };
564
+ return {
565
+ status: "fail",
566
+ errors: [
567
+ 'brief-validation autonomy clause: the standing autonomy clause ("Do not stop to ask clarifying questions...") was not found in the PR body.'
568
+ ]
569
+ };
570
+ }
571
+ function checkProjectField(prBody) {
572
+ if (headerField(prBody, "Project(?:\\(s\\))?", "PROJECT") !== null)
573
+ return { status: "pass", errors: [] };
574
+ return {
575
+ status: "fail",
576
+ errors: [
577
+ "brief-validation Project: no `Project:` field found in the PR body header block (before the first `##` heading). Required per the brief-developer contract — expected `Project: <name>[, <name>]` or `**Project:** …`."
578
+ ]
579
+ };
580
+ }
581
+ function checkForField(prBody) {
582
+ if (headerField(prBody, "For") !== null)
583
+ return { status: "pass", errors: [] };
584
+ return {
585
+ status: "fail",
586
+ errors: [
587
+ "brief-validation For: no `For:` field found in the PR body header block (before the first `##` heading). Required per the brief-authoring skill — expected `For: <model + environment>` or `**For:** …`."
588
+ ]
589
+ };
590
+ }
591
+ function checkClosesN(prBody) {
592
+ const closesPattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s{0,8}:?\s{0,8}#\d+/i;
593
+ if (closesPattern.test(stripCode(prBody))) {
594
+ return { status: "pass", errors: [] };
595
+ }
596
+ if (closesPattern.test(prBody)) {
597
+ return {
598
+ status: "fail",
599
+ errors: [
600
+ "brief-validation Closes #N: `Closes #N` found only inside a code span — GitHub won't auto-close it. Put a bare `Closes #N` on its own line inside the `AEG:CLOSES` anchor."
601
+ ]
602
+ };
603
+ }
604
+ return {
605
+ status: "fail",
606
+ errors: ["brief-validation Closes #N: no `Closes #<N>` (or Fixes/Resolves) reference found in the PR body."]
607
+ };
608
+ }
609
+ function checkForgeTitle(title) {
610
+ const commitStyle = /^(Build|Chore|Docs|Feat|Fix|Perf|Plan|Refactor|Revert|Style|Test)(\([a-z0-9-]+\))?: \S/;
611
+ const taskStyle = /^\[[a-z0-9._-]+\] \S+ — \S/;
612
+ if (commitStyle.test(title) || taskStyle.test(title))
613
+ return { status: "pass", errors: [] };
614
+ return {
615
+ status: "fail",
616
+ errors: [
617
+ `brief-validation title: "${title}" matches neither title grammar — expected \`Type: description\` / \`Type(scope): description\` (commitlint types + Plan) or \`[tranche] id — description\` (task form).`
618
+ ]
619
+ };
620
+ }
621
+ // ../../../packages/aeg-core/src/issue-validation.ts
622
+ function hasRationaleField(body, labelPattern) {
623
+ const re = new RegExp(`(?:\\*\\*|^#{1,4}\\s+)\\s*${labelPattern}`, "im");
624
+ return re.test(body);
625
+ }
626
+ var RATIONALE_FIELDS = [
627
+ { name: "Boundary", pattern: "Boundary" },
628
+ { name: "Sizing", pattern: "Sizing" },
629
+ { name: "Project(s) + blast radius", pattern: "Project\\(s\\)|Project(?:s)?\\s*\\+|blast radius" },
630
+ { name: "Dependency rationale", pattern: "Dependency rationale|Depends[- ]on" },
631
+ { name: "Traps to avoid", pattern: "Traps" },
632
+ { name: "Suggested agent-class", pattern: "(?:Suggested\\s+)?agent-class" },
633
+ { name: "Stop-and-escalate", pattern: "Stop-and-escalate" },
634
+ { name: "Docs to keep coherent", pattern: "Docs to keep coherent|§7" }
635
+ ];
636
+ 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).`);
638
+ return { status: errors.length > 0 ? "fail" : "pass", errors };
639
+ }
640
+ function isTaskIssueLabelSet(labels) {
641
+ return hasLabel("tranche", labels);
642
+ }
643
+
644
+ // ../../../packages/aeg-core/src/coherence-checks.ts
645
+ var R1_GRANDFATHERED_ISSUES = new Set([279, 280, 281, 282]);
646
+ // ../../../packages/aeg-core/src/diagram-model.ts
647
+ import matter from "gray-matter";
648
+ // ../../../packages/aeg-core/src/reader-resolvable-prose.ts
649
+ var SWEPT_CLASSES = new Set(["ships", "reader-facing"]);
650
+ // src/checks/runner.ts
651
+ function defaultParallelism() {
652
+ return Math.max(1, cpus().length);
653
+ }
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";
659
+ }
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)
668
+ return false;
669
+ const regexes = spec.include.map(globToRegex);
670
+ return !opts.changedFiles.some((f) => regexes.some((re) => re.test(f)));
671
+ }
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 };
704
+ }
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
+ };
722
+ }
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;
734
+ }
735
+ } catch {
736
+ malformed = true;
737
+ }
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 };
750
+ }
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];
756
+ if (shouldSkip(spec, opts)) {
757
+ results[i] = { name: spec.name, status: "skipped", exitCode: null, errors: [], durationMs: 0 };
758
+ } else {
759
+ toRun.push(i);
760
+ }
761
+ }
762
+ let cursor = 0;
763
+ const worker = async () => {
764
+ while (cursor < toRun.length) {
765
+ const idx = toRun[cursor];
766
+ cursor += 1;
767
+ const spec = specs[idx];
768
+ results[idx] = await runOne(spec, spec.timeoutMs ?? opts.defaultTimeoutMs);
769
+ }
770
+ };
771
+ const workerCount = Math.max(1, Math.min(opts.parallel, toRun.length));
772
+ await Promise.all(Array.from({ length: workerCount }, worker));
773
+ return results;
774
+ }
775
+
776
+ // src/lib/config.ts
777
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
778
+ import { homedir } from "node:os";
779
+ import { dirname as dirname2, join as join2 } from "node:path";
780
+ import { z } from "zod";
781
+ var CheckEntrySchema = z.object({
782
+ run: z.string(),
783
+ scope: z.enum(["diff", "full"]),
784
+ include: z.array(z.string()).optional(),
785
+ args: z.array(z.string()).optional(),
786
+ timeoutMs: z.number().optional()
787
+ });
788
+ var BRIEF_BUILTINS = [
789
+ "tier",
790
+ "testPlan",
791
+ "testPlanExclusivity",
792
+ "principalPlaceholder",
793
+ "surfaceMap",
794
+ "docUpdateList",
795
+ "worktreeStep0",
796
+ "stopConditions",
797
+ "autonomyClause",
798
+ "project",
799
+ "for",
800
+ "closesN",
801
+ "premiseCoverage",
802
+ "issueRationale"
803
+ ];
804
+ var BriefSectionSchema = z.union([
805
+ z.object({ builtin: z.enum(BRIEF_BUILTINS) }),
806
+ z.object({ heading: z.string().min(1), name: z.string().optional() }),
807
+ z.object({ field: z.string().min(1), name: z.string().optional() }),
808
+ z.object({ phrase: z.string().min(1), name: z.string().optional() })
809
+ ]);
810
+ var BriefSchemaSchema = z.object({
811
+ pr: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
812
+ issue: z.object({ sections: z.array(BriefSectionSchema) }).optional()
813
+ });
814
+ var MANAGED_MANIFEST_VERSION = 1;
815
+ function isSafeRepoRelPath(p) {
816
+ if (p.length === 0)
817
+ return false;
818
+ if (p.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(p))
819
+ return false;
820
+ return p.split(/[\\/]/).every((seg) => seg !== ".." && seg !== "");
821
+ }
822
+ var SafeRepoRelPath = z.string().refine(isSafeRepoRelPath, {
823
+ message: "must be a repo-root-relative path with no `..` segment or absolute root"
824
+ });
825
+ var ManagedBlockRecordSchema = z.object({
826
+ path: SafeRepoRelPath,
827
+ marker: z.string(),
828
+ comment: z.enum(["hash", "html"])
829
+ });
830
+ var ManagedManifestSchema = z.object({
831
+ version: z.number().int().positive(),
832
+ files: z.array(SafeRepoRelPath),
833
+ blocks: z.array(ManagedBlockRecordSchema),
834
+ labels: z.array(z.string())
835
+ });
836
+ var VinayaConfigSchema = z.object({
837
+ rings: z.object({
838
+ ring1_forgeWriteInterception: z.boolean(),
839
+ ring2_asyncAudits: z.boolean()
840
+ }).optional(),
841
+ checks: z.record(z.string(), CheckEntrySchema).optional(),
842
+ briefSchema: BriefSchemaSchema.optional(),
843
+ managed: ManagedManifestSchema.optional()
844
+ });
845
+ var GLOBAL_VINAYA_HOME = join2(homedir(), ".vinaya");
846
+ var GLOBAL_CONFIG_PATH = join2(GLOBAL_VINAYA_HOME, "config.json");
847
+ var LOCAL_CONFIG_FILENAME = "vinaya.config.json";
848
+ function findLocalConfig() {
849
+ let dir = process.cwd();
850
+ while (true) {
851
+ const candidate = join2(dir, LOCAL_CONFIG_FILENAME);
852
+ if (existsSync(candidate))
853
+ return candidate;
854
+ const parent = dirname2(dir);
855
+ if (parent === dir)
856
+ return null;
857
+ dir = parent;
858
+ }
859
+ }
860
+ function configPath() {
861
+ const local = findLocalConfig();
862
+ if (local)
863
+ return local;
864
+ if (existsSync(GLOBAL_CONFIG_PATH))
865
+ return GLOBAL_CONFIG_PATH;
866
+ return null;
867
+ }
868
+ function loadConfigChecked() {
869
+ const path = configPath();
870
+ if (!path)
871
+ return { ok: true, config: null };
872
+ let raw;
873
+ try {
874
+ raw = JSON.parse(readFileSync(path, "utf-8"));
875
+ } catch (err) {
876
+ return { ok: false, path, error: `invalid JSON: ${err.message}` };
877
+ }
878
+ const parsed = VinayaConfigSchema.safeParse(raw);
879
+ if (!parsed.success) {
880
+ const detail = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
881
+ return { ok: false, path, error: detail };
882
+ }
883
+ return { ok: true, config: parsed.data };
884
+ }
885
+
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
+ // src/commands/check.ts
897
+ function git(args) {
898
+ try {
899
+ return execFileSync2("git", args, { encoding: "utf8" }).trim();
900
+ } catch {
901
+ return "";
902
+ }
903
+ }
904
+ function changedFiles() {
905
+ const base = process.env.BASE_SHA || "origin/main";
906
+ let out = git(["diff", "--name-only", `${base}...HEAD`]);
907
+ if (!out)
908
+ out = git(["diff", "--name-only", "main...HEAD"]);
909
+ return out.split(`
910
+ `).map((s) => s.trim()).filter(Boolean);
911
+ }
912
+ function parseParallel(args) {
913
+ for (const a of args) {
914
+ const m = a.match(/^--parallel(?:=(\d+))?$/);
915
+ if (m)
916
+ return m[1] ? Number(m[1]) : defaultParallelism();
917
+ }
918
+ return;
919
+ }
920
+ function configErrorOutcome(path, error) {
921
+ const finding = {
922
+ schema: 1,
923
+ check: "config",
924
+ severity: "error",
925
+ message: `${path}: invalid \`checks\` registration — ${error}`,
926
+ agent_recovery_prompt: `Fix the invalid key/value named above in ${path}, then re-run \`vinaya check\`.`
927
+ };
928
+ return { name: "config", status: "error", exitCode: null, errors: [finding], durationMs: 0 };
929
+ }
930
+ function customSpecsFromConfig() {
931
+ const result = loadConfigChecked();
932
+ if (!result.ok)
933
+ return { specs: [], errorOutcome: configErrorOutcome(result.path, result.error) };
934
+ const checks = result.config?.checks;
935
+ if (!checks)
936
+ return { specs: [], errorOutcome: null };
937
+ const specs = Object.entries(checks).map(([name, entry2]) => ({ name, ...entry2 }));
938
+ return { specs, errorOutcome: null };
939
+ }
940
+ async function checkCommand(args) {
941
+ const jsonOutput = args.includes("--json");
942
+ const diffOnly = args.includes("--diff-only");
943
+ const requestedParallel = parseParallel(args);
944
+ const allRequested = args.includes("--all");
945
+ const positional = args.filter((a) => !a.startsWith("--"));
946
+ const requestedName = positional[0];
947
+ if (!allRequested && !requestedName) {
948
+ console.error("Usage: vinaya check <name> | --all [--json] [--diff-only] [--parallel[=n]]");
949
+ process.exit(2);
950
+ }
951
+ const { specs: customSpecs, errorOutcome } = customSpecsFromConfig();
952
+ const allSpecs = [...coreCheckRegistry(), ...customSpecs];
953
+ const specsToRun = allRequested ? allSpecs : allSpecs.filter((s) => s.name === requestedName);
954
+ if (!allRequested && specsToRun.length === 0) {
955
+ console.error(`Unknown check: ${requestedName}`);
956
+ process.exit(2);
957
+ }
958
+ const changed = diffOnly ? changedFiles() : null;
959
+ const outcomes = specsToRun.length > 0 ? await runChecks(specsToRun, {
960
+ parallel: requestedParallel ?? defaultParallelism(),
961
+ diffOnly,
962
+ changedFiles: changed,
963
+ defaultTimeoutMs: 30000
964
+ }) : [];
965
+ const allOutcomes = errorOutcome ? [...outcomes, errorOutcome] : outcomes;
966
+ for (const o of allOutcomes) {
967
+ for (const e of o.errors)
968
+ emitCheckError(e);
969
+ }
970
+ if (jsonOutput) {
971
+ printJson({ checks: allOutcomes });
972
+ } else {
973
+ for (const o of allOutcomes) {
974
+ const symbol = o.status === "pass" ? "✓" : o.status === "skipped" ? "·" : "✗";
975
+ process.stdout.write(`${symbol} ${o.name}: ${o.status} (${Math.round(o.durationMs)}ms)
976
+ `);
977
+ for (const e of o.errors)
978
+ process.stdout.write(` ${e.severity}: ${e.message}
979
+ `);
980
+ }
981
+ }
982
+ const failed = allOutcomes.some((o) => o.status === "fail" || o.status === "error" || o.status === "timeout");
983
+ process.exit(failed ? 1 : 0);
984
+ }
985
+
986
+ // 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";
990
+
991
+ // src/lib/artifacts.ts
992
+ var CONFIG_PATH = "vinaya.config.json";
993
+ var DOCTRINE_POINTER_PATH = "VINAYA.md";
994
+ var CHECKS_WORKFLOW_PATH = ".github/workflows/vinaya-checks.yml";
995
+ var REVIEW_WORKFLOW_PATH = ".github/workflows/vinaya-review.yml";
996
+ var MANAGED_NOTE = "Managed by Vinaya — created by `vinaya init`. `vinaya upgrade` regenerates it; `vinaya eject` removes it.";
997
+ function starterConfig() {
998
+ return {
999
+ rings: { ring1_forgeWriteInterception: false, ring2_asyncAudits: false },
1000
+ checks: {},
1001
+ briefSchema: {
1002
+ pr: {
1003
+ sections: [
1004
+ { builtin: "tier" },
1005
+ { builtin: "testPlan" },
1006
+ { builtin: "testPlanExclusivity" },
1007
+ { builtin: "closesN" },
1008
+ { builtin: "project" }
1009
+ ]
1010
+ },
1011
+ issue: {
1012
+ sections: [{ builtin: "issueRationale" }, { builtin: "project" }]
1013
+ }
1014
+ }
1015
+ };
1016
+ }
1017
+ function checksWorkflow() {
1018
+ return `# ${MANAGED_NOTE}
1019
+ #
1020
+ # The deterministic gate suite. Runs every registered vinaya check over the
1021
+ # pull request's diff. This is the guarantee: a PR cannot merge red.
1022
+ name: Vinaya Checks
1023
+
1024
+ on:
1025
+ pull_request:
1026
+ types: [opened, synchronize, reopened]
1027
+
1028
+ jobs:
1029
+ vinaya-checks:
1030
+ name: vinaya check --all --diff-only
1031
+ runs-on: ubuntu-latest
1032
+ permissions:
1033
+ contents: read
1034
+ steps:
1035
+ - uses: actions/checkout@v4
1036
+ with:
1037
+ fetch-depth: 0
1038
+ - uses: actions/setup-node@v4
1039
+ with:
1040
+ node-version: 20
1041
+ - name: Run checks
1042
+ run: npx --yes vinaya check --all --diff-only
1043
+ `;
1044
+ }
1045
+ function reviewWorkflow() {
1046
+ return `# ${MANAGED_NOTE}
1047
+ #
1048
+ # The review gate — split from the checks suite so a verdict *comment*
1049
+ # re-triggers it. GitHub fires \`issue_comment\` for a new PR comment, a
1050
+ # different event from \`pull_request\`; the checks workflow (pull_request only)
1051
+ # structurally cannot receive it. A cheap \`contains(..., 'VERDICT')\` guard
1052
+ # runs before any checkout cost, so ordinary PR chat spends no billed minute.
1053
+ name: Vinaya Review Gate
1054
+
1055
+ on:
1056
+ pull_request:
1057
+ types: [opened, synchronize, reopened, labeled, unlabeled]
1058
+ issue_comment:
1059
+ types: [created]
1060
+
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'))
1067
+ runs-on: ubuntu-latest
1068
+ permissions:
1069
+ contents: read
1070
+ pull-requests: read
1071
+ issues: read
1072
+ 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
1078
+ env:
1079
+ 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"
1088
+ - uses: actions/checkout@v4
1089
+ with:
1090
+ ref: \${{ steps.pr.outputs.sha }}
1091
+ fetch-depth: 0
1092
+ - uses: actions/setup-node@v4
1093
+ with:
1094
+ node-version: 20
1095
+ - name: Review gate
1096
+ env:
1097
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
1098
+ run: npx --yes vinaya check --all
1099
+ `;
1100
+ }
1101
+ var HOOK_PREAMBLE = `#!/usr/bin/env sh
1102
+ `;
1103
+ function preCommitBody() {
1104
+ return `# Vinaya commit-time gate. Runs the deterministic checks over your staged
1105
+ # diff before the commit lands.
1106
+ npx --no-install vinaya check --all --diff-only || exit 1`;
1107
+ }
1108
+ function prePushBody() {
1109
+ return `# Vinaya pre-push gate. Runs branch/dispatch checks before the push leaves.
1110
+ npx --no-install vinaya check --all || exit 1`;
1111
+ }
1112
+ function doctrinePointer() {
1113
+ return `<!-- ${MANAGED_NOTE} -->
1114
+ # Vinaya doctrine — read this first
1115
+
1116
+ This repo is governed by Vinaya. The full, canonical doctrine (roles,
1117
+ 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.
1120
+
1121
+ An agent working in this repo follows the governed flow by reading two things:
1122
+
1123
+ 1. **This pointer** — the tool-agnostic entry point at the conventional
1124
+ reading-order path (repo root). It names where the doctrine lives.
1125
+ 2. **\`${CONFIG_PATH}\`** — the ruleset the gates enforce: rings, custom checks,
1126
+ and the brief schema a PR/Issue body must satisfy.
1127
+
1128
+ Live task status is derived from the forge (Issues, labels, comments) via
1129
+ \`vinaya check\` — it is never written into a file here.
1130
+
1131
+ To view the doctrine text: \`vinaya doctor\` reports what is installed; the
1132
+ package's own reference content is the source of truth.
1133
+ `;
1134
+ }
1135
+ function labelOps() {
1136
+ const g = "Labels (create-if-absent; existing labels never modified)";
1137
+ const mk = (name, color, description) => ({
1138
+ kind: "create-label",
1139
+ name,
1140
+ color,
1141
+ description,
1142
+ group: g
1143
+ });
1144
+ return [
1145
+ mk(label("tier-0"), "ededed", "Trivial / mechanical change"),
1146
+ mk(label("tier-1"), "c5def5", "Standard task — code + tests + docs"),
1147
+ mk(label("tier-3"), "d93f0b", "Records a decision; ratification-gated"),
1148
+ mk(label("needs-execution-input"), "fbca04", "Blocked on a missing execution detail"),
1149
+ mk(label("needs-strategy-input"), "fbca04", "Blocked on a strategy/approach decision"),
1150
+ mk(label("needs-principal-input"), "b60205", "Blocked on a Principal decision")
1151
+ ];
1152
+ }
1153
+ var BRANCH_PROTECTION_NOTE = `Recommended (run yourself — vinaya never applies branch protection):
1154
+
1155
+ gh api -X PUT repos/{owner}/{repo}/branches/main/protection \\
1156
+ -F required_pull_request_reviews.required_approving_review_count=1 \\
1157
+ -F required_status_checks.strict=true \\
1158
+ -F 'required_status_checks.contexts[]=vinaya-checks' \\
1159
+ -F enforce_admins=true -F restrictions=`;
1160
+ function buildInitOps(ctx) {
1161
+ const ops = [];
1162
+ const hookMode = 493;
1163
+ ops.push({ kind: "create-file", path: CHECKS_WORKFLOW_PATH, content: checksWorkflow(), group: "CI workflows" });
1164
+ ops.push({ kind: "create-file", path: REVIEW_WORKFLOW_PATH, content: reviewWorkflow(), group: "CI workflows" });
1165
+ ops.push({
1166
+ kind: "managed-block",
1167
+ path: `${ctx.hookDir}/pre-commit`,
1168
+ marker: "pre-commit",
1169
+ body: preCommitBody(),
1170
+ comment: "hash",
1171
+ hostPreamble: HOOK_PREAMBLE,
1172
+ mode: hookMode,
1173
+ group: "Git hooks"
1174
+ });
1175
+ ops.push({
1176
+ kind: "managed-block",
1177
+ path: `${ctx.hookDir}/pre-push`,
1178
+ marker: "pre-push",
1179
+ body: prePushBody(),
1180
+ comment: "hash",
1181
+ hostPreamble: HOOK_PREAMBLE,
1182
+ mode: hookMode,
1183
+ group: "Git hooks"
1184
+ });
1185
+ ops.push({
1186
+ kind: "create-file",
1187
+ path: CONFIG_PATH,
1188
+ content: `${JSON.stringify(starterConfig(), null, 2)}
1189
+ `,
1190
+ group: "Config (starter ruleset)"
1191
+ });
1192
+ ops.push({
1193
+ kind: "create-file",
1194
+ path: DOCTRINE_POINTER_PATH,
1195
+ content: doctrinePointer(),
1196
+ group: "Doctrine pointer"
1197
+ });
1198
+ ops.push(...labelOps());
1199
+ ops.push({ kind: "print", message: BRANCH_PROTECTION_NOTE, group: "Branch protection (printed, never applied)" });
1200
+ return ops;
1201
+ }
1202
+ function buildInitProductOps(name) {
1203
+ const safe = name.trim();
1204
+ return [
1205
+ {
1206
+ kind: "create-label",
1207
+ name: `project:${safe}`,
1208
+ color: "0e8a16",
1209
+ description: `Governed product area: ${safe}`,
1210
+ group: `Governed product area: ${safe}`
1211
+ }
1212
+ ];
1213
+ }
1214
+
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
+ // 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";
1316
+ var MARKER_NS = "vinaya:managed";
1317
+ function markerLines(marker, comment) {
1318
+ const [open, close] = comment === "hash" ? ["#", ""] : ["<!--", " -->"];
1319
+ return {
1320
+ begin: `${open} >>> ${MARKER_NS}:${marker} >>>${close}`,
1321
+ end: `${open} <<< ${MARKER_NS}:${marker} <<<${close}`
1322
+ };
1323
+ }
1324
+ function renderBlock(op) {
1325
+ const { begin, end } = markerLines(op.marker, op.comment);
1326
+ return `${begin}
1327
+ ${op.body}
1328
+ ${end}`;
1329
+ }
1330
+ function abs(repoRoot, relPath) {
1331
+ return join4(repoRoot, relPath);
1332
+ }
1333
+ function containedAbs(repoRoot, relPath) {
1334
+ const root = resolve(repoRoot);
1335
+ const target = resolve(root, relPath);
1336
+ if (target === root)
1337
+ return null;
1338
+ return target === root || target.startsWith(root + sep) ? target : null;
1339
+ }
1340
+ function fileContains(repoRoot, relPath, needle) {
1341
+ const p = abs(repoRoot, relPath);
1342
+ if (!existsSync3(p))
1343
+ return false;
1344
+ return readFileSync2(p, "utf-8").includes(needle);
1345
+ }
1346
+ function planInstall(ops, repoRoot, ownedFiles = new Set) {
1347
+ const entries = [];
1348
+ let hasRefusals = false;
1349
+ for (const op of ops) {
1350
+ switch (op.kind) {
1351
+ case "create-file": {
1352
+ const exists = existsSync3(abs(repoRoot, op.path));
1353
+ let action;
1354
+ if (!exists)
1355
+ action = "create";
1356
+ else if (ownedFiles.has(op.path))
1357
+ action = "skip-owned";
1358
+ else {
1359
+ action = "refuse-foreign";
1360
+ hasRefusals = true;
1361
+ }
1362
+ entries.push({ kind: "create-file", op, action });
1363
+ break;
1364
+ }
1365
+ case "managed-block": {
1366
+ const { begin } = markerLines(op.marker, op.comment);
1367
+ let action;
1368
+ if (fileContains(repoRoot, op.path, begin))
1369
+ action = "skip-present";
1370
+ else if (existsSync3(abs(repoRoot, op.path)))
1371
+ action = "append";
1372
+ else
1373
+ action = "create-host";
1374
+ entries.push({ kind: "managed-block", op, action });
1375
+ break;
1376
+ }
1377
+ case "create-label":
1378
+ entries.push({ kind: "create-label", op });
1379
+ break;
1380
+ case "print":
1381
+ entries.push({ kind: "print", op });
1382
+ break;
1383
+ }
1384
+ }
1385
+ return { entries, hasRefusals };
1386
+ }
1387
+ function indent(text, pad = " ") {
1388
+ return text.split(`
1389
+ `).map((l) => l.length > 0 ? pad + l : l).join(`
1390
+ `);
1391
+ }
1392
+ function renderInstallDiff(plan) {
1393
+ const lines = [];
1394
+ const groups = new Map;
1395
+ for (const e of plan.entries) {
1396
+ const g = e.op.group;
1397
+ if (!groups.has(g))
1398
+ groups.set(g, []);
1399
+ groups.get(g)?.push(e);
1400
+ }
1401
+ for (const [group, es] of groups) {
1402
+ lines.push(`── ${group} ─────────────────────────────`);
1403
+ for (const e of es) {
1404
+ switch (e.kind) {
1405
+ case "create-file":
1406
+ if (e.action === "create") {
1407
+ lines.push(` + create ${e.op.path}`);
1408
+ lines.push(indent(e.op.content));
1409
+ } else if (e.action === "skip-owned") {
1410
+ lines.push(` = keep ${e.op.path} (already vinaya-managed)`);
1411
+ } else {
1412
+ lines.push(` ✖ REFUSE ${e.op.path} — foreign content exists at this path; not overwritten`);
1413
+ }
1414
+ break;
1415
+ case "managed-block":
1416
+ if (e.action === "skip-present") {
1417
+ lines.push(` = keep ${e.op.path} (vinaya block already present)`);
1418
+ } else if (e.action === "append") {
1419
+ lines.push(` ~ append managed block to ${e.op.path} (existing content untouched)`);
1420
+ lines.push(indent(renderBlock(e.op)));
1421
+ } else {
1422
+ lines.push(` + create ${e.op.path} (with managed block)`);
1423
+ lines.push(indent(`${e.op.hostPreamble ?? ""}${renderBlock(e.op)}`));
1424
+ }
1425
+ break;
1426
+ case "create-label":
1427
+ lines.push(` + label ${e.op.name} (created only if absent; existing labels never modified)`);
1428
+ break;
1429
+ case "print":
1430
+ lines.push(` ⓘ ${e.op.message.split(`
1431
+ `).join(`
1432
+ `)}`);
1433
+ break;
1434
+ }
1435
+ }
1436
+ lines.push("");
1437
+ }
1438
+ if (plan.hasRefusals) {
1439
+ lines.push("One or more paths hold foreign content — those artifacts will be SKIPPED, not overwritten.");
1440
+ lines.push("Resolve them manually (or remove the conflicting file) and re-run to install the rest.");
1441
+ lines.push("");
1442
+ }
1443
+ return lines.join(`
1444
+ `);
1445
+ }
1446
+ function writeFileWithDirs(target, content, mode) {
1447
+ mkdirSync2(dirname3(target), { recursive: true });
1448
+ writeFileSync2(target, content, "utf-8");
1449
+ if (mode !== undefined)
1450
+ chmodSync(target, mode);
1451
+ }
1452
+ function appendBlock(repoRoot, op) {
1453
+ const target = abs(repoRoot, op.path);
1454
+ const existing = readFileSync2(target, "utf-8");
1455
+ const sep2 = existing.endsWith(`
1456
+ `) ? `
1457
+ ` : `
1458
+
1459
+ `;
1460
+ writeFileSync2(target, `${existing}${sep2}${renderBlock(op)}
1461
+ `, "utf-8");
1462
+ if (op.mode !== undefined)
1463
+ chmodSync(target, op.mode);
1464
+ }
1465
+ function createHost(repoRoot, op) {
1466
+ const target = abs(repoRoot, op.path);
1467
+ writeFileWithDirs(target, `${op.hostPreamble ?? ""}${renderBlock(op)}
1468
+ `, op.mode);
1469
+ }
1470
+ async function applyInstall(plan, repoRoot, labels, onFilesRecorded) {
1471
+ const files = [];
1472
+ const blocks = [];
1473
+ const createdLabels = [];
1474
+ for (const e of plan.entries) {
1475
+ switch (e.kind) {
1476
+ case "create-file":
1477
+ if (e.action === "create") {
1478
+ writeFileWithDirs(abs(repoRoot, e.op.path), e.op.content, e.op.mode);
1479
+ files.push(e.op.path);
1480
+ } else if (e.action === "skip-owned") {
1481
+ files.push(e.op.path);
1482
+ }
1483
+ break;
1484
+ case "managed-block":
1485
+ if (e.action === "append")
1486
+ appendBlock(repoRoot, e.op);
1487
+ else if (e.action === "create-host")
1488
+ createHost(repoRoot, e.op);
1489
+ blocks.push({ path: e.op.path, marker: e.op.marker, comment: e.op.comment });
1490
+ break;
1491
+ case "print":
1492
+ process.stdout.write(`${e.op.message}
1493
+ `);
1494
+ break;
1495
+ }
1496
+ }
1497
+ const base = {
1498
+ version: MANAGED_MANIFEST_VERSION,
1499
+ files: dedupe(files),
1500
+ blocks: dedupeBlocks(blocks),
1501
+ labels: []
1502
+ };
1503
+ onFilesRecorded?.(base);
1504
+ for (const e of plan.entries) {
1505
+ if (e.kind !== "create-label")
1506
+ 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);
1510
+ }
1511
+ }
1512
+ return { ...base, labels: dedupe(createdLabels) };
1513
+ }
1514
+ function dedupe(xs) {
1515
+ return [...new Set(xs)];
1516
+ }
1517
+ function dedupeBlocks(bs) {
1518
+ const seen = new Set;
1519
+ const out = [];
1520
+ for (const b of bs) {
1521
+ const key = `${b.path}::${b.marker}`;
1522
+ if (seen.has(key))
1523
+ continue;
1524
+ seen.add(key);
1525
+ out.push(b);
1526
+ }
1527
+ return out;
1528
+ }
1529
+ function blockStripLeavesEmpty(body) {
1530
+ const rest = body.split(`
1531
+ `).filter((l) => l.trim().length > 0 && !l.startsWith("#!")).join("");
1532
+ return rest.length === 0;
1533
+ }
1534
+ function stripBlockFromContent(content, marker, comment) {
1535
+ const { begin, end } = markerLines(marker, comment);
1536
+ const lines = content.split(`
1537
+ `);
1538
+ const startIdx = lines.findIndex((l) => l.trim() === begin);
1539
+ if (startIdx === -1)
1540
+ return null;
1541
+ const endIdx = lines.findIndex((l, i) => i >= startIdx && l.trim() === end);
1542
+ if (endIdx === -1)
1543
+ return null;
1544
+ let from = startIdx;
1545
+ if (from > 0 && lines[from - 1]?.trim() === "")
1546
+ from -= 1;
1547
+ lines.splice(from, endIdx - from + 1);
1548
+ return lines.join(`
1549
+ `);
1550
+ }
1551
+ function planEject(manifest, repoRoot) {
1552
+ const actions = [];
1553
+ const escapes = [];
1554
+ for (const b of manifest.blocks) {
1555
+ const p = containedAbs(repoRoot, b.path);
1556
+ if (p === null) {
1557
+ escapes.push(b.path);
1558
+ continue;
1559
+ }
1560
+ if (!existsSync3(p)) {
1561
+ actions.push({
1562
+ kind: "strip-block",
1563
+ path: b.path,
1564
+ marker: b.marker,
1565
+ comment: b.comment,
1566
+ present: false,
1567
+ removesHost: false
1568
+ });
1569
+ continue;
1570
+ }
1571
+ const content = readFileSync2(p, "utf-8");
1572
+ const stripped = stripBlockFromContent(content, b.marker, b.comment);
1573
+ const removesHost = stripped !== null && blockStripLeavesEmpty(stripped);
1574
+ actions.push({
1575
+ kind: "strip-block",
1576
+ path: b.path,
1577
+ marker: b.marker,
1578
+ comment: b.comment,
1579
+ present: stripped !== null,
1580
+ removesHost
1581
+ });
1582
+ }
1583
+ for (const f of manifest.files) {
1584
+ const p = containedAbs(repoRoot, f);
1585
+ if (p === null) {
1586
+ escapes.push(f);
1587
+ continue;
1588
+ }
1589
+ actions.push({ kind: "delete-file", path: f, present: existsSync3(p) });
1590
+ }
1591
+ for (const name of manifest.labels) {
1592
+ actions.push({ kind: "report-label", name });
1593
+ }
1594
+ return { actions, escapes };
1595
+ }
1596
+ function renderEjectDiff(plan) {
1597
+ const lines = [];
1598
+ for (const a of plan.actions) {
1599
+ if (a.kind === "delete-file") {
1600
+ lines.push(a.present ? ` - delete ${a.path}` : ` · gone ${a.path} (already removed)`);
1601
+ } else if (a.kind === "strip-block") {
1602
+ if (!a.present)
1603
+ lines.push(` · gone ${a.path} (managed block already removed)`);
1604
+ else if (a.removesHost)
1605
+ lines.push(` - delete ${a.path} (vinaya-created host; nothing else remains)`);
1606
+ else
1607
+ lines.push(` ~ strip managed block from ${a.path} (your other lines are kept)`);
1608
+ } else {
1609
+ lines.push(` ⓘ label ${a.name} — remove manually if unused (a label may be in use elsewhere; never auto-deleted)`);
1610
+ }
1611
+ }
1612
+ return lines.join(`
1613
+ `);
1614
+ }
1615
+ function applyEject(plan, repoRoot) {
1616
+ for (const a of plan.actions) {
1617
+ if (a.kind === "strip-block") {
1618
+ if (!a.present)
1619
+ continue;
1620
+ const p = containedAbs(repoRoot, a.path);
1621
+ if (p === null)
1622
+ continue;
1623
+ const content = readFileSync2(p, "utf-8");
1624
+ const stripped = stripBlockFromContent(content, a.marker, a.comment);
1625
+ if (stripped === null)
1626
+ continue;
1627
+ if (blockStripLeavesEmpty(stripped))
1628
+ rmSync(p, { force: true });
1629
+ else
1630
+ writeFileSync2(p, stripped.endsWith(`
1631
+ `) ? stripped : `${stripped}
1632
+ `, "utf-8");
1633
+ } else if (a.kind === "delete-file") {
1634
+ const p = containedAbs(repoRoot, a.path);
1635
+ if (a.present && p !== null)
1636
+ rmSync(p, { force: true });
1637
+ }
1638
+ }
1639
+ return {
1640
+ removedLabelsToReport: plan.actions.filter((a) => a.kind === "report-label").map((a) => a.name)
1641
+ };
1642
+ }
1643
+
1644
+ // 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
+ function readVersion() {
1656
+ const pkg = JSON.parse(readFileSync3(join5(packageRoot(), "package.json"), "utf-8"));
1657
+ return pkg.version;
1658
+ }
1659
+ function realDeps() {
1660
+ return {
1661
+ detectRepo: detectGitRepo,
1662
+ ghAuthStatus,
1663
+ branchProtectionConfigured,
1664
+ hookDirFor: resolveHookDir,
1665
+ nodeVersion: () => process.version,
1666
+ bunVersion: () => typeof Bun === "undefined" ? null : Bun.version,
1667
+ packageVersion: readVersion
1668
+ };
1669
+ }
1670
+ var ok = (check, message) => ({ check, severity: "ok", message });
1671
+ var info = (check, message) => ({ check, severity: "info", message });
1672
+ var warn = (check, message) => ({ check, severity: "warn", message });
1673
+ var error = (check, message) => ({ check, severity: "error", message });
1674
+ function readConfig(repoRoot) {
1675
+ const p = join5(repoRoot, CONFIG_PATH);
1676
+ if (!existsSync4(p))
1677
+ return { kind: "missing" };
1678
+ let raw;
1679
+ try {
1680
+ raw = JSON.parse(readFileSync3(p, "utf-8"));
1681
+ } catch (err) {
1682
+ return { kind: "invalid", error: `invalid JSON: ${err.message}` };
1683
+ }
1684
+ const parsed = VinayaConfigSchema.safeParse(raw);
1685
+ if (!parsed.success) {
1686
+ return {
1687
+ kind: "invalid",
1688
+ error: parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")
1689
+ };
1690
+ }
1691
+ return { kind: "ok", config: parsed.data };
1692
+ }
1693
+ function labelForPath(path) {
1694
+ if (path === CONFIG_PATH)
1695
+ return "config";
1696
+ if (path === DOCTRINE_POINTER_PATH)
1697
+ return "doctrine-pointer";
1698
+ return "workflows";
1699
+ }
1700
+ function diagnoseInstall(repoRoot, ctx, manifest) {
1701
+ const findings = [];
1702
+ let hasDrift = false;
1703
+ const ownedFiles = new Set(manifest.files);
1704
+ const blockKey = (path, marker) => `${path}::${marker}`;
1705
+ const ownedBlocks = new Set(manifest.blocks.map((b) => blockKey(b.path, b.marker)));
1706
+ for (const op of buildInitOps(ctx)) {
1707
+ if (op.kind === "create-file") {
1708
+ const check = labelForPath(op.path);
1709
+ const abs2 = join5(repoRoot, op.path);
1710
+ const exists = existsSync4(abs2);
1711
+ const owned = ownedFiles.has(op.path);
1712
+ if (!exists) {
1713
+ 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
+ continue;
1715
+ }
1716
+ const content = readFileSync3(abs2, "utf-8");
1717
+ if (!owned) {
1718
+ 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
+ continue;
1720
+ }
1721
+ if (op.path === CONFIG_PATH) {
1722
+ findings.push(ok(check, `${op.path} present and vinaya-managed.`));
1723
+ continue;
1724
+ }
1725
+ if (content === op.content) {
1726
+ findings.push(ok(check, `${op.path} matches the installed package's generated content.`));
1727
+ } else {
1728
+ hasDrift = true;
1729
+ findings.push(warn(check, `${op.path} has drifted from the installed package's generated content — run \`vinaya upgrade\`.`));
1730
+ }
1731
+ } else if (op.kind === "managed-block") {
1732
+ const check = "hooks";
1733
+ const abs2 = join5(repoRoot, op.path);
1734
+ const owned = ownedBlocks.has(blockKey(op.path, op.marker));
1735
+ if (!existsSync4(abs2)) {
1736
+ 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
+ continue;
1738
+ }
1739
+ const content = readFileSync3(abs2, "utf-8");
1740
+ const { begin, end } = markerLines(op.marker, op.comment);
1741
+ const hasMarkers = content.includes(begin) && content.includes(end);
1742
+ if (!hasMarkers) {
1743
+ findings.push(owned ? error(check, `${op.path}'s vinaya-managed block is missing or corrupted — run \`vinaya upgrade\`.`) : info(check, `${op.path} exists with no vinaya-managed block.`));
1744
+ continue;
1745
+ }
1746
+ if (!owned) {
1747
+ findings.push(warn(check, `${op.path} has a vinaya-managed block that isn't recorded in the manifest.`));
1748
+ } else if (content.includes(renderBlock(op))) {
1749
+ findings.push(ok(check, `${op.path}'s managed block matches the installed package's generator.`));
1750
+ } else {
1751
+ hasDrift = true;
1752
+ findings.push(warn(check, `${op.path}'s managed block has drifted from the installed package's generator — run \`vinaya upgrade\`.`));
1753
+ }
1754
+ if (op.mode !== undefined && (statSync(abs2).mode & 73) === 0) {
1755
+ findings.push(error(check, `${op.path} is not executable.`));
1756
+ }
1757
+ }
1758
+ }
1759
+ return { findings, hasDrift };
1760
+ }
1761
+ function diagnoseCustomChecks(repoRoot, config) {
1762
+ const findings = [];
1763
+ 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}`));
1766
+ }
1767
+ return findings;
1768
+ }
1769
+ async function diagnoseEnvironment(deps, hasDrift) {
1770
+ const findings = [];
1771
+ const auth = await deps.ghAuthStatus();
1772
+ const detail = auth.detail.replace(/\s*\n+\s*/g, "; ");
1773
+ findings.push(auth.authenticated ? info("environment", `gh: authenticated (${detail})`) : warn("environment", `gh: ${detail}`));
1774
+ findings.push(info("environment", `node: ${deps.nodeVersion()}`));
1775
+ const bun = deps.bunVersion();
1776
+ if (bun)
1777
+ findings.push(info("environment", `bun: ${bun}`));
1778
+ const version = deps.packageVersion();
1779
+ findings.push(hasDrift ? warn("environment", `vinaya@${version} — installed artifacts have drifted from this version's generator. Run \`vinaya upgrade\`.`) : info("environment", `vinaya@${version} — installed artifacts match this version's generator.`));
1780
+ return findings;
1781
+ }
1782
+ async function diagnoseBranchProtection(deps, owner, repo) {
1783
+ const configured = await deps.branchProtectionConfigured(owner, repo);
1784
+ if (configured === true)
1785
+ return info("branch-protection", "main branch protection is configured.");
1786
+ if (configured === false) {
1787
+ return info("branch-protection", "main branch protection is not configured — vinaya never applies it; see `vinaya init`'s printed recommendation.");
1788
+ }
1789
+ return info("branch-protection", "main branch protection could not be determined (no gh auth, no remote, or a permission gap).");
1790
+ }
1791
+ function symbolFor(severity) {
1792
+ switch (severity) {
1793
+ case "ok":
1794
+ return "✓";
1795
+ case "info":
1796
+ return "·";
1797
+ case "warn":
1798
+ return "⚠";
1799
+ case "error":
1800
+ return "✗";
1801
+ }
1802
+ }
1803
+ function printReport(findings, healthy) {
1804
+ process.stdout.write(`vinaya doctor
1805
+
1806
+ `);
1807
+ for (const f of findings) {
1808
+ process.stdout.write(`${symbolFor(f.severity)} [${f.check}] ${f.message}
1809
+ `);
1810
+ }
1811
+ process.stdout.write(`
1812
+ ${healthy ? "Healthy — no findings." : "Findings above. vinaya doctor never mutates — nothing was changed."}
1813
+ `);
1814
+ }
1815
+ async function runDoctor(args, deps) {
1816
+ const jsonOutput = args.includes("--json");
1817
+ const repo = await deps.detectRepo();
1818
+ if (!repo) {
1819
+ console.error("Error: not a git repository. Run `vinaya doctor` from inside your repo.");
1820
+ return 1;
1821
+ }
1822
+ const configRead = readConfig(repo.repoRoot);
1823
+ const findings = [];
1824
+ let hasDrift = false;
1825
+ if (configRead.kind === "invalid") {
1826
+ findings.push(error("config", `vinaya.config.json is invalid — ${configRead.error}`));
1827
+ } else if (configRead.kind === "missing" || !configRead.config.managed) {
1828
+ findings.push(error("install", "vinaya is not initialized in this repo — run `vinaya init`."));
1829
+ } else {
1830
+ const manifest = configRead.config.managed;
1831
+ const hookDir = hookDirFromManifest(manifest, deps.hookDirFor(repo.repoRoot));
1832
+ const ctx = { owner: repo.owner, repo: repo.repo, hookDir };
1833
+ const install = diagnoseInstall(repo.repoRoot, ctx, manifest);
1834
+ findings.push(...install.findings);
1835
+ hasDrift = install.hasDrift;
1836
+ findings.push(...diagnoseCustomChecks(repo.repoRoot, configRead.config));
1837
+ }
1838
+ findings.push(...await diagnoseEnvironment(deps, hasDrift));
1839
+ findings.push(await diagnoseBranchProtection(deps, repo.owner, repo.repo));
1840
+ const healthy = findings.every((f) => f.severity === "ok" || f.severity === "info");
1841
+ if (jsonOutput) {
1842
+ printJson({ healthy, findings });
1843
+ } else {
1844
+ printReport(findings, healthy);
1845
+ }
1846
+ return healthy ? 0 : 1;
1847
+ }
1848
+ async function doctorCommand(args) {
1849
+ process.exit(await runDoctor(args, realDeps()));
1850
+ }
1851
+
1852
+ // src/commands/eject.ts
1853
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
1854
+ import { join as join6 } from "node:path";
1855
+
1856
+ // src/lib/prompt.ts
1857
+ var stdinBuffer = "";
1858
+ var stdinEnded = false;
1859
+ var pendingResolvers = [];
1860
+ var reading = false;
1861
+ function flushLines() {
1862
+ while (stdinBuffer.includes(`
1863
+ `) && pendingResolvers.length > 0) {
1864
+ const idx = stdinBuffer.indexOf(`
1865
+ `);
1866
+ const line = stdinBuffer.slice(0, idx).trim();
1867
+ stdinBuffer = stdinBuffer.slice(idx + 1);
1868
+ pendingResolvers.shift()?.(line);
1869
+ }
1870
+ if (stdinEnded && stdinBuffer.length > 0 && pendingResolvers.length > 0) {
1871
+ const line = stdinBuffer.trim();
1872
+ stdinBuffer = "";
1873
+ pendingResolvers.shift()?.(line);
1874
+ }
1875
+ if (stdinEnded) {
1876
+ while (pendingResolvers.length > 0)
1877
+ pendingResolvers.shift()?.("");
1878
+ }
1879
+ }
1880
+ function setupStdinReader() {
1881
+ if (reading)
1882
+ return;
1883
+ reading = true;
1884
+ process.stdin.setEncoding("utf-8");
1885
+ process.stdin.on("data", (chunk) => {
1886
+ stdinBuffer += chunk;
1887
+ flushLines();
1888
+ });
1889
+ process.stdin.on("end", () => {
1890
+ stdinEnded = true;
1891
+ flushLines();
1892
+ });
1893
+ process.stdin.resume();
1894
+ }
1895
+ function prompt(question) {
1896
+ process.stdout.write(question);
1897
+ setupStdinReader();
1898
+ if (stdinEnded && pendingResolvers.length === 0)
1899
+ return Promise.resolve("");
1900
+ return new Promise((resolve2) => {
1901
+ pendingResolvers.push(resolve2);
1902
+ if (stdinEnded)
1903
+ flushLines();
1904
+ });
1905
+ }
1906
+ async function promptYesNo(question, defaultYes = false) {
1907
+ const hint = defaultYes ? "Y/n" : "y/N";
1908
+ const answer = await prompt(`${question} (${hint}): `);
1909
+ if (answer.length === 0)
1910
+ return defaultYes;
1911
+ return answer.toLowerCase().startsWith("y");
1912
+ }
1913
+ function closeStdin() {
1914
+ if (reading) {
1915
+ process.stdin.destroy();
1916
+ reading = false;
1917
+ }
1918
+ }
1919
+
1920
+ // src/commands/eject.ts
1921
+ function realDeps2() {
1922
+ return {
1923
+ detectRepo: detectGitRepo,
1924
+ confirm: async (q) => {
1925
+ const yes = await promptYesNo(q, false);
1926
+ closeStdin();
1927
+ return yes;
1928
+ }
1929
+ };
1930
+ }
1931
+ function parse(args) {
1932
+ return { dryRun: args.includes("--dry-run"), yes: args.includes("--yes") };
1933
+ }
1934
+ function readManifest(repoRoot) {
1935
+ const p = join6(repoRoot, CONFIG_PATH);
1936
+ if (!existsSync5(p))
1937
+ return { kind: "none" };
1938
+ let raw;
1939
+ try {
1940
+ raw = JSON.parse(readFileSync4(p, "utf-8"));
1941
+ } catch (err) {
1942
+ return { kind: "orphan", reason: `vinaya.config.json is not valid JSON: ${err.message}` };
1943
+ }
1944
+ const parsed = VinayaConfigSchema.safeParse(raw);
1945
+ if (!parsed.success) {
1946
+ return {
1947
+ kind: "orphan",
1948
+ reason: `vinaya.config.json failed schema validation: ${parsed.error.issues.map((i) => i.message).join("; ")}`
1949
+ };
1950
+ }
1951
+ if (!parsed.data.managed) {
1952
+ return {
1953
+ kind: "orphan",
1954
+ reason: "vinaya.config.json has no `managed` ownership manifest — cannot determine what to remove."
1955
+ };
1956
+ }
1957
+ return { kind: "ok", manifest: parsed.data.managed };
1958
+ }
1959
+ async function runEject(args, deps) {
1960
+ const parsed = parse(args);
1961
+ const repo = await deps.detectRepo();
1962
+ if (!repo) {
1963
+ console.error("Error: not a git repository. Run `vinaya eject` from inside your repo.");
1964
+ return 1;
1965
+ }
1966
+ const read = readManifest(repo.repoRoot);
1967
+ if (read.kind === "none") {
1968
+ process.stdout.write(`Nothing to eject — this repo is not Vinaya-initialized.
1969
+ `);
1970
+ return 0;
1971
+ }
1972
+ if (read.kind === "orphan") {
1973
+ console.error(`Error: ${read.reason}`);
1974
+ console.error("Refusing to remove anything without a valid ownership record. Restore or fix vinaya.config.json, then re-run.");
1975
+ return 1;
1976
+ }
1977
+ const plan = planEject(read.manifest, repo.repoRoot);
1978
+ if (plan.escapes.length > 0) {
1979
+ console.error("Error: the ownership manifest records paths that resolve OUTSIDE this repo:");
1980
+ for (const p of plan.escapes)
1981
+ console.error(` ${p}`);
1982
+ console.error("Refusing to remove anything. This manifest is corrupt or hand-edited — fix vinaya.config.json, then re-run.");
1983
+ return 1;
1984
+ }
1985
+ process.stdout.write(`vinaya eject — the full diff of every removal:
1986
+
1987
+ `);
1988
+ process.stdout.write(`${renderEjectDiff(plan)}
1989
+ `);
1990
+ if (parsed.dryRun) {
1991
+ process.stdout.write(`
1992
+ --dry-run: nothing was removed.
1993
+ `);
1994
+ return 0;
1995
+ }
1996
+ if (!parsed.yes) {
1997
+ const ok2 = await deps.confirm("Remove these vinaya-installed artifacts?");
1998
+ if (!ok2) {
1999
+ process.stdout.write(`Aborted. Nothing was removed.
2000
+ `);
2001
+ return 0;
2002
+ }
2003
+ }
2004
+ const { removedLabelsToReport } = applyEject(plan, repo.repoRoot);
2005
+ process.stdout.write(`
2006
+ Vinaya ejected.
2007
+ `);
2008
+ if (removedLabelsToReport.length > 0) {
2009
+ process.stdout.write(`These labels were created by vinaya and left in place — remove them manually if unused:
2010
+ `);
2011
+ for (const name of removedLabelsToReport)
2012
+ process.stdout.write(` gh label delete ${name}
2013
+ `);
2014
+ }
2015
+ return 0;
2016
+ }
2017
+ async function ejectCommand(args) {
2018
+ const code = await runEject(args, realDeps2());
2019
+ process.exit(code);
2020
+ }
2021
+
2022
+ // 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() {
2026
+ return {
2027
+ detectRepo: detectGitRepo,
2028
+ checkGhAuth,
2029
+ labelGateway: ghLabelGateway,
2030
+ hookDirFor: resolveHookDir,
2031
+ customHooksPath,
2032
+ confirm: async (q) => {
2033
+ const yes = await promptYesNo(q, false);
2034
+ closeStdin();
2035
+ return yes;
2036
+ }
2037
+ };
2038
+ }
2039
+ function flags(args) {
2040
+ return { dryRun: args.includes("--dry-run"), yes: args.includes("--yes") };
2041
+ }
2042
+ var PRODUCT_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
2043
+ function readManifest2(repoRoot) {
2044
+ const p = join7(repoRoot, CONFIG_PATH);
2045
+ if (!existsSync6(p))
2046
+ return null;
2047
+ try {
2048
+ return VinayaConfigSchema.parse(JSON.parse(readFileSync5(p, "utf-8"))).managed ?? null;
2049
+ } catch {
2050
+ return null;
2051
+ }
2052
+ }
2053
+ 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)}
2057
+ `, "utf-8");
2058
+ }
2059
+ async function runInit(args, deps) {
2060
+ const { dryRun, yes } = flags(args);
2061
+ const repo = await deps.detectRepo();
2062
+ if (!repo) {
2063
+ console.error("Error: not a git repository. Run `vinaya init` from inside your repo.");
2064
+ return 1;
2065
+ }
2066
+ const custom = await deps.customHooksPath(repo.repoRoot);
2067
+ if (custom) {
2068
+ console.error(`Error: this repo routes git hooks through a custom core.hooksPath (${custom}).`);
2069
+ console.error("Vinaya will not guess at a non-standard hook layout. Set core.hooksPath to .husky or unset it, then re-run.");
2070
+ return 1;
2071
+ }
2072
+ const noRemote = !repo.owner || !repo.repo;
2073
+ if (noRemote) {
2074
+ console.warn("Warning: no `origin` remote (or it is not a GitHub URL) — skipping label creation. " + "Re-run `vinaya init` after adding a GitHub remote to create the recommended labels.");
2075
+ }
2076
+ if (!dryRun && !noRemote) {
2077
+ const authed = await deps.checkGhAuth();
2078
+ if (!authed) {
2079
+ console.error("Error: GitHub CLI is not authenticated. Run `gh auth login` first (or use --dry-run to preview).");
2080
+ return 1;
2081
+ }
2082
+ }
2083
+ const ctx = { owner: repo.owner, repo: repo.repo, hookDir: deps.hookDirFor(repo.repoRoot) };
2084
+ const allOps = buildInitOps(ctx);
2085
+ const ops = noRemote ? allOps.filter((op) => op.kind !== "create-label") : allOps;
2086
+ const owned = new Set(readManifest2(repo.repoRoot)?.files ?? []);
2087
+ const plan = planInstall(ops, repo.repoRoot, owned);
2088
+ process.stdout.write(`vinaya init — the full diff of every intended change:
2089
+
2090
+ `);
2091
+ process.stdout.write(`${renderInstallDiff(plan)}
2092
+ `);
2093
+ if (dryRun) {
2094
+ process.stdout.write(`--dry-run: nothing was written.
2095
+ `);
2096
+ return 0;
2097
+ }
2098
+ if (!yes) {
2099
+ const ok2 = await deps.confirm("Install these changes?");
2100
+ if (!ok2) {
2101
+ process.stdout.write(`Aborted. Nothing was written.
2102
+ `);
2103
+ return 0;
2104
+ }
2105
+ }
2106
+ const manifest = await applyInstall(plan, repo.repoRoot, deps.labelGateway(repo.repoRoot), (m) => writeManifest(repo.repoRoot, m));
2107
+ writeManifest(repo.repoRoot, manifest);
2108
+ process.stdout.write("\nVinaya installed. Next: run `vinaya demo break` to see a refusal-then-fix in action.\n");
2109
+ return 0;
2110
+ }
2111
+ async function runInitProduct(args, deps) {
2112
+ const { dryRun, yes } = flags(args);
2113
+ const name = args.filter((a) => !a.startsWith("--"))[0];
2114
+ if (!name) {
2115
+ console.error("Usage: vinaya init product <name>");
2116
+ return 2;
2117
+ }
2118
+ if (!PRODUCT_NAME_RE.test(name)) {
2119
+ console.error(`Error: invalid product name '${name}'. Use a lower-case slug: letters, digits, and hyphens (e.g. mobile, web-app).`);
2120
+ return 2;
2121
+ }
2122
+ const repo = await deps.detectRepo();
2123
+ if (!repo) {
2124
+ console.error("Error: not a git repository. Run `vinaya init product` from inside your repo.");
2125
+ return 1;
2126
+ }
2127
+ const existing = readManifest2(repo.repoRoot);
2128
+ if (!existing?.files.includes(CONFIG_PATH)) {
2129
+ console.error("Error: this repo is not Vinaya-initialized yet. Run `vinaya init` first.");
2130
+ return 1;
2131
+ }
2132
+ const noRemote = !repo.owner || !repo.repo;
2133
+ if (noRemote) {
2134
+ console.warn("Warning: no `origin` remote (or it is not a GitHub URL) — skipping label creation. " + `Re-run 'vinaya init product ${name}' after adding a GitHub remote to create it.`);
2135
+ process.stdout.write(`
2136
+ Nothing to scaffold for '${name}' without a GitHub remote.
2137
+ `);
2138
+ return 0;
2139
+ }
2140
+ const ops = buildInitProductOps(name);
2141
+ const plan = planInstall(ops, repo.repoRoot, new Set(existing.files));
2142
+ process.stdout.write(`vinaya init product ${name} — the full diff:
2143
+
2144
+ `);
2145
+ process.stdout.write(`${renderInstallDiff(plan)}
2146
+ `);
2147
+ if (dryRun) {
2148
+ process.stdout.write(`--dry-run: nothing was written.
2149
+ `);
2150
+ return 0;
2151
+ }
2152
+ if (!yes) {
2153
+ const ok2 = await deps.confirm(`Scaffold governed product area '${name}'?`);
2154
+ if (!ok2) {
2155
+ process.stdout.write(`Aborted. Nothing was written.
2156
+ `);
2157
+ return 0;
2158
+ }
2159
+ }
2160
+ const added = await applyInstall(plan, repo.repoRoot, deps.labelGateway(repo.repoRoot));
2161
+ const merged = {
2162
+ version: existing.version,
2163
+ files: [...new Set([...existing.files, ...added.files])],
2164
+ blocks: dedupeBlocks2([...existing.blocks, ...added.blocks]),
2165
+ labels: [...new Set([...existing.labels, ...added.labels])]
2166
+ };
2167
+ writeManifest(repo.repoRoot, merged);
2168
+ process.stdout.write(`
2169
+ Governed product area '${name}' scaffolded.
2170
+ `);
2171
+ return 0;
2172
+ }
2173
+ function dedupeBlocks2(bs) {
2174
+ const seen = new Set;
2175
+ return bs.filter((b) => {
2176
+ const k = `${b.path}::${b.marker}`;
2177
+ if (seen.has(k))
2178
+ return false;
2179
+ seen.add(k);
2180
+ return true;
2181
+ });
2182
+ }
2183
+ async function initCommand(args) {
2184
+ process.exit(await runInit(args, realDeps3()));
2185
+ }
2186
+ async function initProductCommand(args) {
2187
+ process.exit(await runInitProduct(args, realDeps3()));
2188
+ }
2189
+
2190
+ // src/commands/issue.ts
2191
+ import { execFileSync as execFileSync3 } from "node:child_process";
2192
+
2193
+ // 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";
2196
+ import { tmpdir } from "node:os";
2197
+ import { join as join8 } from "node:path";
2198
+ class ForgeArgError extends Error {
2199
+ }
2200
+ function locateBody(args) {
2201
+ for (let i = 0;i < args.length; i++) {
2202
+ const a = args[i];
2203
+ if (a === "--body-file" || a === "-F") {
2204
+ const p = args[i + 1];
2205
+ if (!p)
2206
+ throw new ForgeArgError("`--body-file` was given with no path.");
2207
+ return { body: readFileSync6(p, "utf8"), source: { kind: "file", argIndex: i + 1, inlineForm: false } };
2208
+ }
2209
+ if (a.startsWith("--body-file=")) {
2210
+ const p = a.slice("--body-file=".length);
2211
+ if (!p)
2212
+ throw new ForgeArgError("`--body-file=` was given with no path.");
2213
+ return { body: readFileSync6(p, "utf8"), source: { kind: "file", argIndex: i, inlineForm: true } };
2214
+ }
2215
+ if (a === "--body" || a === "-b") {
2216
+ const v = args[i + 1];
2217
+ if (v === undefined)
2218
+ throw new ForgeArgError("`--body` was given with no value.");
2219
+ return { body: v, source: { kind: "inline" } };
2220
+ }
2221
+ if (a.startsWith("--body="))
2222
+ return { body: a.slice("--body=".length), source: { kind: "inline" } };
2223
+ }
2224
+ return null;
2225
+ }
2226
+ function resolveShippableArgs(args, bodyResult) {
2227
+ if (bodyResult?.source.kind !== "file") {
2228
+ return { finalArgs: args, cleanup: () => {} };
2229
+ }
2230
+ const dir = mkdtempSync(join8(tmpdir(), "vinaya-forge-body-"));
2231
+ const tempPath = join8(dir, "body.md");
2232
+ writeFileSync4(tempPath, bodyResult.body, "utf8");
2233
+ const finalArgs = [...args];
2234
+ const { argIndex, inlineForm } = bodyResult.source;
2235
+ finalArgs[argIndex] = inlineForm ? `--body-file=${tempPath}` : tempPath;
2236
+ return { finalArgs, cleanup: () => rmSync2(dir, { recursive: true, force: true }) };
2237
+ }
2238
+ function extractTitle(args) {
2239
+ for (let i = 0;i < args.length; i++) {
2240
+ const a = args[i];
2241
+ if (a === "--title" || a === "-t")
2242
+ return args[i + 1] ?? null;
2243
+ if (a.startsWith("--title="))
2244
+ return a.slice("--title=".length);
2245
+ }
2246
+ return null;
2247
+ }
2248
+ function extractLabels(args) {
2249
+ const labels = [];
2250
+ const push = (v) => {
2251
+ if (v) {
2252
+ for (const s of v.split(",").map((x) => x.trim()))
2253
+ if (s)
2254
+ labels.push(s);
2255
+ }
2256
+ };
2257
+ for (let i = 0;i < args.length; i++) {
2258
+ const a = args[i];
2259
+ if (a === "--label" || a === "-l" || a === "--add-label")
2260
+ push(args[i + 1]);
2261
+ else if (a.startsWith("--label="))
2262
+ push(a.slice("--label=".length));
2263
+ else if (a.startsWith("--add-label="))
2264
+ push(a.slice("--add-label=".length));
2265
+ }
2266
+ return labels;
2267
+ }
2268
+ function makeCheckError(check, message, agentRecoveryPrompt) {
2269
+ return {
2270
+ schema: CHECK_SCHEMA_VERSION,
2271
+ check,
2272
+ severity: "error",
2273
+ message,
2274
+ agent_recovery_prompt: agentRecoveryPrompt
2275
+ };
2276
+ }
2277
+ function refuse(errors) {
2278
+ for (const e of errors)
2279
+ emitCheckError(e);
2280
+ process.exit(1);
2281
+ }
2282
+ function resolveSections(kind, retryCommand) {
2283
+ const result = loadConfigChecked();
2284
+ if (!result.ok) {
2285
+ refuse([
2286
+ makeCheckError("config", `${result.path}: invalid vinaya.config.json — ${result.error}`, `Fix the invalid key/value named above in ${result.path}, then re-run \`${retryCommand}\`.`)
2287
+ ]);
2288
+ }
2289
+ return result.config?.briefSchema?.[kind]?.sections ?? [];
2290
+ }
2291
+ var CHECK_BRIEF_SCHEMA = "brief-schema";
2292
+ var CHECK_FORGE_TITLE = "forge-title";
2293
+ function runBuiltin(name, input) {
2294
+ const { body, changedFiles: changedFiles2 } = input;
2295
+ const table = {
2296
+ tier: () => checkTierField(body, readTierFromPrBody),
2297
+ testPlan: () => checkTestPlan(body),
2298
+ testPlanExclusivity: () => checkTestPlanExclusivity(body),
2299
+ principalPlaceholder: () => checkPrincipalPlaceholder(body),
2300
+ surfaceMap: () => checkSurfaceMap(body),
2301
+ docUpdateList: () => checkDocUpdateList(body),
2302
+ worktreeStep0: () => checkWorktreeStep0(body),
2303
+ stopConditions: () => checkStopConditions(body),
2304
+ autonomyClause: () => checkAutonomyClause(body),
2305
+ project: () => checkProjectField(body),
2306
+ for: () => checkForField(body),
2307
+ closesN: () => checkClosesN(body),
2308
+ premiseCoverage: () => checkPremiseCoverage(body, changedFiles2),
2309
+ issueRationale: () => checkIssueRationale(body)
2310
+ };
2311
+ return table[name]().errors;
2312
+ }
2313
+ var BUILTIN_RECOVERY = {
2314
+ tier: "Add a `Tier: 0`, `Tier: 1`, or `Tier: 3` field to the body header block (before the first `##` heading), then re-run `{cmd}`.",
2315
+ testPlan: "Add a Test Plan section with at least one `**[agent]**` or `**[principal]**` checklist item (or the `Test Plan: unit-tests-only` sentinel), then re-run `{cmd}`.",
2316
+ testPlanExclusivity: "Remove either the `Test Plan: unit-tests-only` sentinel or the tagged `- [ ]` checklist items — declare one form, not both — then re-run `{cmd}`.",
2317
+ principalPlaceholder: 'Delete the `**[principal]**` "None" placeholder checklist item entirely (an untickable box blocks the merge gate forever), then re-run `{cmd}`.',
2318
+ surfaceMap: "Add a `## Technical surface map` section listing the files this change touches, then re-run `{cmd}`.",
2319
+ docUpdateList: "Add a `## Documentation-update list` section, then re-run `{cmd}`.",
2320
+ worktreeStep0: "Add the `git worktree add …` Step 0 command to the body, then re-run `{cmd}`.",
2321
+ stopConditions: "Add a `## Stop conditions` section, then re-run `{cmd}`.",
2322
+ autonomyClause: 'Add the standing autonomy clause ("Do not stop to ask clarifying questions…") to the body, then re-run `{cmd}`.',
2323
+ project: "Add a `Project: <name>` field to the body header block (before the first `##` heading), then re-run `{cmd}`.",
2324
+ for: "Add a `For: <model + environment>` field to the body header block (before the first `##` heading), then re-run `{cmd}`.",
2325
+ closesN: "Add a `Closes #<N>` reference naming the task Issue to the body, then re-run `{cmd}`.",
2326
+ premiseCoverage: "Add a `Premise:` assertion whose path matches a file this change touches, then re-run `{cmd}`.",
2327
+ issueRationale: "Add the missing Planner-rationale field named above (every task Issue carries all eight fields), then re-run `{cmd}`."
2328
+ };
2329
+ function escapeRegExp(literal) {
2330
+ return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2331
+ }
2332
+ function runCustomSection(section, body) {
2333
+ if ("heading" in section) {
2334
+ const re = new RegExp(`^#{1,6}\\s+.*${escapeRegExp(section.heading)}`, "im");
2335
+ if (re.test(body))
2336
+ return null;
2337
+ const name2 = section.name ?? section.heading;
2338
+ return `brief-schema ${name2}: no "${section.heading}" heading found in the body.`;
2339
+ }
2340
+ if ("field" in section) {
2341
+ const re = new RegExp(`^(?:\\*\\*)?\\s*${escapeRegExp(section.field)}\\s*(?:\\*\\*)?\\s*:`, "im");
2342
+ if (re.test(body))
2343
+ return null;
2344
+ const name2 = section.name ?? section.field;
2345
+ return `brief-schema ${name2}: no "${section.field}:" field found in the body.`;
2346
+ }
2347
+ if (body.toLowerCase().includes(section.phrase.toLowerCase()))
2348
+ return null;
2349
+ const name = section.name ?? section.phrase;
2350
+ return `brief-schema ${name}: required phrase "${section.phrase}" not found in the body.`;
2351
+ }
2352
+ function customRecovery(section, retryCommand) {
2353
+ const cmd = `\`${retryCommand}\``;
2354
+ if ("heading" in section) {
2355
+ return `Add a \`## ${section.heading}\` heading (with its section) to the body, then re-run ${cmd}.`;
2356
+ }
2357
+ if ("field" in section) {
2358
+ return `Add a \`${section.field}: <value>\` field to the body, then re-run ${cmd}.`;
2359
+ }
2360
+ return `Add the required phrase "${section.phrase}" to the body, then re-run ${cmd}.`;
2361
+ }
2362
+ function validateForgeWrite(input) {
2363
+ const errors = [];
2364
+ if (input.title !== null) {
2365
+ const t = checkForgeTitle(input.title);
2366
+ if (t.status === "fail") {
2367
+ for (const message of t.errors) {
2368
+ errors.push(makeCheckError(CHECK_FORGE_TITLE, message, `Rewrite the \`--title\` to match the forge-title grammar (\`Type: description\` / \`Type(scope): description\`, or \`[tranche] id — description\`), then re-run \`${input.retryCommand}\`.`));
2369
+ }
2370
+ }
2371
+ }
2372
+ for (const section of input.sections) {
2373
+ if ("builtin" in section) {
2374
+ const recovery = BUILTIN_RECOVERY[section.builtin].replace("{cmd}", input.retryCommand);
2375
+ for (const message of runBuiltin(section.builtin, input)) {
2376
+ errors.push(makeCheckError(CHECK_BRIEF_SCHEMA, message, recovery));
2377
+ }
2378
+ } else {
2379
+ const message = runCustomSection(section, input.body);
2380
+ if (message !== null) {
2381
+ errors.push(makeCheckError(CHECK_BRIEF_SCHEMA, message, customRecovery(section, input.retryCommand)));
2382
+ }
2383
+ }
2384
+ }
2385
+ return errors;
2386
+ }
2387
+
2388
+ // src/commands/issue.ts
2389
+ var RETRY_CREATE = "vinaya issue create --validate-only …";
2390
+ var RETRY_EDIT = "vinaya issue edit <n> --validate-only …";
2391
+ function locateBodyOrRefuse(ghArgs, retryCommand) {
2392
+ try {
2393
+ return locateBody(ghArgs);
2394
+ } catch (e) {
2395
+ if (e instanceof ForgeArgError) {
2396
+ refuse([makeCheckError("forge-args", e.message, `Fix the invocation, then re-run \`${retryCommand}\`.`)]);
2397
+ }
2398
+ throw e;
2399
+ }
2400
+ }
2401
+ function fetchForgeLabels(issueRef) {
2402
+ let out;
2403
+ try {
2404
+ out = execFileSync3("gh", ["issue", "view", issueRef, "--json", "labels"], {
2405
+ encoding: "utf8",
2406
+ stdio: ["ignore", "pipe", "pipe"]
2407
+ });
2408
+ } catch {
2409
+ refuse([
2410
+ makeCheckError("forge-fetch", `Could not fetch Issue ${issueRef}'s labels from the forge (\`gh issue view\`) — the rationale gate cannot decide whether it applies.`, `Check \`gh auth status\` and network, then re-run \`${RETRY_EDIT}\`. The edit is refused rather than passed through unvalidated.`)
2411
+ ]);
2412
+ }
2413
+ try {
2414
+ return JSON.parse(out).labels.map((l) => l.name);
2415
+ } catch {
2416
+ refuse([
2417
+ makeCheckError("forge-fetch", `Could not parse \`gh issue view ${issueRef} --json labels\` output.`, `Re-run \`${RETRY_EDIT}\`; the edit is refused rather than passed through unvalidated.`)
2418
+ ]);
2419
+ }
2420
+ }
2421
+ function reportPass(json, command) {
2422
+ if (json) {
2423
+ printJson({ validated: true, written: false, command });
2424
+ } else {
2425
+ process.stdout.write(`✓ all brief-schema gates PASS — nothing written (--validate-only).
2426
+ `);
2427
+ }
2428
+ }
2429
+ function runGhWrite(ghCmd, ghArgs, bodyResult, json) {
2430
+ const { finalArgs, cleanup } = resolveShippableArgs(ghArgs, bodyResult);
2431
+ try {
2432
+ const out = execFileSync3("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
2433
+ const url = out.trim();
2434
+ if (json)
2435
+ printJson({ validated: true, written: true, url });
2436
+ else if (url)
2437
+ process.stdout.write(`${url}
2438
+ `);
2439
+ } finally {
2440
+ cleanup();
2441
+ }
2442
+ }
2443
+ function validateTaskIssue(body, title, retryCommand) {
2444
+ if (body === null) {
2445
+ refuse([
2446
+ makeCheckError("forge-args", "A task Issue (a `vinaya/tranche:*` label) requires a `--body-file <path>` so the rationale gate can validate it.", `Add \`--body-file <path>\`, then re-run \`${retryCommand}\`.`)
2447
+ ]);
2448
+ }
2449
+ const sections = resolveSections("issue", retryCommand);
2450
+ const errors = validateForgeWrite({
2451
+ body,
2452
+ title,
2453
+ sections,
2454
+ changedFiles: [],
2455
+ retryCommand
2456
+ });
2457
+ if (errors.length > 0)
2458
+ refuse(errors);
2459
+ }
2460
+ function issueCreateCommand(args) {
2461
+ const json = args.includes("--json");
2462
+ const validateOnly = args.includes("--validate-only");
2463
+ const ghArgs = args.filter((a) => a !== "--json" && a !== "--validate-only");
2464
+ const bodyResult = locateBodyOrRefuse(ghArgs, RETRY_CREATE);
2465
+ const body = bodyResult?.body ?? null;
2466
+ const title = extractTitle(ghArgs);
2467
+ const labels = extractLabels(ghArgs);
2468
+ if (isTaskIssueLabelSet(labels)) {
2469
+ validateTaskIssue(body, title, RETRY_CREATE);
2470
+ }
2471
+ if (validateOnly) {
2472
+ reportPass(json, "issue create");
2473
+ return;
2474
+ }
2475
+ runGhWrite(["issue", "create"], ghArgs, bodyResult, json);
2476
+ }
2477
+ function issueEditCommand(args) {
2478
+ const json = args.includes("--json");
2479
+ const validateOnly = args.includes("--validate-only");
2480
+ const rest = args.filter((a) => a !== "--json" && a !== "--validate-only");
2481
+ const issueRef = rest[0];
2482
+ if (!issueRef || issueRef.startsWith("-")) {
2483
+ refuse([
2484
+ makeCheckError("forge-args", "`issue edit` requires the target Issue number/URL as the first argument.", "Pass the Issue number, e.g. `vinaya issue edit 123 --body-file <path>`.")
2485
+ ]);
2486
+ }
2487
+ const ghArgs = rest.slice(1);
2488
+ const bodyResult = locateBodyOrRefuse(ghArgs, RETRY_EDIT);
2489
+ const body = bodyResult?.body ?? null;
2490
+ const title = extractTitle(ghArgs);
2491
+ const labels = [...new Set([...fetchForgeLabels(issueRef), ...extractLabels(ghArgs)])];
2492
+ if (isTaskIssueLabelSet(labels)) {
2493
+ validateTaskIssue(body, title, RETRY_EDIT);
2494
+ }
2495
+ if (validateOnly) {
2496
+ reportPass(json, "issue edit");
2497
+ return;
2498
+ }
2499
+ runGhWrite(["issue", "edit", issueRef], ghArgs, bodyResult, json);
2500
+ }
2501
+
2502
+ // 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");
2518
+ var VALID_NAME = /^[a-z0-9][a-z0-9-]*$/;
2519
+ function newCheckCommand(args) {
2520
+ const name = args[0];
2521
+ if (!name || !VALID_NAME.test(name)) {
2522
+ console.error("Usage: vinaya new check <name> (name: lowercase letters, digits, hyphens)");
2523
+ process.exit(2);
2524
+ }
2525
+ const scriptsDir = join9(process.cwd(), CHECKS_DIR);
2526
+ if (!existsSync7(scriptsDir))
2527
+ mkdirSync3(scriptsDir, { recursive: true });
2528
+ const targetPath = join9(scriptsDir, `${name}.ts`);
2529
+ if (existsSync7(targetPath)) {
2530
+ console.error(`Error: ${targetPath} already exists.`);
2531
+ process.exit(1);
2532
+ }
2533
+ const template = readFileSync7(TEMPLATE_PATH, "utf-8");
2534
+ const contents = template.split("{{CHECK_NAME}}").join(name);
2535
+ writeFileSync5(targetPath, contents, "utf-8");
2536
+ chmodSync2(targetPath, 493);
2537
+ const relPath = join9(CHECKS_DIR, `${name}.ts`);
2538
+ const registration = JSON.stringify({ checks: { [name]: { run: `./${relPath}`, scope: "diff" } } }, null, 2);
2539
+ process.stdout.write(`Created ${relPath}
2540
+
2541
+ Register it in vinaya.config.json:
2542
+ ${registration}
2543
+ `);
2544
+ }
2545
+
2546
+ // src/commands/pr.ts
2547
+ import { execFileSync as execFileSync4 } from "node:child_process";
2548
+ var RETRY_CREATE2 = "vinaya pr create --validate-only …";
2549
+ var RETRY_EDIT2 = "vinaya pr edit <n> --validate-only …";
2550
+ function git2(args) {
2551
+ try {
2552
+ return execFileSync4("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
2553
+ } catch {
2554
+ return "";
2555
+ }
2556
+ }
2557
+ function localChangedFiles() {
2558
+ const base = process.env.BASE_SHA || "origin/main";
2559
+ let out = git2(["diff", "--name-only", `${base}...HEAD`]);
2560
+ if (!out)
2561
+ out = git2(["diff", "--name-only", "main...HEAD"]);
2562
+ return out.split(`
2563
+ `).map((s) => s.trim()).filter(Boolean);
2564
+ }
2565
+ function locateBodyOrRefuse2(ghArgs, retryCommand) {
2566
+ try {
2567
+ return locateBody(ghArgs);
2568
+ } catch (e) {
2569
+ if (e instanceof ForgeArgError) {
2570
+ refuse([makeCheckError("forge-args", e.message, `Fix the invocation, then re-run \`${retryCommand}\`.`)]);
2571
+ }
2572
+ throw e;
2573
+ }
2574
+ }
2575
+ function reportPass2(json, command) {
2576
+ if (json) {
2577
+ printJson({ validated: true, written: false, command });
2578
+ } else {
2579
+ process.stdout.write(`✓ all brief-schema gates PASS — nothing written (--validate-only).
2580
+ `);
2581
+ }
2582
+ }
2583
+ function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
2584
+ const { finalArgs, cleanup } = resolveShippableArgs(ghArgs, bodyResult);
2585
+ try {
2586
+ const out = execFileSync4("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
2587
+ const url = out.trim();
2588
+ if (json)
2589
+ printJson({ validated: true, written: true, url });
2590
+ else if (url)
2591
+ process.stdout.write(`${url}
2592
+ `);
2593
+ } finally {
2594
+ cleanup();
2595
+ }
2596
+ }
2597
+ function fetchPrForgeContext(prRef) {
2598
+ let viewOut;
2599
+ try {
2600
+ viewOut = execFileSync4("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
2601
+ encoding: "utf8",
2602
+ stdio: ["ignore", "pipe", "pipe"]
2603
+ });
2604
+ } catch {
2605
+ refuse([
2606
+ makeCheckError("forge-fetch", `Could not fetch PR ${prRef} from the forge (\`gh pr view\`) — the brief-schema gate cannot resolve the target PR's state.`, `Check \`gh auth status\` and network, then re-run \`${RETRY_EDIT2}\`. The edit is refused rather than validated against the local checkout.`)
2607
+ ]);
2608
+ }
2609
+ let parsed;
2610
+ try {
2611
+ parsed = JSON.parse(viewOut);
2612
+ } catch {
2613
+ refuse([
2614
+ makeCheckError("forge-fetch", `Could not parse \`gh pr view ${prRef}\` output.`, `Re-run \`${RETRY_EDIT2}\`; the edit is refused rather than validated against the local checkout.`)
2615
+ ]);
2616
+ }
2617
+ if (!parsed.headRefName) {
2618
+ refuse([
2619
+ makeCheckError("forge-fetch", `\`gh pr view ${prRef}\` returned no head branch — the target PR could not be resolved.`, `Confirm PR ${prRef} exists, then re-run \`${RETRY_EDIT2}\`.`)
2620
+ ]);
2621
+ }
2622
+ const changedFiles2 = (parsed.files ?? []).map((f) => f.path);
2623
+ return { changedFiles: changedFiles2 };
2624
+ }
2625
+ function prCreateCommand(args) {
2626
+ const json = args.includes("--json");
2627
+ const validateOnly = args.includes("--validate-only");
2628
+ const ghArgs = args.filter((a) => a !== "--json" && a !== "--validate-only");
2629
+ const bodyResult = locateBodyOrRefuse2(ghArgs, RETRY_CREATE2);
2630
+ const body = bodyResult?.body ?? null;
2631
+ const title = extractTitle(ghArgs);
2632
+ if (body === null) {
2633
+ refuse([
2634
+ makeCheckError("forge-args", "No `--body-file <path>` (or `--body`) argument found — the brief-schema gate needs the PR body to validate it.", `Add \`--body-file <path>\` (or \`--body\`), then re-run \`${RETRY_CREATE2}\`.`)
2635
+ ]);
2636
+ }
2637
+ const sections = resolveSections("pr", RETRY_CREATE2);
2638
+ const changedFiles2 = localChangedFiles();
2639
+ const errors = validateForgeWrite({
2640
+ body,
2641
+ title,
2642
+ sections,
2643
+ changedFiles: changedFiles2,
2644
+ retryCommand: RETRY_CREATE2
2645
+ });
2646
+ if (errors.length > 0)
2647
+ refuse(errors);
2648
+ if (validateOnly) {
2649
+ reportPass2(json, "pr create");
2650
+ return;
2651
+ }
2652
+ runGhWrite2(["pr", "create"], ghArgs, bodyResult, json);
2653
+ }
2654
+ function prEditCommand(args) {
2655
+ const json = args.includes("--json");
2656
+ const validateOnly = args.includes("--validate-only");
2657
+ const rest = args.filter((a) => a !== "--json" && a !== "--validate-only");
2658
+ const prRef = rest[0];
2659
+ if (!prRef || prRef.startsWith("-")) {
2660
+ refuse([
2661
+ makeCheckError("forge-args", "`pr edit` requires the target PR number/URL as the first argument.", "Pass the PR number, e.g. `vinaya pr edit 123 --body-file <path>`.")
2662
+ ]);
2663
+ }
2664
+ const ghArgs = rest.slice(1);
2665
+ const bodyResult = locateBodyOrRefuse2(ghArgs, RETRY_EDIT2);
2666
+ const body = bodyResult?.body ?? null;
2667
+ const title = extractTitle(ghArgs);
2668
+ if (body === null && title === null) {
2669
+ refuse([
2670
+ makeCheckError("forge-args", "`pr edit` with neither `--body-file`/`--body` nor `--title` — nothing to validate or change.", `Pass a \`--body-file\`/\`--body\` or a \`--title\`, then re-run \`${RETRY_EDIT2}\`.`)
2671
+ ]);
2672
+ }
2673
+ const sections = resolveSections("pr", RETRY_EDIT2);
2674
+ let changedFiles2 = [];
2675
+ if (body !== null) {
2676
+ const ctx = fetchPrForgeContext(prRef);
2677
+ changedFiles2 = ctx.changedFiles;
2678
+ }
2679
+ const errors = validateForgeWrite({
2680
+ body: body ?? "",
2681
+ title,
2682
+ sections: body === null ? [] : sections,
2683
+ changedFiles: changedFiles2,
2684
+ retryCommand: RETRY_EDIT2
2685
+ });
2686
+ if (errors.length > 0)
2687
+ refuse(errors);
2688
+ if (validateOnly) {
2689
+ reportPass2(json, "pr edit");
2690
+ return;
2691
+ }
2692
+ runGhWrite2(["pr", "edit", prRef], ghArgs, bodyResult, json);
2693
+ }
2694
+
2695
+ // src/commands/studio.ts
2696
+ 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";
2699
+ function resolveStudioTarget(cwd) {
2700
+ let dir = cwd;
2701
+ for (;; ) {
2702
+ const webDir = join10(dir, "apps", "vinaya", "web");
2703
+ const pkgPath = join10(webDir, "package.json");
2704
+ if (existsSync8(pkgPath)) {
2705
+ try {
2706
+ const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
2707
+ if (pkg.name === "@atta/vinaya-web") {
2708
+ return { kind: "workspace", webDir };
2709
+ }
2710
+ } catch {}
2711
+ }
2712
+ const parent = dirname6(dir);
2713
+ if (parent === dir)
2714
+ break;
2715
+ dir = parent;
2716
+ }
2717
+ return { kind: "missing" };
2718
+ }
2719
+ function spawnDev(webDir, args) {
2720
+ return new Promise((resolve2) => {
2721
+ const child = spawn2("bun", ["run", "dev", ...args], { cwd: webDir, stdio: "inherit" });
2722
+ child.on("exit", (code) => resolve2(code ?? 0));
2723
+ });
2724
+ }
2725
+ async function runStudio(cwd, args) {
2726
+ const target = resolveStudioTarget(cwd);
2727
+ switch (target.kind) {
2728
+ case "workspace":
2729
+ return spawnDev(target.webDir, args);
2730
+ case "package":
2731
+ return spawnDev(target.packageDir, args);
2732
+ case "missing":
2733
+ console.error("Vinaya Studio isn't available here — install '@vinaya/studio' to run it standalone.");
2734
+ return 1;
2735
+ }
2736
+ }
2737
+
2738
+ // 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() {
2742
+ return {
2743
+ detectRepo: detectGitRepo,
2744
+ hookDirFor: resolveHookDir,
2745
+ confirm: async (q) => {
2746
+ const yes = await promptYesNo(q, false);
2747
+ closeStdin();
2748
+ return yes;
2749
+ }
2750
+ };
2751
+ }
2752
+ function flags2(args) {
2753
+ return { dryRun: args.includes("--dry-run"), yes: args.includes("--yes") };
2754
+ }
2755
+ function readManifest3(repoRoot) {
2756
+ const p = join11(repoRoot, CONFIG_PATH);
2757
+ if (!existsSync9(p))
2758
+ return { kind: "missing" };
2759
+ let raw;
2760
+ try {
2761
+ raw = JSON.parse(readFileSync9(p, "utf-8"));
2762
+ } catch (err) {
2763
+ return { kind: "invalid", error: `invalid JSON: ${err.message}` };
2764
+ }
2765
+ const parsed = VinayaConfigSchema.safeParse(raw);
2766
+ if (!parsed.success) {
2767
+ return {
2768
+ kind: "invalid",
2769
+ error: parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")
2770
+ };
2771
+ }
2772
+ if (!parsed.data.managed)
2773
+ return { kind: "not-initialized" };
2774
+ return { kind: "ok", manifest: parsed.data.managed };
2775
+ }
2776
+ function writeManifestVersion(repoRoot, manifest) {
2777
+ const configAbs = join11(repoRoot, CONFIG_PATH);
2778
+ const seed = JSON.parse(readFileSync9(configAbs, "utf-8"));
2779
+ const updated = { ...manifest, version: MANAGED_MANIFEST_VERSION };
2780
+ writeFileSync6(configAbs, `${JSON.stringify({ ...seed, managed: updated }, null, 2)}
2781
+ `, "utf-8");
2782
+ }
2783
+ function planUpgrade(ops, repoRoot, manifest) {
2784
+ const entries = [];
2785
+ let hasChanges = false;
2786
+ const ownedFiles = new Set(manifest.files);
2787
+ const blockKey = (path, marker) => `${path}::${marker}`;
2788
+ const ownedBlocks = new Set(manifest.blocks.map((b) => blockKey(b.path, b.marker)));
2789
+ for (const op of ops) {
2790
+ if (op.kind === "create-file") {
2791
+ const abs2 = join11(repoRoot, op.path);
2792
+ const exists = existsSync9(abs2);
2793
+ const owned = ownedFiles.has(op.path);
2794
+ let action;
2795
+ if (op.path === CONFIG_PATH) {
2796
+ action = "keep";
2797
+ } else if (!owned) {
2798
+ action = exists ? "refuse-foreign" : "not-installed";
2799
+ } else if (!exists) {
2800
+ action = "recreate";
2801
+ hasChanges = true;
2802
+ } else if (readFileSync9(abs2, "utf-8") !== op.content) {
2803
+ action = "regenerate";
2804
+ hasChanges = true;
2805
+ } else {
2806
+ action = "current";
2807
+ }
2808
+ entries.push({ kind: "create-file", op, action });
2809
+ } else if (op.kind === "managed-block") {
2810
+ const abs2 = join11(repoRoot, op.path);
2811
+ const owned = ownedBlocks.has(blockKey(op.path, op.marker));
2812
+ let action;
2813
+ if (!owned) {
2814
+ action = "not-installed";
2815
+ } else if (!existsSync9(abs2)) {
2816
+ action = "recreate-host";
2817
+ hasChanges = true;
2818
+ } else {
2819
+ const content = readFileSync9(abs2, "utf-8");
2820
+ const { begin, end } = markerLines(op.marker, op.comment);
2821
+ if (!(content.includes(begin) && content.includes(end))) {
2822
+ action = "recreate-append";
2823
+ hasChanges = true;
2824
+ } else if (!content.includes(renderBlock(op))) {
2825
+ action = "regenerate-block";
2826
+ hasChanges = true;
2827
+ } else {
2828
+ action = "current";
2829
+ }
2830
+ }
2831
+ entries.push({ kind: "managed-block", op, action });
2832
+ }
2833
+ }
2834
+ const versionMigration = manifest.version === MANAGED_MANIFEST_VERSION ? null : { from: manifest.version, to: MANAGED_MANIFEST_VERSION };
2835
+ return { entries, versionMigration, hasChanges: hasChanges || versionMigration !== null };
2836
+ }
2837
+ function renderUpgradeDiff(plan) {
2838
+ const lines = [];
2839
+ if (plan.versionMigration) {
2840
+ lines.push("── Manifest ─────────────────────────────");
2841
+ lines.push(` ~ migrate manifest version ${plan.versionMigration.from} → ${plan.versionMigration.to}`);
2842
+ lines.push("");
2843
+ }
2844
+ for (const e of plan.entries) {
2845
+ if (e.kind === "create-file") {
2846
+ switch (e.action) {
2847
+ case "regenerate":
2848
+ lines.push(` ~ regenerate ${e.op.path}`);
2849
+ lines.push(indent(e.op.content));
2850
+ break;
2851
+ case "recreate":
2852
+ lines.push(` + recreate ${e.op.path} (recorded as owned but missing on disk)`);
2853
+ lines.push(indent(e.op.content));
2854
+ break;
2855
+ case "current":
2856
+ lines.push(` = current ${e.op.path}`);
2857
+ break;
2858
+ case "keep":
2859
+ lines.push(` = keep ${e.op.path} (adopter-owned content; only the ownership manifest is regenerated)`);
2860
+ break;
2861
+ case "refuse-foreign":
2862
+ lines.push(` ✖ REFUSE ${e.op.path} — foreign content at a vinaya path not owned by vinaya; not touched`);
2863
+ break;
2864
+ case "not-installed":
2865
+ lines.push(` · skip ${e.op.path} (not installed — that's \`vinaya init\`'s job, not upgrade's)`);
2866
+ break;
2867
+ }
2868
+ } else {
2869
+ switch (e.action) {
2870
+ case "regenerate-block":
2871
+ lines.push(` ~ regenerate managed block in ${e.op.path}`);
2872
+ lines.push(indent(renderBlock(e.op)));
2873
+ break;
2874
+ case "recreate-append":
2875
+ lines.push(` + restore managed block in ${e.op.path} (your other lines untouched)`);
2876
+ lines.push(indent(renderBlock(e.op)));
2877
+ break;
2878
+ case "recreate-host":
2879
+ lines.push(` + recreate ${e.op.path} (recorded as owned but missing on disk — e.g. a fresh clone)`);
2880
+ lines.push(indent(`${e.op.hostPreamble ?? ""}${renderBlock(e.op)}`));
2881
+ break;
2882
+ case "current":
2883
+ lines.push(` = current ${e.op.path}`);
2884
+ break;
2885
+ case "not-installed":
2886
+ lines.push(` · skip ${e.op.path} (not installed — that's \`vinaya init\`'s job, not upgrade's)`);
2887
+ break;
2888
+ }
2889
+ }
2890
+ }
2891
+ return lines.join(`
2892
+ `);
2893
+ }
2894
+ function regenerateBlock(repoRoot, op) {
2895
+ const abs2 = join11(repoRoot, op.path);
2896
+ const content = readFileSync9(abs2, "utf-8");
2897
+ const stripped = stripBlockFromContent(content, op.marker, op.comment);
2898
+ if (stripped !== null) {
2899
+ writeFileSync6(abs2, stripped.endsWith(`
2900
+ `) ? stripped : `${stripped}
2901
+ `, "utf-8");
2902
+ }
2903
+ appendBlock(repoRoot, op);
2904
+ }
2905
+ function applyUpgrade(plan, repoRoot) {
2906
+ for (const e of plan.entries) {
2907
+ if (e.kind === "create-file") {
2908
+ if (e.action === "regenerate" || e.action === "recreate") {
2909
+ writeFileWithDirs(join11(repoRoot, e.op.path), e.op.content, e.op.mode);
2910
+ }
2911
+ } else {
2912
+ if (e.action === "recreate-host")
2913
+ createHost(repoRoot, e.op);
2914
+ else if (e.action === "recreate-append")
2915
+ appendBlock(repoRoot, e.op);
2916
+ else if (e.action === "regenerate-block")
2917
+ regenerateBlock(repoRoot, e.op);
2918
+ }
2919
+ }
2920
+ }
2921
+ async function runUpgrade(args, deps) {
2922
+ const { dryRun, yes } = flags2(args);
2923
+ const repo = await deps.detectRepo();
2924
+ if (!repo) {
2925
+ console.error("Error: not a git repository. Run `vinaya upgrade` from inside your repo.");
2926
+ return 1;
2927
+ }
2928
+ const read = readManifest3(repo.repoRoot);
2929
+ if (read.kind === "missing" || read.kind === "not-initialized") {
2930
+ console.error("Error: vinaya is not initialized in this repo. Run `vinaya init` first.");
2931
+ return 1;
2932
+ }
2933
+ if (read.kind === "invalid") {
2934
+ console.error(`Error: vinaya.config.json is invalid — ${read.error}`);
2935
+ console.error("Fix it before upgrading (upgrade never guesses at a corrupt manifest).");
2936
+ return 1;
2937
+ }
2938
+ const { manifest } = read;
2939
+ if (manifest.version > MANAGED_MANIFEST_VERSION) {
2940
+ console.error(`Error: this repo's vinaya manifest is version ${manifest.version}, newer than the installed vinaya ` + `package understands (version ${MANAGED_MANIFEST_VERSION}). Upgrade the vinaya package itself first, ` + "then re-run `vinaya upgrade`.");
2941
+ return 1;
2942
+ }
2943
+ const hookDir = hookDirFromManifest(manifest, deps.hookDirFor(repo.repoRoot));
2944
+ const ctx = { owner: repo.owner, repo: repo.repo, hookDir };
2945
+ const ops = buildInitOps(ctx);
2946
+ const plan = planUpgrade(ops, repo.repoRoot, manifest);
2947
+ if (!plan.hasChanges) {
2948
+ process.stdout.write(`vinaya upgrade — already current. Nothing to do.
2949
+ `);
2950
+ return 0;
2951
+ }
2952
+ process.stdout.write(`vinaya upgrade — the full diff of every intended change:
2953
+
2954
+ `);
2955
+ process.stdout.write(`${renderUpgradeDiff(plan)}
2956
+ `);
2957
+ if (dryRun) {
2958
+ process.stdout.write(`
2959
+ --dry-run: nothing was written.
2960
+ `);
2961
+ return 0;
2962
+ }
2963
+ if (!yes) {
2964
+ const ok2 = await deps.confirm("Regenerate these vinaya-owned artifacts?");
2965
+ if (!ok2) {
2966
+ process.stdout.write(`Aborted. Nothing was written.
2967
+ `);
2968
+ return 0;
2969
+ }
2970
+ }
2971
+ applyUpgrade(plan, repo.repoRoot);
2972
+ writeManifestVersion(repo.repoRoot, manifest);
2973
+ process.stdout.write(`
2974
+ Vinaya upgraded.
2975
+ `);
2976
+ return 0;
2977
+ }
2978
+ async function upgradeCommand(args) {
2979
+ process.exit(await runUpgrade(args, realDeps4()));
2980
+ }
2981
+
2982
+ // ../sources/src/commands.ts
2983
+ var COMMANDS = [
2984
+ {
2985
+ name: "help",
2986
+ description: "Show this help text",
2987
+ status: "shipped"
2988
+ },
2989
+ {
2990
+ name: "version",
2991
+ description: "Print the CLI version",
2992
+ flags: [{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }],
2993
+ status: "shipped"
2994
+ },
2995
+ {
2996
+ name: "init",
2997
+ description: "Install Vinaya's git hooks, CI workflow, and starter config (diff-and-confirm, non-destructive)",
2998
+ flags: [
2999
+ { flag: "--dry-run", description: "Print the full diff without installing anything" },
3000
+ { flag: "--yes", description: "Skip the confirmation prompt" }
3001
+ ],
3002
+ details: [
3003
+ "It detects your repo, prints the complete diff of every intended change, and waits for your confirmation before installing anything. `--dry-run` prints that same diff and installs nothing. Nothing ever runs automatically on package install.",
3004
+ "It installs one CI workflow that runs `vinaya check --all --diff-only`, alongside your existing workflows — refusing to overwrite rather than touching foreign content already at that path. Git hook stubs invoke the `vinaya` binary directly; if a hook already exists, it appends a delimited managed block, shown verbatim in the diff first, rather than overwriting it.",
3005
+ "`vinaya.config.json` is seeded with a starter ruleset extracted from Vinaya's own battle-tested gates, not invented defaults. Issue and PR templates carrying the brief schema are added alongside your own; tier and `needs:*-input` labels are created only if they don't already exist — your existing labels are never modified.",
3006
+ "The recommended branch-protection command is printed for you to run yourself — it is never applied, and your PATH is never touched. `eject` removes exactly the managed block it owns, or a whole file only if `init` created it."
3007
+ ],
3008
+ status: "shipped"
3009
+ },
3010
+ {
3011
+ name: "init product",
3012
+ description: "Scaffold an additional governed product area in an already-initialized monorepo",
3013
+ status: "shipped"
3014
+ },
3015
+ {
3016
+ name: "check",
3017
+ description: "Run one check, or every registered check",
3018
+ flags: [
3019
+ { flag: "--all", description: "Run every registered check instead of one named check" },
3020
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" },
3021
+ { flag: "--diff-only", description: "Scope diff-declared checks to changed files" },
3022
+ { flag: "--parallel[=n]", description: "Concurrency cap (default: cpu-derived)" }
3023
+ ],
3024
+ status: "shipped"
3025
+ },
3026
+ {
3027
+ name: "new check",
3028
+ description: "Scaffold a custom check into ./scripts/vinaya-checks/",
3029
+ status: "shipped"
3030
+ },
3031
+ {
3032
+ name: "pr create",
3033
+ description: "Open a pull request after full brief-schema validation",
3034
+ flags: [
3035
+ { flag: "--title", description: "PR title (validated against the forge-title grammar)" },
3036
+ { flag: "--body-file", description: "Path to the PR body (stream-safe; the same bytes are validated and sent)" },
3037
+ { flag: "--label", description: "Label(s) to apply (repeatable, comma-separated)" },
3038
+ { flag: "--validate-only", description: "Run every gate and report PASS without opening the PR" },
3039
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
3040
+ ],
3041
+ details: [
3042
+ "Runs the config-defined brief-schema gate (`briefSchema.pr` in `vinaya.config.json`) LOCALLY before any `gh` write — prevention, not detection. On any failure it refuses with the versioned CheckError contract (one JSON line per finding on stderr, exit 1) whose `agent_recovery_prompt` names the exact corrective command."
3043
+ ],
3044
+ status: "shipped"
3045
+ },
3046
+ {
3047
+ name: "pr edit",
3048
+ description: "Edit an existing pull request (<n>) after full brief-schema validation",
3049
+ flags: [
3050
+ { flag: "--title", description: "New PR title (validated against the forge-title grammar)" },
3051
+ { flag: "--body-file", description: "Path to the new PR body (stream-safe; same bytes validated and sent)" },
3052
+ { flag: "--validate-only", description: "Run every gate and report PASS without editing the PR" },
3053
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
3054
+ ],
3055
+ details: [
3056
+ "The target PR's real head branch and changed files are fetched from the forge to build the validation context — a failed fetch is a hard refusal, never a fall-back to the local checkout."
3057
+ ],
3058
+ status: "shipped"
3059
+ },
3060
+ {
3061
+ name: "issue create",
3062
+ description: "Open an issue after full brief-schema validation",
3063
+ flags: [
3064
+ { flag: "--title", description: "Issue title (validated on task Issues)" },
3065
+ { flag: "--body-file", description: "Path to the Issue body (stream-safe; same bytes validated and sent)" },
3066
+ { flag: "--label", description: "Label(s) to apply; a `vinaya/tranche:*` label marks a task Issue" },
3067
+ { flag: "--validate-only", description: "Run every gate and report PASS without opening the Issue" },
3068
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
3069
+ ],
3070
+ details: [
3071
+ "A task Issue (any `vinaya/tranche:*` label) must carry the full Planner rationale (`briefSchema.issue`); non-task Issues pass through unvalidated."
3072
+ ],
3073
+ status: "shipped"
3074
+ },
3075
+ {
3076
+ name: "issue edit",
3077
+ description: "Edit an existing issue (<n>) after full brief-schema validation",
3078
+ flags: [
3079
+ { flag: "--title", description: "New Issue title (validated on task Issues)" },
3080
+ { flag: "--body-file", description: "Path to the new Issue body (stream-safe; same bytes validated and sent)" },
3081
+ { flag: "--validate-only", description: "Run every gate and report PASS without editing the Issue" },
3082
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
3083
+ ],
3084
+ details: [
3085
+ "The target Issue's actual labels are fetched from the forge and unioned with argv to decide task-Issue applicability — a failed fetch is a hard refusal."
3086
+ ],
3087
+ status: "shipped"
3088
+ },
3089
+ {
3090
+ name: "doctor",
3091
+ description: "Diagnose hook, workflow, and config health — report only, never mutates",
3092
+ flags: [{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }],
3093
+ status: "shipped"
3094
+ },
3095
+ {
3096
+ name: "upgrade",
3097
+ description: "Regenerate hooks, workflow, and config to the current contract version (diff-and-confirm)",
3098
+ flags: [
3099
+ { flag: "--dry-run", description: "Print the full diff without regenerating anything" },
3100
+ { flag: "--yes", description: "Skip the confirmation prompt" }
3101
+ ],
3102
+ status: "shipped"
3103
+ },
3104
+ {
3105
+ name: "eject",
3106
+ description: "Remove every Vinaya-installed artifact, restoring the repo to stock",
3107
+ flags: [
3108
+ { flag: "--dry-run", description: "Print the full removal diff without removing anything" },
3109
+ { flag: "--yes", description: "Skip the confirmation prompt" }
3110
+ ],
3111
+ status: "shipped"
3112
+ },
3113
+ {
3114
+ name: "demo break",
3115
+ description: "Run a guided refusal-then-fix demo on an isolated, discardable branch",
3116
+ status: "planned"
3117
+ },
3118
+ {
3119
+ name: "waiver",
3120
+ description: "Apply the principal-verified 'vinaya/waiver:docs' label after prompting for a reason",
3121
+ status: "planned"
3122
+ },
3123
+ {
3124
+ name: "studio",
3125
+ description: "Launch local Vinaya Studio against this repo (requires a Vinaya workspace checkout)",
3126
+ status: "shipped"
3127
+ }
3128
+ ];
3129
+ // ../sources/src/select-source.ts
3130
+ import { z as z2 } from "zod";
3131
+ var StateSourceConfigSchema = z2.discriminatedUnion("kind", [
3132
+ z2.object({ kind: z2.literal("forge"), owner: z2.string(), repo: z2.string() }),
3133
+ z2.object({ kind: z2.literal("file"), root: z2.string().optional() })
3134
+ ]);
3135
+ // src/lib/output.ts
3136
+ var NAME_COLUMN_WIDTH = 28;
3137
+ var PLANNED_MARKER = "[planned — not yet implemented] ";
3138
+ function row(indent2, name, description) {
3139
+ const padded = name.length >= NAME_COLUMN_WIDTH ? `${name} ` : name.padEnd(NAME_COLUMN_WIDTH);
3140
+ return `${indent2}${padded}${description}`;
3141
+ }
3142
+ function printHelp() {
3143
+ const lines = ["vinaya — Vinaya CLI", "", "USAGE", " vinaya <command> [options]", "", "COMMANDS"];
3144
+ for (const command of COMMANDS) {
3145
+ const marker = command.status === "planned" ? PLANNED_MARKER : "";
3146
+ lines.push(row(" ", command.name, `${marker}${command.description}`));
3147
+ for (const flag of command.flags ?? []) {
3148
+ lines.push(row(" ", flag.flag, `${marker}${flag.description}`));
3149
+ }
3150
+ }
3151
+ lines.push("", "Run 'vinaya version' to check what's installed.");
3152
+ process.stdout.write(`${lines.join(`
3153
+ `)}
3154
+ `);
3155
+ }
3156
+
3157
+ // src/index.ts
3158
+ var PACKAGE_ROOT = join12(dirname7(fileURLToPath4(import.meta.url)), "..");
3159
+ function readVersion2() {
3160
+ const pkg = JSON.parse(readFileSync10(join12(PACKAGE_ROOT, "package.json"), "utf-8"));
3161
+ return pkg.version;
3162
+ }
3163
+ var [, , command, ...args] = process.argv;
3164
+ if (!command || command === "help" || command === "--help" || command === "-h") {
3165
+ printHelp();
3166
+ process.exit(0);
3167
+ }
3168
+ try {
3169
+ switch (command) {
3170
+ case "version": {
3171
+ const version = readVersion2();
3172
+ if (args.includes("--json")) {
3173
+ printJson({ version });
3174
+ } else {
3175
+ process.stdout.write(`${version}
3176
+ `);
3177
+ }
3178
+ break;
3179
+ }
3180
+ case "studio": {
3181
+ const code = await runStudio(process.cwd(), args);
3182
+ process.exit(code);
3183
+ break;
3184
+ }
3185
+ case "init": {
3186
+ const [subcommand, ...rest] = args;
3187
+ if (subcommand === "product") {
3188
+ await initProductCommand(rest);
3189
+ } else {
3190
+ await initCommand(args);
3191
+ }
3192
+ break;
3193
+ }
3194
+ case "eject": {
3195
+ await ejectCommand(args);
3196
+ break;
3197
+ }
3198
+ case "doctor": {
3199
+ await doctorCommand(args);
3200
+ break;
3201
+ }
3202
+ case "upgrade": {
3203
+ await upgradeCommand(args);
3204
+ break;
3205
+ }
3206
+ case "check": {
3207
+ await checkCommand(args);
3208
+ break;
3209
+ }
3210
+ case "new": {
3211
+ const [subcommand, ...rest] = args;
3212
+ if (subcommand === "check") {
3213
+ newCheckCommand(rest);
3214
+ } else {
3215
+ console.error(`Unknown 'new' subcommand: ${subcommand ?? "(none)"}`);
3216
+ process.exit(2);
3217
+ }
3218
+ break;
3219
+ }
3220
+ case "pr": {
3221
+ const [subcommand, ...rest] = args;
3222
+ if (subcommand === "create") {
3223
+ prCreateCommand(rest);
3224
+ } else if (subcommand === "edit") {
3225
+ prEditCommand(rest);
3226
+ } else {
3227
+ console.error(`Unknown 'pr' subcommand: ${subcommand ?? "(none)"} (expected 'create' or 'edit')`);
3228
+ process.exit(2);
3229
+ }
3230
+ break;
3231
+ }
3232
+ case "issue": {
3233
+ const [subcommand, ...rest] = args;
3234
+ if (subcommand === "create") {
3235
+ issueCreateCommand(rest);
3236
+ } else if (subcommand === "edit") {
3237
+ issueEditCommand(rest);
3238
+ } else {
3239
+ console.error(`Unknown 'issue' subcommand: ${subcommand ?? "(none)"} (expected 'create' or 'edit')`);
3240
+ process.exit(2);
3241
+ }
3242
+ break;
3243
+ }
3244
+ default:
3245
+ console.error(`Unknown command: ${command}`);
3246
+ printHelp();
3247
+ process.exit(2);
3248
+ }
3249
+ } catch (error2) {
3250
+ const message = error2 instanceof Error ? error2.message : String(error2);
3251
+ console.error(`Error: ${message}`);
3252
+ process.exit(1);
3253
+ }