@attalabs/vinaya 0.12.0 → 0.17.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/README.md +33 -3
- package/aeg-root/contracts/planner-brief.md +1 -1
- package/aeg-root/enforcement.md +5 -5
- package/aeg-root/roles/developer.md +1 -0
- package/aeg-root/roles/planner.md +2 -2
- package/aeg-root/roles/reviewer.md +17 -4
- package/aeg-root/roles/security.md +16 -5
- package/aeg-root/skills/aeg/SKILL.md +1 -1
- package/aeg-root/state-machine.md +2 -2
- package/aeg-root/templates/pr-report-template.md +2 -0
- package/aeg-root/tranche-model.md +1 -1
- package/dist/checks/bin/check-body-bare-digits.js +3545 -0
- package/dist/checks/bin/check-branch-topology.js +99 -10
- package/dist/checks/bin/check-brief-shape.js +99 -10
- package/dist/checks/bin/check-closes-n.js +99 -10
- package/dist/checks/bin/check-coherence.js +106 -12
- package/dist/checks/bin/check-dead-branch-push.js +99 -10
- package/dist/checks/bin/check-dispatch-readiness.js +106 -12
- package/dist/checks/bin/check-doc-coverage-push.js +106 -12
- package/dist/checks/bin/check-doc-coverage.js +109 -16
- package/dist/checks/bin/check-evidence-fresh.js +99 -10
- package/dist/checks/bin/check-first-push-dispatch.js +106 -12
- package/dist/checks/bin/check-issue-assignment.js +99 -10
- package/dist/checks/bin/check-no-disk-state.js +99 -10
- package/dist/checks/bin/check-reader-resolvable-prose.js +99 -10
- package/dist/checks/bin/check-registry-gates.js +99 -10
- package/dist/checks/bin/check-review-gate.js +109 -25
- package/dist/checks/bin/check-single-plan-pr.js +99 -10
- package/dist/checks/bin/check-test-plan.js +99 -10
- package/dist/index.js +1325 -236
- package/package.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/required-server-files.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/[root-of-the-server]__0e95dv5._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/1q96_modules_@clerk_nextjs_dist_esm_app-router_client_keyless-creator-reader_0lom2js.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__029-1ch._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0gw1_g2._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0q56ohl._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0rr_lq7._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0zfjc2o._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__19jyp3m._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1_ojzp1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__1wsl6ro._.js → [root-of-the-server]__1ccgndq._.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1d_8nm4._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1dfzjrd._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1zkwtx8._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__210ehg9._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0-n77dx._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_00yjcv1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_01605g7._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0o54suy._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0rix02i._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_1u3abhy._.js → _0x999pb._.js} +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_0ndy563._.js → _1-6rzip._.js} +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_18-d2be._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_087xa25._.js → _1_talrs._.js} +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_11vnfnm._.js → _1ar5uqe._.js} +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_02drwa_._.js → _1c9b9j-._.js} +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1gw6vc9._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_app_studio_18kqbvs._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_app_studio_projects_[name]_tranches_[slug]_20yap_f._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_animate_components_index_ts_186fje1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_basic_components_index_ts_0xk1vda._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1mexqnauhtbtp.js → 0_jnwbarv-cqk.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{2ieu4oi-pp_bz.js → 0l_vubs1ephe5.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0ojfbn3yvy-8_.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1jx5_q1g9l-cm.js → 0phqfqwbnt_ey.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0qcu17x3h94a9.js → 17r0szvqeeyhx.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0ve1-nz-mu_lr.js → 18x3rbqzeutpz.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{22y4d5yrovcv-.js → 1bqpfvromtwn1.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{39w6jbw3rleez.js → 24ojyzmt_fkg3.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/27sqffqm2wbxx.css +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0tmcnl-z8yt2_.js → 30lyvn0yzwdiz.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1khy4oilrab6g.js → 3m0_gk59s-rbx.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1eo32v1tbjy7c.js → 40x7f5hm7a5tk.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0-08g_jjalnd9.js → 453zhecht_m1q.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/server.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0qu0xgvifbacu.css +0 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1t7yy_s34022p.js +0 -1
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{ZHVE9Y9G8XKR91g27SgVX → p5sWGp4ANwORiaX4Kk6U2}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{ZHVE9Y9G8XKR91g27SgVX → p5sWGp4ANwORiaX4Kk6U2}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{ZHVE9Y9G8XKR91g27SgVX → p5sWGp4ANwORiaX4Kk6U2}/_ssgManifest.js +0 -0
package/dist/index.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
5
|
-
import { dirname as dirname8, join as
|
|
4
|
+
import { readFileSync as readFileSync21 } from "node:fs";
|
|
5
|
+
import { dirname as dirname8, join as join22 } from "node:path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
7
7
|
|
|
8
8
|
// src/commands/archive.ts
|
|
9
|
-
import { execFileSync as
|
|
9
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
10
10
|
// ../../packages/aeg-forge-state/src/strip-code.ts
|
|
11
11
|
function maskCode(body) {
|
|
12
12
|
const fill = (line) => " ".repeat(line.length);
|
|
13
|
-
|
|
13
|
+
const blocksMasked = maskIndentedCode(maskFencedCode(body, fill), fill);
|
|
14
|
+
return blocksMasked.split(`
|
|
15
|
+
`).map((line) => replaceInlineSpans(line, (span) => " ".repeat(span.length))).join(`
|
|
16
|
+
`);
|
|
14
17
|
}
|
|
15
18
|
function stripCode(body, options = {}) {
|
|
16
19
|
const normalised = body.replace(/\r\n?/g, `
|
|
@@ -19,7 +22,50 @@ function stripCode(body, options = {}) {
|
|
|
19
22
|
const blocksStripped = maskIndentedCode(maskFencedCode(normalised, blank), blank);
|
|
20
23
|
if (options.inlineSpans === "keep")
|
|
21
24
|
return blocksStripped;
|
|
22
|
-
return blocksStripped.
|
|
25
|
+
return blocksStripped.split(`
|
|
26
|
+
`).map((line) => replaceInlineSpans(line, () => "")).join(`
|
|
27
|
+
`);
|
|
28
|
+
}
|
|
29
|
+
function replaceInlineSpans(line, replace) {
|
|
30
|
+
let result = "";
|
|
31
|
+
let i = 0;
|
|
32
|
+
const runLengthAt = (at) => {
|
|
33
|
+
let j = at;
|
|
34
|
+
while (j < line.length && line[j] === "`")
|
|
35
|
+
j++;
|
|
36
|
+
return j - at;
|
|
37
|
+
};
|
|
38
|
+
while (i < line.length) {
|
|
39
|
+
if (line[i] !== "`") {
|
|
40
|
+
result += line[i];
|
|
41
|
+
i++;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const openLen = runLengthAt(i);
|
|
45
|
+
const openEnd = i + openLen;
|
|
46
|
+
let j = openEnd;
|
|
47
|
+
let closeEnd = -1;
|
|
48
|
+
while (j < line.length) {
|
|
49
|
+
if (line[j] !== "`") {
|
|
50
|
+
j++;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const runLen = runLengthAt(j);
|
|
54
|
+
if (runLen === openLen) {
|
|
55
|
+
closeEnd = j + runLen;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
j += runLen;
|
|
59
|
+
}
|
|
60
|
+
if (closeEnd === -1) {
|
|
61
|
+
result += line.slice(i, openEnd);
|
|
62
|
+
i = openEnd;
|
|
63
|
+
} else {
|
|
64
|
+
result += replace(line.slice(i, closeEnd));
|
|
65
|
+
i = closeEnd;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
23
69
|
}
|
|
24
70
|
var FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})([^\r\n]*)\r?$/;
|
|
25
71
|
var FENCE_CLOSE = /^ {0,3}(`+|~+)[ \t]*\r?$/;
|
|
@@ -53,6 +99,10 @@ function scanFencedCode(body) {
|
|
|
53
99
|
});
|
|
54
100
|
return { lines, unterminated: fenceChar !== null };
|
|
55
101
|
}
|
|
102
|
+
function hasUnterminatedFence(body) {
|
|
103
|
+
return scanFencedCode(body.replace(/\r\n?/g, `
|
|
104
|
+
`)).unterminated;
|
|
105
|
+
}
|
|
56
106
|
var LIST_MARKER = /^ {0,3}(?:[-*+]|\d{1,9}[.)])(?:\s|$)/;
|
|
57
107
|
function indentWidth(line) {
|
|
58
108
|
let width = 0;
|
|
@@ -240,6 +290,42 @@ function matchesLabel(key, name) {
|
|
|
240
290
|
function hasLabel(key, names) {
|
|
241
291
|
return names.some((n) => matchesLabel(key, n));
|
|
242
292
|
}
|
|
293
|
+
|
|
294
|
+
// ../../packages/aeg-forge-state/src/list-tasks.ts
|
|
295
|
+
var PROJECT_SLUG = /^[a-z0-9][a-z0-9-]*$/i;
|
|
296
|
+
var PROJECT_FIELD = /^\s*(?:\*\*)?Project(?:\(s\))?(?:\*\*)?\s*:\s*(?:\*\*)?\s*(.+)$/im;
|
|
297
|
+
var VALUE_EDGE_MARKUP = /^[`.;\s]+|[`.;\s]+$/g;
|
|
298
|
+
function unwrapValue(raw) {
|
|
299
|
+
return raw.replace(/\*\*/g, "").replace(VALUE_EDGE_MARKUP, "");
|
|
300
|
+
}
|
|
301
|
+
function projectFieldFromBody(body) {
|
|
302
|
+
const m = stripCode(body, { inlineSpans: "keep" }).match(PROJECT_FIELD);
|
|
303
|
+
if (m)
|
|
304
|
+
return parseFieldValue(m[1] ?? "");
|
|
305
|
+
if (hasUnterminatedFence(body)) {
|
|
306
|
+
const raw = body.match(PROJECT_FIELD);
|
|
307
|
+
if (raw)
|
|
308
|
+
return { declared: true, names: [], unparsed: [(raw[1] ?? "").trim()], unreadable: true };
|
|
309
|
+
}
|
|
310
|
+
return { declared: false, names: [], unparsed: [], unreadable: false };
|
|
311
|
+
}
|
|
312
|
+
function parseFieldValue(value) {
|
|
313
|
+
const names = [];
|
|
314
|
+
const unparsed = [];
|
|
315
|
+
for (const raw of value.split(",")) {
|
|
316
|
+
const name = unwrapValue(raw);
|
|
317
|
+
if (name.length === 0)
|
|
318
|
+
continue;
|
|
319
|
+
if (PROJECT_SLUG.test(name))
|
|
320
|
+
names.push(name);
|
|
321
|
+
else
|
|
322
|
+
unparsed.push(raw.trim());
|
|
323
|
+
}
|
|
324
|
+
return { declared: true, names: [...new Set(names)], unparsed, unreadable: false };
|
|
325
|
+
}
|
|
326
|
+
function projectsFromBody(body) {
|
|
327
|
+
return projectFieldFromBody(body).names;
|
|
328
|
+
}
|
|
243
329
|
// ../../packages/aeg-forge-state/src/github-token.ts
|
|
244
330
|
import { execFile as execFile2 } from "node:child_process";
|
|
245
331
|
import { promisify as promisify2 } from "node:util";
|
|
@@ -392,6 +478,85 @@ function splitTableRow(row) {
|
|
|
392
478
|
parts.pop();
|
|
393
479
|
return parts.map((p) => p.split(PLACEHOLDER).join("|"));
|
|
394
480
|
}
|
|
481
|
+
// ../../packages/aeg-core/src/blast-radius-domains.ts
|
|
482
|
+
function resolveWorkspaceEntry(entry2, listDirs) {
|
|
483
|
+
if (entry2.endsWith("/*")) {
|
|
484
|
+
const baseDir = entry2.slice(0, -2);
|
|
485
|
+
return listDirs(baseDir).map((name) => `${baseDir}/${name}`);
|
|
486
|
+
}
|
|
487
|
+
if (entry2.includes("*"))
|
|
488
|
+
return [];
|
|
489
|
+
return [entry2];
|
|
490
|
+
}
|
|
491
|
+
function deriveWorkspacePackageDomains(workspaces, listDirs) {
|
|
492
|
+
const positive = workspaces.filter((entry2) => !entry2.startsWith("!"));
|
|
493
|
+
const negative = workspaces.filter((entry2) => entry2.startsWith("!")).map((entry2) => entry2.slice(1));
|
|
494
|
+
const excluded = new Set;
|
|
495
|
+
for (const entry2 of negative) {
|
|
496
|
+
for (const member of resolveWorkspaceEntry(entry2, listDirs))
|
|
497
|
+
excluded.add(member);
|
|
498
|
+
}
|
|
499
|
+
const domains = new Set;
|
|
500
|
+
for (const entry2 of positive) {
|
|
501
|
+
for (const member of resolveWorkspaceEntry(entry2, listDirs)) {
|
|
502
|
+
if (excluded.has(member))
|
|
503
|
+
continue;
|
|
504
|
+
const segments = member.split("/");
|
|
505
|
+
if (segments[0] !== "packages" || segments.length < 2)
|
|
506
|
+
continue;
|
|
507
|
+
domains.add(`packages/${segments[1]}`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return [...domains].sort();
|
|
511
|
+
}
|
|
512
|
+
function parsePnpmWorkspaceYaml(content) {
|
|
513
|
+
const unquote = (s) => {
|
|
514
|
+
const t = s.trim();
|
|
515
|
+
if (t.length >= 2 && (t[0] === "'" && t.at(-1) === "'" || t[0] === '"' && t.at(-1) === '"')) {
|
|
516
|
+
return t.slice(1, -1);
|
|
517
|
+
}
|
|
518
|
+
return t;
|
|
519
|
+
};
|
|
520
|
+
const inlineMatch = content.match(/^packages:\s*\[(.*)\]\s*$/m);
|
|
521
|
+
if (inlineMatch) {
|
|
522
|
+
return (inlineMatch[1] ?? "").split(",").map(unquote).filter((s) => s.length > 0);
|
|
523
|
+
}
|
|
524
|
+
const entries = [];
|
|
525
|
+
let inBlock = false;
|
|
526
|
+
for (const rawLine of content.split(`
|
|
527
|
+
`)) {
|
|
528
|
+
const line = rawLine.replace(/#.*$/, "");
|
|
529
|
+
if (/^packages:\s*$/.test(line)) {
|
|
530
|
+
inBlock = true;
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
if (!inBlock)
|
|
534
|
+
continue;
|
|
535
|
+
const itemMatch = line.match(/^\s+-\s*(.+?)\s*$/);
|
|
536
|
+
if (itemMatch?.[1]) {
|
|
537
|
+
entries.push(unquote(itemMatch[1]));
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (line.trim().length === 0)
|
|
541
|
+
continue;
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
return entries;
|
|
545
|
+
}
|
|
546
|
+
var CROSS_CUTTING_CANDIDATES = [
|
|
547
|
+
"bun.lock",
|
|
548
|
+
"package-lock.json",
|
|
549
|
+
"pnpm-lock.yaml",
|
|
550
|
+
"yarn.lock",
|
|
551
|
+
"turbo.json",
|
|
552
|
+
"biome.json",
|
|
553
|
+
"tsconfig.json",
|
|
554
|
+
".github/workflows",
|
|
555
|
+
".husky"
|
|
556
|
+
];
|
|
557
|
+
function deriveBuiltinCrossCuttingDefaults(exists) {
|
|
558
|
+
return CROSS_CUTTING_CANDIDATES.filter((path) => exists(path));
|
|
559
|
+
}
|
|
395
560
|
// ../../packages/aeg-core/src/state-machine-model.ts
|
|
396
561
|
var FORGE_FACT_INPUTS = [
|
|
397
562
|
{
|
|
@@ -457,12 +622,30 @@ var DERIVED_STATUSES = [
|
|
|
457
622
|
"incoherent"
|
|
458
623
|
];
|
|
459
624
|
var DERIVABLE_STATUSES = DERIVED_STATUSES.filter((s) => s !== "backlog");
|
|
625
|
+
// ../../packages/aeg-core/src/file-classify.ts
|
|
626
|
+
function isCodeFile(p) {
|
|
627
|
+
if (p.endsWith(".md"))
|
|
628
|
+
return false;
|
|
629
|
+
return /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|sql|css)$/.test(p);
|
|
630
|
+
}
|
|
460
631
|
// ../../packages/aeg-core/src/waiver-label.ts
|
|
461
632
|
var WAIVER_LABEL = label("waiver-docs");
|
|
462
633
|
var WAIVER_LABEL_REVIEW = label("waiver-review");
|
|
634
|
+
var PRINCIPAL_ALLOWLIST = ["daniboomerang"];
|
|
635
|
+
function isPrincipal(login, principalAllowlist) {
|
|
636
|
+
if (login === null)
|
|
637
|
+
return false;
|
|
638
|
+
const normalized = login.toLowerCase();
|
|
639
|
+
return principalAllowlist.some((p) => p.toLowerCase() === normalized);
|
|
640
|
+
}
|
|
463
641
|
|
|
464
642
|
// ../../packages/aeg-core/src/doc-owners.ts
|
|
465
643
|
var DOC_OWNERS_PATH = ".vinaya/doc-owners";
|
|
644
|
+
function classifyDocOwnersManifest(content) {
|
|
645
|
+
if (content === null)
|
|
646
|
+
return "absent";
|
|
647
|
+
return content.trim() === "" ? "empty" : "present";
|
|
648
|
+
}
|
|
466
649
|
function parseDocOwners(content) {
|
|
467
650
|
const bindings = [];
|
|
468
651
|
const errors = [];
|
|
@@ -507,11 +690,19 @@ function globToRegex(pat) {
|
|
|
507
690
|
re += "$";
|
|
508
691
|
return new RegExp(re);
|
|
509
692
|
}
|
|
693
|
+
function isUrlPointer(p) {
|
|
694
|
+
return /^https?:\/\//.test(p);
|
|
695
|
+
}
|
|
696
|
+
function pointerToPath(p) {
|
|
697
|
+
const idx = p.indexOf("#");
|
|
698
|
+
return idx === -1 ? p : p.slice(0, idx);
|
|
699
|
+
}
|
|
510
700
|
var SEPARATOR = /(?:[ \t]*[—–][ \t]*|[ \t]+-[ \t]+)/.source;
|
|
511
701
|
// ../../packages/aeg-core/src/pr-tier.ts
|
|
702
|
+
var TIER_FIELD = /(\*\*)?\s*Tier\s*(\*\*)?\s*:\s*(\*\*)?\s*([013])\b/i;
|
|
512
703
|
function readTierFromPrBody(prBody) {
|
|
513
704
|
const searchIn = anchoredRegion(prBody, "TIER") ?? prBody;
|
|
514
|
-
const m = searchIn.match(
|
|
705
|
+
const m = searchIn.match(TIER_FIELD);
|
|
515
706
|
if (!m)
|
|
516
707
|
return null;
|
|
517
708
|
const t = Number(m[4]);
|
|
@@ -789,19 +980,105 @@ function checkIssueRationale(body) {
|
|
|
789
980
|
function isTaskIssueLabelSet(labels) {
|
|
790
981
|
return hasLabel("tranche", labels);
|
|
791
982
|
}
|
|
983
|
+
var PATH_TEXT = (body) => stripCode(body, { inlineSpans: "keep" });
|
|
984
|
+
var DOC_EXTENSION_RE = /\.(?:md|mdx|txt)$/i;
|
|
985
|
+
function namesPath(text, path) {
|
|
986
|
+
const escaped = path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
987
|
+
const occurrences = new RegExp(`(?:^|[^\\w./-])(${escaped}(?:/[\\w.@-]+)*)(?![\\w-])`, "gm");
|
|
988
|
+
for (const m of text.matchAll(occurrences)) {
|
|
989
|
+
if (!DOC_EXTENSION_RE.test(m[1]))
|
|
990
|
+
return true;
|
|
991
|
+
}
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
var BLAST_RADIUS_ACK_RE = /(?:\*\*)?blast-radius-ack(?:\*\*)?\s*[:—–-]/i;
|
|
995
|
+
function checkBlastRadiusScope(body, _labels, sharedPackages, projectPaths) {
|
|
996
|
+
if (sharedPackages.length === 0)
|
|
997
|
+
return { status: "pass", errors: [] };
|
|
998
|
+
const text = [
|
|
999
|
+
rationaleFieldText(PATH_TEXT(body), "Boundary"),
|
|
1000
|
+
rationaleFieldText(PATH_TEXT(body), "Project\\(s\\)|Project(?:s)?\\s*\\+|blast radius")
|
|
1001
|
+
].join(`
|
|
1002
|
+
`);
|
|
1003
|
+
const named = sharedPackages.filter((d) => namesPath(text, d));
|
|
1004
|
+
if (named.length === 0)
|
|
1005
|
+
return { status: "pass", errors: [] };
|
|
1006
|
+
const registered = new Set(projectPaths.map((p) => p.name.trim()));
|
|
1007
|
+
const declared = projectsFromBody(body);
|
|
1008
|
+
const projects = declared.filter((p) => registered.has(p));
|
|
1009
|
+
const ownedPaths = projectPaths.filter((p) => projects.includes(p.name)).map((p) => p.path.replace(/\/+$/, ""));
|
|
1010
|
+
const unowned = named.filter((d) => !ownedPaths.some((owned) => d === owned || d.startsWith(`${owned}/`)));
|
|
1011
|
+
if (unowned.length === 0)
|
|
1012
|
+
return { status: "pass", errors: [] };
|
|
1013
|
+
if (projects.length > 1)
|
|
1014
|
+
return { status: "pass", errors: [] };
|
|
1015
|
+
if (BLAST_RADIUS_ACK_RE.test(PATH_TEXT(body)))
|
|
1016
|
+
return { status: "pass", errors: [] };
|
|
1017
|
+
const unregistered = declared.filter((p) => !registered.has(p));
|
|
1018
|
+
return {
|
|
1019
|
+
status: "fail",
|
|
1020
|
+
errors: [
|
|
1021
|
+
`issue-validation blast radius: the rationale names ${unowned.join(", ")} — a shared collision domain no declared project (${projects.join(", ") || "none"}) owns — but declares a single registered project and no \`blast-radius-ack:\` line.${unregistered.length > 0 ? ` Not counted: ${unregistered.join(", ")} — no row in \`.vinaya/projects.md\`, so it adds no review lens.` : ""} Project(s) drives the review fan-out (.vinaya/projects.md); list every consumer in the blast radius, or add \`blast-radius-ack: <why one lens is enough>\`.`
|
|
1022
|
+
]
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
var BRIEF_MARKERS = [
|
|
1026
|
+
{ name: "## References", pattern: /(?:^#{1,6}\s*|\*\*)\s*References\b/im },
|
|
1027
|
+
{ name: "Technical surface map", pattern: /(?:^#{1,6}\s*|\*\*)\s*Technical surface map\b/im },
|
|
1028
|
+
{ name: "Premise", pattern: /(?:^#{1,6}\s*|\*\*)\s*Premise(?:\*\*)?\s*[:—–]/im },
|
|
1029
|
+
{ name: "Step 0", pattern: /(?:^#{1,6}\s*|\*\*)\s*Step 0\b/im },
|
|
1030
|
+
{ name: "Test Plan", pattern: /(?:^#{1,6}\s*|\*\*)\s*Test Plan\b/im }
|
|
1031
|
+
];
|
|
1032
|
+
function checkNoBriefContent(body) {
|
|
1033
|
+
const text = stripCode(body);
|
|
1034
|
+
const errors = BRIEF_MARKERS.filter((m) => m.pattern.test(text)).map((m) => `issue-validation brief content: the Issue body carries a brief-shaped "${m.name}" section. Brief-time content (surface pointers, skills-to-read, premise, test plan) belongs in the brief, not the Issue — it goes stale before work starts (aeg-root/skills/brief-authoring/SKILL.md). Move it to the brief.`);
|
|
1035
|
+
return { status: errors.length > 0 ? "fail" : "pass", errors };
|
|
1036
|
+
}
|
|
1037
|
+
function rationaleFieldText(text, labelPattern) {
|
|
1038
|
+
const re = new RegExp(`(?:\\*\\*|^#{1,4}\\s+)\\s*(?:${labelPattern})[^\\n]*\\n?([\\s\\S]*?)(?=\\n\\s*(?:\\*\\*[A-Z]|#{1,4}\\s)|(?![\\s\\S]))`, "im");
|
|
1039
|
+
const m = re.exec(text);
|
|
1040
|
+
return m ? m[0] : "";
|
|
1041
|
+
}
|
|
1042
|
+
var DOC_PATH_RE = /(?:(?:aeg-root|apps|packages|specs|docs|tools|\.claude|\.github)\/[\w./@-]*\.(?:md|mdx)|\.claude\/(?:skills|rules)\/[\w./-]+|\b(?:docs-index|decisions|projects|state-machine|enforcement|process|README|CLAUDE)\.md\b|\b[\w-]+-(?:spec|decisions|backlog)\.md\b)/i;
|
|
1043
|
+
var NO_DOC_SURFACE_RE = /(?:\*\*)?\s*no-doc-surface/i;
|
|
1044
|
+
function checkRationaleNamesDocs(body) {
|
|
1045
|
+
const text = PATH_TEXT(body);
|
|
1046
|
+
const scope = [rationaleFieldText(text, "Docs to keep coherent|§7"), rationaleFieldText(text, "Traps")].join(`
|
|
1047
|
+
`);
|
|
1048
|
+
if (NO_DOC_SURFACE_RE.test(scope))
|
|
1049
|
+
return { status: "pass", errors: [] };
|
|
1050
|
+
if (DOC_PATH_RE.test(scope))
|
|
1051
|
+
return { status: "pass", errors: [] };
|
|
1052
|
+
return {
|
|
1053
|
+
status: "fail",
|
|
1054
|
+
errors: [
|
|
1055
|
+
'issue-validation docs read: neither "Docs to keep coherent" nor "Traps to avoid" names a concrete doc path (aeg-root/…, .claude/skills/…, apps/*/CLAUDE.md, apps/*/specs/…). Naming one is the artifact of having read the surface being planned — the forge write triggers no skill-check hook, so this field is the only read-obligation signal. For a genuinely doc-less surface, write the explicit `no-doc-surface` sentinel.'
|
|
1056
|
+
]
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
792
1059
|
|
|
793
1060
|
// ../../packages/aeg-core/src/coherence-checks.ts
|
|
794
1061
|
var R1_GRANDFATHERED_ISSUES = new Set([279, 280, 281, 282]);
|
|
795
1062
|
// ../../packages/aeg-core/src/verdict-extraction.ts
|
|
1063
|
+
var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
|
|
1064
|
+
function extractHeadSha(comment) {
|
|
1065
|
+
const m = comment.match(HEAD_SHA_PATTERN);
|
|
1066
|
+
return m ? m[1].toLowerCase() : null;
|
|
1067
|
+
}
|
|
796
1068
|
function extractVerdict(comments, valuePattern, missingLabel) {
|
|
797
1069
|
const clearHits = comments.filter((c) => valuePattern.test(c));
|
|
798
1070
|
if (clearHits.length > 0) {
|
|
799
1071
|
const latest = clearHits[clearHits.length - 1];
|
|
800
1072
|
const m = latest.match(valuePattern);
|
|
801
|
-
return {
|
|
1073
|
+
return {
|
|
1074
|
+
value: m[1].toUpperCase().replace(/[_-]/g, " "),
|
|
1075
|
+
headSha: extractHeadSha(latest),
|
|
1076
|
+
danglingNote: null
|
|
1077
|
+
};
|
|
802
1078
|
}
|
|
803
1079
|
return {
|
|
804
1080
|
value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
|
|
1081
|
+
headSha: null,
|
|
805
1082
|
danglingNote: `no ${missingLabel} verdict comment found on this PR`
|
|
806
1083
|
};
|
|
807
1084
|
}
|
|
@@ -963,6 +1240,7 @@ var CHECKS_WORKFLOW_PATH = ".github/workflows/vinaya-checks.yml";
|
|
|
963
1240
|
var REVIEW_WORKFLOW_PATH = ".github/workflows/vinaya-review.yml";
|
|
964
1241
|
var REVIEW_VERDICT_WORKFLOW_PATH = ".github/workflows/vinaya-review-verdict.yml";
|
|
965
1242
|
var ARCHIVIST_WORKFLOW_PATH = ".github/workflows/vinaya-archivist.yml";
|
|
1243
|
+
var BODY_CHECKS_WORKFLOW_PATH = ".github/workflows/vinaya-body-checks.yml";
|
|
966
1244
|
var MANAGED_NOTE = "Managed by Vinaya — created by `vinaya init`. `vinaya upgrade` regenerates it; `vinaya eject` removes it.";
|
|
967
1245
|
function starterConfig() {
|
|
968
1246
|
return {
|
|
@@ -989,9 +1267,31 @@ function ownVersion() {
|
|
|
989
1267
|
return pkg.version;
|
|
990
1268
|
}
|
|
991
1269
|
var SETUP_BUN_SHA = "0c5077e51419868618aeaa5fe8019c62421857d6";
|
|
992
|
-
function vinayaSetupSteps(selfHost) {
|
|
1270
|
+
function vinayaSetupSteps(selfHost, sourceTrust = "pull-request") {
|
|
993
1271
|
if (!selfHost)
|
|
994
1272
|
return "";
|
|
1273
|
+
if (sourceTrust === "trusted") {
|
|
1274
|
+
return ` # Pinned to a commit, not the mutable \`v2\` tag. A repoint of
|
|
1275
|
+
# \`v2\` would execute new upstream code in every adopter on the next run,
|
|
1276
|
+
# with no diff anywhere to review. Resolved from the \`v2\` tag on
|
|
1277
|
+
# 2026-08-14. Bun's own version still comes from the repo's
|
|
1278
|
+
# \`packageManager\` field, not from this pin.
|
|
1279
|
+
- uses: oven-sh/setup-bun@${SETUP_BUN_SHA}
|
|
1280
|
+
# This repo declares the \`@attalabs/vinaya\` workspace package itself, so
|
|
1281
|
+
# \`npx @attalabs/vinaya\` resolves to that local member instead of the
|
|
1282
|
+
# registry and dies on its unbuilt \`bin\`. Build and run the trusted
|
|
1283
|
+
# default-branch copy directly.
|
|
1284
|
+
#
|
|
1285
|
+
# This workflow executes only a trusted checkout selected by its event:
|
|
1286
|
+
# never the pull request head or merge ref. The review-authority decision
|
|
1287
|
+
# therefore comes from code the pull request cannot alter. \`--ignore-scripts\`
|
|
1288
|
+
# remains defense in depth against lifecycle hooks in that trusted tree.
|
|
1289
|
+
- name: Build the trusted Vinaya CLI
|
|
1290
|
+
run: |
|
|
1291
|
+
bun install --frozen-lockfile --ignore-scripts
|
|
1292
|
+
bun run --cwd ${selfHost.dir} build
|
|
1293
|
+
`;
|
|
1294
|
+
}
|
|
995
1295
|
return ` # Pinned to a commit, not the mutable \`v2\` tag. This is the first
|
|
996
1296
|
# THIRD-PARTY action this generator writes into an adopter's repository,
|
|
997
1297
|
# and it runs in the same job that then builds and executes pull-request
|
|
@@ -1095,7 +1395,7 @@ jobs:
|
|
|
1095
1395
|
- uses: actions/setup-node@v4
|
|
1096
1396
|
with:
|
|
1097
1397
|
node-version: 20
|
|
1098
|
-
${vinayaSetupSteps(selfHost)}${adopterSetupStep(ciSetup)} - name: Run checks
|
|
1398
|
+
${vinayaSetupSteps(selfHost, "pull-request")}${adopterSetupStep(ciSetup)} - name: Run checks
|
|
1099
1399
|
env:
|
|
1100
1400
|
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
1101
1401
|
PR_NUMBER: \${{ github.event.pull_request.number }}
|
|
@@ -1129,21 +1429,27 @@ ${vinayaSetupSteps(selfHost)}${adopterSetupStep(ciSetup)} - name: Run check
|
|
|
1129
1429
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
1130
1430
|
`;
|
|
1131
1431
|
}
|
|
1132
|
-
function reviewWorkflow(selfHost
|
|
1432
|
+
function reviewWorkflow(selfHost) {
|
|
1133
1433
|
return `# ${MANAGED_NOTE}
|
|
1134
1434
|
#
|
|
1135
|
-
# The required review gate
|
|
1435
|
+
# The required review gate. \`pull_request_target\` is the trust boundary:
|
|
1436
|
+
# GitHub loads this workflow and its default checkout from the repository's
|
|
1437
|
+
# default branch, never from the pull request being judged. The job does not
|
|
1438
|
+
# fetch, check out, install, build, or execute pull-request content.
|
|
1439
|
+
#
|
|
1440
|
+
# The verdict-comment
|
|
1136
1441
|
# half lives in its own workflow (vinaya-review-verdict.yml): a new PR
|
|
1137
|
-
# comment fires a different GitHub event that this
|
|
1442
|
+
# comment fires a different GitHub event that this
|
|
1138
1443
|
# workflow structurally cannot receive — and keeping the comment path in a
|
|
1139
1444
|
# separate FILE means this workflow's runs never list permanently-skipped
|
|
1140
1445
|
# comment jobs on the PR's checks panel. When a clean final verdict lands,
|
|
1141
1446
|
# the verdict workflow re-runs this one, so the required check below goes
|
|
1142
1447
|
# green natively with no manual rerun.
|
|
1143
1448
|
name: Vinaya Review Gate
|
|
1449
|
+
run-name: "Vinaya Review Gate PR #\${{ github.event.pull_request.number }} @ \${{ github.event.pull_request.head.sha }}"
|
|
1144
1450
|
|
|
1145
1451
|
on:
|
|
1146
|
-
|
|
1452
|
+
pull_request_target:
|
|
1147
1453
|
types: [opened, synchronize, reopened, labeled, unlabeled]
|
|
1148
1454
|
|
|
1149
1455
|
# One run per pull request COMMIT, always. Several \`types:\` above can fire in the
|
|
@@ -1183,30 +1489,96 @@ jobs:
|
|
|
1183
1489
|
steps:
|
|
1184
1490
|
- uses: actions/checkout@v4
|
|
1185
1491
|
with:
|
|
1186
|
-
#
|
|
1187
|
-
|
|
1492
|
+
# Explicit trusted ref: never use the PR head/merge ref in this job.
|
|
1493
|
+
ref: \${{ github.event.repository.default_branch }}
|
|
1188
1494
|
persist-credentials: false
|
|
1189
1495
|
fetch-depth: 0
|
|
1190
1496
|
- uses: actions/setup-node@v4
|
|
1191
1497
|
with:
|
|
1192
1498
|
node-version: 20
|
|
1193
|
-
${vinayaSetupSteps(selfHost)}
|
|
1499
|
+
${vinayaSetupSteps(selfHost, "trusted")} - name: Review gate
|
|
1194
1500
|
env:
|
|
1195
1501
|
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
1196
1502
|
# PR_NUMBER is what makes the review-gate check EVALUATE: without
|
|
1197
1503
|
# it the adapter reads "no PR yet — local dev" and exits 0, and
|
|
1198
1504
|
# the gate is green regardless of review state.
|
|
1199
1505
|
PR_NUMBER: \${{ github.event.pull_request.number }}
|
|
1200
|
-
BRANCH: \${{ github.head_ref }}
|
|
1201
1506
|
run: ${vinayaRun(selfHost, "check review-gate")}
|
|
1202
1507
|
`;
|
|
1203
1508
|
}
|
|
1204
|
-
function
|
|
1509
|
+
function bodyChecksWorkflow(selfHost) {
|
|
1510
|
+
return `# ${MANAGED_NOTE}
|
|
1511
|
+
#
|
|
1512
|
+
# Required, PR-content-independent checks that need a trust anchor no pull
|
|
1513
|
+
# request can rewrite. \`pull_request_target\` is the same boundary
|
|
1514
|
+
# \`vinaya-review.yml\` uses: GitHub loads this workflow and its default
|
|
1515
|
+
# checkout from the repository's default branch, never from the pull
|
|
1516
|
+
# request being judged. The job does not fetch, check out, install, build,
|
|
1517
|
+
# or execute pull-request content — every check registered here reads only
|
|
1518
|
+
# live-fetched PR metadata (\`gh pr view\`), never the diff or repo tree, the
|
|
1519
|
+
# same shape \`review-gate\` already requires of its own checks.
|
|
1520
|
+
#
|
|
1521
|
+
# \`body-bare-digits\` is the one check here today: its Changesets-release
|
|
1522
|
+
# exemption live-fetches the PR's real author, keyed on \`PR_NUMBER\` — on a
|
|
1523
|
+
# \`pull_request\` trigger that value is PR-editable (the PR's own workflow
|
|
1524
|
+
# YAML controls it), so an attacker could redirect it to any
|
|
1525
|
+
# already-approved PR by the configured release actor. Found live (round 5,
|
|
1526
|
+
# security review, PR #165), and verified no env-var or git-state signal
|
|
1527
|
+
# inside a \`pull_request\` job closes it — \`pull_request_target\` does,
|
|
1528
|
+
# because the workflow text assigning \`PR_NUMBER\` comes from THIS file on
|
|
1529
|
+
# the default branch, which a pull request cannot edit.
|
|
1530
|
+
name: Vinaya Body Checks
|
|
1531
|
+
run-name: "Vinaya Body Checks PR #\${{ github.event.pull_request.number }} @ \${{ github.event.pull_request.head.sha }}"
|
|
1532
|
+
|
|
1533
|
+
on:
|
|
1534
|
+
pull_request_target:
|
|
1535
|
+
types: [opened, synchronize, reopened, edited]
|
|
1536
|
+
|
|
1537
|
+
# Same collapsing rationale as \`vinaya-review.yml\`'s concurrency group — see
|
|
1538
|
+
# that file's own comment for the two measured failure modes (duplicate
|
|
1539
|
+
# runs from simultaneous event types; an old rerun cancelling the current
|
|
1540
|
+
# commit's run) this group exists to prevent.
|
|
1541
|
+
concurrency:
|
|
1542
|
+
group: vinaya-body-checks-\${{ github.event.pull_request.number || github.ref }}-\${{ github.event.pull_request.head.sha || github.sha }}
|
|
1543
|
+
cancel-in-progress: true
|
|
1544
|
+
|
|
1545
|
+
jobs:
|
|
1546
|
+
vinaya-body-checks:
|
|
1547
|
+
name: vinaya check body-bare-digits
|
|
1548
|
+
runs-on: ubuntu-latest
|
|
1549
|
+
permissions:
|
|
1550
|
+
contents: read
|
|
1551
|
+
pull-requests: read
|
|
1552
|
+
steps:
|
|
1553
|
+
- uses: actions/checkout@v4
|
|
1554
|
+
with:
|
|
1555
|
+
# Explicit trusted ref: never use the PR head/merge ref in this job.
|
|
1556
|
+
ref: \${{ github.event.repository.default_branch }}
|
|
1557
|
+
persist-credentials: false
|
|
1558
|
+
fetch-depth: 0
|
|
1559
|
+
- uses: actions/setup-node@v4
|
|
1560
|
+
with:
|
|
1561
|
+
node-version: 20
|
|
1562
|
+
${vinayaSetupSteps(selfHost, "trusted")} - name: Body checks
|
|
1563
|
+
env:
|
|
1564
|
+
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
1565
|
+
# PR_NUMBER is what makes the Changesets-release exemption
|
|
1566
|
+
# EVALUATE its live gh pr view fetch: without it the check reads
|
|
1567
|
+
# "no PR yet — local dev" and falls through to the ordinary
|
|
1568
|
+
# bare-digit scan.
|
|
1569
|
+
PR_NUMBER: \${{ github.event.pull_request.number }}
|
|
1570
|
+
# PR_BODY is what makes body-bare-digits EVALUATE at all — the bin
|
|
1571
|
+
# reads \`process.env.PR_BODY\` only, never fetches it itself.
|
|
1572
|
+
PR_BODY: \${{ github.event.pull_request.body }}
|
|
1573
|
+
run: ${vinayaRun(selfHost, "check body-bare-digits")}
|
|
1574
|
+
`;
|
|
1575
|
+
}
|
|
1576
|
+
function reviewVerdictWorkflow(selfHost) {
|
|
1205
1577
|
return `# ${MANAGED_NOTE}
|
|
1206
1578
|
#
|
|
1207
1579
|
# The verdict-comment half of the review gate. A reviewer's verdict arrives
|
|
1208
1580
|
# as a PR comment (\`VERDICT: APPROVE\` / \`VERDICT: PASS\`), which fires
|
|
1209
|
-
# GitHub's \`issue_comment\` event — an event the required
|
|
1581
|
+
# GitHub's \`issue_comment\` event — an event the required PR-event
|
|
1210
1582
|
# workflow cannot receive. This workflow evaluates the gate on that comment
|
|
1211
1583
|
# and, when the evaluation is clean, RE-RUNS the required workflow so its
|
|
1212
1584
|
# check goes green natively with no manual rerun. (Writing check-run
|
|
@@ -1214,11 +1586,10 @@ function reviewVerdictWorkflow(selfHost, ciSetup) {
|
|
|
1214
1586
|
# restricts check-run updates to the owning workflow — re-running is the
|
|
1215
1587
|
# supported channel.)
|
|
1216
1588
|
#
|
|
1217
|
-
# Privilege split, deliberate: \`evaluate\`
|
|
1218
|
-
#
|
|
1219
|
-
# \`actions: write\` but checks out and executes nothing
|
|
1220
|
-
#
|
|
1221
|
-
# content ran. A malicious branch can at worst fail its own evaluation.
|
|
1589
|
+
# Privilege split, deliberate: \`evaluate\` executes only the default branch's
|
|
1590
|
+
# trusted gate code and holds NO write permission; \`retrigger\` holds
|
|
1591
|
+
# \`actions: write\` but checks out and executes nothing. Neither job checks
|
|
1592
|
+
# out or executes pull-request content.
|
|
1222
1593
|
name: Vinaya Review Gate (on verdict)
|
|
1223
1594
|
|
|
1224
1595
|
on:
|
|
@@ -1235,38 +1606,36 @@ jobs:
|
|
|
1235
1606
|
pull-requests: read
|
|
1236
1607
|
issues: read
|
|
1237
1608
|
outputs:
|
|
1238
|
-
|
|
1609
|
+
number: \${{ steps.pr.outputs.number }}
|
|
1239
1610
|
sha: \${{ steps.pr.outputs.sha }}
|
|
1240
1611
|
steps:
|
|
1241
|
-
# issue_comment payloads carry no PR head SHA
|
|
1242
|
-
#
|
|
1243
|
-
# ref is the repo's default branch, not the PR head).
|
|
1612
|
+
# issue_comment payloads carry no PR head SHA — resolve it for immutable
|
|
1613
|
+
# run identity only. It never selects executable code.
|
|
1244
1614
|
- name: Resolve PR head
|
|
1245
1615
|
id: pr
|
|
1246
1616
|
env:
|
|
1247
1617
|
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
1248
1618
|
run: |
|
|
1249
1619
|
NUMBER="\${{ github.event.issue.number }}"
|
|
1250
|
-
BRANCH=$(gh pr view "$NUMBER" --repo "\${{ github.repository }}" --json headRefName -q .headRefName)
|
|
1251
1620
|
SHA=$(gh pr view "$NUMBER" --repo "\${{ github.repository }}" --json headRefOid -q .headRefOid)
|
|
1252
1621
|
echo "number=$NUMBER" >> "$GITHUB_OUTPUT"
|
|
1253
|
-
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
|
|
1254
1622
|
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
|
|
1255
1623
|
- uses: actions/checkout@v4
|
|
1256
1624
|
with:
|
|
1257
|
-
|
|
1625
|
+
# issue_comment already sources workflows from the default branch;
|
|
1626
|
+
# make the trusted code checkout explicit too.
|
|
1627
|
+
ref: \${{ github.event.repository.default_branch }}
|
|
1258
1628
|
persist-credentials: false
|
|
1259
1629
|
fetch-depth: 0
|
|
1260
1630
|
- uses: actions/setup-node@v4
|
|
1261
1631
|
with:
|
|
1262
1632
|
node-version: 20
|
|
1263
|
-
${vinayaSetupSteps(selfHost)}
|
|
1633
|
+
${vinayaSetupSteps(selfHost, "trusted")} - name: Review gate (verdict evaluation)
|
|
1264
1634
|
env:
|
|
1265
1635
|
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
1266
1636
|
# Same wiring as the required workflow: PR_NUMBER is what makes
|
|
1267
1637
|
# the adapter evaluate instead of no-op'ing as "local dev".
|
|
1268
1638
|
PR_NUMBER: \${{ steps.pr.outputs.number }}
|
|
1269
|
-
BRANCH: \${{ steps.pr.outputs.branch }}
|
|
1270
1639
|
run: ${vinayaRun(selfHost, "check review-gate")}
|
|
1271
1640
|
|
|
1272
1641
|
# Executes nothing; consumes only the evaluator's outputs.
|
|
@@ -1287,16 +1656,11 @@ ${vinayaSetupSteps(selfHost)}${adopterSetupStep(ciSetup)} - name: Review ga
|
|
|
1287
1656
|
- name: Re-run the required review gate for this branch
|
|
1288
1657
|
env:
|
|
1289
1658
|
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
1290
|
-
|
|
1659
|
+
PR_NUMBER: \${{ needs.evaluate.outputs.number }}
|
|
1291
1660
|
HEAD_SHA: \${{ needs.evaluate.outputs.sha }}
|
|
1292
1661
|
run: |
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
# gate. Reachable now that this job runs on a failed evaluation —
|
|
1296
|
-
# \`Resolve PR head\` is the only producer of this output, and its
|
|
1297
|
-
# failure used to be unreachable from here.
|
|
1298
|
-
if [ -z "$BRANCH" ]; then
|
|
1299
|
-
echo "No branch resolved (the evaluate job failed before resolving it) - nothing to re-run."
|
|
1662
|
+
if [ -z "$PR_NUMBER" ]; then
|
|
1663
|
+
echo "No PR number resolved (the evaluate job failed before resolving it) - nothing to re-run."
|
|
1300
1664
|
exit 0
|
|
1301
1665
|
fi
|
|
1302
1666
|
# Same guard, same producer: an empty SHA would make the selection
|
|
@@ -1306,43 +1670,28 @@ ${vinayaSetupSteps(selfHost)}${adopterSetupStep(ciSetup)} - name: Review ga
|
|
|
1306
1670
|
echo "No head SHA resolved (the evaluate job failed before resolving it) - nothing to re-run."
|
|
1307
1671
|
exit 0
|
|
1308
1672
|
fi
|
|
1309
|
-
#
|
|
1310
|
-
#
|
|
1311
|
-
#
|
|
1312
|
-
#
|
|
1313
|
-
#
|
|
1314
|
-
#
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
# single-expression flag of its own, NOT a passthrough to jq, so jq's
|
|
1327
|
-
# variable-binding flag cannot be used with it: pflag swallows that
|
|
1328
|
-
# flag as the expression and gh exits 1 on the stray token — which
|
|
1329
|
-
# under \`bash -e\` kills the step before the empty-RUN_ID no-op can
|
|
1330
|
-
# run, so every verdict reddens and nothing reruns. Filter on the
|
|
1331
|
-
# server and keep the jq expression free of variables.
|
|
1332
|
-
RUN_ID=$(gh run list --repo "\${{ github.repository }}" \\
|
|
1333
|
-
--workflow vinaya-review.yml --branch "$BRANCH" \\
|
|
1334
|
-
--commit "$HEAD_SHA" \\
|
|
1335
|
-
--json databaseId,event,status,conclusion \\
|
|
1336
|
-
--jq '[.[]
|
|
1337
|
-
| select(.event=="pull_request")
|
|
1338
|
-
| select(.status=="completed")
|
|
1339
|
-
| select(.conclusion!="cancelled")]
|
|
1340
|
-
| .[0].databaseId // empty')
|
|
1673
|
+
# pull_request_target runs execute at the DEFAULT branch SHA, so a
|
|
1674
|
+
# run's head_sha cannot identify the PR commit. GitHub's nested
|
|
1675
|
+
# pull_requests snapshot is also not immutable: an old rerun can
|
|
1676
|
+
# expose the PR's current head. The required workflow therefore
|
|
1677
|
+
# records PR number + head SHA in run-name at creation, and this
|
|
1678
|
+
# query matches that immutable display_title exactly.
|
|
1679
|
+
set -o pipefail
|
|
1680
|
+
RUN_TITLE="Vinaya Review Gate PR #$PR_NUMBER @ $HEAD_SHA"
|
|
1681
|
+
RUN_ID=$(gh api --paginate \\
|
|
1682
|
+
"repos/\${{ github.repository }}/actions/workflows/vinaya-review.yml/runs?event=pull_request_target&per_page=100" \\
|
|
1683
|
+
| jq -sr --arg title "$RUN_TITLE" '
|
|
1684
|
+
[.[].workflow_runs[]
|
|
1685
|
+
| select(.display_title == $title)
|
|
1686
|
+
| select(.event == "pull_request_target")
|
|
1687
|
+
| select(.status == "completed")
|
|
1688
|
+
| select(.conclusion != "cancelled")
|
|
1689
|
+
| .id][0] // empty')
|
|
1341
1690
|
if [ -z "$RUN_ID" ]; then
|
|
1342
|
-
echo "No completed, non-cancelled
|
|
1691
|
+
echo "No completed, non-cancelled pull_request_target run of vinaya-review.yml for PR #$PR_NUMBER at $HEAD_SHA - nothing to re-run."
|
|
1343
1692
|
exit 0
|
|
1344
1693
|
fi
|
|
1345
|
-
echo "Re-running vinaya-review.yml run $RUN_ID for $HEAD_SHA"
|
|
1694
|
+
echo "Re-running vinaya-review.yml run $RUN_ID for PR #$PR_NUMBER at $HEAD_SHA"
|
|
1346
1695
|
# Two verdict comments landing together run two retriggers in
|
|
1347
1696
|
# parallel (this workflow has no concurrency group, deliberately —
|
|
1348
1697
|
# a queued retrigger is a delayed one). Both can select the same
|
|
@@ -1536,6 +1885,14 @@ function starterDocOwners() {
|
|
|
1536
1885
|
# This file absent, OR no glob matches any changed code file → silent no-op.
|
|
1537
1886
|
# The gate has no opinion until you teach it one.
|
|
1538
1887
|
#
|
|
1888
|
+
# \`vinaya doctor\` (never \`vinaya check\`) separately reports — it does not
|
|
1889
|
+
# fail the check — any binding whose code glob matches zero tracked files
|
|
1890
|
+
# anywhere in the repo, or whose in-repo pointer doesn't exist on disk. This
|
|
1891
|
+
# catches what the dormancy rule above structurally cannot: a glob matching
|
|
1892
|
+
# nothing trivially satisfies "no changed file matched" on every diff,
|
|
1893
|
+
# forever, so a binding naming deleted or renamed code reads as healthy
|
|
1894
|
+
# indefinitely unless something walks the whole repo, not just one diff.
|
|
1895
|
+
#
|
|
1539
1896
|
# ── No-doc allow-list ───────────────────────────────────────────────────────
|
|
1540
1897
|
# Surfaces that legitimately need no bound doc are listed below. Format:
|
|
1541
1898
|
# # no-doc: <glob> — <reason>
|
|
@@ -1575,6 +1932,13 @@ var BRANCH_PROTECTION_NOTE = `Recommended (run yourself — vinaya never applies
|
|
|
1575
1932
|
-F required_status_checks.strict=true \\
|
|
1576
1933
|
-F 'required_status_checks.contexts[]=vinaya-checks' \\
|
|
1577
1934
|
-F enforce_admins=true -F restrictions=`;
|
|
1935
|
+
var CODEOWNERS_NOTE = `Recommended: require review on vinaya's own workflow files.
|
|
1936
|
+
|
|
1937
|
+
echo '/.github/workflows/** @your-github-login' >> .github/CODEOWNERS
|
|
1938
|
+
|
|
1939
|
+
Then add this flag to the branch-protection command above:
|
|
1940
|
+
|
|
1941
|
+
-F required_pull_request_reviews.require_code_owner_reviews=true`;
|
|
1578
1942
|
function buildInitOps(ctx) {
|
|
1579
1943
|
const ops = [];
|
|
1580
1944
|
const hookMode = 493;
|
|
@@ -1587,13 +1951,13 @@ function buildInitOps(ctx) {
|
|
|
1587
1951
|
ops.push({
|
|
1588
1952
|
kind: "create-file",
|
|
1589
1953
|
path: REVIEW_WORKFLOW_PATH,
|
|
1590
|
-
content: reviewWorkflow(ctx.selfHost
|
|
1954
|
+
content: reviewWorkflow(ctx.selfHost),
|
|
1591
1955
|
group: "CI workflows"
|
|
1592
1956
|
});
|
|
1593
1957
|
ops.push({
|
|
1594
1958
|
kind: "create-file",
|
|
1595
1959
|
path: REVIEW_VERDICT_WORKFLOW_PATH,
|
|
1596
|
-
content: reviewVerdictWorkflow(ctx.selfHost
|
|
1960
|
+
content: reviewVerdictWorkflow(ctx.selfHost),
|
|
1597
1961
|
group: "CI workflows"
|
|
1598
1962
|
});
|
|
1599
1963
|
ops.push({
|
|
@@ -1602,6 +1966,12 @@ function buildInitOps(ctx) {
|
|
|
1602
1966
|
content: archivistWorkflow(ctx.selfHost),
|
|
1603
1967
|
group: "CI workflows"
|
|
1604
1968
|
});
|
|
1969
|
+
ops.push({
|
|
1970
|
+
kind: "create-file",
|
|
1971
|
+
path: BODY_CHECKS_WORKFLOW_PATH,
|
|
1972
|
+
content: bodyChecksWorkflow(ctx.selfHost),
|
|
1973
|
+
group: "CI workflows"
|
|
1974
|
+
});
|
|
1605
1975
|
ops.push({
|
|
1606
1976
|
kind: "managed-block",
|
|
1607
1977
|
path: `${ctx.hookDir}/pre-commit`,
|
|
@@ -1652,6 +2022,7 @@ function buildInitOps(ctx) {
|
|
|
1652
2022
|
});
|
|
1653
2023
|
ops.push(...labelOps());
|
|
1654
2024
|
ops.push({ kind: "print", message: BRANCH_PROTECTION_NOTE, group: "Branch protection (printed, never applied)" });
|
|
2025
|
+
ops.push({ kind: "print", message: CODEOWNERS_NOTE, group: "Branch protection (printed, never applied)" });
|
|
1655
2026
|
return ops;
|
|
1656
2027
|
}
|
|
1657
2028
|
function buildInitProductOps(name) {
|
|
@@ -1668,11 +2039,12 @@ function buildInitProductOps(name) {
|
|
|
1668
2039
|
}
|
|
1669
2040
|
|
|
1670
2041
|
// src/lib/ops.ts
|
|
1671
|
-
import { execFileSync as
|
|
2042
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
1672
2043
|
import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1673
2044
|
import { dirname as dirname3, isAbsolute, join as join4, resolve, sep } from "node:path";
|
|
1674
2045
|
|
|
1675
2046
|
// src/lib/config.ts
|
|
2047
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1676
2048
|
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
1677
2049
|
import { homedir } from "node:os";
|
|
1678
2050
|
import { dirname as dirname2, join as join3 } from "node:path";
|
|
@@ -1800,7 +2172,9 @@ var VinayaConfigSchema = z.object({
|
|
|
1800
2172
|
briefSchema: BriefSchemaSchema.optional(),
|
|
1801
2173
|
managed: ManagedManifestSchema.optional(),
|
|
1802
2174
|
principals: z.array(z.string()).min(1).optional(),
|
|
1803
|
-
|
|
2175
|
+
releaseActor: z.string().min(1).optional(),
|
|
2176
|
+
ci: z.object({ setup: z.string().min(1) }).optional(),
|
|
2177
|
+
blastRadius: z.object({ extraDomains: z.array(z.string()).optional() }).optional()
|
|
1804
2178
|
});
|
|
1805
2179
|
var GLOBAL_VINAYA_HOME = join3(homedir(), ".vinaya");
|
|
1806
2180
|
var GLOBAL_CONFIG_PATH = join3(GLOBAL_VINAYA_HOME, "config.json");
|
|
@@ -1833,6 +2207,9 @@ function globalChecksIgnoredWarning(path) {
|
|
|
1833
2207
|
function globalPrincipalsIgnoredWarning(path) {
|
|
1834
2208
|
return `${path}: "principals" in the global config is ignored — principals may only be declared from a repo-local vinaya.config.json.`;
|
|
1835
2209
|
}
|
|
2210
|
+
function globalReleaseActorIgnoredWarning(path) {
|
|
2211
|
+
return `${path}: "releaseActor" in the global config is ignored — releaseActor may only be declared from a repo-local vinaya.config.json.`;
|
|
2212
|
+
}
|
|
1836
2213
|
function stripGlobalOnlyKeys(config, path) {
|
|
1837
2214
|
if (path !== GLOBAL_CONFIG_PATH)
|
|
1838
2215
|
return config;
|
|
@@ -1845,8 +2222,81 @@ function stripGlobalOnlyKeys(config, path) {
|
|
|
1845
2222
|
console.error(`⚠ ${globalPrincipalsIgnoredWarning(path)}`);
|
|
1846
2223
|
result = { ...result, principals: undefined };
|
|
1847
2224
|
}
|
|
2225
|
+
if (result.releaseActor) {
|
|
2226
|
+
console.error(`⚠ ${globalReleaseActorIgnoredWarning(path)}`);
|
|
2227
|
+
result = { ...result, releaseActor: undefined };
|
|
2228
|
+
}
|
|
1848
2229
|
return result;
|
|
1849
2230
|
}
|
|
2231
|
+
function resolvePrincipalAllowlist(config) {
|
|
2232
|
+
return config?.principals ?? PRINCIPAL_ALLOWLIST;
|
|
2233
|
+
}
|
|
2234
|
+
function trustAnchorRepo() {
|
|
2235
|
+
const wellFormed = (slug) => /^[^/\s]+\/[^/\s]+$/.test(slug) ? slug : null;
|
|
2236
|
+
const fromRunner = process.env.GITHUB_REPOSITORY?.trim();
|
|
2237
|
+
if (fromRunner) {
|
|
2238
|
+
const validated = wellFormed(fromRunner);
|
|
2239
|
+
if (validated)
|
|
2240
|
+
return validated;
|
|
2241
|
+
}
|
|
2242
|
+
try {
|
|
2243
|
+
const url = execFileSync2("git", ["remote", "get-url", "origin"], {
|
|
2244
|
+
encoding: "utf-8",
|
|
2245
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2246
|
+
}).trim();
|
|
2247
|
+
const m = url.match(/github\.com[:/]([^/]+)\/(.+?)(?:\.git)?\/?$/);
|
|
2248
|
+
return m?.[1] && m[2] ? wellFormed(`${m[1]}/${m[2]}`) : null;
|
|
2249
|
+
} catch {
|
|
2250
|
+
return null;
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
function ghFetchTrustAnchorConfig() {
|
|
2254
|
+
const repo = trustAnchorRepo();
|
|
2255
|
+
if (!repo)
|
|
2256
|
+
throw new Error("could not resolve repo identity for the trust-anchor read");
|
|
2257
|
+
return execFileSync2("gh", ["api", `repos/${repo}/contents/${LOCAL_CONFIG_FILENAME}`, "--jq", ".content"], {
|
|
2258
|
+
encoding: "utf-8",
|
|
2259
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
function firstLine(err) {
|
|
2263
|
+
return err?.message?.split(`
|
|
2264
|
+
`)[0] ?? "unknown error";
|
|
2265
|
+
}
|
|
2266
|
+
function isMissingFileError(err) {
|
|
2267
|
+
const stderr = err?.stderr;
|
|
2268
|
+
const haystack = [
|
|
2269
|
+
err?.message ?? "",
|
|
2270
|
+
typeof stderr === "string" ? stderr : stderr?.toString() ?? ""
|
|
2271
|
+
].join(`
|
|
2272
|
+
`);
|
|
2273
|
+
return /\b404\b|not found/i.test(haystack);
|
|
2274
|
+
}
|
|
2275
|
+
function loadTrustAnchorConfig(fetcher = ghFetchTrustAnchorConfig) {
|
|
2276
|
+
const warn = (why) => process.stdout.write(`⚠ could not read the trust-anchor config (\`principals\`/\`releaseActor\`) from the default branch (${why}) — falling back to vinaya's built-in defaults.
|
|
2277
|
+
`);
|
|
2278
|
+
let base64;
|
|
2279
|
+
try {
|
|
2280
|
+
base64 = fetcher().trim();
|
|
2281
|
+
} catch (err) {
|
|
2282
|
+
if (!isMissingFileError(err))
|
|
2283
|
+
warn(firstLine(err));
|
|
2284
|
+
return null;
|
|
2285
|
+
}
|
|
2286
|
+
if (!base64)
|
|
2287
|
+
return null;
|
|
2288
|
+
try {
|
|
2289
|
+
const raw = Buffer.from(base64, "base64").toString("utf-8");
|
|
2290
|
+
const parsed = VinayaConfigSchema.safeParse(JSON.parse(raw));
|
|
2291
|
+
if (parsed.success)
|
|
2292
|
+
return parsed.data;
|
|
2293
|
+
warn("it did not satisfy the config schema");
|
|
2294
|
+
return null;
|
|
2295
|
+
} catch {
|
|
2296
|
+
warn("it was not readable as JSON");
|
|
2297
|
+
return null;
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
1850
2300
|
function readRepoCiSetup(repoRoot) {
|
|
1851
2301
|
const p = join3(repoRoot, "vinaya.config.json");
|
|
1852
2302
|
if (!existsSync2(p))
|
|
@@ -1897,7 +2347,7 @@ function resolveManagedBlockPath(repoRoot, opPath) {
|
|
|
1897
2347
|
if (!opPath.startsWith(".git/"))
|
|
1898
2348
|
return join4(repoRoot, opPath);
|
|
1899
2349
|
try {
|
|
1900
|
-
const commonDir =
|
|
2350
|
+
const commonDir = execFileSync3("git", ["-C", repoRoot, "rev-parse", "--git-common-dir"], {
|
|
1901
2351
|
encoding: "utf8"
|
|
1902
2352
|
}).trim();
|
|
1903
2353
|
const gitDir = isAbsolute(commonDir) ? commonDir : join4(repoRoot, commonDir);
|
|
@@ -2463,7 +2913,7 @@ function realDeps() {
|
|
|
2463
2913
|
return { detectRepo: detectGitRepo };
|
|
2464
2914
|
}
|
|
2465
2915
|
function sh(args, input) {
|
|
2466
|
-
return
|
|
2916
|
+
return execFileSync4(args[0], args.slice(1), { encoding: "utf8", input }).trim();
|
|
2467
2917
|
}
|
|
2468
2918
|
function shJson(args) {
|
|
2469
2919
|
return JSON.parse(sh(args));
|
|
@@ -2639,7 +3089,7 @@ async function archiveTrancheCommand(args) {
|
|
|
2639
3089
|
}
|
|
2640
3090
|
|
|
2641
3091
|
// src/commands/audit.ts
|
|
2642
|
-
import { execFileSync as
|
|
3092
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
2643
3093
|
|
|
2644
3094
|
// src/lib/envelope.ts
|
|
2645
3095
|
var ENVELOPE_SCHEMA_VERSION = 1;
|
|
@@ -2667,7 +3117,7 @@ var DIRECT_PUSH_MARKER = `<!-- ${DIRECT_PUSH_LABEL} -->`;
|
|
|
2667
3117
|
var DIRECT_PUSH_LABEL_DESCRIPTION = "A commit landed on main with no associated merged PR (ring-2 detection)";
|
|
2668
3118
|
function shSoft(args, input) {
|
|
2669
3119
|
try {
|
|
2670
|
-
return
|
|
3120
|
+
return execFileSync5(args[0], args.slice(1), {
|
|
2671
3121
|
encoding: "utf8",
|
|
2672
3122
|
input,
|
|
2673
3123
|
timeout: COMMAND_TIMEOUT_MS
|
|
@@ -2691,7 +3141,7 @@ function shSoftJson(args) {
|
|
|
2691
3141
|
}
|
|
2692
3142
|
}
|
|
2693
3143
|
function sh2(args, input) {
|
|
2694
|
-
return
|
|
3144
|
+
return execFileSync5(args[0], args.slice(1), { encoding: "utf8", input }).trim();
|
|
2695
3145
|
}
|
|
2696
3146
|
function listTaskBranches() {
|
|
2697
3147
|
const out = shSoft(["git", "ls-remote", "--heads", "origin", "task/*"]);
|
|
@@ -2941,7 +3391,7 @@ async function auditCommand(args) {
|
|
|
2941
3391
|
}
|
|
2942
3392
|
|
|
2943
3393
|
// src/commands/check.ts
|
|
2944
|
-
import { execFileSync as
|
|
3394
|
+
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
2945
3395
|
|
|
2946
3396
|
// src/checks/contract.ts
|
|
2947
3397
|
var CHECK_SCHEMA_VERSION = 1;
|
|
@@ -3051,6 +3501,21 @@ function coreCheckRegistry() {
|
|
|
3051
3501
|
BRANCH: { optional: true }
|
|
3052
3502
|
}
|
|
3053
3503
|
},
|
|
3504
|
+
{
|
|
3505
|
+
name: "body-bare-digits",
|
|
3506
|
+
run: bin("check-body-bare-digits"),
|
|
3507
|
+
scope: "diff",
|
|
3508
|
+
timeoutMs: 15000,
|
|
3509
|
+
requiresOpenPr: true,
|
|
3510
|
+
ownWorkflow: true,
|
|
3511
|
+
env: {
|
|
3512
|
+
PR_BODY: { optional: true },
|
|
3513
|
+
PR_NUMBER: { optional: true },
|
|
3514
|
+
GITHUB_REPOSITORY: { optional: true },
|
|
3515
|
+
GITHUB_TOKEN: { optional: true },
|
|
3516
|
+
GH_TOKEN: { optional: true }
|
|
3517
|
+
}
|
|
3518
|
+
},
|
|
3054
3519
|
{
|
|
3055
3520
|
name: "no-disk-state",
|
|
3056
3521
|
run: bin("check-no-disk-state"),
|
|
@@ -3076,7 +3541,6 @@ function coreCheckRegistry() {
|
|
|
3076
3541
|
timeoutMs: 30000,
|
|
3077
3542
|
ownWorkflow: true,
|
|
3078
3543
|
env: {
|
|
3079
|
-
BRANCH: { optional: true },
|
|
3080
3544
|
PR_NUMBER: { optional: true },
|
|
3081
3545
|
GITHUB_REPOSITORY: { optional: true },
|
|
3082
3546
|
GITHUB_TOKEN: { optional: true },
|
|
@@ -3478,7 +3942,7 @@ async function runChecks(specs, opts) {
|
|
|
3478
3942
|
// src/commands/check.ts
|
|
3479
3943
|
function git(args) {
|
|
3480
3944
|
try {
|
|
3481
|
-
return
|
|
3945
|
+
return execFileSync6("git", args, { encoding: "utf8" }).trim();
|
|
3482
3946
|
} catch {
|
|
3483
3947
|
return "";
|
|
3484
3948
|
}
|
|
@@ -3611,11 +4075,13 @@ async function checkCommand(args) {
|
|
|
3611
4075
|
process.stdout.write(`${renderPlanTable(result)}
|
|
3612
4076
|
`);
|
|
3613
4077
|
}
|
|
3614
|
-
process.
|
|
4078
|
+
process.exitCode = result.failures.length > 0 ? 1 : 0;
|
|
4079
|
+
return;
|
|
3615
4080
|
}
|
|
3616
4081
|
if (!allRequested && !requestedName) {
|
|
3617
4082
|
console.error("Usage: vinaya check <name> | --all | --plan [--json] [--diff-only] [--local] [--parallel[=n]]");
|
|
3618
|
-
process.
|
|
4083
|
+
process.exitCode = 2;
|
|
4084
|
+
return;
|
|
3619
4085
|
}
|
|
3620
4086
|
if (refusals.length > 0) {
|
|
3621
4087
|
const outcome = {
|
|
@@ -3636,7 +4102,8 @@ async function checkCommand(args) {
|
|
|
3636
4102
|
process.stdout.write(` ${e.severity}: ${e.message}
|
|
3637
4103
|
`);
|
|
3638
4104
|
}
|
|
3639
|
-
process.
|
|
4105
|
+
process.exitCode = 1;
|
|
4106
|
+
return;
|
|
3640
4107
|
}
|
|
3641
4108
|
const allSpecs = result.resolved.map((entry2) => entry2.spec);
|
|
3642
4109
|
const notices = substitutionNotices(result.resolved);
|
|
@@ -3650,7 +4117,8 @@ async function checkCommand(args) {
|
|
|
3650
4117
|
const specsToRun = allRequested ? allSpecs.filter(runsUnderAll) : allSpecs.filter((s) => s.name === requestedName);
|
|
3651
4118
|
if (!allRequested && specsToRun.length === 0) {
|
|
3652
4119
|
console.error(`Unknown check: ${requestedName}`);
|
|
3653
|
-
process.
|
|
4120
|
+
process.exitCode = 2;
|
|
4121
|
+
return;
|
|
3654
4122
|
}
|
|
3655
4123
|
const changed = diffOnly ? changedFiles() : null;
|
|
3656
4124
|
const outcomes = specsToRun.length > 0 ? await runChecks(specsToRun, {
|
|
@@ -3677,18 +4145,18 @@ async function checkCommand(args) {
|
|
|
3677
4145
|
}
|
|
3678
4146
|
}
|
|
3679
4147
|
const failed = outcomes.some((o) => o.status === "fail" || o.status === "error" || o.status === "timeout");
|
|
3680
|
-
process.
|
|
4148
|
+
process.exitCode = failed ? 1 : 0;
|
|
3681
4149
|
}
|
|
3682
4150
|
|
|
3683
4151
|
// src/commands/demo.ts
|
|
3684
|
-
import { execFileSync as
|
|
4152
|
+
import { execFileSync as execFileSync7, spawnSync } from "node:child_process";
|
|
3685
4153
|
import { existsSync as existsSync6, readFileSync as readFileSync5, rmSync as rmSync2, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3686
4154
|
import { isAbsolute as isAbsolute2, join as join7 } from "node:path";
|
|
3687
4155
|
var DEMO_BRANCH_PREFIX = "vinaya/demo-break-";
|
|
3688
4156
|
var FIXTURE_PATH = ".vinaya-demo-brief.md";
|
|
3689
4157
|
var STATE_FILENAME = "vinaya-demo-state.json";
|
|
3690
4158
|
function gitCapture(repoRoot, args) {
|
|
3691
|
-
return
|
|
4159
|
+
return execFileSync7("git", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
3692
4160
|
}
|
|
3693
4161
|
function currentBranchName(repoRoot) {
|
|
3694
4162
|
try {
|
|
@@ -3699,7 +4167,7 @@ function currentBranchName(repoRoot) {
|
|
|
3699
4167
|
}
|
|
3700
4168
|
function branchExists(repoRoot, name) {
|
|
3701
4169
|
try {
|
|
3702
|
-
|
|
4170
|
+
execFileSync7("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${name}`], {
|
|
3703
4171
|
cwd: repoRoot,
|
|
3704
4172
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3705
4173
|
});
|
|
@@ -3784,12 +4252,12 @@ Test Plan: unit-tests-only
|
|
|
3784
4252
|
## Stop Conditions
|
|
3785
4253
|
Autonomy: Do not stop to ask clarifying questions — this is a scripted demo; proceed and clean up automatically.
|
|
3786
4254
|
|
|
3787
|
-
Illustrative Step 0 (never actually run by this demo):
|
|
4255
|
+
Illustrative \`Step 0\` (never actually run by this demo):
|
|
3788
4256
|
\`\`\`
|
|
3789
4257
|
git worktree add .worktrees/demo-fixture -b demo/fixture origin/main
|
|
3790
4258
|
\`\`\`
|
|
3791
4259
|
|
|
3792
|
-
Closes
|
|
4260
|
+
Closes \`#0\` (fixture value — no real Issue; discarded with the demo branch)
|
|
3793
4261
|
`;
|
|
3794
4262
|
}
|
|
3795
4263
|
function cleanup(repoRoot, originalBranch, demoBranch, statePath) {
|
|
@@ -3901,7 +4369,8 @@ async function demoBreakCommand(args) {
|
|
|
3901
4369
|
}
|
|
3902
4370
|
|
|
3903
4371
|
// src/commands/doctor.ts
|
|
3904
|
-
import {
|
|
4372
|
+
import { execFileSync as execFileSync8 } from "node:child_process";
|
|
4373
|
+
import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync2 } from "node:fs";
|
|
3905
4374
|
import { join as join9 } from "node:path";
|
|
3906
4375
|
|
|
3907
4376
|
// src/lib/self-host.ts
|
|
@@ -4182,6 +4651,90 @@ function diagnoseCustomChecks(repoRoot, config) {
|
|
|
4182
4651
|
}
|
|
4183
4652
|
return findings;
|
|
4184
4653
|
}
|
|
4654
|
+
function listTrackedFiles(repoRoot) {
|
|
4655
|
+
try {
|
|
4656
|
+
return execFileSync8("git", ["ls-files"], { cwd: repoRoot, encoding: "utf-8" }).split(`
|
|
4657
|
+
`).map((s) => s.trim()).filter(Boolean);
|
|
4658
|
+
} catch {
|
|
4659
|
+
return [];
|
|
4660
|
+
}
|
|
4661
|
+
}
|
|
4662
|
+
function diagnoseDocOwnersHealth(repoRoot) {
|
|
4663
|
+
const path = join9(repoRoot, DOC_OWNERS_PATH);
|
|
4664
|
+
const content = existsSync8(path) ? readFileSync8(path, "utf-8") : null;
|
|
4665
|
+
const state = classifyDocOwnersManifest(content);
|
|
4666
|
+
if (state === "absent" || state === "empty")
|
|
4667
|
+
return [];
|
|
4668
|
+
const { bindings } = parseDocOwners(content);
|
|
4669
|
+
if (bindings.length === 0)
|
|
4670
|
+
return [];
|
|
4671
|
+
const codeFiles = listTrackedFiles(repoRoot).filter(isCodeFile);
|
|
4672
|
+
const findings = [];
|
|
4673
|
+
for (const b of bindings) {
|
|
4674
|
+
let flagged = false;
|
|
4675
|
+
const re = globToRegex(b.glob);
|
|
4676
|
+
if (!codeFiles.some((f) => re.test(f))) {
|
|
4677
|
+
flagged = true;
|
|
4678
|
+
findings.push(warn("doc-owners", `${DOC_OWNERS_PATH}:${b.lineNum} binds glob '${b.glob}', which matches none of the ${codeFiles.length} ` + "tracked code file(s) in this repo — the code it names may have been deleted, renamed, or never " + "existed. Repoint the binding to the code's new location, or remove it."));
|
|
4679
|
+
}
|
|
4680
|
+
if (!isUrlPointer(b.pointer)) {
|
|
4681
|
+
const pointerPath = join9(repoRoot, pointerToPath(b.pointer));
|
|
4682
|
+
if (!existsSync8(pointerPath)) {
|
|
4683
|
+
flagged = true;
|
|
4684
|
+
findings.push(warn("doc-owners", `${DOC_OWNERS_PATH}:${b.lineNum} points to ${b.pointer}, which does not exist on disk. Repoint the ` + "binding, or add the missing doc."));
|
|
4685
|
+
}
|
|
4686
|
+
}
|
|
4687
|
+
if (!flagged) {
|
|
4688
|
+
findings.push(ok("doc-owners", `${DOC_OWNERS_PATH}:${b.lineNum} — '${b.glob}' → ${b.pointer} is live.`));
|
|
4689
|
+
}
|
|
4690
|
+
}
|
|
4691
|
+
return findings;
|
|
4692
|
+
}
|
|
4693
|
+
var LEGACY_AEG_PACKAGES_PATH = ".aeg/packages";
|
|
4694
|
+
function readWorkspacesForDoctor(repoRoot) {
|
|
4695
|
+
const fromPackageJson = () => {
|
|
4696
|
+
try {
|
|
4697
|
+
const pkg = JSON.parse(readFileSync8(join9(repoRoot, "package.json"), "utf-8"));
|
|
4698
|
+
return Array.isArray(pkg.workspaces) ? pkg.workspaces.filter((w) => typeof w === "string") : [];
|
|
4699
|
+
} catch {
|
|
4700
|
+
return [];
|
|
4701
|
+
}
|
|
4702
|
+
};
|
|
4703
|
+
const fromPnpmWorkspaceYaml = () => {
|
|
4704
|
+
try {
|
|
4705
|
+
return parsePnpmWorkspaceYaml(readFileSync8(join9(repoRoot, "pnpm-workspace.yaml"), "utf-8"));
|
|
4706
|
+
} catch {
|
|
4707
|
+
return [];
|
|
4708
|
+
}
|
|
4709
|
+
};
|
|
4710
|
+
return [...fromPackageJson(), ...fromPnpmWorkspaceYaml()];
|
|
4711
|
+
}
|
|
4712
|
+
function listChildDirsForDoctor(dir, repoRoot) {
|
|
4713
|
+
try {
|
|
4714
|
+
return readdirSync3(join9(repoRoot, dir), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
4715
|
+
} catch {
|
|
4716
|
+
return [];
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
4719
|
+
function diagnoseBlastRadiusDeprecation(repoRoot, config) {
|
|
4720
|
+
const derived = deriveWorkspacePackageDomains(readWorkspacesForDoctor(repoRoot), (dir) => listChildDirsForDoctor(dir, repoRoot));
|
|
4721
|
+
const defaults = deriveBuiltinCrossCuttingDefaults((p) => existsSync8(join9(repoRoot, p)));
|
|
4722
|
+
const covered = new Set([...derived, ...defaults]);
|
|
4723
|
+
const configExtra = new Set(config?.blastRadius?.extraDomains ?? []);
|
|
4724
|
+
const legacyPath = join9(repoRoot, LEGACY_AEG_PACKAGES_PATH);
|
|
4725
|
+
if (!existsSync8(legacyPath)) {
|
|
4726
|
+
return [
|
|
4727
|
+
info("blast-radius", `checkBlastRadiusScope is active via live derivation (${derived.length} packages/* domain(s)) + built-in defaults (${defaults.length} present) — no ${LEGACY_AEG_PACKAGES_PATH}, and the check is not dormant.`)
|
|
4728
|
+
];
|
|
4729
|
+
}
|
|
4730
|
+
const legacyEntries = readFileSync8(legacyPath, "utf-8").split(`
|
|
4731
|
+
`).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
4732
|
+
const uncovered = legacyEntries.filter((e) => !covered.has(e) && !configExtra.has(e));
|
|
4733
|
+
const migrationNote = uncovered.length === 0 ? "every entry is already covered by live derivation, the built-in defaults, or vinaya.config.json blastRadius.extraDomains — the file can be deleted." : `migrate ${uncovered.length} entr${uncovered.length === 1 ? "y" : "ies"} not yet covered before deleting it: ${uncovered.join(", ")}.`;
|
|
4734
|
+
return [
|
|
4735
|
+
warn("blast-radius", `${LEGACY_AEG_PACKAGES_PATH} is deprecated — checkBlastRadiusScope now derives packages/* domains live and ships built-in cross-cutting defaults. Declare anything beyond those in vinaya.config.json's blastRadius.extraDomains instead. ${migrationNote}`)
|
|
4736
|
+
];
|
|
4737
|
+
}
|
|
4185
4738
|
function diagnoseEnvDeclarations(repoRoot, config) {
|
|
4186
4739
|
const resolved = resolveChecks(coreCheckRegistry(), config?.checks).resolved;
|
|
4187
4740
|
const specs = resolved.map((entry2) => entry2.source === "config" ? { ...entry2.spec, run: join9(repoRoot, entry2.spec.run) } : entry2.spec);
|
|
@@ -4242,6 +4795,48 @@ async function diagnoseBranchProtection(deps, owner, repo) {
|
|
|
4242
4795
|
}
|
|
4243
4796
|
return info("branch-protection", "main branch protection could not be determined (no gh auth, no remote, or a permission gap).");
|
|
4244
4797
|
}
|
|
4798
|
+
function coversWorkflowsDir(pattern) {
|
|
4799
|
+
const bare = pattern.replace(/\*+$/, "").replace(/\/+$/, "");
|
|
4800
|
+
return bare === ".github/workflows" || bare === "/.github/workflows";
|
|
4801
|
+
}
|
|
4802
|
+
function diagnoseCodeowners(repoRoot) {
|
|
4803
|
+
const path = join9(repoRoot, ".github", "CODEOWNERS");
|
|
4804
|
+
if (!existsSync8(path)) {
|
|
4805
|
+
return info("codeowners", "no .github/CODEOWNERS — vinaya's workflow files have no required-review protection; see `vinaya init`'s printed recommendation.");
|
|
4806
|
+
}
|
|
4807
|
+
let body;
|
|
4808
|
+
try {
|
|
4809
|
+
body = readFileSync8(path, "utf-8");
|
|
4810
|
+
} catch (err) {
|
|
4811
|
+
return warn("codeowners", `.github/CODEOWNERS exists but could not be read: ${err.message}`);
|
|
4812
|
+
}
|
|
4813
|
+
const covered = body.split(`
|
|
4814
|
+
`).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).some((line) => coversWorkflowsDir(line.split(/\s+/)[0] ?? ""));
|
|
4815
|
+
return covered ? info("codeowners", ".github/CODEOWNERS covers .github/workflows/**.") : warn("codeowners", ".github/CODEOWNERS exists but has no entry covering .github/workflows/** — workflow file edits can merge unreviewed.");
|
|
4816
|
+
}
|
|
4817
|
+
var TEST_INVOCATION_SUBSTRINGS = ["npm test", "npm run test", "bun test", "bunx turbo test"];
|
|
4818
|
+
function diagnoseTestCi(repoRoot) {
|
|
4819
|
+
let pkg;
|
|
4820
|
+
try {
|
|
4821
|
+
pkg = JSON.parse(readFileSync8(join9(repoRoot, "package.json"), "utf-8"));
|
|
4822
|
+
} catch {
|
|
4823
|
+
return [];
|
|
4824
|
+
}
|
|
4825
|
+
const testScript = pkg?.scripts?.test;
|
|
4826
|
+
if (typeof testScript !== "string" || testScript.trim() === "")
|
|
4827
|
+
return [];
|
|
4828
|
+
const workflowsDir = join9(repoRoot, ".github/workflows");
|
|
4829
|
+
const files = existsSync8(workflowsDir) ? readdirSync3(workflowsDir).filter((name) => statSync2(join9(workflowsDir, name)).isFile()) : [];
|
|
4830
|
+
const invoked = files.some((name) => {
|
|
4831
|
+
const content = readFileSync8(join9(workflowsDir, name), "utf-8");
|
|
4832
|
+
return TEST_INVOCATION_SUBSTRINGS.some((s) => content.includes(s));
|
|
4833
|
+
});
|
|
4834
|
+
if (invoked)
|
|
4835
|
+
return [];
|
|
4836
|
+
return [
|
|
4837
|
+
warn("test-ci", "no workflow under .github/workflows/ appears to invoke this repo's `package.json` test script — " + "vinaya requires a Test Plan on every PR and enforces it as a blocking gate, but nothing here verifies that tests actually run.")
|
|
4838
|
+
];
|
|
4839
|
+
}
|
|
4245
4840
|
function symbolFor(severity) {
|
|
4246
4841
|
switch (severity) {
|
|
4247
4842
|
case "ok":
|
|
@@ -4295,12 +4890,16 @@ async function runDoctor(args, deps) {
|
|
|
4295
4890
|
hasDrift = install.hasDrift;
|
|
4296
4891
|
findings.push(...await diagnoseHookRouting(repo.repoRoot, hookDir, deps.readHooksPath));
|
|
4297
4892
|
findings.push(...diagnoseCustomChecks(repo.repoRoot, configRead.config));
|
|
4893
|
+
findings.push(...diagnoseDocOwnersHealth(repo.repoRoot));
|
|
4298
4894
|
}
|
|
4895
|
+
findings.push(...diagnoseBlastRadiusDeprecation(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
|
|
4299
4896
|
findings.push(...diagnoseEnvDeclarations(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
|
|
4300
4897
|
findings.push(...diagnoseCheckClassification(configRead.kind === "ok" ? configRead.config : null));
|
|
4301
4898
|
findings.push(...diagnoseGlobalConfigChecks());
|
|
4302
4899
|
findings.push(...await diagnoseEnvironment(deps, hasDrift));
|
|
4303
4900
|
findings.push(await diagnoseBranchProtection(deps, repo.owner, repo.repo));
|
|
4901
|
+
findings.push(diagnoseCodeowners(repo.repoRoot));
|
|
4902
|
+
findings.push(...diagnoseTestCi(repo.repoRoot));
|
|
4304
4903
|
const healthy = findings.every((f) => f.severity === "ok" || f.severity === "info");
|
|
4305
4904
|
if (jsonOutput) {
|
|
4306
4905
|
printJson({ healthy, findings });
|
|
@@ -4314,8 +4913,9 @@ async function doctorCommand(args) {
|
|
|
4314
4913
|
}
|
|
4315
4914
|
|
|
4316
4915
|
// src/commands/doctrine.ts
|
|
4317
|
-
import { existsSync as existsSync9 } from "node:fs";
|
|
4916
|
+
import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync9 } from "node:fs";
|
|
4318
4917
|
import { dirname as dirname4, join as join10, sep as sep3 } from "node:path";
|
|
4918
|
+
import matter2 from "gray-matter";
|
|
4319
4919
|
var ENTRY_SEGMENTS = ["skills", "aeg", "SKILL.md"];
|
|
4320
4920
|
function resolveDoctrineRoot(pkg = packageRoot(import.meta.url)) {
|
|
4321
4921
|
const candidates = [join10(pkg, "aeg-root")];
|
|
@@ -4327,6 +4927,15 @@ function resolveDoctrineRoot(pkg = packageRoot(import.meta.url)) {
|
|
|
4327
4927
|
}
|
|
4328
4928
|
return null;
|
|
4329
4929
|
}
|
|
4930
|
+
function listRoleNames(root) {
|
|
4931
|
+
const rolesDir = join10(root, "roles");
|
|
4932
|
+
if (!existsSync9(rolesDir))
|
|
4933
|
+
return [];
|
|
4934
|
+
return readdirSync4(rolesDir).filter((name) => name.endsWith(".md")).map((name) => name.slice(0, -".md".length)).filter((roleName) => {
|
|
4935
|
+
const { data } = matter2(readFileSync9(join10(rolesDir, `${roleName}.md`), "utf8"));
|
|
4936
|
+
return data.actor !== "human";
|
|
4937
|
+
}).sort();
|
|
4938
|
+
}
|
|
4330
4939
|
function doctrineCommand(args) {
|
|
4331
4940
|
const root = resolveDoctrineRoot();
|
|
4332
4941
|
if (root === null) {
|
|
@@ -4334,6 +4943,24 @@ function doctrineCommand(args) {
|
|
|
4334
4943
|
`);
|
|
4335
4944
|
process.exit(1);
|
|
4336
4945
|
}
|
|
4946
|
+
const roleFlagIndex = args.indexOf("--role");
|
|
4947
|
+
if (roleFlagIndex !== -1) {
|
|
4948
|
+
const roleName = args[roleFlagIndex + 1];
|
|
4949
|
+
const validRoleNames = listRoleNames(root);
|
|
4950
|
+
if (roleName === undefined || roleName.startsWith("--") || !validRoleNames.includes(roleName)) {
|
|
4951
|
+
process.stderr.write(`vinaya doctrine --role: '${roleName ?? ""}' is not a known role. ` + `Valid role names: ${validRoleNames.join(", ")}
|
|
4952
|
+
`);
|
|
4953
|
+
process.exit(1);
|
|
4954
|
+
}
|
|
4955
|
+
const entry3 = join10(root, "roles", `${roleName}.md`);
|
|
4956
|
+
if (args.includes("--json")) {
|
|
4957
|
+
printJson({ root, entry: entry3 });
|
|
4958
|
+
return;
|
|
4959
|
+
}
|
|
4960
|
+
process.stdout.write(`${entry3}
|
|
4961
|
+
`);
|
|
4962
|
+
return;
|
|
4963
|
+
}
|
|
4337
4964
|
const entry2 = join10(root, ...ENTRY_SEGMENTS);
|
|
4338
4965
|
if (args.includes("--json")) {
|
|
4339
4966
|
printJson({ root, entry: entry2 });
|
|
@@ -4344,7 +4971,7 @@ function doctrineCommand(args) {
|
|
|
4344
4971
|
}
|
|
4345
4972
|
|
|
4346
4973
|
// src/commands/eject.ts
|
|
4347
|
-
import { existsSync as existsSync10, readFileSync as
|
|
4974
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "node:fs";
|
|
4348
4975
|
import { join as join11 } from "node:path";
|
|
4349
4976
|
function realDeps4() {
|
|
4350
4977
|
return {
|
|
@@ -4367,7 +4994,7 @@ function readManifest(repoRoot) {
|
|
|
4367
4994
|
return { kind: "none" };
|
|
4368
4995
|
let raw;
|
|
4369
4996
|
try {
|
|
4370
|
-
raw = JSON.parse(
|
|
4997
|
+
raw = JSON.parse(readFileSync10(p, "utf-8"));
|
|
4371
4998
|
} catch (err) {
|
|
4372
4999
|
return { kind: "orphan", reason: `vinaya.config.json is not valid JSON: ${err.message}` };
|
|
4373
5000
|
}
|
|
@@ -4457,11 +5084,11 @@ async function ejectCommand(args) {
|
|
|
4457
5084
|
}
|
|
4458
5085
|
|
|
4459
5086
|
// src/commands/init.ts
|
|
4460
|
-
import { existsSync as existsSync12, readFileSync as
|
|
5087
|
+
import { existsSync as existsSync12, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "node:fs";
|
|
4461
5088
|
import { join as join13 } from "node:path";
|
|
4462
5089
|
|
|
4463
5090
|
// src/lib/registry-write.ts
|
|
4464
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as
|
|
5091
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync4 } from "node:fs";
|
|
4465
5092
|
import { dirname as dirname5, join as join12 } from "node:path";
|
|
4466
5093
|
var PROJECTS_REGISTRY_PATH = ".vinaya/projects.md";
|
|
4467
5094
|
function rowLine(name, path, specsPath) {
|
|
@@ -4532,7 +5159,7 @@ function planRegistryRow(repoRoot, name, path, specsPath) {
|
|
|
4532
5159
|
if (!existsSync11(abs2)) {
|
|
4533
5160
|
return { action: "create-host", rowLine: line, path: PROJECTS_REGISTRY_PATH };
|
|
4534
5161
|
}
|
|
4535
|
-
const existing =
|
|
5162
|
+
const existing = readFileSync11(abs2, "utf-8");
|
|
4536
5163
|
const already = parseRegistry(existing).some((p) => p.name === name);
|
|
4537
5164
|
return { action: already ? "skip-present" : "append-row", rowLine: line, path: PROJECTS_REGISTRY_PATH };
|
|
4538
5165
|
}
|
|
@@ -4542,7 +5169,7 @@ function applyRegistryRow(repoRoot, plan, name, path, specsPath) {
|
|
|
4542
5169
|
mkdirSync3(dirname5(abs2), { recursive: true });
|
|
4543
5170
|
writeFileSync4(abs2, freshProjectsRegistry(name, path, specsPath), "utf-8");
|
|
4544
5171
|
} else if (plan.action === "append-row") {
|
|
4545
|
-
const existing =
|
|
5172
|
+
const existing = readFileSync11(abs2, "utf-8");
|
|
4546
5173
|
writeFileSync4(abs2, appendRegistryRow(existing, name, path, specsPath), "utf-8");
|
|
4547
5174
|
}
|
|
4548
5175
|
}
|
|
@@ -4581,14 +5208,14 @@ function readManifest2(repoRoot) {
|
|
|
4581
5208
|
if (!existsSync12(p))
|
|
4582
5209
|
return null;
|
|
4583
5210
|
try {
|
|
4584
|
-
return VinayaConfigSchema.parse(JSON.parse(
|
|
5211
|
+
return VinayaConfigSchema.parse(JSON.parse(readFileSync12(p, "utf-8"))).managed ?? null;
|
|
4585
5212
|
} catch {
|
|
4586
5213
|
return null;
|
|
4587
5214
|
}
|
|
4588
5215
|
}
|
|
4589
5216
|
function writeManifest(repoRoot, manifest) {
|
|
4590
5217
|
const configAbs = join13(repoRoot, CONFIG_PATH);
|
|
4591
|
-
const seed = JSON.parse(
|
|
5218
|
+
const seed = JSON.parse(readFileSync12(configAbs, "utf-8"));
|
|
4592
5219
|
writeFileSync5(configAbs, `${JSON.stringify({ ...seed, managed: manifest }, null, 2)}
|
|
4593
5220
|
`, "utf-8");
|
|
4594
5221
|
}
|
|
@@ -4750,11 +5377,12 @@ async function initProductCommand(args) {
|
|
|
4750
5377
|
}
|
|
4751
5378
|
|
|
4752
5379
|
// src/commands/issue.ts
|
|
4753
|
-
import { execFileSync as
|
|
5380
|
+
import { execFileSync as execFileSync10 } from "node:child_process";
|
|
4754
5381
|
|
|
4755
5382
|
// src/lib/forge-write.ts
|
|
4756
|
-
import {
|
|
4757
|
-
import {
|
|
5383
|
+
import { execFileSync as execFileSync9 } from "node:child_process";
|
|
5384
|
+
import { existsSync as existsSync13, mkdtempSync, readdirSync as readdirSync5, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
|
|
5385
|
+
import { readFileSync as readFileSync13 } from "node:fs";
|
|
4758
5386
|
import { tmpdir } from "node:os";
|
|
4759
5387
|
import { join as join14 } from "node:path";
|
|
4760
5388
|
class ForgeArgError extends Error {
|
|
@@ -4766,13 +5394,13 @@ function locateBody(args) {
|
|
|
4766
5394
|
const p = args[i + 1];
|
|
4767
5395
|
if (!p)
|
|
4768
5396
|
throw new ForgeArgError("`--body-file` was given with no path.");
|
|
4769
|
-
return { body:
|
|
5397
|
+
return { body: readFileSync13(p, "utf8"), source: { kind: "file", argIndex: i + 1, inlineForm: false } };
|
|
4770
5398
|
}
|
|
4771
5399
|
if (a.startsWith("--body-file=")) {
|
|
4772
5400
|
const p = a.slice("--body-file=".length);
|
|
4773
5401
|
if (!p)
|
|
4774
5402
|
throw new ForgeArgError("`--body-file=` was given with no path.");
|
|
4775
|
-
return { body:
|
|
5403
|
+
return { body: readFileSync13(p, "utf8"), source: { kind: "file", argIndex: i, inlineForm: true } };
|
|
4776
5404
|
}
|
|
4777
5405
|
if (a === "--body" || a === "-b") {
|
|
4778
5406
|
const v = args[i + 1];
|
|
@@ -4956,6 +5584,86 @@ function validateForgeWrite(input) {
|
|
|
4956
5584
|
}
|
|
4957
5585
|
return errors;
|
|
4958
5586
|
}
|
|
5587
|
+
function git2(args) {
|
|
5588
|
+
try {
|
|
5589
|
+
return execFileSync9("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
5590
|
+
} catch {
|
|
5591
|
+
return "";
|
|
5592
|
+
}
|
|
5593
|
+
}
|
|
5594
|
+
function repoRoot() {
|
|
5595
|
+
return git2(["rev-parse", "--show-toplevel"]);
|
|
5596
|
+
}
|
|
5597
|
+
function readWorkspaceGlobs(root) {
|
|
5598
|
+
const fromPackageJson = () => {
|
|
5599
|
+
try {
|
|
5600
|
+
const pkg = JSON.parse(readFileSync13(join14(root, "package.json"), "utf8"));
|
|
5601
|
+
return Array.isArray(pkg.workspaces) ? pkg.workspaces.filter((w) => typeof w === "string") : [];
|
|
5602
|
+
} catch {
|
|
5603
|
+
return [];
|
|
5604
|
+
}
|
|
5605
|
+
};
|
|
5606
|
+
const fromPnpmWorkspaceYaml = () => {
|
|
5607
|
+
try {
|
|
5608
|
+
return parsePnpmWorkspaceYaml(readFileSync13(join14(root, "pnpm-workspace.yaml"), "utf8"));
|
|
5609
|
+
} catch {
|
|
5610
|
+
return [];
|
|
5611
|
+
}
|
|
5612
|
+
};
|
|
5613
|
+
return [...fromPackageJson(), ...fromPnpmWorkspaceYaml()];
|
|
5614
|
+
}
|
|
5615
|
+
function listWorkspaceChildDirs(dir, root) {
|
|
5616
|
+
try {
|
|
5617
|
+
return readdirSync5(join14(root, dir), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
5618
|
+
} catch {
|
|
5619
|
+
return [];
|
|
5620
|
+
}
|
|
5621
|
+
}
|
|
5622
|
+
function readConfigExtraDomains(root) {
|
|
5623
|
+
try {
|
|
5624
|
+
const parsed = VinayaConfigSchema.safeParse(JSON.parse(readFileSync13(join14(root, "vinaya.config.json"), "utf8")));
|
|
5625
|
+
return parsed.success ? parsed.data.blastRadius?.extraDomains ?? [] : [];
|
|
5626
|
+
} catch {
|
|
5627
|
+
return [];
|
|
5628
|
+
}
|
|
5629
|
+
}
|
|
5630
|
+
function readSharedPackages(root = repoRoot()) {
|
|
5631
|
+
if (!root)
|
|
5632
|
+
return [];
|
|
5633
|
+
const derived = deriveWorkspacePackageDomains(readWorkspaceGlobs(root), (dir) => listWorkspaceChildDirs(dir, root));
|
|
5634
|
+
const defaults = deriveBuiltinCrossCuttingDefaults((p) => existsSync13(join14(root, p)));
|
|
5635
|
+
const configExtra = readConfigExtraDomains(root);
|
|
5636
|
+
return [...new Set([...derived, ...defaults, ...configExtra])];
|
|
5637
|
+
}
|
|
5638
|
+
function readProjectPaths(root = repoRoot()) {
|
|
5639
|
+
if (!root)
|
|
5640
|
+
return [];
|
|
5641
|
+
try {
|
|
5642
|
+
return parseRegistry(readFileSync13(join14(root, ".vinaya/projects.md"), "utf8"));
|
|
5643
|
+
} catch {
|
|
5644
|
+
return [];
|
|
5645
|
+
}
|
|
5646
|
+
}
|
|
5647
|
+
var CHECK_ISSUE_CONTENT = "issue-content";
|
|
5648
|
+
var ISSUE_CONTENT_RECOVERY = {
|
|
5649
|
+
blastRadius: "Add a second registered `Project(s)` this task also touches, or a `blast-radius-ack: <why one lens is enough>` line, then re-run `{cmd}`.",
|
|
5650
|
+
noBriefContent: "Move the brief-shaped section named above out of the Issue body and into the brief — the Issue carries the Planner's durable rationale, not the brief's just-in-time surface — then re-run `{cmd}`.",
|
|
5651
|
+
rationaleNamesDocs: 'Name a concrete doc/skill path (e.g. `aeg-root/…`, `.claude/skills/…/SKILL.md`) in "Docs to keep coherent" or "Traps", or write the `no-doc-surface` sentinel if the surface genuinely has none, then re-run `{cmd}`.'
|
|
5652
|
+
};
|
|
5653
|
+
function validateIssueContent(input) {
|
|
5654
|
+
const findings = [
|
|
5655
|
+
[checkBlastRadiusScope(input.body, input.labels, input.sharedPackages, input.projectPaths).errors, "blastRadius"],
|
|
5656
|
+
[checkNoBriefContent(input.body).errors, "noBriefContent"],
|
|
5657
|
+
[checkRationaleNamesDocs(input.body).errors, "rationaleNamesDocs"]
|
|
5658
|
+
];
|
|
5659
|
+
const errors = [];
|
|
5660
|
+
for (const [messages, kind] of findings) {
|
|
5661
|
+
const recovery = ISSUE_CONTENT_RECOVERY[kind].replace("{cmd}", input.retryCommand);
|
|
5662
|
+
for (const message of messages)
|
|
5663
|
+
errors.push(makeCheckError(CHECK_ISSUE_CONTENT, message, recovery));
|
|
5664
|
+
}
|
|
5665
|
+
return errors;
|
|
5666
|
+
}
|
|
4959
5667
|
|
|
4960
5668
|
// src/commands/issue.ts
|
|
4961
5669
|
var RETRY_CREATE = "vinaya issue create --validate-only …";
|
|
@@ -4973,7 +5681,7 @@ function locateBodyOrRefuse(ghArgs, retryCommand) {
|
|
|
4973
5681
|
function fetchForgeLabels(issueRef) {
|
|
4974
5682
|
let out;
|
|
4975
5683
|
try {
|
|
4976
|
-
out =
|
|
5684
|
+
out = execFileSync10("gh", ["issue", "view", issueRef, "--json", "labels"], {
|
|
4977
5685
|
encoding: "utf8",
|
|
4978
5686
|
stdio: ["ignore", "pipe", "pipe"]
|
|
4979
5687
|
});
|
|
@@ -5001,7 +5709,7 @@ function reportPass(json, command) {
|
|
|
5001
5709
|
function runGhWrite(ghCmd, ghArgs, bodyResult, json) {
|
|
5002
5710
|
const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
|
|
5003
5711
|
try {
|
|
5004
|
-
const out =
|
|
5712
|
+
const out = execFileSync10("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
|
|
5005
5713
|
const url = out.trim();
|
|
5006
5714
|
if (json)
|
|
5007
5715
|
printJson({ validated: true, written: true, url });
|
|
@@ -5012,22 +5720,31 @@ function runGhWrite(ghCmd, ghArgs, bodyResult, json) {
|
|
|
5012
5720
|
cleanup2();
|
|
5013
5721
|
}
|
|
5014
5722
|
}
|
|
5015
|
-
function validateTaskIssue(body, title, retryCommand) {
|
|
5723
|
+
function validateTaskIssue(body, title, labels, retryCommand) {
|
|
5016
5724
|
if (body === null) {
|
|
5017
5725
|
refuse([
|
|
5018
5726
|
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}\`.`)
|
|
5019
5727
|
]);
|
|
5020
5728
|
}
|
|
5021
5729
|
const sections = resolveSections("issue", retryCommand);
|
|
5022
|
-
const
|
|
5730
|
+
const schemaErrors = validateForgeWrite({
|
|
5023
5731
|
body,
|
|
5024
5732
|
title,
|
|
5025
5733
|
sections,
|
|
5026
5734
|
changedFiles: [],
|
|
5027
5735
|
retryCommand
|
|
5028
5736
|
});
|
|
5029
|
-
if (
|
|
5030
|
-
refuse(
|
|
5737
|
+
if (schemaErrors.length > 0)
|
|
5738
|
+
refuse(schemaErrors);
|
|
5739
|
+
const contentErrors = validateIssueContent({
|
|
5740
|
+
body,
|
|
5741
|
+
labels,
|
|
5742
|
+
sharedPackages: readSharedPackages(),
|
|
5743
|
+
projectPaths: readProjectPaths(),
|
|
5744
|
+
retryCommand
|
|
5745
|
+
});
|
|
5746
|
+
if (contentErrors.length > 0)
|
|
5747
|
+
refuse(contentErrors);
|
|
5031
5748
|
}
|
|
5032
5749
|
function issueCreateCommand(args) {
|
|
5033
5750
|
const json = args.includes("--json");
|
|
@@ -5038,7 +5755,7 @@ function issueCreateCommand(args) {
|
|
|
5038
5755
|
const title = extractTitle(ghArgs);
|
|
5039
5756
|
const labels = extractLabels(ghArgs);
|
|
5040
5757
|
if (isTaskIssueLabelSet(labels)) {
|
|
5041
|
-
validateTaskIssue(body, title, RETRY_CREATE);
|
|
5758
|
+
validateTaskIssue(body, title, labels, RETRY_CREATE);
|
|
5042
5759
|
}
|
|
5043
5760
|
if (validateOnly) {
|
|
5044
5761
|
reportPass(json, "issue create");
|
|
@@ -5062,7 +5779,7 @@ function issueEditCommand(args) {
|
|
|
5062
5779
|
const title = extractTitle(ghArgs);
|
|
5063
5780
|
const labels = [...new Set([...fetchForgeLabels(issueRef), ...extractLabels(ghArgs)])];
|
|
5064
5781
|
if (isTaskIssueLabelSet(labels)) {
|
|
5065
|
-
validateTaskIssue(body, title, RETRY_EDIT);
|
|
5782
|
+
validateTaskIssue(body, title, labels, RETRY_EDIT);
|
|
5066
5783
|
}
|
|
5067
5784
|
if (validateOnly) {
|
|
5068
5785
|
reportPass(json, "issue edit");
|
|
@@ -5072,7 +5789,7 @@ function issueEditCommand(args) {
|
|
|
5072
5789
|
}
|
|
5073
5790
|
|
|
5074
5791
|
// src/commands/new-check.ts
|
|
5075
|
-
import { chmodSync as chmodSync2, existsSync as
|
|
5792
|
+
import { chmodSync as chmodSync2, existsSync as existsSync14, mkdirSync as mkdirSync4, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "node:fs";
|
|
5076
5793
|
import { join as join15 } from "node:path";
|
|
5077
5794
|
var TEMPLATE_PATH = join15(packageRoot(import.meta.url), "templates", "custom-check.template.ts");
|
|
5078
5795
|
var CHECKS_DIR = join15("scripts", "vinaya-checks");
|
|
@@ -5093,15 +5810,15 @@ function newCheckCommand(args) {
|
|
|
5093
5810
|
process.exit(2);
|
|
5094
5811
|
}
|
|
5095
5812
|
const scriptsDir = join15(process.cwd(), CHECKS_DIR);
|
|
5096
|
-
if (!
|
|
5813
|
+
if (!existsSync14(scriptsDir))
|
|
5097
5814
|
mkdirSync4(scriptsDir, { recursive: true });
|
|
5098
5815
|
const fileStem = name.slice(name.indexOf("/") + 1);
|
|
5099
5816
|
const targetPath = join15(scriptsDir, `${fileStem}.ts`);
|
|
5100
|
-
if (
|
|
5817
|
+
if (existsSync14(targetPath)) {
|
|
5101
5818
|
console.error(`Error: ${targetPath} already exists.`);
|
|
5102
5819
|
process.exit(1);
|
|
5103
5820
|
}
|
|
5104
|
-
const template =
|
|
5821
|
+
const template = readFileSync14(TEMPLATE_PATH, "utf-8");
|
|
5105
5822
|
const contents = template.split("{{CHECK_NAME}}").join(name);
|
|
5106
5823
|
writeFileSync7(targetPath, contents, "utf-8");
|
|
5107
5824
|
chmodSync2(targetPath, 493);
|
|
@@ -5115,21 +5832,21 @@ ${registration}
|
|
|
5115
5832
|
}
|
|
5116
5833
|
|
|
5117
5834
|
// src/commands/pr.ts
|
|
5118
|
-
import { execFileSync as
|
|
5835
|
+
import { execFileSync as execFileSync11 } from "node:child_process";
|
|
5119
5836
|
var RETRY_CREATE2 = "vinaya pr create --validate-only …";
|
|
5120
5837
|
var RETRY_EDIT2 = "vinaya pr edit <n> --validate-only …";
|
|
5121
|
-
function
|
|
5838
|
+
function git3(args) {
|
|
5122
5839
|
try {
|
|
5123
|
-
return
|
|
5840
|
+
return execFileSync11("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
5124
5841
|
} catch {
|
|
5125
5842
|
return "";
|
|
5126
5843
|
}
|
|
5127
5844
|
}
|
|
5128
5845
|
function localChangedFiles() {
|
|
5129
5846
|
const base = process.env.BASE_SHA || "origin/main";
|
|
5130
|
-
let out =
|
|
5847
|
+
let out = git3(["diff", "--name-only", `${base}...HEAD`]);
|
|
5131
5848
|
if (!out)
|
|
5132
|
-
out =
|
|
5849
|
+
out = git3(["diff", "--name-only", "main...HEAD"]);
|
|
5133
5850
|
return out.split(`
|
|
5134
5851
|
`).map((s) => s.trim()).filter(Boolean);
|
|
5135
5852
|
}
|
|
@@ -5154,7 +5871,7 @@ function reportPass2(json, command) {
|
|
|
5154
5871
|
function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
|
|
5155
5872
|
const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
|
|
5156
5873
|
try {
|
|
5157
|
-
const out =
|
|
5874
|
+
const out = execFileSync11("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
|
|
5158
5875
|
const url = out.trim();
|
|
5159
5876
|
if (json)
|
|
5160
5877
|
printJson({ validated: true, written: true, url });
|
|
@@ -5168,7 +5885,7 @@ function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
|
|
|
5168
5885
|
function fetchPrForgeContext(prRef) {
|
|
5169
5886
|
let viewOut;
|
|
5170
5887
|
try {
|
|
5171
|
-
viewOut =
|
|
5888
|
+
viewOut = execFileSync11("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
|
|
5172
5889
|
encoding: "utf8",
|
|
5173
5890
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5174
5891
|
});
|
|
@@ -5207,8 +5924,8 @@ function prCreateCommand(args) {
|
|
|
5207
5924
|
}
|
|
5208
5925
|
const sections = resolveSections("pr", RETRY_CREATE2);
|
|
5209
5926
|
const changedFiles2 = localChangedFiles();
|
|
5210
|
-
const headCommit =
|
|
5211
|
-
const branch = headCommit === "" ? "" :
|
|
5927
|
+
const headCommit = git3(["rev-parse", "--verify", "--quiet", "HEAD"]);
|
|
5928
|
+
const branch = headCommit === "" ? "" : git3(["symbolic-ref", "--quiet", "--short", "HEAD"]);
|
|
5212
5929
|
const errors = validateForgeWrite({
|
|
5213
5930
|
body,
|
|
5214
5931
|
title,
|
|
@@ -5270,14 +5987,14 @@ function prEditCommand(args) {
|
|
|
5270
5987
|
}
|
|
5271
5988
|
|
|
5272
5989
|
// src/commands/pr-report.ts
|
|
5273
|
-
import { execFileSync as
|
|
5274
|
-
import { existsSync as
|
|
5990
|
+
import { execFileSync as execFileSync12, spawnSync as spawnSync2 } from "node:child_process";
|
|
5991
|
+
import { existsSync as existsSync15, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
|
|
5275
5992
|
import { join as join16 } from "node:path";
|
|
5276
5993
|
var EVIDENCE_START = "<!-- AEG:EVIDENCE:START -->";
|
|
5277
5994
|
var EVIDENCE_END = "<!-- AEG:EVIDENCE:END -->";
|
|
5278
|
-
function
|
|
5995
|
+
function git4(args) {
|
|
5279
5996
|
try {
|
|
5280
|
-
return
|
|
5997
|
+
return execFileSync12("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
5281
5998
|
} catch {
|
|
5282
5999
|
return "";
|
|
5283
6000
|
}
|
|
@@ -5295,7 +6012,7 @@ class GitCommandError extends Error {
|
|
|
5295
6012
|
}
|
|
5296
6013
|
function gitStrict(args) {
|
|
5297
6014
|
try {
|
|
5298
|
-
return
|
|
6015
|
+
return execFileSync12("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
5299
6016
|
} catch (err) {
|
|
5300
6017
|
const stderr = err.stderr;
|
|
5301
6018
|
throw new GitCommandError(args, String(stderr ?? err.message).trim() || "non-zero exit");
|
|
@@ -5316,7 +6033,7 @@ function resolveMergeBase(head) {
|
|
|
5316
6033
|
const primary = process.env.BASE_SHA || "origin/main";
|
|
5317
6034
|
const tried = primary === "main" ? [primary] : [primary, "main"];
|
|
5318
6035
|
for (const ref of tried) {
|
|
5319
|
-
const base =
|
|
6036
|
+
const base = git4(["merge-base", ref, head]);
|
|
5320
6037
|
if (base)
|
|
5321
6038
|
return base;
|
|
5322
6039
|
}
|
|
@@ -5331,7 +6048,7 @@ function computeGroupA() {
|
|
|
5331
6048
|
function resolveSelfEntry() {
|
|
5332
6049
|
const root = packageRoot(import.meta.url);
|
|
5333
6050
|
const dist = join16(root, "dist", "index.js");
|
|
5334
|
-
return
|
|
6051
|
+
return existsSync15(dist) ? dist : join16(root, "src", "index.ts");
|
|
5335
6052
|
}
|
|
5336
6053
|
var FAILING_STATUSES = new Set(["fail", "error", "timeout"]);
|
|
5337
6054
|
function anyGateFailed(outcomes) {
|
|
@@ -5436,7 +6153,7 @@ async function prReportCommand(args) {
|
|
|
5436
6153
|
throw err;
|
|
5437
6154
|
}
|
|
5438
6155
|
if (writePath) {
|
|
5439
|
-
const existing =
|
|
6156
|
+
const existing = existsSync15(writePath) ? readFileSync15(writePath, "utf8") : "";
|
|
5440
6157
|
const updated = replaceEvidenceBlock(existing, result.blockInner);
|
|
5441
6158
|
writeFileSync8(writePath, updated);
|
|
5442
6159
|
process.stdout.write(`Wrote AEG:EVIDENCE block to ${writePath}
|
|
@@ -5449,12 +6166,12 @@ async function prReportCommand(args) {
|
|
|
5449
6166
|
}
|
|
5450
6167
|
|
|
5451
6168
|
// src/commands/quickstart.ts
|
|
5452
|
-
import { execFileSync as
|
|
5453
|
-
import { existsSync as
|
|
6169
|
+
import { execFileSync as execFileSync13 } from "node:child_process";
|
|
6170
|
+
import { existsSync as existsSync17, readFileSync as readFileSync17 } from "node:fs";
|
|
5454
6171
|
import { join as join18 } from "node:path";
|
|
5455
6172
|
|
|
5456
6173
|
// src/lib/doc-owners-write.ts
|
|
5457
|
-
import { existsSync as
|
|
6174
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync5, readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "node:fs";
|
|
5458
6175
|
import { dirname as dirname6, join as join17 } from "node:path";
|
|
5459
6176
|
function bindingLine(glob, pointer) {
|
|
5460
6177
|
return `${glob} ${pointer}`;
|
|
@@ -5470,23 +6187,23 @@ function appendDocOwnersBinding(content, glob, pointer) {
|
|
|
5470
6187
|
return `${content}${sep4}${bindingLine(glob, pointer)}
|
|
5471
6188
|
`;
|
|
5472
6189
|
}
|
|
5473
|
-
function planDocOwnersBinding(
|
|
5474
|
-
const abs2 = join17(
|
|
6190
|
+
function planDocOwnersBinding(repoRoot2, glob, pointer) {
|
|
6191
|
+
const abs2 = join17(repoRoot2, DOC_OWNERS_PATH);
|
|
5475
6192
|
const line = bindingLine(glob, pointer);
|
|
5476
|
-
if (!
|
|
6193
|
+
if (!existsSync16(abs2)) {
|
|
5477
6194
|
return { action: "create-host", line, path: DOC_OWNERS_PATH };
|
|
5478
6195
|
}
|
|
5479
|
-
const existing =
|
|
6196
|
+
const existing = readFileSync16(abs2, "utf-8");
|
|
5480
6197
|
const already = parseDocOwners(existing).bindings.some((b) => b.glob === glob);
|
|
5481
6198
|
return { action: already ? "skip-present" : "append-row", line, path: DOC_OWNERS_PATH };
|
|
5482
6199
|
}
|
|
5483
|
-
function applyDocOwnersBinding(
|
|
5484
|
-
const abs2 = join17(
|
|
6200
|
+
function applyDocOwnersBinding(repoRoot2, plan, glob, pointer) {
|
|
6201
|
+
const abs2 = join17(repoRoot2, DOC_OWNERS_PATH);
|
|
5485
6202
|
if (plan.action === "create-host") {
|
|
5486
6203
|
mkdirSync5(dirname6(abs2), { recursive: true });
|
|
5487
6204
|
writeFileSync9(abs2, freshDocOwners(glob, pointer), "utf-8");
|
|
5488
6205
|
} else if (plan.action === "append-row") {
|
|
5489
|
-
const existing =
|
|
6206
|
+
const existing = readFileSync16(abs2, "utf-8");
|
|
5490
6207
|
writeFileSync9(abs2, appendDocOwnersBinding(existing, glob, pointer), "utf-8");
|
|
5491
6208
|
}
|
|
5492
6209
|
}
|
|
@@ -5543,13 +6260,13 @@ var GLOB_INJECTION_RE = /[\s\r\n]/;
|
|
|
5543
6260
|
var POINTER_INJECTION_RE = /[\r\n]/;
|
|
5544
6261
|
var NAME_INJECTION_RE = /[\s|\r\n]/;
|
|
5545
6262
|
var PATH_INJECTION_RE2 = /[|\r\n]/;
|
|
5546
|
-
function pointerIsMissingLocalFile(
|
|
6263
|
+
function pointerIsMissingLocalFile(repoRoot2, pointer) {
|
|
5547
6264
|
if (/^https?:\/\//i.test(pointer))
|
|
5548
6265
|
return false;
|
|
5549
6266
|
const path = pointer.split("#")[0] ?? pointer;
|
|
5550
|
-
return !
|
|
6267
|
+
return !existsSync17(join18(repoRoot2, path));
|
|
5551
6268
|
}
|
|
5552
|
-
async function bindDocOwnerLoop(deps,
|
|
6269
|
+
async function bindDocOwnerLoop(deps, repoRoot2) {
|
|
5553
6270
|
let any = false;
|
|
5554
6271
|
for (;; ) {
|
|
5555
6272
|
if (any && !await deps.confirm("Bind another doc→code pair?", false))
|
|
@@ -5564,11 +6281,11 @@ async function bindDocOwnerLoop(deps, repoRoot) {
|
|
|
5564
6281
|
}
|
|
5565
6282
|
if (GLOB_INJECTION_RE.test(glob) || POINTER_INJECTION_RE.test(pointer)) {
|
|
5566
6283
|
console.error(red("Error: glob must not contain whitespace, and the pointer must not contain a newline."));
|
|
5567
|
-
} else if (pointerIsMissingLocalFile(
|
|
6284
|
+
} else if (pointerIsMissingLocalFile(repoRoot2, pointer)) {
|
|
5568
6285
|
console.error(red(`✗ "${pointer}" does not exist in this repo (and isn't a URL).`));
|
|
5569
6286
|
} else {
|
|
5570
|
-
const plan = planDocOwnersBinding(
|
|
5571
|
-
applyDocOwnersBinding(
|
|
6287
|
+
const plan = planDocOwnersBinding(repoRoot2, glob, pointer);
|
|
6288
|
+
applyDocOwnersBinding(repoRoot2, plan, glob, pointer);
|
|
5572
6289
|
process.stdout.write(`${green(renderDocOwnersBindingDiffLine(plan))}
|
|
5573
6290
|
`);
|
|
5574
6291
|
any = true;
|
|
@@ -5614,7 +6331,7 @@ async function registerProjectLoop(deps) {
|
|
|
5614
6331
|
}
|
|
5615
6332
|
}
|
|
5616
6333
|
function readPackageVersion() {
|
|
5617
|
-
const pkg = JSON.parse(
|
|
6334
|
+
const pkg = JSON.parse(readFileSync17(join18(packageRoot(import.meta.url), "package.json"), "utf-8"));
|
|
5618
6335
|
return pkg.version;
|
|
5619
6336
|
}
|
|
5620
6337
|
function realDeps6() {
|
|
@@ -5644,8 +6361,8 @@ function realDeps6() {
|
|
|
5644
6361
|
closeStdin
|
|
5645
6362
|
};
|
|
5646
6363
|
}
|
|
5647
|
-
function execGit(
|
|
5648
|
-
return
|
|
6364
|
+
function execGit(repoRoot2, args) {
|
|
6365
|
+
return execFileSync13("git", args, { cwd: repoRoot2, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
5649
6366
|
}
|
|
5650
6367
|
function errorDetail(err) {
|
|
5651
6368
|
const stderr = err.stderr;
|
|
@@ -5653,31 +6370,31 @@ function errorDetail(err) {
|
|
|
5653
6370
|
return stderr.toString().trim();
|
|
5654
6371
|
return err instanceof Error ? err.message : String(err);
|
|
5655
6372
|
}
|
|
5656
|
-
function gitStatusShort(
|
|
5657
|
-
return execGit(
|
|
6373
|
+
function gitStatusShort(repoRoot2) {
|
|
6374
|
+
return execGit(repoRoot2, ["status", "--short"]);
|
|
5658
6375
|
}
|
|
5659
|
-
function commitInstall(
|
|
5660
|
-
execGit(
|
|
5661
|
-
const staged = execGit(
|
|
6376
|
+
function commitInstall(repoRoot2) {
|
|
6377
|
+
execGit(repoRoot2, ["add", "-A"]);
|
|
6378
|
+
const staged = execGit(repoRoot2, ["diff", "--cached", "--stat"]);
|
|
5662
6379
|
if (!staged)
|
|
5663
6380
|
return { committed: false, message: "Nothing to commit — working tree already clean." };
|
|
5664
6381
|
try {
|
|
5665
|
-
execGit(
|
|
6382
|
+
execGit(repoRoot2, ["commit", "-m", "Chore: install Vinaya"]);
|
|
5666
6383
|
return { committed: true, message: "✓ Committed `Chore: install Vinaya`." };
|
|
5667
6384
|
} catch (err) {
|
|
5668
6385
|
return { committed: false, message: `git commit failed: ${errorDetail(err)}` };
|
|
5669
6386
|
}
|
|
5670
6387
|
}
|
|
5671
|
-
function pushInstall(
|
|
6388
|
+
function pushInstall(repoRoot2) {
|
|
5672
6389
|
try {
|
|
5673
|
-
const branch = execGit(
|
|
6390
|
+
const branch = execGit(repoRoot2, ["symbolic-ref", "--short", "HEAD"]);
|
|
5674
6391
|
let hasUpstream = true;
|
|
5675
6392
|
try {
|
|
5676
|
-
execGit(
|
|
6393
|
+
execGit(repoRoot2, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
|
|
5677
6394
|
} catch {
|
|
5678
6395
|
hasUpstream = false;
|
|
5679
6396
|
}
|
|
5680
|
-
execGit(
|
|
6397
|
+
execGit(repoRoot2, hasUpstream ? ["push"] : ["push", "-u", "origin", branch]);
|
|
5681
6398
|
return { pushed: true, message: "✓ Pushed." };
|
|
5682
6399
|
} catch (err) {
|
|
5683
6400
|
return { pushed: false, message: `git push failed: ${errorDetail(err)}` };
|
|
@@ -5761,11 +6478,331 @@ async function quickstartCommand(args) {
|
|
|
5761
6478
|
process.exit(await runQuickstart(args, realDeps6()));
|
|
5762
6479
|
}
|
|
5763
6480
|
|
|
6481
|
+
// src/commands/review-post.ts
|
|
6482
|
+
import { execFileSync as execFileSync14 } from "node:child_process";
|
|
6483
|
+
import { readFileSync as readFileSync18, rmSync as rmSync4, writeFileSync as writeFileSync10 } from "node:fs";
|
|
6484
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
6485
|
+
import { join as join19 } from "node:path";
|
|
6486
|
+
class FindingsParseError extends Error {
|
|
6487
|
+
}
|
|
6488
|
+
var CODE_REVIEW_SEVERITIES = ["BLOCKER", "MAJOR", "MINOR"];
|
|
6489
|
+
var SECURITY_SEVERITIES = ["CRITICAL", "HIGH", "MEDIUM", "LOW"];
|
|
6490
|
+
function parseFindingsFile(content, allowedSeverities) {
|
|
6491
|
+
const lines = content.split(`
|
|
6492
|
+
`).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
6493
|
+
return lines.map((line, idx) => {
|
|
6494
|
+
const parts = line.split("|");
|
|
6495
|
+
if (parts.length !== 3) {
|
|
6496
|
+
throw new FindingsParseError(`findings file line ${idx + 1}: expected exactly 3 \`|\`-delimited fields (SEVERITY|file:line|description), found ${parts.length}: ${line}`);
|
|
6497
|
+
}
|
|
6498
|
+
const severity = parts[0].trim().toUpperCase();
|
|
6499
|
+
const location = parts[1].trim();
|
|
6500
|
+
const description = parts[2].trim();
|
|
6501
|
+
if (!allowedSeverities.includes(severity)) {
|
|
6502
|
+
throw new FindingsParseError(`findings file line ${idx + 1}: severity "${parts[0]}" is not one of ${allowedSeverities.join("|")}: ${line}`);
|
|
6503
|
+
}
|
|
6504
|
+
if (!location || !description) {
|
|
6505
|
+
throw new FindingsParseError(`findings file line ${idx + 1}: file:line and description must both be non-empty: ${line}`);
|
|
6506
|
+
}
|
|
6507
|
+
return { severity, location, description };
|
|
6508
|
+
});
|
|
6509
|
+
}
|
|
6510
|
+
function sortBySeverity(findings, order) {
|
|
6511
|
+
return findings.map((f, i) => ({ f, i })).sort((a, b) => order.indexOf(a.f.severity) - order.indexOf(b.f.severity) || a.i - b.i).map(({ f }) => f);
|
|
6512
|
+
}
|
|
6513
|
+
function renderFindingsSection(findings) {
|
|
6514
|
+
if (findings.length === 0)
|
|
6515
|
+
return "None.";
|
|
6516
|
+
return findings.map((f, i) => `${i + 1}. [${f.severity}] ${f.location} — ${f.description}`).join(`
|
|
6517
|
+
`);
|
|
6518
|
+
}
|
|
6519
|
+
function renderTokensLine(role, roleLabel, input) {
|
|
6520
|
+
return `Tokens: ${input.taskId}: ${role} — ${roleLabel} — ${input.model} — ${input.tokensIn}/${input.tokensOut}/${input.cost}`;
|
|
6521
|
+
}
|
|
6522
|
+
var CODE_REVIEW_VERDICT_TEXT = {
|
|
6523
|
+
APPROVE: "APPROVE",
|
|
6524
|
+
REQUEST_CHANGES: "REQUEST CHANGES"
|
|
6525
|
+
};
|
|
6526
|
+
function renderCodeReviewComment(input) {
|
|
6527
|
+
const sorted = sortBySeverity(input.findings, CODE_REVIEW_SEVERITIES);
|
|
6528
|
+
return [
|
|
6529
|
+
`VERDICT: ${CODE_REVIEW_VERDICT_TEXT[input.verdict]}`,
|
|
6530
|
+
"",
|
|
6531
|
+
`Judged head: ${input.headSha}`,
|
|
6532
|
+
"",
|
|
6533
|
+
`BRIEF CONFORMANCE: ${input.briefConformance}`,
|
|
6534
|
+
`SPEC CONFORMANCE: ${input.specConformance}`,
|
|
6535
|
+
"",
|
|
6536
|
+
"FINDINGS (ordered by severity):",
|
|
6537
|
+
renderFindingsSection(sorted),
|
|
6538
|
+
"",
|
|
6539
|
+
`SCOPE: ${input.scope}`,
|
|
6540
|
+
`TESTS: ${input.tests}`,
|
|
6541
|
+
`DOCS: ${input.docs}`,
|
|
6542
|
+
"",
|
|
6543
|
+
renderTokensLine("review", "Reviewer", input)
|
|
6544
|
+
].join(`
|
|
6545
|
+
`);
|
|
6546
|
+
}
|
|
6547
|
+
function isNoneFoundClaim(value) {
|
|
6548
|
+
return value.trim().toLowerCase().replace(/[-_]+/g, " ").replace(/\s+/g, " ") === "none found";
|
|
6549
|
+
}
|
|
6550
|
+
function renderSecurityComment(input) {
|
|
6551
|
+
const sorted = sortBySeverity(input.findings, SECURITY_SEVERITIES);
|
|
6552
|
+
const lines = [
|
|
6553
|
+
`VERDICT: ${input.verdict}`,
|
|
6554
|
+
"",
|
|
6555
|
+
`Judged head: ${input.headSha}`,
|
|
6556
|
+
"",
|
|
6557
|
+
"FINDINGS (ordered by severity):",
|
|
6558
|
+
renderFindingsSection(sorted),
|
|
6559
|
+
"",
|
|
6560
|
+
`CONFIG SCAN: ${input.configScan}`,
|
|
6561
|
+
""
|
|
6562
|
+
];
|
|
6563
|
+
if (input.secretsEvidence !== null) {
|
|
6564
|
+
lines.push("```", input.secretsEvidence, "```", "");
|
|
6565
|
+
}
|
|
6566
|
+
lines.push(`SECRETS: ${input.secrets}`, "", renderTokensLine("security", "Security", input));
|
|
6567
|
+
return lines.join(`
|
|
6568
|
+
`);
|
|
6569
|
+
}
|
|
6570
|
+
function isBoundToHead(extraction, headSha) {
|
|
6571
|
+
if (!extraction.headSha)
|
|
6572
|
+
return false;
|
|
6573
|
+
return headSha.toLowerCase().startsWith(extraction.headSha.toLowerCase());
|
|
6574
|
+
}
|
|
6575
|
+
function checkExtraction(extraction, expectedValue, headSha) {
|
|
6576
|
+
if (extraction.danglingNote) {
|
|
6577
|
+
return {
|
|
6578
|
+
ok: false,
|
|
6579
|
+
reason: `no clean VERDICT was found on re-fetch (${extraction.danglingNote}) — the posted comment does not match the gate's line-anchored \`VERDICT:\` pattern.`
|
|
6580
|
+
};
|
|
6581
|
+
}
|
|
6582
|
+
if (extraction.value !== expectedValue) {
|
|
6583
|
+
return {
|
|
6584
|
+
ok: false,
|
|
6585
|
+
reason: `re-extraction found VERDICT "${extraction.value}", expected "${expectedValue}" — the posted comment's VERDICT line does not match what this command rendered.`
|
|
6586
|
+
};
|
|
6587
|
+
}
|
|
6588
|
+
if (!extraction.headSha) {
|
|
6589
|
+
return { ok: false, reason: "the winning VERDICT comment carries no `Judged head:` line on re-fetch." };
|
|
6590
|
+
}
|
|
6591
|
+
if (!isBoundToHead(extraction, headSha)) {
|
|
6592
|
+
return {
|
|
6593
|
+
ok: false,
|
|
6594
|
+
reason: `re-extraction found \`Judged head: ${extraction.headSha}\`, which does not cover the resolved head ${headSha}.`
|
|
6595
|
+
};
|
|
6596
|
+
}
|
|
6597
|
+
return { ok: true, reason: "clean" };
|
|
6598
|
+
}
|
|
6599
|
+
function principalBodies(comments, principalAllowlist) {
|
|
6600
|
+
return comments.filter((c) => isPrincipal(c.author, principalAllowlist)).map((c) => c.body);
|
|
6601
|
+
}
|
|
6602
|
+
function verifyPostedCodeReview(comments, verdict, headSha, principalAllowlist) {
|
|
6603
|
+
return checkExtraction(extractCodeReviewVerdict(principalBodies(comments, principalAllowlist)), CODE_REVIEW_VERDICT_TEXT[verdict], headSha);
|
|
6604
|
+
}
|
|
6605
|
+
function verifyPostedSecurity(comments, verdict, headSha, principalAllowlist) {
|
|
6606
|
+
return checkExtraction(extractSecurityReviewVerdict(principalBodies(comments, principalAllowlist)), verdict, headSha);
|
|
6607
|
+
}
|
|
6608
|
+
function parseFlags(args) {
|
|
6609
|
+
const map = new Map;
|
|
6610
|
+
for (let i = 0;i < args.length; i++) {
|
|
6611
|
+
const a = args[i];
|
|
6612
|
+
if (a.startsWith("--")) {
|
|
6613
|
+
const value = args[i + 1];
|
|
6614
|
+
if (value === undefined || value.startsWith("--")) {
|
|
6615
|
+
map.set(a, "");
|
|
6616
|
+
continue;
|
|
6617
|
+
}
|
|
6618
|
+
map.set(a, value);
|
|
6619
|
+
i++;
|
|
6620
|
+
}
|
|
6621
|
+
}
|
|
6622
|
+
return map;
|
|
6623
|
+
}
|
|
6624
|
+
function refuseCmd(message, recovery) {
|
|
6625
|
+
refuse([makeCheckError("review-post", message, recovery)]);
|
|
6626
|
+
}
|
|
6627
|
+
function requireFlag(flags2, name) {
|
|
6628
|
+
const v = flags2.get(name);
|
|
6629
|
+
if (v === undefined || v === "") {
|
|
6630
|
+
refuseCmd(`Missing required \`${name}\` flag.`, `Pass \`${name} <value>\`, then re-run \`vinaya review post ...\`.`);
|
|
6631
|
+
}
|
|
6632
|
+
return v;
|
|
6633
|
+
}
|
|
6634
|
+
function requireTokenField(flags2, name) {
|
|
6635
|
+
const v = requireFlag(flags2, name);
|
|
6636
|
+
if (v !== "-" && !/^\d+$/.test(v)) {
|
|
6637
|
+
refuseCmd(`\`${name} ${v}\` is neither a non-negative integer nor \`-\` (unknown).`, `Pass a whole number or \`-\` for ${name}.`);
|
|
6638
|
+
}
|
|
6639
|
+
return v;
|
|
6640
|
+
}
|
|
6641
|
+
function readFindingsFile(path, allowedSeverities) {
|
|
6642
|
+
if (!path)
|
|
6643
|
+
return [];
|
|
6644
|
+
let content;
|
|
6645
|
+
try {
|
|
6646
|
+
content = readFileSync18(path, "utf8");
|
|
6647
|
+
} catch {
|
|
6648
|
+
refuseCmd(`Could not read findings file at ${path}.`, "Check the path and re-run.");
|
|
6649
|
+
}
|
|
6650
|
+
try {
|
|
6651
|
+
return parseFindingsFile(content, allowedSeverities);
|
|
6652
|
+
} catch (err) {
|
|
6653
|
+
if (err instanceof FindingsParseError) {
|
|
6654
|
+
refuseCmd(err.message, "Fix the malformed line in the findings file, then re-run.");
|
|
6655
|
+
}
|
|
6656
|
+
throw err;
|
|
6657
|
+
}
|
|
6658
|
+
}
|
|
6659
|
+
function normalizeCodeReviewVerdict(raw) {
|
|
6660
|
+
const v = raw.trim().toUpperCase().replace(/[-\s]+/g, "_");
|
|
6661
|
+
if (v === "APPROVE")
|
|
6662
|
+
return "APPROVE";
|
|
6663
|
+
if (v === "REQUEST_CHANGES")
|
|
6664
|
+
return "REQUEST_CHANGES";
|
|
6665
|
+
refuseCmd(`\`--verdict ${raw}\` is not APPROVE or REQUEST_CHANGES.`, "Pass `--verdict APPROVE` or `--verdict REQUEST_CHANGES`.");
|
|
6666
|
+
}
|
|
6667
|
+
function normalizeSecurityVerdict(raw) {
|
|
6668
|
+
const v = raw.trim().toUpperCase();
|
|
6669
|
+
if (v === "PASS")
|
|
6670
|
+
return "PASS";
|
|
6671
|
+
if (v === "FAIL")
|
|
6672
|
+
return "FAIL";
|
|
6673
|
+
refuseCmd(`\`--verdict ${raw}\` is not PASS or FAIL.`, "Pass `--verdict PASS` or `--verdict FAIL`.");
|
|
6674
|
+
}
|
|
6675
|
+
function gh(args) {
|
|
6676
|
+
try {
|
|
6677
|
+
return execFileSync14("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
6678
|
+
} catch (err) {
|
|
6679
|
+
const stderr = err.stderr;
|
|
6680
|
+
throw new Error(String(stderr ?? err.message).trim() || "gh command failed");
|
|
6681
|
+
}
|
|
6682
|
+
}
|
|
6683
|
+
function resolveHeadSha(pr) {
|
|
6684
|
+
let out;
|
|
6685
|
+
try {
|
|
6686
|
+
out = gh(["pr", "view", pr, "--json", "headRefOid", "-q", ".headRefOid"]);
|
|
6687
|
+
} catch (err) {
|
|
6688
|
+
refuseCmd(`Could not resolve PR ${pr}'s head via \`gh pr view --json headRefOid\`: ${err instanceof Error ? err.message : String(err)}`, "Confirm `gh auth status` passes and the PR number is correct, then re-run.");
|
|
6689
|
+
}
|
|
6690
|
+
if (!out) {
|
|
6691
|
+
refuseCmd(`\`gh pr view ${pr} --json headRefOid\` returned no head sha.`, "Confirm PR exists, then re-run.");
|
|
6692
|
+
}
|
|
6693
|
+
return out;
|
|
6694
|
+
}
|
|
6695
|
+
function postComment(pr, body) {
|
|
6696
|
+
const tmp = join19(tmpdir2(), `vinaya-review-post-${process.pid}-${Date.now()}.md`);
|
|
6697
|
+
writeFileSync10(tmp, body);
|
|
6698
|
+
try {
|
|
6699
|
+
return gh(["pr", "comment", pr, "--body-file", tmp]).trim();
|
|
6700
|
+
} catch (err) {
|
|
6701
|
+
refuseCmd(`Rendered comment failed to post via \`gh pr comment\`: ${err instanceof Error ? err.message : String(err)}`, "Check `gh auth status`/network, then re-run — nothing was posted.");
|
|
6702
|
+
} finally {
|
|
6703
|
+
rmSync4(tmp, { force: true });
|
|
6704
|
+
}
|
|
6705
|
+
}
|
|
6706
|
+
function fetchComments(pr) {
|
|
6707
|
+
const out = gh(["pr", "view", pr, "--json", "comments"]);
|
|
6708
|
+
const parsed = JSON.parse(out);
|
|
6709
|
+
return parsed.comments.map((c) => ({ body: c.body, author: c.author?.login ?? null }));
|
|
6710
|
+
}
|
|
6711
|
+
async function reviewPostCommand(args) {
|
|
6712
|
+
const json = args.includes("--json");
|
|
6713
|
+
const flags2 = parseFlags(args.filter((a) => a !== "--json"));
|
|
6714
|
+
const role = flags2.get("--role");
|
|
6715
|
+
if (role !== "code-reviewer" && role !== "security") {
|
|
6716
|
+
refuseCmd(`\`--role ${role ?? "(missing)"}\` is not \`code-reviewer\` or \`security\`.`, "Pass `--role code-reviewer` or `--role security`.");
|
|
6717
|
+
}
|
|
6718
|
+
const pr = requireFlag(flags2, "--pr");
|
|
6719
|
+
const taskId = requireFlag(flags2, "--task-id");
|
|
6720
|
+
const model = requireFlag(flags2, "--model");
|
|
6721
|
+
const tokensIn = requireTokenField(flags2, "--tokens-in");
|
|
6722
|
+
const tokensOut = requireTokenField(flags2, "--tokens-out");
|
|
6723
|
+
const cost = requireFlag(flags2, "--cost");
|
|
6724
|
+
const tokens = { taskId, model, tokensIn, tokensOut, cost };
|
|
6725
|
+
const headSha = resolveHeadSha(pr);
|
|
6726
|
+
const principalAllowlist = resolvePrincipalAllowlist(loadTrustAnchorConfig());
|
|
6727
|
+
let body;
|
|
6728
|
+
let verify;
|
|
6729
|
+
if (role === "code-reviewer") {
|
|
6730
|
+
const verdict = normalizeCodeReviewVerdict(requireFlag(flags2, "--verdict"));
|
|
6731
|
+
const findings = readFindingsFile(flags2.get("--findings-file"), CODE_REVIEW_SEVERITIES);
|
|
6732
|
+
if (findings.some((f) => f.severity === "BLOCKER") && verdict === "APPROVE") {
|
|
6733
|
+
refuseCmd("Findings include a BLOCKER but `--verdict APPROVE` — reviewer.md requires REQUEST CHANGES whenever any BLOCKER finding exists.", "Pass `--verdict REQUEST_CHANGES`, or fix the finding severity if BLOCKER was a miscategorization.");
|
|
6734
|
+
}
|
|
6735
|
+
const input = {
|
|
6736
|
+
...tokens,
|
|
6737
|
+
headSha,
|
|
6738
|
+
verdict,
|
|
6739
|
+
briefConformance: requireFlag(flags2, "--brief-conformance"),
|
|
6740
|
+
specConformance: requireFlag(flags2, "--spec-conformance"),
|
|
6741
|
+
findings,
|
|
6742
|
+
scope: requireFlag(flags2, "--scope"),
|
|
6743
|
+
tests: requireFlag(flags2, "--tests"),
|
|
6744
|
+
docs: requireFlag(flags2, "--docs")
|
|
6745
|
+
};
|
|
6746
|
+
body = renderCodeReviewComment(input);
|
|
6747
|
+
verify = (comments2) => verifyPostedCodeReview(comments2, verdict, headSha, principalAllowlist);
|
|
6748
|
+
} else {
|
|
6749
|
+
const verdict = normalizeSecurityVerdict(requireFlag(flags2, "--verdict"));
|
|
6750
|
+
const findings = readFindingsFile(flags2.get("--findings-file"), SECURITY_SEVERITIES);
|
|
6751
|
+
if (findings.some((f) => f.severity === "CRITICAL" || f.severity === "HIGH") && verdict === "PASS") {
|
|
6752
|
+
refuseCmd("Findings include a CRITICAL/HIGH finding but `--verdict PASS` — security.md requires FAIL whenever any CRITICAL or HIGH finding exists.", "Pass `--verdict FAIL`, or fix the finding severity if it was a miscategorization.");
|
|
6753
|
+
}
|
|
6754
|
+
const secrets = requireFlag(flags2, "--secrets");
|
|
6755
|
+
const secretsEvidenceFile = flags2.get("--secrets-evidence-file");
|
|
6756
|
+
if (isNoneFoundClaim(secrets) && !secretsEvidenceFile) {
|
|
6757
|
+
refuseCmd('`--secrets` normalizes to "none found" but no `--secrets-evidence-file` was given — security.md: "SECRETS: none found" with no scan output pasted is an unbacked self-attestation.', "Pass `--secrets-evidence-file <path>` containing the actual scanner output, or change `--secrets` to describe what was found instead.");
|
|
6758
|
+
}
|
|
6759
|
+
let secretsEvidence = null;
|
|
6760
|
+
if (secretsEvidenceFile) {
|
|
6761
|
+
try {
|
|
6762
|
+
secretsEvidence = readFileSync18(secretsEvidenceFile, "utf8");
|
|
6763
|
+
} catch {
|
|
6764
|
+
refuseCmd(`Could not read secrets evidence file at ${secretsEvidenceFile}.`, "Check the path and re-run.");
|
|
6765
|
+
}
|
|
6766
|
+
}
|
|
6767
|
+
const input = {
|
|
6768
|
+
...tokens,
|
|
6769
|
+
headSha,
|
|
6770
|
+
verdict,
|
|
6771
|
+
findings,
|
|
6772
|
+
configScan: requireFlag(flags2, "--config-scan"),
|
|
6773
|
+
secrets,
|
|
6774
|
+
secretsEvidence
|
|
6775
|
+
};
|
|
6776
|
+
body = renderSecurityComment(input);
|
|
6777
|
+
verify = (comments2) => verifyPostedSecurity(comments2, verdict, headSha, principalAllowlist);
|
|
6778
|
+
}
|
|
6779
|
+
const url = postComment(pr, body);
|
|
6780
|
+
let comments;
|
|
6781
|
+
try {
|
|
6782
|
+
comments = fetchComments(pr);
|
|
6783
|
+
} catch (err) {
|
|
6784
|
+
refuseCmd(`Posted the comment (${url}) but could not re-fetch PR ${pr}'s comments to self-verify: ${err instanceof Error ? err.message : String(err)}`, "Check `gh auth status`/network and manually confirm the posted comment parses cleanly — this command could not verify it.");
|
|
6785
|
+
}
|
|
6786
|
+
const result = verify(comments);
|
|
6787
|
+
if (!result.ok) {
|
|
6788
|
+
refuseCmd(`Posted comment ${url}, but self-verification FAILED on re-parse: ${result.reason}`, "The posted comment does not re-parse clean through the same extractCodeReviewVerdict/extractSecurityReviewVerdict functions the merge gate calls. Do not treat the post as valid — inspect the comment and this command for drift, fix, and re-run.");
|
|
6789
|
+
}
|
|
6790
|
+
if (json) {
|
|
6791
|
+
printJson({ posted: true, url, role, headSha, selfVerified: true });
|
|
6792
|
+
} else {
|
|
6793
|
+
process.stdout.write(`${body}
|
|
6794
|
+
|
|
6795
|
+
Posted: ${url}
|
|
6796
|
+
Self-verification: clean — re-parsed VERDICT is bound to head ${headSha}.
|
|
6797
|
+
`);
|
|
6798
|
+
}
|
|
6799
|
+
}
|
|
6800
|
+
|
|
5764
6801
|
// src/commands/studio.ts
|
|
5765
6802
|
import { execFile as execFile5, spawn as spawn2 } from "node:child_process";
|
|
5766
|
-
import { existsSync as
|
|
6803
|
+
import { existsSync as existsSync18, readFileSync as readFileSync19, renameSync } from "node:fs";
|
|
5767
6804
|
import net from "node:net";
|
|
5768
|
-
import { dirname as dirname7, join as
|
|
6805
|
+
import { dirname as dirname7, join as join20 } from "node:path";
|
|
5769
6806
|
import { promisify as promisify5 } from "node:util";
|
|
5770
6807
|
|
|
5771
6808
|
// src/lib/studio-bundle.ts
|
|
@@ -5781,25 +6818,25 @@ var FALLBACK_PORT = 3108;
|
|
|
5781
6818
|
function resolveStudioTarget(cwd, moduleUrl = import.meta.url) {
|
|
5782
6819
|
let dir = cwd;
|
|
5783
6820
|
for (;; ) {
|
|
5784
|
-
const webDir =
|
|
5785
|
-
const pkgPath =
|
|
5786
|
-
if (
|
|
6821
|
+
const webDir = join20(dir, "apps", "vinaya-studio", "web");
|
|
6822
|
+
const pkgPath = join20(webDir, "package.json");
|
|
6823
|
+
if (existsSync18(pkgPath)) {
|
|
5787
6824
|
try {
|
|
5788
|
-
const pkg = JSON.parse(
|
|
6825
|
+
const pkg = JSON.parse(readFileSync19(pkgPath, "utf-8"));
|
|
5789
6826
|
if (pkg.name === "@atta/vinaya-studio-web") {
|
|
5790
6827
|
return { kind: "workspace", webDir };
|
|
5791
6828
|
}
|
|
5792
6829
|
} catch {}
|
|
5793
6830
|
}
|
|
5794
|
-
if (
|
|
6831
|
+
if (existsSync18(join20(dir, ".git")))
|
|
5795
6832
|
break;
|
|
5796
6833
|
const parent = dirname7(dir);
|
|
5797
6834
|
if (parent === dir)
|
|
5798
6835
|
break;
|
|
5799
6836
|
dir = parent;
|
|
5800
6837
|
}
|
|
5801
|
-
const standaloneWebDir =
|
|
5802
|
-
if (
|
|
6838
|
+
const standaloneWebDir = join20(packageRoot(moduleUrl), "studio-standalone", "apps", "vinaya-studio", "web");
|
|
6839
|
+
if (existsSync18(join20(standaloneWebDir, "server.js"))) {
|
|
5803
6840
|
return { kind: "package", packageDir: standaloneWebDir };
|
|
5804
6841
|
}
|
|
5805
6842
|
return { kind: "missing" };
|
|
@@ -5819,9 +6856,9 @@ function isPortFree(port) {
|
|
|
5819
6856
|
});
|
|
5820
6857
|
}
|
|
5821
6858
|
function ensureStudioNodeModules(bundleRoot) {
|
|
5822
|
-
const real =
|
|
5823
|
-
const packed =
|
|
5824
|
-
if (!
|
|
6859
|
+
const real = join20(bundleRoot, "node_modules");
|
|
6860
|
+
const packed = join20(bundleRoot, STUDIO_NODE_MODULES_PACKED_DIRNAME);
|
|
6861
|
+
if (!existsSync18(real) && existsSync18(packed)) {
|
|
5825
6862
|
renameSync(packed, real);
|
|
5826
6863
|
}
|
|
5827
6864
|
}
|
|
@@ -5847,8 +6884,8 @@ async function runStudio(cwd, args, moduleUrl = import.meta.url) {
|
|
|
5847
6884
|
case "workspace":
|
|
5848
6885
|
return spawnDev(target.webDir, args);
|
|
5849
6886
|
case "package": {
|
|
5850
|
-
const bundleRoot =
|
|
5851
|
-
return spawnStandalone(cwd,
|
|
6887
|
+
const bundleRoot = join20(target.packageDir, "..", "..", "..");
|
|
6888
|
+
return spawnStandalone(cwd, join20(target.packageDir, "server.js"), bundleRoot);
|
|
5852
6889
|
}
|
|
5853
6890
|
case "missing":
|
|
5854
6891
|
console.error("Vinaya Studio isn't available in this install — no published @attalabs/vinaya build bundles the Studio app yet. Inside a checkout that contains Studio's source (apps/vinaya-studio/web), `vinaya studio` runs it directly.");
|
|
@@ -5857,8 +6894,8 @@ async function runStudio(cwd, args, moduleUrl = import.meta.url) {
|
|
|
5857
6894
|
}
|
|
5858
6895
|
|
|
5859
6896
|
// src/commands/upgrade.ts
|
|
5860
|
-
import { existsSync as
|
|
5861
|
-
import { join as
|
|
6897
|
+
import { existsSync as existsSync19, readFileSync as readFileSync20, rmSync as rmSync5, writeFileSync as writeFileSync11 } from "node:fs";
|
|
6898
|
+
import { join as join21 } from "node:path";
|
|
5862
6899
|
function realDeps7() {
|
|
5863
6900
|
return {
|
|
5864
6901
|
detectRepo: detectGitRepo,
|
|
@@ -5875,13 +6912,13 @@ function realDeps7() {
|
|
|
5875
6912
|
function flags2(args) {
|
|
5876
6913
|
return { dryRun: args.includes("--dry-run"), yes: args.includes("--yes") };
|
|
5877
6914
|
}
|
|
5878
|
-
function readManifest3(
|
|
5879
|
-
const p =
|
|
5880
|
-
if (!
|
|
6915
|
+
function readManifest3(repoRoot2) {
|
|
6916
|
+
const p = join21(repoRoot2, CONFIG_PATH);
|
|
6917
|
+
if (!existsSync19(p))
|
|
5881
6918
|
return { kind: "missing" };
|
|
5882
6919
|
let raw;
|
|
5883
6920
|
try {
|
|
5884
|
-
raw = JSON.parse(
|
|
6921
|
+
raw = JSON.parse(readFileSync20(p, "utf-8"));
|
|
5885
6922
|
} catch (err) {
|
|
5886
6923
|
return { kind: "invalid", error: `invalid JSON: ${err.message}` };
|
|
5887
6924
|
}
|
|
@@ -5896,31 +6933,31 @@ function readManifest3(repoRoot) {
|
|
|
5896
6933
|
return { kind: "not-initialized" };
|
|
5897
6934
|
return { kind: "ok", manifest: parsed.data.managed };
|
|
5898
6935
|
}
|
|
5899
|
-
function writeManifestVersion(
|
|
5900
|
-
const configAbs =
|
|
5901
|
-
const seed = JSON.parse(
|
|
6936
|
+
function writeManifestVersion(repoRoot2, manifest) {
|
|
6937
|
+
const configAbs = join21(repoRoot2, CONFIG_PATH);
|
|
6938
|
+
const seed = JSON.parse(readFileSync20(configAbs, "utf-8"));
|
|
5902
6939
|
const updated = { ...manifest, version: MANAGED_MANIFEST_VERSION };
|
|
5903
|
-
|
|
6940
|
+
writeFileSync11(configAbs, `${JSON.stringify({ ...seed, managed: updated }, null, 2)}
|
|
5904
6941
|
`, "utf-8");
|
|
5905
6942
|
}
|
|
5906
|
-
function stripFor(
|
|
5907
|
-
const abs2 = resolveManagedBlockPath(
|
|
5908
|
-
if (!
|
|
6943
|
+
function stripFor(repoRoot2, path, marker, comment) {
|
|
6944
|
+
const abs2 = resolveManagedBlockPath(repoRoot2, path);
|
|
6945
|
+
if (!existsSync19(abs2))
|
|
5909
6946
|
return { path, marker, comment, present: false, removesHost: false };
|
|
5910
|
-
const stripped = stripBlockFromContent(
|
|
6947
|
+
const stripped = stripBlockFromContent(readFileSync20(abs2, "utf-8"), marker, comment);
|
|
5911
6948
|
if (stripped === null)
|
|
5912
6949
|
return { path, marker, comment, present: false, removesHost: false };
|
|
5913
6950
|
return { path, marker, comment, present: true, removesHost: blockStripLeavesEmpty(stripped) };
|
|
5914
6951
|
}
|
|
5915
|
-
function planHookRouting(
|
|
6952
|
+
function planHookRouting(repoRoot2, manifest, recorded, hooksPathValue) {
|
|
5916
6953
|
const none = { target: recorded, strips: [], arm: false, migratesManifest: false, blockedReason: null };
|
|
5917
6954
|
if (recorded === ".husky")
|
|
5918
6955
|
return none;
|
|
5919
6956
|
if (recorded === TRACKED_HOOK_DIR) {
|
|
5920
|
-
const strips = manifest.blocks.filter((b) => b.path.startsWith(`${TRACKED_HOOK_DIR}/`)).map((b) => stripFor(
|
|
6957
|
+
const strips = manifest.blocks.filter((b) => b.path.startsWith(`${TRACKED_HOOK_DIR}/`)).map((b) => stripFor(repoRoot2, `.git/hooks/${b.marker}`, b.marker, b.comment)).filter((s) => s.present);
|
|
5921
6958
|
if (hooksPathValue === TRACKED_HOOK_DIR)
|
|
5922
6959
|
return { ...none, strips };
|
|
5923
|
-
const foreign = foreignRawHooks(
|
|
6960
|
+
const foreign = foreignRawHooks(repoRoot2);
|
|
5924
6961
|
if (foreign.length > 0) {
|
|
5925
6962
|
return {
|
|
5926
6963
|
...none,
|
|
@@ -5933,17 +6970,17 @@ function planHookRouting(repoRoot, manifest, recorded, hooksPathValue) {
|
|
|
5933
6970
|
const legacy = manifest.blocks.filter((b) => b.path.startsWith(".git/hooks/"));
|
|
5934
6971
|
const blocked = [];
|
|
5935
6972
|
for (const b of legacy) {
|
|
5936
|
-
const abs2 = resolveManagedBlockPath(
|
|
5937
|
-
if (!
|
|
6973
|
+
const abs2 = resolveManagedBlockPath(repoRoot2, b.path);
|
|
6974
|
+
if (!existsSync19(abs2))
|
|
5938
6975
|
continue;
|
|
5939
|
-
const stripped = stripBlockFromContent(
|
|
6976
|
+
const stripped = stripBlockFromContent(readFileSync20(abs2, "utf-8"), b.marker, b.comment);
|
|
5940
6977
|
if (stripped === null)
|
|
5941
6978
|
blocked.push(`${b.path} exists without vinaya's managed block`);
|
|
5942
6979
|
else if (!blockStripLeavesEmpty(stripped))
|
|
5943
6980
|
blocked.push(`${b.path} carries your own lines alongside the managed block`);
|
|
5944
6981
|
}
|
|
5945
6982
|
const legacyNames = new Set(legacy.map((b) => b.path.slice(".git/hooks/".length)));
|
|
5946
|
-
for (const f of activeRawHooks(
|
|
6983
|
+
for (const f of activeRawHooks(repoRoot2)) {
|
|
5947
6984
|
if (!legacyNames.has(f))
|
|
5948
6985
|
blocked.push(`.git/hooks/${f} is an active raw hook vinaya does not manage`);
|
|
5949
6986
|
}
|
|
@@ -5951,7 +6988,7 @@ function planHookRouting(repoRoot, manifest, recorded, hooksPathValue) {
|
|
|
5951
6988
|
return { ...none, blockedReason: blocked.join("; ") };
|
|
5952
6989
|
return {
|
|
5953
6990
|
target: TRACKED_HOOK_DIR,
|
|
5954
|
-
strips: legacy.map((b) => stripFor(
|
|
6991
|
+
strips: legacy.map((b) => stripFor(repoRoot2, b.path, b.marker, b.comment)).filter((s) => s.present),
|
|
5955
6992
|
arm: hooksPathValue !== TRACKED_HOOK_DIR,
|
|
5956
6993
|
migratesManifest: true,
|
|
5957
6994
|
blockedReason: null
|
|
@@ -5963,7 +7000,7 @@ function translateHookPaths(manifest) {
|
|
|
5963
7000
|
blocks: manifest.blocks.map((b) => b.path.startsWith(".git/hooks/") ? { ...b, path: `${TRACKED_HOOK_DIR}/${b.path.slice(".git/hooks/".length)}` } : b)
|
|
5964
7001
|
};
|
|
5965
7002
|
}
|
|
5966
|
-
function planUpgrade(ops,
|
|
7003
|
+
function planUpgrade(ops, repoRoot2, manifest, routing) {
|
|
5967
7004
|
const entries = [];
|
|
5968
7005
|
let hasChanges = routing.arm || routing.migratesManifest || routing.strips.some((s) => s.present);
|
|
5969
7006
|
const ownedFiles = new Set(manifest.files);
|
|
@@ -5971,8 +7008,8 @@ function planUpgrade(ops, repoRoot, manifest, routing) {
|
|
|
5971
7008
|
const ownedBlocks = new Set(manifest.blocks.map((b) => blockKey(b.path, b.marker)));
|
|
5972
7009
|
for (const op of ops) {
|
|
5973
7010
|
if (op.kind === "create-file") {
|
|
5974
|
-
const abs2 =
|
|
5975
|
-
const exists =
|
|
7011
|
+
const abs2 = join21(repoRoot2, op.path);
|
|
7012
|
+
const exists = existsSync19(abs2);
|
|
5976
7013
|
const owned = ownedFiles.has(op.path);
|
|
5977
7014
|
let action;
|
|
5978
7015
|
if (op.path === CONFIG_PATH || op.path === DOC_OWNERS_PATH) {
|
|
@@ -5982,7 +7019,7 @@ function planUpgrade(ops, repoRoot, manifest, routing) {
|
|
|
5982
7019
|
} else if (!exists) {
|
|
5983
7020
|
action = "recreate";
|
|
5984
7021
|
hasChanges = true;
|
|
5985
|
-
} else if (
|
|
7022
|
+
} else if (readFileSync20(abs2, "utf-8") !== op.content) {
|
|
5986
7023
|
action = "regenerate";
|
|
5987
7024
|
hasChanges = true;
|
|
5988
7025
|
} else {
|
|
@@ -5990,16 +7027,16 @@ function planUpgrade(ops, repoRoot, manifest, routing) {
|
|
|
5990
7027
|
}
|
|
5991
7028
|
entries.push({ kind: "create-file", op, action });
|
|
5992
7029
|
} else if (op.kind === "managed-block") {
|
|
5993
|
-
const abs2 = resolveManagedBlockPath(
|
|
7030
|
+
const abs2 = resolveManagedBlockPath(repoRoot2, op.path);
|
|
5994
7031
|
const owned = ownedBlocks.has(blockKey(op.path, op.marker));
|
|
5995
7032
|
let action;
|
|
5996
7033
|
if (!owned) {
|
|
5997
7034
|
action = "not-installed";
|
|
5998
|
-
} else if (!
|
|
7035
|
+
} else if (!existsSync19(abs2)) {
|
|
5999
7036
|
action = "recreate-host";
|
|
6000
7037
|
hasChanges = true;
|
|
6001
7038
|
} else {
|
|
6002
|
-
const content =
|
|
7039
|
+
const content = readFileSync20(abs2, "utf-8");
|
|
6003
7040
|
const { begin, end } = markerLines(op.marker, op.comment);
|
|
6004
7041
|
if (!(content.includes(begin) && content.includes(end))) {
|
|
6005
7042
|
action = "recreate-append";
|
|
@@ -6097,45 +7134,45 @@ function renderUpgradeDiff(plan) {
|
|
|
6097
7134
|
return lines.join(`
|
|
6098
7135
|
`);
|
|
6099
7136
|
}
|
|
6100
|
-
function regenerateBlock(
|
|
6101
|
-
const abs2 = resolveManagedBlockPath(
|
|
6102
|
-
const content =
|
|
7137
|
+
function regenerateBlock(repoRoot2, op) {
|
|
7138
|
+
const abs2 = resolveManagedBlockPath(repoRoot2, op.path);
|
|
7139
|
+
const content = readFileSync20(abs2, "utf-8");
|
|
6103
7140
|
const stripped = stripBlockFromContent(content, op.marker, op.comment);
|
|
6104
7141
|
if (stripped !== null) {
|
|
6105
|
-
|
|
7142
|
+
writeFileSync11(abs2, stripped.endsWith(`
|
|
6106
7143
|
`) ? stripped : `${stripped}
|
|
6107
7144
|
`, "utf-8");
|
|
6108
7145
|
}
|
|
6109
|
-
appendBlock(
|
|
7146
|
+
appendBlock(repoRoot2, op);
|
|
6110
7147
|
}
|
|
6111
|
-
function applyUpgrade(plan,
|
|
7148
|
+
function applyUpgrade(plan, repoRoot2) {
|
|
6112
7149
|
for (const e of plan.entries) {
|
|
6113
7150
|
if (e.kind === "create-file") {
|
|
6114
7151
|
if (e.action === "regenerate" || e.action === "recreate") {
|
|
6115
|
-
writeFileWithDirs(
|
|
7152
|
+
writeFileWithDirs(join21(repoRoot2, e.op.path), e.op.content, e.op.mode);
|
|
6116
7153
|
}
|
|
6117
7154
|
} else {
|
|
6118
7155
|
if (e.action === "recreate-host")
|
|
6119
|
-
createHost(
|
|
7156
|
+
createHost(repoRoot2, e.op);
|
|
6120
7157
|
else if (e.action === "recreate-append")
|
|
6121
|
-
appendBlock(
|
|
7158
|
+
appendBlock(repoRoot2, e.op);
|
|
6122
7159
|
else if (e.action === "regenerate-block")
|
|
6123
|
-
regenerateBlock(
|
|
7160
|
+
regenerateBlock(repoRoot2, e.op);
|
|
6124
7161
|
}
|
|
6125
7162
|
}
|
|
6126
7163
|
for (const s of plan.routing.strips) {
|
|
6127
7164
|
if (!s.present)
|
|
6128
7165
|
continue;
|
|
6129
|
-
const abs2 = resolveManagedBlockPath(
|
|
6130
|
-
if (!
|
|
7166
|
+
const abs2 = resolveManagedBlockPath(repoRoot2, s.path);
|
|
7167
|
+
if (!existsSync19(abs2))
|
|
6131
7168
|
continue;
|
|
6132
|
-
const stripped = stripBlockFromContent(
|
|
7169
|
+
const stripped = stripBlockFromContent(readFileSync20(abs2, "utf-8"), s.marker, s.comment);
|
|
6133
7170
|
if (stripped === null)
|
|
6134
7171
|
continue;
|
|
6135
7172
|
if (blockStripLeavesEmpty(stripped))
|
|
6136
|
-
|
|
7173
|
+
rmSync5(abs2, { force: true });
|
|
6137
7174
|
else
|
|
6138
|
-
|
|
7175
|
+
writeFileSync11(abs2, stripped.endsWith(`
|
|
6139
7176
|
`) ? stripped : `${stripped}
|
|
6140
7177
|
`, "utf-8");
|
|
6141
7178
|
}
|
|
@@ -6217,16 +7254,16 @@ async function upgradeCommand(args) {
|
|
|
6217
7254
|
}
|
|
6218
7255
|
|
|
6219
7256
|
// src/commands/waiver.ts
|
|
6220
|
-
import { execFileSync as
|
|
7257
|
+
import { execFileSync as execFileSync15 } from "node:child_process";
|
|
6221
7258
|
function labelFor(kind) {
|
|
6222
7259
|
return kind === "docs" ? WAIVER_LABEL : WAIVER_LABEL_REVIEW;
|
|
6223
7260
|
}
|
|
6224
|
-
function
|
|
6225
|
-
return
|
|
7261
|
+
function gh2(args) {
|
|
7262
|
+
return execFileSync15("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], env: process.env }).trim();
|
|
6226
7263
|
}
|
|
6227
7264
|
function detectPrNumber() {
|
|
6228
7265
|
try {
|
|
6229
|
-
const out =
|
|
7266
|
+
const out = gh2(["pr", "view", "--json", "number", "-q", ".number"]);
|
|
6230
7267
|
return /^\d+$/.test(out) ? out : null;
|
|
6231
7268
|
} catch {
|
|
6232
7269
|
return null;
|
|
@@ -6322,8 +7359,8 @@ Reason: ${reason}`];
|
|
|
6322
7359
|
}
|
|
6323
7360
|
process.stdout.write(`Applying \`${label2}\` to PR #${prNumber} under your authenticated \`gh\` identity — vinaya never fabricates an actor.
|
|
6324
7361
|
`);
|
|
6325
|
-
|
|
6326
|
-
|
|
7362
|
+
execFileSync15("gh", editArgs, { stdio: "inherit", env: process.env });
|
|
7363
|
+
execFileSync15("gh", commentArgs, { stdio: "inherit", env: process.env });
|
|
6327
7364
|
process.stdout.write(`
|
|
6328
7365
|
✓ Label applied and reason posted as a PR comment.
|
|
6329
7366
|
`);
|
|
@@ -6483,6 +7520,48 @@ var COMMANDS = [
|
|
|
6483
7520
|
],
|
|
6484
7521
|
status: "shipped"
|
|
6485
7522
|
},
|
|
7523
|
+
{
|
|
7524
|
+
name: "review post",
|
|
7525
|
+
description: "Render, post, and self-verify a code-reviewer or security-review verdict comment on a PR",
|
|
7526
|
+
flags: [
|
|
7527
|
+
{ flag: "--role", description: "`code-reviewer` or `security` — selects which role template is rendered" },
|
|
7528
|
+
{
|
|
7529
|
+
flag: "--pr",
|
|
7530
|
+
description: "Target PR number — its real head is resolved via `gh pr view`, never caller-supplied"
|
|
7531
|
+
},
|
|
7532
|
+
{
|
|
7533
|
+
flag: "--verdict",
|
|
7534
|
+
description: "code-reviewer: `APPROVE` | `REQUEST_CHANGES`. security: `PASS` | `FAIL`"
|
|
7535
|
+
},
|
|
7536
|
+
{
|
|
7537
|
+
flag: "--findings-file",
|
|
7538
|
+
description: "One finding per line: `SEVERITY|file:line|description` (`|`-delimited). Omit for zero findings."
|
|
7539
|
+
},
|
|
7540
|
+
{ flag: "--brief-conformance", description: "code-reviewer only: the BRIEF CONFORMANCE line" },
|
|
7541
|
+
{ flag: "--spec-conformance", description: "code-reviewer only: the SPEC CONFORMANCE line" },
|
|
7542
|
+
{ flag: "--scope", description: "code-reviewer only: the SCOPE line" },
|
|
7543
|
+
{ flag: "--tests", description: "code-reviewer only: the TESTS line" },
|
|
7544
|
+
{ flag: "--docs", description: "code-reviewer only: the DOCS line" },
|
|
7545
|
+
{ flag: "--config-scan", description: "security only: the CONFIG SCAN line" },
|
|
7546
|
+
{ flag: "--secrets", description: "security only: the SECRETS line" },
|
|
7547
|
+
{
|
|
7548
|
+
flag: "--secrets-evidence-file",
|
|
7549
|
+
description: 'security only: required whenever `--secrets` claims "none found" — the pasted scanner output backing that claim'
|
|
7550
|
+
},
|
|
7551
|
+
{ flag: "--task-id", description: "the closing `Tokens:` line's task id" },
|
|
7552
|
+
{ flag: "--model", description: "the closing `Tokens:` line's model name" },
|
|
7553
|
+
{ flag: "--tokens-in", description: "a non-negative integer, or `-` if unknown" },
|
|
7554
|
+
{ flag: "--tokens-out", description: "a non-negative integer, or `-` if unknown" },
|
|
7555
|
+
{ flag: "--cost", description: "free text, or `-` if unknown" },
|
|
7556
|
+
{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }
|
|
7557
|
+
],
|
|
7558
|
+
details: [
|
|
7559
|
+
"Every structural line (`VERDICT:`, `Judged head:`) is rendered from this command's own validated enum/sha inputs — never from a caller-supplied string — so a Reviewer's free-typed prose can no longer produce a shape the merge gate's line-anchored regex fails to see.",
|
|
7560
|
+
"Refuses a contradictory verdict before posting anything: a BLOCKER finding with `--verdict APPROVE`, or a CRITICAL/HIGH finding with `--verdict PASS`, is rejected outright, mirroring each role doc's own consistency rule.",
|
|
7561
|
+
"After posting, re-fetches the PR's comments and runs them through the exact `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` functions `checkReviewGate` calls — the same functions, not a second implementation — and exits non-zero naming precisely what failed to re-parse if the post does not come back clean and bound to the resolved head. There is no `--skip-verify` escape."
|
|
7562
|
+
],
|
|
7563
|
+
status: "shipped"
|
|
7564
|
+
},
|
|
6486
7565
|
{
|
|
6487
7566
|
name: "doctor",
|
|
6488
7567
|
description: "Diagnose hook, workflow, and config health — report only, never mutates",
|
|
@@ -6628,9 +7707,9 @@ function printHelp() {
|
|
|
6628
7707
|
}
|
|
6629
7708
|
|
|
6630
7709
|
// src/index.ts
|
|
6631
|
-
var PACKAGE_ROOT =
|
|
7710
|
+
var PACKAGE_ROOT = join22(dirname8(fileURLToPath2(import.meta.url)), "..");
|
|
6632
7711
|
function readVersion2() {
|
|
6633
|
-
const pkg = JSON.parse(
|
|
7712
|
+
const pkg = JSON.parse(readFileSync21(join22(PACKAGE_ROOT, "package.json"), "utf-8"));
|
|
6634
7713
|
return pkg.version;
|
|
6635
7714
|
}
|
|
6636
7715
|
var [, , command, ...args] = process.argv;
|
|
@@ -6733,6 +7812,16 @@ try {
|
|
|
6733
7812
|
}
|
|
6734
7813
|
break;
|
|
6735
7814
|
}
|
|
7815
|
+
case "review": {
|
|
7816
|
+
const [subcommand, ...rest] = args;
|
|
7817
|
+
if (subcommand === "post") {
|
|
7818
|
+
await reviewPostCommand(rest);
|
|
7819
|
+
} else {
|
|
7820
|
+
console.error(`Unknown 'review' subcommand: ${subcommand ?? "(none)"} (expected 'post')`);
|
|
7821
|
+
process.exit(2);
|
|
7822
|
+
}
|
|
7823
|
+
break;
|
|
7824
|
+
}
|
|
6736
7825
|
case "demo": {
|
|
6737
7826
|
const [subcommand, ...rest] = args;
|
|
6738
7827
|
if (subcommand === "break") {
|