@attalabs/vinaya 0.2.0 → 0.4.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/aeg-root/enforcement.md +5 -4
- package/aeg-root/glossary.md +4 -0
- package/aeg-root/roles/archivist.md +1 -1
- package/aeg-root/roles/developer.md +2 -2
- package/aeg-root/roles/reviewer.md +1 -1
- package/aeg-root/roles/security.md +1 -1
- package/aeg-root/state-machine.md +17 -3
- package/aeg-root/tranche-model.md +5 -3
- package/dist/checks/bin/check-branch-topology.js +76 -14
- package/dist/checks/bin/check-brief-shape.js +76 -14
- package/dist/checks/bin/check-closes-n.js +76 -14
- package/dist/checks/bin/check-coherence.js +76 -14
- package/dist/checks/bin/check-dead-branch-push.js +76 -14
- package/dist/checks/bin/check-dispatch-readiness.js +76 -14
- package/dist/checks/bin/check-doc-coverage-push.js +76 -14
- package/dist/checks/bin/check-doc-coverage.js +76 -14
- package/dist/checks/bin/check-first-push-dispatch.js +76 -14
- package/dist/checks/bin/check-issue-assignment.js +76 -14
- package/dist/checks/bin/check-no-disk-state.js +76 -14
- package/dist/checks/bin/check-reader-resolvable-prose.js +76 -14
- package/dist/checks/bin/check-registry-gates.js +76 -14
- package/dist/checks/bin/check-review-gate.js +81 -15
- package/dist/checks/bin/check-single-plan-pr.js +76 -14
- package/dist/checks/bin/check-test-plan.js +76 -14
- package/dist/index.js +427 -68
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1334,7 +1334,9 @@ function coreCheckRegistry() {
|
|
|
1334
1334
|
timeoutMs: 15000,
|
|
1335
1335
|
env: {
|
|
1336
1336
|
BASE_SHA: { optional: true },
|
|
1337
|
-
PR_NUMBER: { optional: true }
|
|
1337
|
+
PR_NUMBER: { optional: true },
|
|
1338
|
+
GITHUB_TOKEN: { optional: true },
|
|
1339
|
+
GH_TOKEN: { optional: true }
|
|
1338
1340
|
}
|
|
1339
1341
|
},
|
|
1340
1342
|
{
|
|
@@ -1359,7 +1361,11 @@ function coreCheckRegistry() {
|
|
|
1359
1361
|
run: bin("check-registry-gates"),
|
|
1360
1362
|
scope: "full",
|
|
1361
1363
|
timeoutMs: 30000,
|
|
1362
|
-
env: {
|
|
1364
|
+
env: {
|
|
1365
|
+
AEG_REPO: { optional: true },
|
|
1366
|
+
GITHUB_TOKEN: { optional: true },
|
|
1367
|
+
GH_TOKEN: { optional: true }
|
|
1368
|
+
}
|
|
1363
1369
|
},
|
|
1364
1370
|
{
|
|
1365
1371
|
name: "review-gate",
|
|
@@ -1368,7 +1374,9 @@ function coreCheckRegistry() {
|
|
|
1368
1374
|
timeoutMs: 30000,
|
|
1369
1375
|
env: {
|
|
1370
1376
|
BRANCH: { optional: true },
|
|
1371
|
-
PR_NUMBER: { optional: true }
|
|
1377
|
+
PR_NUMBER: { optional: true },
|
|
1378
|
+
GITHUB_TOKEN: { optional: true },
|
|
1379
|
+
GH_TOKEN: { optional: true }
|
|
1372
1380
|
}
|
|
1373
1381
|
},
|
|
1374
1382
|
{
|
|
@@ -1378,7 +1386,9 @@ function coreCheckRegistry() {
|
|
|
1378
1386
|
timeoutMs: 30000,
|
|
1379
1387
|
env: {
|
|
1380
1388
|
BRANCH: { optional: true },
|
|
1381
|
-
AEG_REPO: { optional: true }
|
|
1389
|
+
AEG_REPO: { optional: true },
|
|
1390
|
+
GITHUB_TOKEN: { optional: true },
|
|
1391
|
+
GH_TOKEN: { optional: true }
|
|
1382
1392
|
}
|
|
1383
1393
|
},
|
|
1384
1394
|
{
|
|
@@ -1386,7 +1396,11 @@ function coreCheckRegistry() {
|
|
|
1386
1396
|
run: bin("check-dead-branch-push"),
|
|
1387
1397
|
scope: "full",
|
|
1388
1398
|
timeoutMs: 30000,
|
|
1389
|
-
env: {
|
|
1399
|
+
env: {
|
|
1400
|
+
BRANCH: { optional: true },
|
|
1401
|
+
GITHUB_TOKEN: { optional: true },
|
|
1402
|
+
GH_TOKEN: { optional: true }
|
|
1403
|
+
}
|
|
1390
1404
|
},
|
|
1391
1405
|
{
|
|
1392
1406
|
name: "first-push-dispatch",
|
|
@@ -1421,12 +1435,62 @@ function coreCheckRegistry() {
|
|
|
1421
1435
|
timeoutMs: 30000,
|
|
1422
1436
|
env: {
|
|
1423
1437
|
AEG_REPO: { optional: true },
|
|
1424
|
-
BRANCH: { optional: true }
|
|
1438
|
+
BRANCH: { optional: true },
|
|
1439
|
+
GITHUB_TOKEN: { optional: true },
|
|
1440
|
+
GH_TOKEN: { optional: true }
|
|
1425
1441
|
}
|
|
1426
1442
|
}
|
|
1427
1443
|
];
|
|
1428
1444
|
}
|
|
1429
1445
|
|
|
1446
|
+
// src/checks/resolver.ts
|
|
1447
|
+
var NAMESPACE_SEGMENT = /^[a-z0-9][a-z0-9-]*$/;
|
|
1448
|
+
var RESERVED_PREFIX = "vinaya";
|
|
1449
|
+
function isValidNamespacedKey(key) {
|
|
1450
|
+
const parts = key.split("/");
|
|
1451
|
+
if (parts.length !== 2)
|
|
1452
|
+
return false;
|
|
1453
|
+
const [prefix, rest] = parts;
|
|
1454
|
+
if (!prefix || !rest)
|
|
1455
|
+
return false;
|
|
1456
|
+
if (!NAMESPACE_SEGMENT.test(prefix) || !NAMESPACE_SEGMENT.test(rest))
|
|
1457
|
+
return false;
|
|
1458
|
+
if (prefix === RESERVED_PREFIX)
|
|
1459
|
+
return false;
|
|
1460
|
+
return true;
|
|
1461
|
+
}
|
|
1462
|
+
function resolveChecks(core, configChecks) {
|
|
1463
|
+
const resolved = core.map((spec) => ({ name: spec.name, state: "default", source: "core", spec }));
|
|
1464
|
+
const failures = [];
|
|
1465
|
+
if (!configChecks)
|
|
1466
|
+
return { resolved, failures };
|
|
1467
|
+
const indexByName = new Map(resolved.map((entry2, index) => [entry2.name, index]));
|
|
1468
|
+
for (const [key, entry2] of Object.entries(configChecks)) {
|
|
1469
|
+
const coreIndex = indexByName.get(key);
|
|
1470
|
+
if (coreIndex !== undefined) {
|
|
1471
|
+
resolved[coreIndex] = {
|
|
1472
|
+
name: key,
|
|
1473
|
+
state: "overridden",
|
|
1474
|
+
source: "config",
|
|
1475
|
+
spec: { name: key, ...entry2 }
|
|
1476
|
+
};
|
|
1477
|
+
continue;
|
|
1478
|
+
}
|
|
1479
|
+
if (isValidNamespacedKey(key)) {
|
|
1480
|
+
resolved.push({ name: key, state: "additive", source: "config", spec: { name: key, ...entry2 } });
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
failures.push({ key, reason: 'bare key has no "/" and matches no core check id' });
|
|
1484
|
+
}
|
|
1485
|
+
return { resolved, failures };
|
|
1486
|
+
}
|
|
1487
|
+
function overriddenNextMinorWarning(name) {
|
|
1488
|
+
return `check "${name}" shares its name with a core check — it will replace the core check starting next minor (currently it runs alongside it under the flat registry).`;
|
|
1489
|
+
}
|
|
1490
|
+
function bareKeyNextMinorWarning(key) {
|
|
1491
|
+
return `check "${key}" has no namespace and matches no core check — it will be rejected starting next minor. Rename it to "<yourname>/x".`;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1430
1494
|
// src/checks/runner.ts
|
|
1431
1495
|
import { spawn } from "node:child_process";
|
|
1432
1496
|
import { cpus } from "node:os";
|
|
@@ -1470,11 +1534,79 @@ function installSignalForwarding() {
|
|
|
1470
1534
|
process.on("SIGINT", () => forward("SIGINT", 130));
|
|
1471
1535
|
process.on("SIGTERM", () => forward("SIGTERM", 143));
|
|
1472
1536
|
}
|
|
1473
|
-
|
|
1537
|
+
var ENV_BASELINE_KEYS = ["PATH", "LANG", "HOME", "HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY", "TMPDIR"];
|
|
1538
|
+
function buildCheckEnv(env, callerEnv = process.env) {
|
|
1539
|
+
const out = {};
|
|
1540
|
+
for (const key of ENV_BASELINE_KEYS) {
|
|
1541
|
+
const value = callerEnv[key];
|
|
1542
|
+
if (value !== undefined)
|
|
1543
|
+
out[key] = value;
|
|
1544
|
+
}
|
|
1545
|
+
if (!env)
|
|
1546
|
+
return out;
|
|
1547
|
+
for (const [key, decl] of Object.entries(env)) {
|
|
1548
|
+
if (decl === true || typeof decl === "object" && "optional" in decl) {
|
|
1549
|
+
const value = callerEnv[key];
|
|
1550
|
+
if (value !== undefined)
|
|
1551
|
+
out[key] = value;
|
|
1552
|
+
} else if (typeof decl === "object" && "anyOf" in decl) {
|
|
1553
|
+
for (const member of decl.anyOf) {
|
|
1554
|
+
const value = callerEnv[member];
|
|
1555
|
+
if (value !== undefined)
|
|
1556
|
+
out[member] = value;
|
|
1557
|
+
}
|
|
1558
|
+
} else if (typeof decl === "string") {
|
|
1559
|
+
out[key] = decl;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
return out;
|
|
1563
|
+
}
|
|
1564
|
+
function missingEnvErrors(spec, callerEnv) {
|
|
1565
|
+
if (!spec.env)
|
|
1566
|
+
return [];
|
|
1567
|
+
const errors = [];
|
|
1568
|
+
for (const [key, decl] of Object.entries(spec.env)) {
|
|
1569
|
+
if (decl === true) {
|
|
1570
|
+
if (callerEnv[key] === undefined) {
|
|
1571
|
+
errors.push({
|
|
1572
|
+
schema: CHECK_SCHEMA_VERSION,
|
|
1573
|
+
check: spec.name,
|
|
1574
|
+
severity: "error",
|
|
1575
|
+
message: `Could not run check "${spec.name}": required environment variable \`${key}\` is not set.`,
|
|
1576
|
+
agent_recovery_prompt: `Set \`${key}\` in the environment before re-running \`vinaya check ${spec.name}\`, or relax its declaration to \`{ optional: true }\` in its \`env\` registration if the check can tolerate absence.`
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
} else if (typeof decl === "object" && decl !== null && "anyOf" in decl) {
|
|
1580
|
+
const satisfied = decl.anyOf.some((member) => callerEnv[member] !== undefined);
|
|
1581
|
+
if (!satisfied) {
|
|
1582
|
+
errors.push({
|
|
1583
|
+
schema: CHECK_SCHEMA_VERSION,
|
|
1584
|
+
check: spec.name,
|
|
1585
|
+
severity: "error",
|
|
1586
|
+
message: `Could not run check "${spec.name}": none of its required environment variables (${decl.anyOf.join(", ")}) are set.`,
|
|
1587
|
+
agent_recovery_prompt: `Set one of ${decl.anyOf.join(", ")} in the environment before re-running \`vinaya check ${spec.name}\`.`
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
return errors;
|
|
1593
|
+
}
|
|
1594
|
+
async function runOne(spec, timeoutMs, callerEnv) {
|
|
1474
1595
|
const start = performance.now();
|
|
1596
|
+
const envErrors = missingEnvErrors(spec, callerEnv);
|
|
1597
|
+
if (envErrors.length > 0) {
|
|
1598
|
+
return {
|
|
1599
|
+
name: spec.name,
|
|
1600
|
+
status: "error",
|
|
1601
|
+
exitCode: null,
|
|
1602
|
+
errors: envErrors,
|
|
1603
|
+
durationMs: performance.now() - start
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1475
1606
|
const proc = spawn(spec.run, spec.args ?? [], {
|
|
1476
1607
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1477
|
-
detached: true
|
|
1608
|
+
detached: true,
|
|
1609
|
+
env: buildCheckEnv(spec.env, callerEnv)
|
|
1478
1610
|
});
|
|
1479
1611
|
function killTree(signal) {
|
|
1480
1612
|
const pid = proc.pid;
|
|
@@ -1590,6 +1722,7 @@ async function runOne(spec, timeoutMs) {
|
|
|
1590
1722
|
return { name: spec.name, status, exitCode, errors, durationMs };
|
|
1591
1723
|
}
|
|
1592
1724
|
async function runChecks(specs, opts) {
|
|
1725
|
+
const callerEnv = opts.callerEnv ?? process.env;
|
|
1593
1726
|
const results = new Array(specs.length);
|
|
1594
1727
|
const toRun = [];
|
|
1595
1728
|
for (let i = 0;i < specs.length; i++) {
|
|
@@ -1606,7 +1739,7 @@ async function runChecks(specs, opts) {
|
|
|
1606
1739
|
const idx = toRun[cursor];
|
|
1607
1740
|
cursor += 1;
|
|
1608
1741
|
const spec = specs[idx];
|
|
1609
|
-
results[idx] = await runOne(spec, spec.timeoutMs ?? opts.defaultTimeoutMs);
|
|
1742
|
+
results[idx] = await runOne(spec, spec.timeoutMs ?? opts.defaultTimeoutMs, callerEnv);
|
|
1610
1743
|
}
|
|
1611
1744
|
};
|
|
1612
1745
|
const workerCount = Math.max(1, Math.min(opts.parallel, toRun.length));
|
|
@@ -1763,6 +1896,15 @@ function configPath() {
|
|
|
1763
1896
|
return GLOBAL_CONFIG_PATH;
|
|
1764
1897
|
return null;
|
|
1765
1898
|
}
|
|
1899
|
+
function globalChecksIgnoredWarning(path) {
|
|
1900
|
+
return `${path}: "checks" registration in the global config is ignored — checks may only be registered from a repo-local vinaya.config.json.`;
|
|
1901
|
+
}
|
|
1902
|
+
function stripGlobalChecks(config, path) {
|
|
1903
|
+
if (path !== GLOBAL_CONFIG_PATH || !config.checks || Object.keys(config.checks).length === 0)
|
|
1904
|
+
return config;
|
|
1905
|
+
console.error(`⚠ ${globalChecksIgnoredWarning(path)}`);
|
|
1906
|
+
return { ...config, checks: undefined };
|
|
1907
|
+
}
|
|
1766
1908
|
function loadConfigChecked() {
|
|
1767
1909
|
const path = configPath();
|
|
1768
1910
|
if (!path)
|
|
@@ -1778,32 +1920,7 @@ function loadConfigChecked() {
|
|
|
1778
1920
|
const detail = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
|
|
1779
1921
|
return { ok: false, path, error: detail };
|
|
1780
1922
|
}
|
|
1781
|
-
return { ok: true, config: parsed.data };
|
|
1782
|
-
}
|
|
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).`;
|
|
1923
|
+
return { ok: true, config: stripGlobalChecks(parsed.data, path) };
|
|
1807
1924
|
}
|
|
1808
1925
|
|
|
1809
1926
|
// src/commands/check.ts
|
|
@@ -1843,34 +1960,118 @@ function configErrorOutcome(path, error) {
|
|
|
1843
1960
|
function customSpecsFromConfig() {
|
|
1844
1961
|
const result = loadConfigChecked();
|
|
1845
1962
|
if (!result.ok)
|
|
1846
|
-
return { specs: [], errorOutcome: configErrorOutcome(result.path, result.error) };
|
|
1963
|
+
return { specs: [], errorOutcome: configErrorOutcome(result.path, result.error), checks: undefined };
|
|
1847
1964
|
const checks = result.config?.checks;
|
|
1848
1965
|
if (!checks)
|
|
1849
|
-
return { specs: [], errorOutcome: null };
|
|
1966
|
+
return { specs: [], errorOutcome: null, checks: undefined };
|
|
1850
1967
|
const specs = Object.entries(checks).map(([name, entry2]) => ({ name, ...entry2 }));
|
|
1851
|
-
return { specs, errorOutcome: null };
|
|
1968
|
+
return { specs, errorOutcome: null, checks };
|
|
1969
|
+
}
|
|
1970
|
+
function classifyEnvValue(value) {
|
|
1971
|
+
if (value === true)
|
|
1972
|
+
return "passthrough";
|
|
1973
|
+
if (typeof value === "string")
|
|
1974
|
+
return "literal";
|
|
1975
|
+
if ("anyOf" in value)
|
|
1976
|
+
return "anyOf";
|
|
1977
|
+
return "optional";
|
|
1978
|
+
}
|
|
1979
|
+
function renderPlanJson(result) {
|
|
1980
|
+
const checks = {};
|
|
1981
|
+
for (const entry2 of result.resolved) {
|
|
1982
|
+
const env = {};
|
|
1983
|
+
let envAnyOf;
|
|
1984
|
+
for (const [key, value] of Object.entries(entry2.spec.env ?? {})) {
|
|
1985
|
+
env[key] = classifyEnvValue(value);
|
|
1986
|
+
if (typeof value === "object" && value !== null && "anyOf" in value) {
|
|
1987
|
+
envAnyOf ??= {};
|
|
1988
|
+
envAnyOf[key] = value.anyOf;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
checks[entry2.name] = { state: entry2.state, source: entry2.source, env, envAnyOf, scope: entry2.spec.scope };
|
|
1992
|
+
}
|
|
1993
|
+
return {
|
|
1994
|
+
schema: 1,
|
|
1995
|
+
checks,
|
|
1996
|
+
roles: { available: false, reason: "role resolution not implemented yet — see task 6" },
|
|
1997
|
+
errors: result.failures
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
2000
|
+
function envCellFor(spec) {
|
|
2001
|
+
const entries = Object.entries(spec.env ?? {});
|
|
2002
|
+
if (entries.length === 0)
|
|
2003
|
+
return "—";
|
|
2004
|
+
return entries.map(([key, value]) => {
|
|
2005
|
+
if (value === true)
|
|
2006
|
+
return key;
|
|
2007
|
+
if (typeof value === "string")
|
|
2008
|
+
return `${key}=<redacted>`;
|
|
2009
|
+
if ("anyOf" in value)
|
|
2010
|
+
return value.anyOf.join("|");
|
|
2011
|
+
return `${key}?`;
|
|
2012
|
+
}).join(", ");
|
|
2013
|
+
}
|
|
2014
|
+
function renderPlanTable(result) {
|
|
2015
|
+
const header = ["NAME", "STATE", "SOURCE", "ENV"];
|
|
2016
|
+
const rows = result.resolved.map((entry2) => [entry2.name, entry2.state, entry2.source, envCellFor(entry2.spec)]);
|
|
2017
|
+
const failureRows = result.failures.map((f) => [f.key, "FAILED", "—", f.reason]);
|
|
2018
|
+
const table = [header, ...rows, ...failureRows];
|
|
2019
|
+
const widths = header.map((_, col) => Math.max(...table.map((row) => row[col]?.length ?? 0)));
|
|
2020
|
+
return table.map((row) => row.map((cell, col) => (cell ?? "").padEnd(widths[col] ?? 0)).join(" ")).join(`
|
|
2021
|
+
`);
|
|
2022
|
+
}
|
|
2023
|
+
function resolveForPlan(configResult) {
|
|
2024
|
+
if (configResult.ok) {
|
|
2025
|
+
return resolveChecks(coreCheckRegistry(), configResult.config?.checks);
|
|
2026
|
+
}
|
|
2027
|
+
const core = resolveChecks(coreCheckRegistry(), undefined);
|
|
2028
|
+
return {
|
|
2029
|
+
resolved: core.resolved,
|
|
2030
|
+
failures: [{ key: configResult.path, reason: `invalid \`checks\` registration — ${configResult.error}` }]
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
function printClassificationWarnings(configChecks) {
|
|
2034
|
+
const classification = resolveChecks(coreCheckRegistry(), configChecks);
|
|
2035
|
+
for (const entry2 of classification.resolved) {
|
|
2036
|
+
if (entry2.state === "overridden")
|
|
2037
|
+
process.stdout.write(`${overriddenNextMinorWarning(entry2.name)}
|
|
2038
|
+
`);
|
|
2039
|
+
}
|
|
2040
|
+
for (const failure of classification.failures) {
|
|
2041
|
+
process.stdout.write(`${bareKeyNextMinorWarning(failure.key)}
|
|
2042
|
+
`);
|
|
2043
|
+
}
|
|
1852
2044
|
}
|
|
1853
2045
|
async function checkCommand(args) {
|
|
1854
2046
|
const jsonOutput = args.includes("--json");
|
|
1855
2047
|
const diffOnly = args.includes("--diff-only");
|
|
1856
2048
|
const requestedParallel = parseParallel(args);
|
|
1857
2049
|
const allRequested = args.includes("--all");
|
|
2050
|
+
const planRequested = args.includes("--plan");
|
|
1858
2051
|
const positional = args.filter((a) => !a.startsWith("--"));
|
|
1859
2052
|
const requestedName = positional[0];
|
|
2053
|
+
if (planRequested) {
|
|
2054
|
+
const result = resolveForPlan(loadConfigChecked());
|
|
2055
|
+
if (jsonOutput) {
|
|
2056
|
+
process.stdout.write(`${JSON.stringify(renderPlanJson(result), null, 2)}
|
|
2057
|
+
`);
|
|
2058
|
+
} else {
|
|
2059
|
+
process.stdout.write(`${renderPlanTable(result)}
|
|
2060
|
+
`);
|
|
2061
|
+
}
|
|
2062
|
+
process.exit(result.failures.length > 0 ? 1 : 0);
|
|
2063
|
+
}
|
|
1860
2064
|
if (!allRequested && !requestedName) {
|
|
1861
|
-
console.error("Usage: vinaya check <name> | --all [--json] [--diff-only] [--parallel[=n]]");
|
|
2065
|
+
console.error("Usage: vinaya check <name> | --all | --plan [--json] [--diff-only] [--parallel[=n]]");
|
|
1862
2066
|
process.exit(2);
|
|
1863
2067
|
}
|
|
1864
|
-
const { specs: customSpecs, errorOutcome } = customSpecsFromConfig();
|
|
2068
|
+
const { specs: customSpecs, errorOutcome, checks: configChecks } = customSpecsFromConfig();
|
|
1865
2069
|
const allSpecs = [...coreCheckRegistry(), ...customSpecs];
|
|
1866
2070
|
const specsToRun = allRequested ? allSpecs : allSpecs.filter((s) => s.name === requestedName);
|
|
1867
2071
|
if (!allRequested && specsToRun.length === 0) {
|
|
1868
2072
|
console.error(`Unknown check: ${requestedName}`);
|
|
1869
2073
|
process.exit(2);
|
|
1870
2074
|
}
|
|
1871
|
-
for (const name of checksMissingEnvDeclaration(specsToRun)) {
|
|
1872
|
-
console.error(`⚠ ${envDeclarationWarning(name)}`);
|
|
1873
|
-
}
|
|
1874
2075
|
const changed = diffOnly ? changedFiles() : null;
|
|
1875
2076
|
const outcomes = specsToRun.length > 0 ? await runChecks(specsToRun, {
|
|
1876
2077
|
parallel: requestedParallel ?? defaultParallelism(),
|
|
@@ -1894,6 +2095,7 @@ async function checkCommand(args) {
|
|
|
1894
2095
|
process.stdout.write(` ${e.severity}: ${e.message}
|
|
1895
2096
|
`);
|
|
1896
2097
|
}
|
|
2098
|
+
printClassificationWarnings(configChecks);
|
|
1897
2099
|
}
|
|
1898
2100
|
const failed = allOutcomes.some((o) => o.status === "fail" || o.status === "error" || o.status === "timeout");
|
|
1899
2101
|
process.exit(failed ? 1 : 0);
|
|
@@ -1901,7 +2103,7 @@ async function checkCommand(args) {
|
|
|
1901
2103
|
|
|
1902
2104
|
// src/commands/demo.ts
|
|
1903
2105
|
import { execFileSync as execFileSync5, spawnSync } from "node:child_process";
|
|
1904
|
-
import { existsSync as
|
|
2106
|
+
import { existsSync as existsSync5, readFileSync as readFileSync2, rmSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1905
2107
|
import { isAbsolute, join as join5 } from "node:path";
|
|
1906
2108
|
var DEMO_BRANCH_PREFIX = "vinaya/demo-break-";
|
|
1907
2109
|
var FIXTURE_PATH = ".vinaya-demo-brief.md";
|
|
@@ -1944,9 +2146,9 @@ function gitDirAbs(repoRoot) {
|
|
|
1944
2146
|
function recoverFromCrash(repoRoot, statePath) {
|
|
1945
2147
|
const currentBranch = currentBranchName(repoRoot);
|
|
1946
2148
|
let state = null;
|
|
1947
|
-
if (
|
|
2149
|
+
if (existsSync5(statePath)) {
|
|
1948
2150
|
try {
|
|
1949
|
-
state = JSON.parse(
|
|
2151
|
+
state = JSON.parse(readFileSync2(statePath, "utf-8"));
|
|
1950
2152
|
} catch {
|
|
1951
2153
|
state = null;
|
|
1952
2154
|
}
|
|
@@ -2016,14 +2218,14 @@ function cleanup(repoRoot, originalBranch, demoBranch, statePath) {
|
|
|
2016
2218
|
gitCapture(repoRoot, ["clean", "-fd"]);
|
|
2017
2219
|
gitCapture(repoRoot, ["checkout", originalBranch]);
|
|
2018
2220
|
gitCapture(repoRoot, ["branch", "-D", demoBranch]);
|
|
2019
|
-
if (
|
|
2221
|
+
if (existsSync5(statePath))
|
|
2020
2222
|
rmSync(statePath, { force: true });
|
|
2021
2223
|
}
|
|
2022
2224
|
async function runDemoBreak(repoRoot, args) {
|
|
2023
2225
|
const keep = args.includes("--keep");
|
|
2024
2226
|
const hookDir = resolveHookDir(repoRoot);
|
|
2025
2227
|
const hookPath = join5(repoRoot, hookDir, "pre-commit");
|
|
2026
|
-
if (!
|
|
2228
|
+
if (!existsSync5(hookPath)) {
|
|
2027
2229
|
console.error("Vinaya hooks are not installed in this repo. Run `vinaya init` first.");
|
|
2028
2230
|
return 1;
|
|
2029
2231
|
}
|
|
@@ -2081,7 +2283,7 @@ Attempting to commit…
|
|
|
2081
2283
|
process.stdout.write(`✓ Commit passed — the fix worked.
|
|
2082
2284
|
`);
|
|
2083
2285
|
if (keep) {
|
|
2084
|
-
if (
|
|
2286
|
+
if (existsSync5(statePath))
|
|
2085
2287
|
unlinkSync(statePath);
|
|
2086
2288
|
process.stdout.write(`
|
|
2087
2289
|
--keep: leaving \`${demoBranch}\` checked out for you to inspect. Clean up with:
|
|
@@ -2117,6 +2319,7 @@ var CONFIG_PATH = "vinaya.config.json";
|
|
|
2117
2319
|
var DOCTRINE_POINTER_PATH = "VINAYA.md";
|
|
2118
2320
|
var CHECKS_WORKFLOW_PATH = ".github/workflows/vinaya-checks.yml";
|
|
2119
2321
|
var REVIEW_WORKFLOW_PATH = ".github/workflows/vinaya-review.yml";
|
|
2322
|
+
var REVIEW_VERDICT_WORKFLOW_PATH = ".github/workflows/vinaya-review-verdict.yml";
|
|
2120
2323
|
var ARCHIVIST_WORKFLOW_PATH = ".github/workflows/vinaya-archivist.yml";
|
|
2121
2324
|
var MANAGED_NOTE = "Managed by Vinaya — created by `vinaya init`. `vinaya upgrade` regenerates it; `vinaya eject` removes it.";
|
|
2122
2325
|
function starterConfig() {
|
|
@@ -2156,6 +2359,8 @@ jobs:
|
|
|
2156
2359
|
runs-on: ubuntu-latest
|
|
2157
2360
|
permissions:
|
|
2158
2361
|
contents: read
|
|
2362
|
+
pull-requests: read
|
|
2363
|
+
issues: read
|
|
2159
2364
|
steps:
|
|
2160
2365
|
- uses: actions/checkout@v4
|
|
2161
2366
|
with:
|
|
@@ -2164,36 +2369,94 @@ jobs:
|
|
|
2164
2369
|
with:
|
|
2165
2370
|
node-version: 20
|
|
2166
2371
|
- name: Run checks
|
|
2372
|
+
env:
|
|
2373
|
+
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
2374
|
+
PR_NUMBER: \${{ github.event.pull_request.number }}
|
|
2375
|
+
BRANCH: \${{ github.head_ref }}
|
|
2167
2376
|
run: npx --yes @attalabs/vinaya check --all --diff-only
|
|
2168
2377
|
`;
|
|
2169
2378
|
}
|
|
2170
2379
|
function reviewWorkflow() {
|
|
2171
2380
|
return `# ${MANAGED_NOTE}
|
|
2172
2381
|
#
|
|
2173
|
-
# The review gate —
|
|
2174
|
-
#
|
|
2175
|
-
#
|
|
2176
|
-
# structurally cannot receive
|
|
2177
|
-
#
|
|
2382
|
+
# The required review gate — pull_request events only. The verdict-comment
|
|
2383
|
+
# half lives in its own workflow (vinaya-review-verdict.yml): a new PR
|
|
2384
|
+
# comment fires a different GitHub event that this pull_request-only
|
|
2385
|
+
# workflow structurally cannot receive — and keeping the comment path in a
|
|
2386
|
+
# separate FILE means this workflow's runs never list permanently-skipped
|
|
2387
|
+
# comment jobs on the PR's checks panel. When a clean final verdict lands,
|
|
2388
|
+
# the verdict workflow re-runs this one, so the required check below goes
|
|
2389
|
+
# green natively with no manual rerun.
|
|
2178
2390
|
name: Vinaya Review Gate
|
|
2179
2391
|
|
|
2180
2392
|
on:
|
|
2181
2393
|
pull_request:
|
|
2182
2394
|
types: [opened, synchronize, reopened, labeled, unlabeled]
|
|
2183
|
-
issue_comment:
|
|
2184
|
-
types: [created]
|
|
2185
2395
|
|
|
2186
2396
|
jobs:
|
|
2187
2397
|
vinaya-review:
|
|
2188
2398
|
name: vinaya review gate
|
|
2189
|
-
if: >
|
|
2190
|
-
github.event_name == 'pull_request' ||
|
|
2191
|
-
(github.event.issue.pull_request != null && contains(github.event.comment.body, 'VERDICT'))
|
|
2192
2399
|
runs-on: ubuntu-latest
|
|
2193
2400
|
permissions:
|
|
2194
2401
|
contents: read
|
|
2195
2402
|
pull-requests: read
|
|
2196
2403
|
issues: read
|
|
2404
|
+
steps:
|
|
2405
|
+
- uses: actions/checkout@v4
|
|
2406
|
+
with:
|
|
2407
|
+
# This job executes repo content; don't leave the token in
|
|
2408
|
+
# .git/config for scripts to read.
|
|
2409
|
+
persist-credentials: false
|
|
2410
|
+
fetch-depth: 0
|
|
2411
|
+
- uses: actions/setup-node@v4
|
|
2412
|
+
with:
|
|
2413
|
+
node-version: 20
|
|
2414
|
+
- name: Review gate
|
|
2415
|
+
env:
|
|
2416
|
+
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
2417
|
+
# PR_NUMBER is what makes the review-gate check EVALUATE: without
|
|
2418
|
+
# it the adapter reads "no PR yet — local dev" and exits 0, and
|
|
2419
|
+
# the gate is green regardless of review state.
|
|
2420
|
+
PR_NUMBER: \${{ github.event.pull_request.number }}
|
|
2421
|
+
BRANCH: \${{ github.head_ref }}
|
|
2422
|
+
run: npx --yes @attalabs/vinaya check --all
|
|
2423
|
+
`;
|
|
2424
|
+
}
|
|
2425
|
+
function reviewVerdictWorkflow() {
|
|
2426
|
+
return `# ${MANAGED_NOTE}
|
|
2427
|
+
#
|
|
2428
|
+
# The verdict-comment half of the review gate. A reviewer's verdict arrives
|
|
2429
|
+
# as a PR comment (\`VERDICT: APPROVE\` / \`VERDICT: PASS\`), which fires
|
|
2430
|
+
# GitHub's \`issue_comment\` event — an event the required pull_request
|
|
2431
|
+
# workflow cannot receive. This workflow evaluates the gate on that comment
|
|
2432
|
+
# and, when the evaluation is clean, RE-RUNS the required workflow so its
|
|
2433
|
+
# check goes green natively with no manual rerun. (Writing check-run
|
|
2434
|
+
# conclusions directly is no longer possible: GitHub's 2025-02-12 change
|
|
2435
|
+
# restricts check-run updates to the owning workflow — re-running is the
|
|
2436
|
+
# supported channel.)
|
|
2437
|
+
#
|
|
2438
|
+
# Privilege split, deliberate: \`evaluate\` checks out and executes repo
|
|
2439
|
+
# content and therefore holds NO write permission; \`retrigger\` holds
|
|
2440
|
+
# \`actions: write\` but checks out and executes nothing — its only inputs
|
|
2441
|
+
# are the evaluator's outputs, resolved via \`gh pr view\` before any repo
|
|
2442
|
+
# content ran. A malicious branch can at worst fail its own evaluation.
|
|
2443
|
+
name: Vinaya Review Gate (on verdict)
|
|
2444
|
+
|
|
2445
|
+
on:
|
|
2446
|
+
issue_comment:
|
|
2447
|
+
types: [created]
|
|
2448
|
+
|
|
2449
|
+
jobs:
|
|
2450
|
+
evaluate:
|
|
2451
|
+
name: vinaya review gate (verdict check)
|
|
2452
|
+
if: github.event.issue.pull_request != null && contains(github.event.comment.body, 'VERDICT')
|
|
2453
|
+
runs-on: ubuntu-latest
|
|
2454
|
+
permissions:
|
|
2455
|
+
contents: read
|
|
2456
|
+
pull-requests: read
|
|
2457
|
+
issues: read
|
|
2458
|
+
outputs:
|
|
2459
|
+
branch: \${{ steps.pr.outputs.branch }}
|
|
2197
2460
|
steps:
|
|
2198
2461
|
# issue_comment payloads carry no PR head SHA/branch — resolve them
|
|
2199
2462
|
# before checkout, and check out that exact commit (the event's default
|
|
@@ -2203,24 +2466,54 @@ jobs:
|
|
|
2203
2466
|
env:
|
|
2204
2467
|
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
2205
2468
|
run: |
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
else
|
|
2209
|
-
NUMBER="\${{ github.event.issue.number }}"
|
|
2210
|
-
fi
|
|
2469
|
+
NUMBER="\${{ github.event.issue.number }}"
|
|
2470
|
+
BRANCH=$(gh pr view "$NUMBER" --repo "\${{ github.repository }}" --json headRefName -q .headRefName)
|
|
2211
2471
|
SHA=$(gh pr view "$NUMBER" --repo "\${{ github.repository }}" --json headRefOid -q .headRefOid)
|
|
2472
|
+
echo "number=$NUMBER" >> "$GITHUB_OUTPUT"
|
|
2473
|
+
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
|
|
2212
2474
|
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
|
|
2213
2475
|
- uses: actions/checkout@v4
|
|
2214
2476
|
with:
|
|
2215
2477
|
ref: \${{ steps.pr.outputs.sha }}
|
|
2478
|
+
persist-credentials: false
|
|
2216
2479
|
fetch-depth: 0
|
|
2217
2480
|
- uses: actions/setup-node@v4
|
|
2218
2481
|
with:
|
|
2219
2482
|
node-version: 20
|
|
2220
|
-
- name: Review gate
|
|
2483
|
+
- name: Review gate (verdict evaluation)
|
|
2221
2484
|
env:
|
|
2222
2485
|
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
2223
|
-
|
|
2486
|
+
# Same wiring as the required workflow: PR_NUMBER is what makes
|
|
2487
|
+
# the adapter evaluate instead of no-op'ing as "local dev".
|
|
2488
|
+
PR_NUMBER: \${{ steps.pr.outputs.number }}
|
|
2489
|
+
BRANCH: \${{ steps.pr.outputs.branch }}
|
|
2490
|
+
run: npx --yes @attalabs/vinaya check review-gate
|
|
2491
|
+
|
|
2492
|
+
# Executes nothing; consumes only the evaluator's outputs. Fires only on a
|
|
2493
|
+
# clean evaluation — a failed one leaves the standing red untouched.
|
|
2494
|
+
retrigger:
|
|
2495
|
+
name: vinaya review gate (retrigger)
|
|
2496
|
+
if: needs.evaluate.result == 'success'
|
|
2497
|
+
needs: evaluate
|
|
2498
|
+
runs-on: ubuntu-latest
|
|
2499
|
+
permissions:
|
|
2500
|
+
actions: write
|
|
2501
|
+
steps:
|
|
2502
|
+
- name: Re-run the required review gate for this branch
|
|
2503
|
+
env:
|
|
2504
|
+
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
2505
|
+
BRANCH: \${{ needs.evaluate.outputs.branch }}
|
|
2506
|
+
run: |
|
|
2507
|
+
RUN_ID=$(gh run list --repo "\${{ github.repository }}" \\
|
|
2508
|
+
--workflow vinaya-review.yml --branch "$BRANCH" \\
|
|
2509
|
+
--status completed \\
|
|
2510
|
+
--json databaseId,event \\
|
|
2511
|
+
--jq '[.[] | select(.event=="pull_request")][0].databaseId // empty')
|
|
2512
|
+
if [ -z "$RUN_ID" ]; then
|
|
2513
|
+
echo "No completed pull_request run of vinaya-review.yml for branch $BRANCH - nothing to re-run."
|
|
2514
|
+
exit 0
|
|
2515
|
+
fi
|
|
2516
|
+
gh run rerun "$RUN_ID" --repo "\${{ github.repository }}"
|
|
2224
2517
|
`;
|
|
2225
2518
|
}
|
|
2226
2519
|
function archivistWorkflow() {
|
|
@@ -2420,6 +2713,12 @@ function buildInitOps(ctx) {
|
|
|
2420
2713
|
const hookMode = 493;
|
|
2421
2714
|
ops.push({ kind: "create-file", path: CHECKS_WORKFLOW_PATH, content: checksWorkflow(), group: "CI workflows" });
|
|
2422
2715
|
ops.push({ kind: "create-file", path: REVIEW_WORKFLOW_PATH, content: reviewWorkflow(), group: "CI workflows" });
|
|
2716
|
+
ops.push({
|
|
2717
|
+
kind: "create-file",
|
|
2718
|
+
path: REVIEW_VERDICT_WORKFLOW_PATH,
|
|
2719
|
+
content: reviewVerdictWorkflow(),
|
|
2720
|
+
group: "CI workflows"
|
|
2721
|
+
});
|
|
2423
2722
|
ops.push({ kind: "create-file", path: ARCHIVIST_WORKFLOW_PATH, content: archivistWorkflow(), group: "CI workflows" });
|
|
2424
2723
|
ops.push({
|
|
2425
2724
|
kind: "managed-block",
|
|
@@ -2477,6 +2776,31 @@ function buildInitProductOps(name) {
|
|
|
2477
2776
|
];
|
|
2478
2777
|
}
|
|
2479
2778
|
|
|
2779
|
+
// src/lib/env-lint.ts
|
|
2780
|
+
import { existsSync as existsSync6, readFileSync as readFileSync3 } from "node:fs";
|
|
2781
|
+
var ENV_READ_PATTERN = /\b(?:process\.env|Bun\.env|Deno\.env)\b/;
|
|
2782
|
+
function checksMissingEnvDeclaration(specs) {
|
|
2783
|
+
const names = [];
|
|
2784
|
+
for (const spec of specs) {
|
|
2785
|
+
if (spec.env)
|
|
2786
|
+
continue;
|
|
2787
|
+
if (!existsSync6(spec.run))
|
|
2788
|
+
continue;
|
|
2789
|
+
let source;
|
|
2790
|
+
try {
|
|
2791
|
+
source = readFileSync3(spec.run, "utf8");
|
|
2792
|
+
} catch {
|
|
2793
|
+
continue;
|
|
2794
|
+
}
|
|
2795
|
+
if (ENV_READ_PATTERN.test(source))
|
|
2796
|
+
names.push(spec.name);
|
|
2797
|
+
}
|
|
2798
|
+
return names;
|
|
2799
|
+
}
|
|
2800
|
+
function envDeclarationWarning(name) {
|
|
2801
|
+
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).`;
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2480
2804
|
// src/lib/ops.ts
|
|
2481
2805
|
import { chmodSync, existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2482
2806
|
import { dirname as dirname3, join as join7, resolve, sep } from "node:path";
|
|
@@ -2941,6 +3265,34 @@ function diagnoseEnvDeclarations(repoRoot, config) {
|
|
|
2941
3265
|
}
|
|
2942
3266
|
return findings;
|
|
2943
3267
|
}
|
|
3268
|
+
function diagnoseCheckClassification(config) {
|
|
3269
|
+
const classification = resolveChecks(coreCheckRegistry(), config?.checks);
|
|
3270
|
+
const findings = [];
|
|
3271
|
+
for (const entry2 of classification.resolved) {
|
|
3272
|
+
if (entry2.state === "overridden")
|
|
3273
|
+
findings.push(warn("checks", overriddenNextMinorWarning(entry2.name)));
|
|
3274
|
+
}
|
|
3275
|
+
for (const failure of classification.failures) {
|
|
3276
|
+
findings.push(warn("checks", bareKeyNextMinorWarning(failure.key)));
|
|
3277
|
+
}
|
|
3278
|
+
return findings;
|
|
3279
|
+
}
|
|
3280
|
+
function diagnoseGlobalConfigChecks() {
|
|
3281
|
+
if (!existsSync8(GLOBAL_CONFIG_PATH))
|
|
3282
|
+
return [];
|
|
3283
|
+
let raw;
|
|
3284
|
+
try {
|
|
3285
|
+
raw = JSON.parse(readFileSync5(GLOBAL_CONFIG_PATH, "utf-8"));
|
|
3286
|
+
} catch (err) {
|
|
3287
|
+
return [info("checks", `${GLOBAL_CONFIG_PATH} is invalid JSON — ${err.message}`)];
|
|
3288
|
+
}
|
|
3289
|
+
const parsed = VinayaConfigSchema.safeParse(raw);
|
|
3290
|
+
if (!parsed.success)
|
|
3291
|
+
return [];
|
|
3292
|
+
if (!parsed.data.checks || Object.keys(parsed.data.checks).length === 0)
|
|
3293
|
+
return [];
|
|
3294
|
+
return [warn("checks", globalChecksIgnoredWarning(GLOBAL_CONFIG_PATH))];
|
|
3295
|
+
}
|
|
2944
3296
|
async function diagnoseEnvironment(deps, hasDrift) {
|
|
2945
3297
|
const findings = [];
|
|
2946
3298
|
const auth = await deps.ghAuthStatus();
|
|
@@ -3011,6 +3363,8 @@ async function runDoctor(args, deps) {
|
|
|
3011
3363
|
findings.push(...diagnoseCustomChecks(repo.repoRoot, configRead.config));
|
|
3012
3364
|
}
|
|
3013
3365
|
findings.push(...diagnoseEnvDeclarations(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
|
|
3366
|
+
findings.push(...diagnoseCheckClassification(configRead.kind === "ok" ? configRead.config : null));
|
|
3367
|
+
findings.push(...diagnoseGlobalConfigChecks());
|
|
3014
3368
|
findings.push(...await diagnoseEnvironment(deps, hasDrift));
|
|
3015
3369
|
findings.push(await diagnoseBranchProtection(deps, repo.owner, repo.repo));
|
|
3016
3370
|
const healthy = findings.every((f) => f.severity === "ok" || f.severity === "info");
|
|
@@ -4297,10 +4651,15 @@ var COMMANDS = [
|
|
|
4297
4651
|
{ flag: "--all", description: "Run every registered check instead of one named check" },
|
|
4298
4652
|
{ flag: "--json", description: "Enveloped JSON output (schema: 1)" },
|
|
4299
4653
|
{ flag: "--diff-only", description: "Scope diff-declared checks to changed files" },
|
|
4300
|
-
{ flag: "--parallel[=n]", description: "Concurrency cap (default: cpu-derived)" }
|
|
4654
|
+
{ flag: "--parallel[=n]", description: "Concurrency cap (default: cpu-derived)" },
|
|
4655
|
+
{
|
|
4656
|
+
flag: "--plan",
|
|
4657
|
+
description: "Print the resolved check registry (default/overridden/additive) without running anything"
|
|
4658
|
+
}
|
|
4301
4659
|
],
|
|
4302
4660
|
details: [
|
|
4303
|
-
"
|
|
4661
|
+
"Each spawned check's child process sees only a fixed baseline (`PATH`, `LANG`, `HOME`, `HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY`, `TMPDIR`) plus whatever its `CheckSpec['env']` declaration explicitly forwards — never the full parent environment. A required (`true`) or unsatisfied `anyOf` declaration missing from the caller's environment synthesizes a `CheckError` before the check ever spawns. Declare `env` (a core check's own registration, or `vinaya.config.json`'s `checks.<name>.env` for a custom one) for any check that reads `process.env`/`Bun.env`/`Deno.env` directly — `vinaya doctor` carries the permanent diagnostic for one that doesn't.",
|
|
4662
|
+
"`--plan` composes with `--json`. It requires zero env vars and never prints an env value — only how each one resolves (passthrough, optional, literal, or anyOf). A `FAIL_CLOSED` entry (a bare key with no namespace matching no core check) always renders inline rather than being dropped, and exits non-zero. `--plan` previews what the next minor's execution will enforce — `vinaya check` itself still runs the flat, unvalidated registry this release."
|
|
4304
4663
|
],
|
|
4305
4664
|
status: "shipped"
|
|
4306
4665
|
},
|