@attalabs/vinaya 0.1.2 → 0.2.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.
@@ -44,6 +44,7 @@ The same check implementations run at ring 0 and ring 1 — one codebase, two en
44
44
  | Starting the Dig (before authoring a brief) / starting Step 0 (before executing one) / **every push on a task branch before its PR exists** | Ever started work only to find out halfway it was blocked on something else? | hook | Refuses to start work until every precondition for the task is checked live and found clear. | **`verify-dispatch` CLI** (`packages/aeg-core/bin/verify-dispatch.ts` — mandated by `roles/developer.md` and `skills/brief-authoring/SKILL.md`; **now also hook-automated** — task 25) | Every dispatch precondition is re-checked live against the forge (the task's row/Issue are derived directly from a Milestone + `vinaya/tranche:<slug>`-labeled Issues as of the forge-native migration — no `origin/main` file read involved; leftover-branch detection still fetches `origin/main` for its own commit-count comparison): the task's row and Issue exist; the Issue passes the rationale gate; every `depends-on` PR is merged and no `conflicts-with` PR is open/in-flight; every named project's prior tranche is archived. (2026-07-13: the immediately-prior task's three-predicate archival bar — the row-adjacency predicate — was removed once the provenance-posting signal it existed to protect became automated.) A `NOT READY` result is a stop condition. Every `gh` call this script makes carries an explicit `-R <owner>/<repo>` (task 23) — from a linked `git worktree add` checkout, an untargeted `gh issue list`/`gh issue view`/`gh pr list` silently scopes to the wrong repo (or returns nothing), which previously produced false prior-tranche-archival blockers. **`.husky/pre-push`** now runs this gate itself on a `task/*/*` branch's first push (before a PR exists), refusing on genuine `NOT READY` and failing OPEN — loudly — when a `gh auth status` reachability probe fails or `verify-dispatch`'s own repo/token resolution fails (task 18's fail-open precedent). The gate reads only the `dispatch-readiness:` predicate, not verify-dispatch's combined exit code, which also folds in `leftover-detection` (a pre-Step-0 "safe to branch fresh" advisory that would otherwise false-block every push after the first on a task legitimately mid-flight — discovered live while building this gate). Once a PR exists, later pushes skip the gate: dispatch was already validated once. | `packages/aeg-core/bin/verify-dispatch.ts` |
45
45
  | Opening a task PR whose surface includes real code | Ever had a PR's description quietly stop matching what the code does? | event | Refuses a code-carrying pull request whose description no longer matches what it changes. | **Brief Validation — `checkPremiseCoverage`** | The PR body carries a `Premise:` block with at least one assertion (`contains`/`absent`/`sha256`) whose path falls inside the task's surface map — re-assertable at any point via `verify-dispatch --premise` or `verify-task`. | `packages/aeg-core/src/brief-validation.ts` |
46
46
  | Opening a task PR (final self-check before creation) | Ever opened a PR and only then discovered the tests were failing? | event | Runs the whole exit check before a pull request is created, so failures surface first. | **`verify-task` CLI** (`packages/aeg-core/bin/verify-task.ts` — mandated by `roles/developer.md`; **now also hook-automated** — task 25) | Typecheck, lint, tests, build, `verify-docs --pr`, and the premise coverage/recheck pair all pass, as one summary, against the PR's actual diff, before `open-pr.ts` is invoked. **`open-pr.ts` now runs this composite itself** for task branches (via `gatePlanForBranch`), invoked wholesale rather than partially re-implemented — non-task branches are unaffected (byte-identical gate set). `verify-docs --pr` runs twice on a task-branch PR-open as an accepted, measured overlap (~4s with a warm turbo cache). | `packages/aeg-core/bin/verify-task.ts` |
47
+ | Spawning a check (`vinaya check`, ring-0 pre-push AND ring-1 CI) | Ever had a check quietly read a secret it had no business seeing, because nothing scoped what it could reach? | hook | Governs which environment variables a spawned check's child process can see, instead of every check inheriting the full parent environment unconditionally. | **Env allowlist** (`CheckSpec['env']`, `apps/vinaya/cli/src/checks/contract.ts`; construction: `buildCheckEnv`, `apps/vinaya/cli/src/checks/runner.ts`) | **Warn status today (task 2):** the grammar (`true` / `{ optional: true }` / `{ anyOf: [...] }` / a literal string) and the construction function exist and are unit-tested, but `runOne`'s `spawn()` call still passes no `env` override — every check still inherits the full parent environment, unchanged. `vinaya check` prints one warning line per check whose bin reads `process.env`/`Bun.env`/`Deno.env` with no `env` declared (fires identically at the ring-0 pre-push hook and ring-1 CI, since both invoke the same `vinaya check --all`); `vinaya doctor` carries the same diagnostic permanently. All 15 core checks in `registry.ts` carry an audited declaration. **Once flipped (task 3, a later minor):** a spawned check's child process sees only a fixed baseline (`PATH`, `LANG`, `HOME`, `HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY`, `TMPDIR`) plus whatever its own `env` declaration explicitly forwards — an undeclared variable a check's own code reads becomes genuinely invisible to it, not merely warned about. | `apps/vinaya/cli/src/checks/runner.ts` |
47
48
 
48
49
  **Documentation coverage, specifically** (a historical pain point): the code→document ownership rule is enforced at *two* prevention chokepoints — at every push (over the branch's cumulative change set) and again at pull-request creation and editing. A change to owned code cannot be published, let alone turned into a pull request, without its owning document.
49
50
 
@@ -2773,7 +2773,7 @@ var CHECK_NAME = "coherence";
2773
2773
  var execFileAsync4 = promisify4(execFile4);
2774
2774
  function git(args) {
2775
2775
  try {
2776
- return execFileSync2("git", args, { encoding: "utf8" }).trim();
2776
+ return execFileSync2("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
2777
2777
  } catch {
2778
2778
  return "";
2779
2779
  }
@@ -2773,7 +2773,7 @@ var CHECK_NAME = "dispatch-readiness";
2773
2773
  var execFileAsync4 = promisify4(execFile4);
2774
2774
  function git(args) {
2775
2775
  try {
2776
- return execFileSync2("git", args, { encoding: "utf8" }).trim();
2776
+ return execFileSync2("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
2777
2777
  } catch {
2778
2778
  return "";
2779
2779
  }
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync11 } from "node:fs";
4
+ import { readFileSync as readFileSync12 } from "node:fs";
5
5
  import { dirname as dirname5, join as join15 } from "node:path";
6
6
  import { fileURLToPath as fileURLToPath2 } from "node:url";
7
7
 
@@ -1276,91 +1276,153 @@ function coreCheckRegistry() {
1276
1276
  name: "brief-shape",
1277
1277
  run: bin("check-brief-shape"),
1278
1278
  scope: "diff",
1279
- timeoutMs: 15000
1279
+ timeoutMs: 15000,
1280
+ env: { PR_BODY: { optional: true } }
1280
1281
  },
1281
1282
  {
1282
1283
  name: "doc-coverage",
1283
1284
  run: bin("check-doc-coverage"),
1284
1285
  scope: "diff",
1285
- timeoutMs: 15000
1286
+ timeoutMs: 15000,
1287
+ env: {
1288
+ BASE_SHA: { optional: true },
1289
+ PR_BODY: { optional: true },
1290
+ PR_BODY_FILE: { optional: true },
1291
+ PR_LABELS: { optional: true },
1292
+ WAIVER_LABEL_ACTOR: { optional: true }
1293
+ }
1286
1294
  },
1287
1295
  {
1288
1296
  name: "coherence",
1289
1297
  run: bin("check-coherence"),
1290
1298
  scope: "full",
1291
- timeoutMs: 30000
1299
+ timeoutMs: 30000,
1300
+ env: {
1301
+ AEG_REPO: { optional: true },
1302
+ BRANCH: { optional: true },
1303
+ GITHUB_TOKEN: { optional: true },
1304
+ GH_TOKEN: { optional: true }
1305
+ }
1292
1306
  },
1293
1307
  {
1294
1308
  name: "dispatch-readiness",
1295
1309
  run: bin("check-dispatch-readiness"),
1296
1310
  scope: "full",
1297
- timeoutMs: 30000
1311
+ timeoutMs: 30000,
1312
+ env: {
1313
+ AEG_REPO: { optional: true },
1314
+ BRANCH: { optional: true },
1315
+ GITHUB_TOKEN: { optional: true },
1316
+ GH_TOKEN: { optional: true }
1317
+ }
1298
1318
  },
1299
1319
  {
1300
1320
  name: "closes-n",
1301
1321
  run: bin("check-closes-n"),
1302
1322
  scope: "diff",
1303
- timeoutMs: 15000
1323
+ timeoutMs: 15000,
1324
+ env: {
1325
+ BRANCH: { optional: true },
1326
+ PR_BODY: { optional: true },
1327
+ AEG_REPO: { optional: true }
1328
+ }
1304
1329
  },
1305
1330
  {
1306
1331
  name: "single-plan-pr",
1307
1332
  run: bin("check-single-plan-pr"),
1308
1333
  scope: "diff",
1309
- timeoutMs: 15000
1334
+ timeoutMs: 15000,
1335
+ env: {
1336
+ BASE_SHA: { optional: true },
1337
+ PR_NUMBER: { optional: true }
1338
+ }
1310
1339
  },
1311
1340
  {
1312
1341
  name: "test-plan",
1313
1342
  run: bin("check-test-plan"),
1314
1343
  scope: "diff",
1315
- timeoutMs: 15000
1344
+ timeoutMs: 15000,
1345
+ env: {
1346
+ PR_BODY: { optional: true },
1347
+ BRANCH: { optional: true }
1348
+ }
1316
1349
  },
1317
1350
  {
1318
1351
  name: "no-disk-state",
1319
1352
  run: bin("check-no-disk-state"),
1320
1353
  scope: "diff",
1321
- timeoutMs: 15000
1354
+ timeoutMs: 15000,
1355
+ env: { BASE_SHA: { optional: true } }
1322
1356
  },
1323
1357
  {
1324
1358
  name: "registry-gates",
1325
1359
  run: bin("check-registry-gates"),
1326
1360
  scope: "full",
1327
- timeoutMs: 30000
1361
+ timeoutMs: 30000,
1362
+ env: { AEG_REPO: { optional: true } }
1328
1363
  },
1329
1364
  {
1330
1365
  name: "review-gate",
1331
1366
  run: bin("check-review-gate"),
1332
1367
  scope: "full",
1333
- timeoutMs: 30000
1368
+ timeoutMs: 30000,
1369
+ env: {
1370
+ BRANCH: { optional: true },
1371
+ PR_NUMBER: { optional: true }
1372
+ }
1334
1373
  },
1335
1374
  {
1336
1375
  name: "branch-topology",
1337
1376
  run: bin("check-branch-topology"),
1338
1377
  scope: "full",
1339
- timeoutMs: 30000
1378
+ timeoutMs: 30000,
1379
+ env: {
1380
+ BRANCH: { optional: true },
1381
+ AEG_REPO: { optional: true }
1382
+ }
1340
1383
  },
1341
1384
  {
1342
1385
  name: "dead-branch-push",
1343
1386
  run: bin("check-dead-branch-push"),
1344
1387
  scope: "full",
1345
- timeoutMs: 30000
1388
+ timeoutMs: 30000,
1389
+ env: { BRANCH: { optional: true } }
1346
1390
  },
1347
1391
  {
1348
1392
  name: "first-push-dispatch",
1349
1393
  run: bin("check-first-push-dispatch"),
1350
1394
  scope: "full",
1351
- timeoutMs: 30000
1395
+ timeoutMs: 30000,
1396
+ env: {
1397
+ BRANCH: { optional: true },
1398
+ AEG_REPO: { optional: true },
1399
+ GITHUB_TOKEN: { optional: true },
1400
+ GH_TOKEN: { optional: true }
1401
+ }
1352
1402
  },
1353
1403
  {
1354
1404
  name: "doc-coverage-push",
1355
1405
  run: bin("check-doc-coverage-push"),
1356
1406
  scope: "diff",
1357
- timeoutMs: 15000
1407
+ timeoutMs: 15000,
1408
+ env: {
1409
+ OVERRIDE_DOCS: { optional: true },
1410
+ BASE_SHA: { optional: true },
1411
+ PR_BODY: { optional: true },
1412
+ PR_BODY_FILE: { optional: true },
1413
+ PR_LABELS: { optional: true },
1414
+ WAIVER_LABEL_ACTOR: { optional: true }
1415
+ }
1358
1416
  },
1359
1417
  {
1360
1418
  name: "issue-assignment",
1361
1419
  run: bin("check-issue-assignment"),
1362
1420
  scope: "full",
1363
- timeoutMs: 30000
1421
+ timeoutMs: 30000,
1422
+ env: {
1423
+ AEG_REPO: { optional: true },
1424
+ BRANCH: { optional: true }
1425
+ }
1364
1426
  }
1365
1427
  ];
1366
1428
  }
@@ -1390,17 +1452,70 @@ function shouldSkip(spec, opts) {
1390
1452
  return !opts.changedFiles.some((f) => regexes.some((re) => re.test(f)));
1391
1453
  }
1392
1454
  var KILL_GRACE_MS = 2000;
1455
+ var activeKillers = new Set;
1456
+ var signalForwardingInstalled = false;
1457
+ function installSignalForwarding() {
1458
+ if (signalForwardingInstalled)
1459
+ return;
1460
+ signalForwardingInstalled = true;
1461
+ const forward = (_signal, exitCode) => {
1462
+ for (const kill of activeKillers)
1463
+ kill("SIGTERM");
1464
+ setTimeout(() => {
1465
+ for (const kill of activeKillers)
1466
+ kill("SIGKILL");
1467
+ process.exit(exitCode);
1468
+ }, KILL_GRACE_MS);
1469
+ };
1470
+ process.on("SIGINT", () => forward("SIGINT", 130));
1471
+ process.on("SIGTERM", () => forward("SIGTERM", 143));
1472
+ }
1393
1473
  async function runOne(spec, timeoutMs) {
1394
1474
  const start = performance.now();
1395
1475
  const proc = spawn(spec.run, spec.args ?? [], {
1396
- stdio: ["ignore", "pipe", "pipe"]
1476
+ stdio: ["ignore", "pipe", "pipe"],
1477
+ detached: true
1397
1478
  });
1479
+ function killTree(signal) {
1480
+ const pid = proc.pid;
1481
+ if (pid === undefined)
1482
+ return;
1483
+ try {
1484
+ process.kill(-pid, signal);
1485
+ } catch {
1486
+ try {
1487
+ proc.kill(signal);
1488
+ } catch {}
1489
+ }
1490
+ }
1491
+ installSignalForwarding();
1492
+ activeKillers.add(killTree);
1493
+ function groupStillAlive(pid) {
1494
+ try {
1495
+ process.kill(-pid, 0);
1496
+ return true;
1497
+ } catch {
1498
+ return false;
1499
+ }
1500
+ }
1398
1501
  let timedOut = false;
1399
- let killTimer;
1502
+ let escalationDone = Promise.resolve();
1503
+ let cancelEscalation;
1400
1504
  const timer = setTimeout(() => {
1401
1505
  timedOut = true;
1402
- proc.kill("SIGTERM");
1403
- killTimer = setTimeout(() => proc.kill("SIGKILL"), KILL_GRACE_MS);
1506
+ killTree("SIGTERM");
1507
+ escalationDone = new Promise((resolve) => {
1508
+ const graceTimer = setTimeout(() => {
1509
+ const pid = proc.pid;
1510
+ if (pid !== undefined && groupStillAlive(pid))
1511
+ killTree("SIGKILL");
1512
+ resolve();
1513
+ }, KILL_GRACE_MS);
1514
+ cancelEscalation = () => {
1515
+ clearTimeout(graceTimer);
1516
+ resolve();
1517
+ };
1518
+ });
1404
1519
  }, timeoutMs);
1405
1520
  let stderrText = "";
1406
1521
  proc.stderr.setEncoding("utf-8");
@@ -1417,7 +1532,13 @@ async function runOne(spec, timeoutMs) {
1417
1532
  });
1418
1533
  });
1419
1534
  clearTimeout(timer);
1420
- clearTimeout(killTimer);
1535
+ if (timedOut) {
1536
+ const pid = proc.pid;
1537
+ if (pid === undefined || !groupStillAlive(pid))
1538
+ cancelEscalation?.();
1539
+ await escalationDone;
1540
+ }
1541
+ activeKillers.delete(killTree);
1421
1542
  const durationMs = performance.now() - start;
1422
1543
  if (timedOut) {
1423
1544
  return { name: spec.name, status: "timeout", exitCode: null, errors: [], durationMs };
@@ -1498,13 +1619,70 @@ import { existsSync as existsSync4, mkdirSync, readFileSync, writeFileSync } fro
1498
1619
  import { homedir } from "node:os";
1499
1620
  import { dirname as dirname2, join as join4 } from "node:path";
1500
1621
  import { z } from "zod";
1622
+ var EnvEntrySchema = z.union([
1623
+ z.literal(true),
1624
+ z.object({ optional: z.literal(true) }),
1625
+ z.object({ anyOf: z.array(z.string()).min(2) }),
1626
+ z.string()
1627
+ ]);
1501
1628
  var CheckEntrySchema = z.object({
1502
1629
  run: z.string(),
1503
1630
  scope: z.enum(["diff", "full"]),
1504
1631
  include: z.array(z.string()).optional(),
1505
1632
  args: z.array(z.string()).optional(),
1506
- timeoutMs: z.number().optional()
1633
+ timeoutMs: z.number().optional(),
1634
+ env: z.record(z.string(), EnvEntrySchema).optional()
1635
+ }).superRefine((entry2, ctx) => {
1636
+ if (!entry2.env)
1637
+ return;
1638
+ for (const [key, value] of Object.entries(entry2.env)) {
1639
+ if (typeof value !== "object" || value === null || !("anyOf" in value))
1640
+ continue;
1641
+ const members = value.anyOf;
1642
+ if (new Set(members).size !== members.length) {
1643
+ ctx.addIssue({
1644
+ code: z.ZodIssueCode.custom,
1645
+ message: `env.${key}.anyOf has duplicate members`,
1646
+ path: ["env", key, "anyOf"]
1647
+ });
1648
+ }
1649
+ if (!members.includes(key)) {
1650
+ ctx.addIssue({
1651
+ code: z.ZodIssueCode.custom,
1652
+ message: `env.${key}.anyOf must include "${key}" itself as a member`,
1653
+ path: ["env", key, "anyOf"]
1654
+ });
1655
+ }
1656
+ }
1507
1657
  });
1658
+ var HIGH_ENTROPY_MIN_LENGTH = 20;
1659
+ function looksHighEntropy(value) {
1660
+ if (value.length < HIGH_ENTROPY_MIN_LENGTH)
1661
+ return false;
1662
+ if (/\s/.test(value))
1663
+ return false;
1664
+ const classes = [/[a-z]/.test(value), /[A-Z]/.test(value), /[0-9]/.test(value)].filter(Boolean).length;
1665
+ return classes >= 2;
1666
+ }
1667
+ function lintEnvDeclarations(checks) {
1668
+ const warnings = [];
1669
+ if (!checks)
1670
+ return warnings;
1671
+ for (const [checkName, entry2] of Object.entries(checks)) {
1672
+ if (!entry2.env)
1673
+ continue;
1674
+ for (const [key, value] of Object.entries(entry2.env)) {
1675
+ if (typeof value !== "string")
1676
+ continue;
1677
+ if (value === "true" || value === "false") {
1678
+ warnings.push(`check "${checkName}" env.${key} is the literal string "${value}" — did you mean the boolean form \`${key}: true\` (forward the caller's value) rather than a hardcoded literal?`);
1679
+ } else if (looksHighEntropy(value)) {
1680
+ warnings.push(`check "${checkName}" env.${key} looks like a high-entropy literal (possible secret committed to config) — env declarations should reference variable NAMES the caller sets, not literal secret values.`);
1681
+ }
1682
+ }
1683
+ }
1684
+ return warnings;
1685
+ }
1508
1686
  var BRIEF_BUILTINS = [
1509
1687
  "tier",
1510
1688
  "testPlan",
@@ -1603,6 +1781,31 @@ function loadConfigChecked() {
1603
1781
  return { ok: true, config: parsed.data };
1604
1782
  }
1605
1783
 
1784
+ // src/lib/env-lint.ts
1785
+ import { existsSync as existsSync5, readFileSync as readFileSync2 } from "node:fs";
1786
+ var ENV_READ_PATTERN = /\b(?:process\.env|Bun\.env|Deno\.env)\b/;
1787
+ function checksMissingEnvDeclaration(specs) {
1788
+ const names = [];
1789
+ for (const spec of specs) {
1790
+ if (spec.env)
1791
+ continue;
1792
+ if (!existsSync5(spec.run))
1793
+ continue;
1794
+ let source;
1795
+ try {
1796
+ source = readFileSync2(spec.run, "utf8");
1797
+ } catch {
1798
+ continue;
1799
+ }
1800
+ if (ENV_READ_PATTERN.test(source))
1801
+ names.push(spec.name);
1802
+ }
1803
+ return names;
1804
+ }
1805
+ function envDeclarationWarning(name) {
1806
+ return `check "${name}" reads environment variables directly but declares no \`env\` — it will lose environment access at the next minor. Declare \`env\` for it (CheckSpec['env'] in src/checks/contract.ts, or vinaya.config.json's checks.<name>.env).`;
1807
+ }
1808
+
1606
1809
  // src/commands/check.ts
1607
1810
  function git(args) {
1608
1811
  try {
@@ -1665,6 +1868,9 @@ async function checkCommand(args) {
1665
1868
  console.error(`Unknown check: ${requestedName}`);
1666
1869
  process.exit(2);
1667
1870
  }
1871
+ for (const name of checksMissingEnvDeclaration(specsToRun)) {
1872
+ console.error(`⚠ ${envDeclarationWarning(name)}`);
1873
+ }
1668
1874
  const changed = diffOnly ? changedFiles() : null;
1669
1875
  const outcomes = specsToRun.length > 0 ? await runChecks(specsToRun, {
1670
1876
  parallel: requestedParallel ?? defaultParallelism(),
@@ -1695,7 +1901,7 @@ async function checkCommand(args) {
1695
1901
 
1696
1902
  // src/commands/demo.ts
1697
1903
  import { execFileSync as execFileSync5, spawnSync } from "node:child_process";
1698
- import { existsSync as existsSync5, readFileSync as readFileSync2, rmSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
1904
+ import { existsSync as existsSync6, readFileSync as readFileSync3, rmSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
1699
1905
  import { isAbsolute, join as join5 } from "node:path";
1700
1906
  var DEMO_BRANCH_PREFIX = "vinaya/demo-break-";
1701
1907
  var FIXTURE_PATH = ".vinaya-demo-brief.md";
@@ -1738,9 +1944,9 @@ function gitDirAbs(repoRoot) {
1738
1944
  function recoverFromCrash(repoRoot, statePath) {
1739
1945
  const currentBranch = currentBranchName(repoRoot);
1740
1946
  let state = null;
1741
- if (existsSync5(statePath)) {
1947
+ if (existsSync6(statePath)) {
1742
1948
  try {
1743
- state = JSON.parse(readFileSync2(statePath, "utf-8"));
1949
+ state = JSON.parse(readFileSync3(statePath, "utf-8"));
1744
1950
  } catch {
1745
1951
  state = null;
1746
1952
  }
@@ -1810,14 +2016,14 @@ function cleanup(repoRoot, originalBranch, demoBranch, statePath) {
1810
2016
  gitCapture(repoRoot, ["clean", "-fd"]);
1811
2017
  gitCapture(repoRoot, ["checkout", originalBranch]);
1812
2018
  gitCapture(repoRoot, ["branch", "-D", demoBranch]);
1813
- if (existsSync5(statePath))
2019
+ if (existsSync6(statePath))
1814
2020
  rmSync(statePath, { force: true });
1815
2021
  }
1816
2022
  async function runDemoBreak(repoRoot, args) {
1817
2023
  const keep = args.includes("--keep");
1818
2024
  const hookDir = resolveHookDir(repoRoot);
1819
2025
  const hookPath = join5(repoRoot, hookDir, "pre-commit");
1820
- if (!existsSync5(hookPath)) {
2026
+ if (!existsSync6(hookPath)) {
1821
2027
  console.error("Vinaya hooks are not installed in this repo. Run `vinaya init` first.");
1822
2028
  return 1;
1823
2029
  }
@@ -1875,7 +2081,7 @@ Attempting to commit…
1875
2081
  process.stdout.write(`✓ Commit passed — the fix worked.
1876
2082
  `);
1877
2083
  if (keep) {
1878
- if (existsSync5(statePath))
2084
+ if (existsSync6(statePath))
1879
2085
  unlinkSync(statePath);
1880
2086
  process.stdout.write(`
1881
2087
  --keep: leaving \`${demoBranch}\` checked out for you to inspect. Clean up with:
@@ -1902,7 +2108,7 @@ async function demoBreakCommand(args) {
1902
2108
  }
1903
2109
 
1904
2110
  // src/commands/doctor.ts
1905
- import { existsSync as existsSync7, readFileSync as readFileSync4, statSync } from "node:fs";
2111
+ import { existsSync as existsSync8, readFileSync as readFileSync5, statSync } from "node:fs";
1906
2112
  import { join as join8 } from "node:path";
1907
2113
 
1908
2114
  // src/lib/artifacts.ts
@@ -2272,7 +2478,7 @@ function buildInitProductOps(name) {
2272
2478
  }
2273
2479
 
2274
2480
  // src/lib/ops.ts
2275
- import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
2481
+ import { chmodSync, existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
2276
2482
  import { dirname as dirname3, join as join7, resolve, sep } from "node:path";
2277
2483
  var MARKER_NS = "vinaya:managed";
2278
2484
  function markerLines(marker, comment) {
@@ -2300,9 +2506,9 @@ function containedAbs(repoRoot, relPath) {
2300
2506
  }
2301
2507
  function fileContains(repoRoot, relPath, needle) {
2302
2508
  const p = abs(repoRoot, relPath);
2303
- if (!existsSync6(p))
2509
+ if (!existsSync7(p))
2304
2510
  return false;
2305
- return readFileSync3(p, "utf-8").includes(needle);
2511
+ return readFileSync4(p, "utf-8").includes(needle);
2306
2512
  }
2307
2513
  function planInstall(ops, repoRoot, ownedFiles = new Set) {
2308
2514
  const entries = [];
@@ -2310,7 +2516,7 @@ function planInstall(ops, repoRoot, ownedFiles = new Set) {
2310
2516
  for (const op of ops) {
2311
2517
  switch (op.kind) {
2312
2518
  case "create-file": {
2313
- const exists = existsSync6(abs(repoRoot, op.path));
2519
+ const exists = existsSync7(abs(repoRoot, op.path));
2314
2520
  let action;
2315
2521
  if (!exists)
2316
2522
  action = "create";
@@ -2328,7 +2534,7 @@ function planInstall(ops, repoRoot, ownedFiles = new Set) {
2328
2534
  let action;
2329
2535
  if (fileContains(repoRoot, op.path, begin))
2330
2536
  action = "skip-present";
2331
- else if (existsSync6(abs(repoRoot, op.path)))
2537
+ else if (existsSync7(abs(repoRoot, op.path)))
2332
2538
  action = "append";
2333
2539
  else
2334
2540
  action = "create-host";
@@ -2412,7 +2618,7 @@ function writeFileWithDirs(target, content, mode) {
2412
2618
  }
2413
2619
  function appendBlock(repoRoot, op) {
2414
2620
  const target = abs(repoRoot, op.path);
2415
- const existing = readFileSync3(target, "utf-8");
2621
+ const existing = readFileSync4(target, "utf-8");
2416
2622
  const sep2 = existing.endsWith(`
2417
2623
  `) ? `
2418
2624
  ` : `
@@ -2523,7 +2729,7 @@ function planEject(manifest, repoRoot) {
2523
2729
  escapes.push(b.path);
2524
2730
  continue;
2525
2731
  }
2526
- if (!existsSync6(p)) {
2732
+ if (!existsSync7(p)) {
2527
2733
  actions.push({
2528
2734
  kind: "strip-block",
2529
2735
  path: b.path,
@@ -2534,7 +2740,7 @@ function planEject(manifest, repoRoot) {
2534
2740
  });
2535
2741
  continue;
2536
2742
  }
2537
- const content = readFileSync3(p, "utf-8");
2743
+ const content = readFileSync4(p, "utf-8");
2538
2744
  const stripped = stripBlockFromContent(content, b.marker, b.comment);
2539
2745
  const removesHost = stripped !== null && blockStripLeavesEmpty(stripped);
2540
2746
  actions.push({
@@ -2552,7 +2758,7 @@ function planEject(manifest, repoRoot) {
2552
2758
  escapes.push(f);
2553
2759
  continue;
2554
2760
  }
2555
- actions.push({ kind: "delete-file", path: f, present: existsSync6(p) });
2761
+ actions.push({ kind: "delete-file", path: f, present: existsSync7(p) });
2556
2762
  }
2557
2763
  for (const name of manifest.labels) {
2558
2764
  actions.push({ kind: "report-label", name });
@@ -2586,7 +2792,7 @@ function applyEject(plan, repoRoot) {
2586
2792
  const p = containedAbs(repoRoot, a.path);
2587
2793
  if (p === null)
2588
2794
  continue;
2589
- const content = readFileSync3(p, "utf-8");
2795
+ const content = readFileSync4(p, "utf-8");
2590
2796
  const stripped = stripBlockFromContent(content, a.marker, a.comment);
2591
2797
  if (stripped === null)
2592
2798
  continue;
@@ -2609,7 +2815,7 @@ function applyEject(plan, repoRoot) {
2609
2815
 
2610
2816
  // src/commands/doctor.ts
2611
2817
  function readVersion() {
2612
- const pkg = JSON.parse(readFileSync4(join8(packageRoot(import.meta.url), "package.json"), "utf-8"));
2818
+ const pkg = JSON.parse(readFileSync5(join8(packageRoot(import.meta.url), "package.json"), "utf-8"));
2613
2819
  return pkg.version;
2614
2820
  }
2615
2821
  function realDeps3() {
@@ -2629,11 +2835,11 @@ var warn = (check, message) => ({ check, severity: "warn", message });
2629
2835
  var error = (check, message) => ({ check, severity: "error", message });
2630
2836
  function readConfig(repoRoot) {
2631
2837
  const p = join8(repoRoot, CONFIG_PATH);
2632
- if (!existsSync7(p))
2838
+ if (!existsSync8(p))
2633
2839
  return { kind: "missing" };
2634
2840
  let raw;
2635
2841
  try {
2636
- raw = JSON.parse(readFileSync4(p, "utf-8"));
2842
+ raw = JSON.parse(readFileSync5(p, "utf-8"));
2637
2843
  } catch (err) {
2638
2844
  return { kind: "invalid", error: `invalid JSON: ${err.message}` };
2639
2845
  }
@@ -2663,13 +2869,13 @@ function diagnoseInstall(repoRoot, ctx, manifest) {
2663
2869
  if (op.kind === "create-file") {
2664
2870
  const check = labelForPath(op.path);
2665
2871
  const abs2 = join8(repoRoot, op.path);
2666
- const exists = existsSync7(abs2);
2872
+ const exists = existsSync8(abs2);
2667
2873
  const owned = ownedFiles.has(op.path);
2668
2874
  if (!exists) {
2669
2875
  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\`.`));
2670
2876
  continue;
2671
2877
  }
2672
- const content = readFileSync4(abs2, "utf-8");
2878
+ const content = readFileSync5(abs2, "utf-8");
2673
2879
  if (!owned) {
2674
2880
  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.`));
2675
2881
  continue;
@@ -2688,11 +2894,11 @@ function diagnoseInstall(repoRoot, ctx, manifest) {
2688
2894
  const check = "hooks";
2689
2895
  const abs2 = join8(repoRoot, op.path);
2690
2896
  const owned = ownedBlocks.has(blockKey(op.path, op.marker));
2691
- if (!existsSync7(abs2)) {
2897
+ if (!existsSync8(abs2)) {
2692
2898
  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.`));
2693
2899
  continue;
2694
2900
  }
2695
- const content = readFileSync4(abs2, "utf-8");
2901
+ const content = readFileSync5(abs2, "utf-8");
2696
2902
  const { begin, end } = markerLines(op.marker, op.comment);
2697
2903
  const hasMarkers = content.includes(begin) && content.includes(end);
2698
2904
  if (!hasMarkers) {
@@ -2718,7 +2924,20 @@ function diagnoseCustomChecks(repoRoot, config) {
2718
2924
  const findings = [];
2719
2925
  for (const [name, entry2] of Object.entries(config.checks ?? {})) {
2720
2926
  const scriptAbs = join8(repoRoot, entry2.run);
2721
- findings.push(existsSync7(scriptAbs) ? ok("checks", `custom check '${name}' → ${entry2.run}`) : error("checks", `custom check '${name}' points at a missing script: ${entry2.run}`));
2927
+ findings.push(existsSync8(scriptAbs) ? ok("checks", `custom check '${name}' → ${entry2.run}`) : error("checks", `custom check '${name}' points at a missing script: ${entry2.run}`));
2928
+ }
2929
+ return findings;
2930
+ }
2931
+ function diagnoseEnvDeclarations(repoRoot, config) {
2932
+ const customSpecs = Object.entries(config?.checks ?? {}).map(([name, entry2]) => ({
2933
+ name,
2934
+ ...entry2,
2935
+ run: join8(repoRoot, entry2.run)
2936
+ }));
2937
+ const missing = checksMissingEnvDeclaration([...coreCheckRegistry(), ...customSpecs]);
2938
+ const findings = missing.map((name) => info("env", envDeclarationWarning(name)));
2939
+ for (const message of lintEnvDeclarations(config?.checks)) {
2940
+ findings.push(warn("env", message));
2722
2941
  }
2723
2942
  return findings;
2724
2943
  }
@@ -2791,6 +3010,7 @@ async function runDoctor(args, deps) {
2791
3010
  hasDrift = install.hasDrift;
2792
3011
  findings.push(...diagnoseCustomChecks(repo.repoRoot, configRead.config));
2793
3012
  }
3013
+ findings.push(...diagnoseEnvDeclarations(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
2794
3014
  findings.push(...await diagnoseEnvironment(deps, hasDrift));
2795
3015
  findings.push(await diagnoseBranchProtection(deps, repo.owner, repo.repo));
2796
3016
  const healthy = findings.every((f) => f.severity === "ok" || f.severity === "info");
@@ -2806,7 +3026,7 @@ async function doctorCommand(args) {
2806
3026
  }
2807
3027
 
2808
3028
  // src/commands/eject.ts
2809
- import { existsSync as existsSync8, readFileSync as readFileSync5 } from "node:fs";
3029
+ import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
2810
3030
  import { join as join9 } from "node:path";
2811
3031
 
2812
3032
  // src/lib/prompt.ts
@@ -2889,11 +3109,11 @@ function parse(args) {
2889
3109
  }
2890
3110
  function readManifest(repoRoot) {
2891
3111
  const p = join9(repoRoot, CONFIG_PATH);
2892
- if (!existsSync8(p))
3112
+ if (!existsSync9(p))
2893
3113
  return { kind: "none" };
2894
3114
  let raw;
2895
3115
  try {
2896
- raw = JSON.parse(readFileSync5(p, "utf-8"));
3116
+ raw = JSON.parse(readFileSync6(p, "utf-8"));
2897
3117
  } catch (err) {
2898
3118
  return { kind: "orphan", reason: `vinaya.config.json is not valid JSON: ${err.message}` };
2899
3119
  }
@@ -2976,7 +3196,7 @@ async function ejectCommand(args) {
2976
3196
  }
2977
3197
 
2978
3198
  // src/commands/init.ts
2979
- import { existsSync as existsSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
3199
+ import { existsSync as existsSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
2980
3200
  import { join as join10 } from "node:path";
2981
3201
  function realDeps5() {
2982
3202
  return {
@@ -2998,17 +3218,17 @@ function flags(args) {
2998
3218
  var PRODUCT_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
2999
3219
  function readManifest2(repoRoot) {
3000
3220
  const p = join10(repoRoot, CONFIG_PATH);
3001
- if (!existsSync9(p))
3221
+ if (!existsSync10(p))
3002
3222
  return null;
3003
3223
  try {
3004
- return VinayaConfigSchema.parse(JSON.parse(readFileSync6(p, "utf-8"))).managed ?? null;
3224
+ return VinayaConfigSchema.parse(JSON.parse(readFileSync7(p, "utf-8"))).managed ?? null;
3005
3225
  } catch {
3006
3226
  return null;
3007
3227
  }
3008
3228
  }
3009
3229
  function writeManifest(repoRoot, manifest) {
3010
3230
  const configAbs = join10(repoRoot, CONFIG_PATH);
3011
- const seed = JSON.parse(readFileSync6(configAbs, "utf-8"));
3231
+ const seed = JSON.parse(readFileSync7(configAbs, "utf-8"));
3012
3232
  writeFileSync4(configAbs, `${JSON.stringify({ ...seed, managed: manifest }, null, 2)}
3013
3233
  `, "utf-8");
3014
3234
  }
@@ -3148,7 +3368,7 @@ import { execFileSync as execFileSync6 } from "node:child_process";
3148
3368
 
3149
3369
  // src/lib/forge-write.ts
3150
3370
  import { mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "node:fs";
3151
- import { readFileSync as readFileSync7 } from "node:fs";
3371
+ import { readFileSync as readFileSync8 } from "node:fs";
3152
3372
  import { tmpdir } from "node:os";
3153
3373
  import { join as join11 } from "node:path";
3154
3374
  class ForgeArgError extends Error {
@@ -3160,13 +3380,13 @@ function locateBody(args) {
3160
3380
  const p = args[i + 1];
3161
3381
  if (!p)
3162
3382
  throw new ForgeArgError("`--body-file` was given with no path.");
3163
- return { body: readFileSync7(p, "utf8"), source: { kind: "file", argIndex: i + 1, inlineForm: false } };
3383
+ return { body: readFileSync8(p, "utf8"), source: { kind: "file", argIndex: i + 1, inlineForm: false } };
3164
3384
  }
3165
3385
  if (a.startsWith("--body-file=")) {
3166
3386
  const p = a.slice("--body-file=".length);
3167
3387
  if (!p)
3168
3388
  throw new ForgeArgError("`--body-file=` was given with no path.");
3169
- return { body: readFileSync7(p, "utf8"), source: { kind: "file", argIndex: i, inlineForm: true } };
3389
+ return { body: readFileSync8(p, "utf8"), source: { kind: "file", argIndex: i, inlineForm: true } };
3170
3390
  }
3171
3391
  if (a === "--body" || a === "-b") {
3172
3392
  const v = args[i + 1];
@@ -3456,7 +3676,7 @@ function issueEditCommand(args) {
3456
3676
  }
3457
3677
 
3458
3678
  // src/commands/new-check.ts
3459
- import { chmodSync as chmodSync2, existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
3679
+ import { chmodSync as chmodSync2, existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "node:fs";
3460
3680
  import { join as join12 } from "node:path";
3461
3681
  var TEMPLATE_PATH = join12(packageRoot(import.meta.url), "templates", "custom-check.template.ts");
3462
3682
  var CHECKS_DIR = join12("scripts", "vinaya-checks");
@@ -3468,14 +3688,14 @@ function newCheckCommand(args) {
3468
3688
  process.exit(2);
3469
3689
  }
3470
3690
  const scriptsDir = join12(process.cwd(), CHECKS_DIR);
3471
- if (!existsSync10(scriptsDir))
3691
+ if (!existsSync11(scriptsDir))
3472
3692
  mkdirSync3(scriptsDir, { recursive: true });
3473
3693
  const targetPath = join12(scriptsDir, `${name}.ts`);
3474
- if (existsSync10(targetPath)) {
3694
+ if (existsSync11(targetPath)) {
3475
3695
  console.error(`Error: ${targetPath} already exists.`);
3476
3696
  process.exit(1);
3477
3697
  }
3478
- const template = readFileSync8(TEMPLATE_PATH, "utf-8");
3698
+ const template = readFileSync9(TEMPLATE_PATH, "utf-8");
3479
3699
  const contents = template.split("{{CHECK_NAME}}").join(name);
3480
3700
  writeFileSync6(targetPath, contents, "utf-8");
3481
3701
  chmodSync2(targetPath, 493);
@@ -3639,16 +3859,16 @@ function prEditCommand(args) {
3639
3859
 
3640
3860
  // src/commands/studio.ts
3641
3861
  import { spawn as spawn2 } from "node:child_process";
3642
- import { existsSync as existsSync11, readFileSync as readFileSync9 } from "node:fs";
3862
+ import { existsSync as existsSync12, readFileSync as readFileSync10 } from "node:fs";
3643
3863
  import { dirname as dirname4, join as join13 } from "node:path";
3644
3864
  function resolveStudioTarget(cwd) {
3645
3865
  let dir = cwd;
3646
3866
  for (;; ) {
3647
3867
  const webDir = join13(dir, "apps", "vinaya", "web");
3648
3868
  const pkgPath = join13(webDir, "package.json");
3649
- if (existsSync11(pkgPath)) {
3869
+ if (existsSync12(pkgPath)) {
3650
3870
  try {
3651
- const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
3871
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf-8"));
3652
3872
  if (pkg.name === "@atta/vinaya-web") {
3653
3873
  return { kind: "workspace", webDir };
3654
3874
  }
@@ -3681,7 +3901,7 @@ async function runStudio(cwd, args) {
3681
3901
  }
3682
3902
 
3683
3903
  // src/commands/upgrade.ts
3684
- import { existsSync as existsSync12, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "node:fs";
3904
+ import { existsSync as existsSync13, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "node:fs";
3685
3905
  import { join as join14 } from "node:path";
3686
3906
  function realDeps6() {
3687
3907
  return {
@@ -3699,11 +3919,11 @@ function flags2(args) {
3699
3919
  }
3700
3920
  function readManifest3(repoRoot) {
3701
3921
  const p = join14(repoRoot, CONFIG_PATH);
3702
- if (!existsSync12(p))
3922
+ if (!existsSync13(p))
3703
3923
  return { kind: "missing" };
3704
3924
  let raw;
3705
3925
  try {
3706
- raw = JSON.parse(readFileSync10(p, "utf-8"));
3926
+ raw = JSON.parse(readFileSync11(p, "utf-8"));
3707
3927
  } catch (err) {
3708
3928
  return { kind: "invalid", error: `invalid JSON: ${err.message}` };
3709
3929
  }
@@ -3720,7 +3940,7 @@ function readManifest3(repoRoot) {
3720
3940
  }
3721
3941
  function writeManifestVersion(repoRoot, manifest) {
3722
3942
  const configAbs = join14(repoRoot, CONFIG_PATH);
3723
- const seed = JSON.parse(readFileSync10(configAbs, "utf-8"));
3943
+ const seed = JSON.parse(readFileSync11(configAbs, "utf-8"));
3724
3944
  const updated = { ...manifest, version: MANAGED_MANIFEST_VERSION };
3725
3945
  writeFileSync7(configAbs, `${JSON.stringify({ ...seed, managed: updated }, null, 2)}
3726
3946
  `, "utf-8");
@@ -3734,7 +3954,7 @@ function planUpgrade(ops, repoRoot, manifest) {
3734
3954
  for (const op of ops) {
3735
3955
  if (op.kind === "create-file") {
3736
3956
  const abs2 = join14(repoRoot, op.path);
3737
- const exists = existsSync12(abs2);
3957
+ const exists = existsSync13(abs2);
3738
3958
  const owned = ownedFiles.has(op.path);
3739
3959
  let action;
3740
3960
  if (op.path === CONFIG_PATH) {
@@ -3744,7 +3964,7 @@ function planUpgrade(ops, repoRoot, manifest) {
3744
3964
  } else if (!exists) {
3745
3965
  action = "recreate";
3746
3966
  hasChanges = true;
3747
- } else if (readFileSync10(abs2, "utf-8") !== op.content) {
3967
+ } else if (readFileSync11(abs2, "utf-8") !== op.content) {
3748
3968
  action = "regenerate";
3749
3969
  hasChanges = true;
3750
3970
  } else {
@@ -3757,11 +3977,11 @@ function planUpgrade(ops, repoRoot, manifest) {
3757
3977
  let action;
3758
3978
  if (!owned) {
3759
3979
  action = "not-installed";
3760
- } else if (!existsSync12(abs2)) {
3980
+ } else if (!existsSync13(abs2)) {
3761
3981
  action = "recreate-host";
3762
3982
  hasChanges = true;
3763
3983
  } else {
3764
- const content = readFileSync10(abs2, "utf-8");
3984
+ const content = readFileSync11(abs2, "utf-8");
3765
3985
  const { begin, end } = markerLines(op.marker, op.comment);
3766
3986
  if (!(content.includes(begin) && content.includes(end))) {
3767
3987
  action = "recreate-append";
@@ -3838,7 +4058,7 @@ function renderUpgradeDiff(plan) {
3838
4058
  }
3839
4059
  function regenerateBlock(repoRoot, op) {
3840
4060
  const abs2 = join14(repoRoot, op.path);
3841
- const content = readFileSync10(abs2, "utf-8");
4061
+ const content = readFileSync11(abs2, "utf-8");
3842
4062
  const stripped = stripBlockFromContent(content, op.marker, op.comment);
3843
4063
  if (stripped !== null) {
3844
4064
  writeFileSync7(abs2, stripped.endsWith(`
@@ -4079,6 +4299,9 @@ var COMMANDS = [
4079
4299
  { flag: "--diff-only", description: "Scope diff-declared checks to changed files" },
4080
4300
  { flag: "--parallel[=n]", description: "Concurrency cap (default: cpu-derived)" }
4081
4301
  ],
4302
+ details: [
4303
+ "Warns (stderr, every output mode, never affecting the exit code) for any check whose executable reads `process.env`/`Bun.env`/`Deno.env` directly with no `env` declared on its `CheckSpec` — those checks will lose environment access once the env allowlist is wired as the spawn default in a later minor. Declare `env` (a core check's own registration, or `vinaya.config.json`'s `checks.<name>.env` for a custom one) to silence the warning ahead of that flip."
4304
+ ],
4082
4305
  status: "shipped"
4083
4306
  },
4084
4307
  {
@@ -4148,6 +4371,9 @@ var COMMANDS = [
4148
4371
  name: "doctor",
4149
4372
  description: "Diagnose hook, workflow, and config health — report only, never mutates",
4150
4373
  flags: [{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }],
4374
+ details: [
4375
+ 'Carries the same env-declaration diagnostic `vinaya check` warns with — permanently, at `info` severity, not just ahead of the spawn-default flip — plus a `warn`-severity lint over suspicious `env` literal forms (a stray `"true"`/`"false"` string, or a high-entropy literal that reads like a leaked secret committed to config).'
4376
+ ],
4151
4377
  status: "shipped"
4152
4378
  },
4153
4379
  {
@@ -4252,7 +4478,7 @@ function printHelp() {
4252
4478
  // src/index.ts
4253
4479
  var PACKAGE_ROOT = join15(dirname5(fileURLToPath2(import.meta.url)), "..");
4254
4480
  function readVersion2() {
4255
- const pkg = JSON.parse(readFileSync11(join15(PACKAGE_ROOT, "package.json"), "utf-8"));
4481
+ const pkg = JSON.parse(readFileSync12(join15(PACKAGE_ROOT, "package.json"), "utf-8"));
4256
4482
  return pkg.version;
4257
4483
  }
4258
4484
  var [, , command, ...args] = process.argv;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@attalabs/vinaya",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Vinaya — Agentic Engineering Harness. Deterministic checks every AI coding agent must satisfy before merge.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -7,6 +7,16 @@
7
7
  * never sleep past it inside this file. This file is standalone (no import
8
8
  * from the vinaya CLI's own source tree) because it lives in YOUR repo, not
9
9
  * inside `@attalabs/vinaya`.
10
+ *
11
+ * If this check reads `process.env` directly, declare which variables it
12
+ * needs on its REGISTRATION in `vinaya.config.json` (not in this file —
13
+ * there is no CheckSpec type to attach it to here) via `checks.{{CHECK_NAME}}.env`.
14
+ * Four forms: `"MY_VAR": true` forwards your value verbatim; `{ "optional":
15
+ * true }` forwards it if set, tolerating absence; `{ "anyOf": ["A", "B"] }`
16
+ * forwards whichever of those you've set, each under its own name; a
17
+ * literal string sets the key to that exact value. Example:
18
+ * { "checks": { "{{CHECK_NAME}}": { "run": "...", "scope": "diff",
19
+ * "env": { "MY_TOKEN": { "optional": true } } } } }
10
20
  */
11
21
 
12
22
  type CheckError = {