@attalabs/vinaya 0.12.0 → 0.16.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 +26 -2
- package/aeg-root/contracts/planner-brief.md +1 -1
- package/aeg-root/enforcement.md +5 -5
- 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/tranche-model.md +1 -1
- package/dist/checks/bin/check-branch-topology.js +28 -5
- package/dist/checks/bin/check-brief-shape.js +28 -5
- package/dist/checks/bin/check-closes-n.js +28 -5
- package/dist/checks/bin/check-coherence.js +30 -6
- package/dist/checks/bin/check-dead-branch-push.js +28 -5
- package/dist/checks/bin/check-dispatch-readiness.js +30 -6
- package/dist/checks/bin/check-doc-coverage-push.js +30 -6
- package/dist/checks/bin/check-doc-coverage.js +30 -6
- package/dist/checks/bin/check-evidence-fresh.js +28 -5
- package/dist/checks/bin/check-first-push-dispatch.js +30 -6
- package/dist/checks/bin/check-issue-assignment.js +28 -5
- package/dist/checks/bin/check-no-disk-state.js +28 -5
- package/dist/checks/bin/check-reader-resolvable-prose.js +28 -5
- package/dist/checks/bin/check-registry-gates.js +28 -5
- package/dist/checks/bin/check-review-gate.js +33 -8
- package/dist/checks/bin/check-single-plan-pr.js +28 -5
- package/dist/checks/bin/check-test-plan.js +28 -5
- package/dist/index.js +777 -73
- 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/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.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.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.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.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.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]__0q56ohl._.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]__1_ojzp1._.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]__1wsl6ro._.js → [root-of-the-server]__1qpl4m_._.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/_00yjcv1._.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/_18-d2be._.js +1 -1
- 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/node_modules_1vo08dj._.js +2 -2
- 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/{2ieu4oi-pp_bz.js → 0ek6qlqhokys0.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1khy4oilrab6g.js → 0fm4p9os2lsom.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1eo32v1tbjy7c.js → 0ywkq74falx-x.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1t7yy_s34022p.js → 1s9ixp5nhnevp.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/0qu0xgvifbacu.css +0 -1
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{ZHVE9Y9G8XKR91g27SgVX → R7xvB2iuNFp_jxENpL7xv}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{ZHVE9Y9G8XKR91g27SgVX → R7xvB2iuNFp_jxENpL7xv}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{ZHVE9Y9G8XKR91g27SgVX → R7xvB2iuNFp_jxENpL7xv}/_ssgManifest.js +0 -0
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
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 readFileSync20 } 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);
|
|
@@ -392,6 +392,85 @@ function splitTableRow(row) {
|
|
|
392
392
|
parts.pop();
|
|
393
393
|
return parts.map((p) => p.split(PLACEHOLDER).join("|"));
|
|
394
394
|
}
|
|
395
|
+
// ../../packages/aeg-core/src/blast-radius-domains.ts
|
|
396
|
+
function resolveWorkspaceEntry(entry2, listDirs) {
|
|
397
|
+
if (entry2.endsWith("/*")) {
|
|
398
|
+
const baseDir = entry2.slice(0, -2);
|
|
399
|
+
return listDirs(baseDir).map((name) => `${baseDir}/${name}`);
|
|
400
|
+
}
|
|
401
|
+
if (entry2.includes("*"))
|
|
402
|
+
return [];
|
|
403
|
+
return [entry2];
|
|
404
|
+
}
|
|
405
|
+
function deriveWorkspacePackageDomains(workspaces, listDirs) {
|
|
406
|
+
const positive = workspaces.filter((entry2) => !entry2.startsWith("!"));
|
|
407
|
+
const negative = workspaces.filter((entry2) => entry2.startsWith("!")).map((entry2) => entry2.slice(1));
|
|
408
|
+
const excluded = new Set;
|
|
409
|
+
for (const entry2 of negative) {
|
|
410
|
+
for (const member of resolveWorkspaceEntry(entry2, listDirs))
|
|
411
|
+
excluded.add(member);
|
|
412
|
+
}
|
|
413
|
+
const domains = new Set;
|
|
414
|
+
for (const entry2 of positive) {
|
|
415
|
+
for (const member of resolveWorkspaceEntry(entry2, listDirs)) {
|
|
416
|
+
if (excluded.has(member))
|
|
417
|
+
continue;
|
|
418
|
+
const segments = member.split("/");
|
|
419
|
+
if (segments[0] !== "packages" || segments.length < 2)
|
|
420
|
+
continue;
|
|
421
|
+
domains.add(`packages/${segments[1]}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return [...domains].sort();
|
|
425
|
+
}
|
|
426
|
+
function parsePnpmWorkspaceYaml(content) {
|
|
427
|
+
const unquote = (s) => {
|
|
428
|
+
const t = s.trim();
|
|
429
|
+
if (t.length >= 2 && (t[0] === "'" && t.at(-1) === "'" || t[0] === '"' && t.at(-1) === '"')) {
|
|
430
|
+
return t.slice(1, -1);
|
|
431
|
+
}
|
|
432
|
+
return t;
|
|
433
|
+
};
|
|
434
|
+
const inlineMatch = content.match(/^packages:\s*\[(.*)\]\s*$/m);
|
|
435
|
+
if (inlineMatch) {
|
|
436
|
+
return (inlineMatch[1] ?? "").split(",").map(unquote).filter((s) => s.length > 0);
|
|
437
|
+
}
|
|
438
|
+
const entries = [];
|
|
439
|
+
let inBlock = false;
|
|
440
|
+
for (const rawLine of content.split(`
|
|
441
|
+
`)) {
|
|
442
|
+
const line = rawLine.replace(/#.*$/, "");
|
|
443
|
+
if (/^packages:\s*$/.test(line)) {
|
|
444
|
+
inBlock = true;
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (!inBlock)
|
|
448
|
+
continue;
|
|
449
|
+
const itemMatch = line.match(/^\s+-\s*(.+?)\s*$/);
|
|
450
|
+
if (itemMatch?.[1]) {
|
|
451
|
+
entries.push(unquote(itemMatch[1]));
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (line.trim().length === 0)
|
|
455
|
+
continue;
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
return entries;
|
|
459
|
+
}
|
|
460
|
+
var CROSS_CUTTING_CANDIDATES = [
|
|
461
|
+
"bun.lock",
|
|
462
|
+
"package-lock.json",
|
|
463
|
+
"pnpm-lock.yaml",
|
|
464
|
+
"yarn.lock",
|
|
465
|
+
"turbo.json",
|
|
466
|
+
"biome.json",
|
|
467
|
+
"tsconfig.json",
|
|
468
|
+
".github/workflows",
|
|
469
|
+
".husky"
|
|
470
|
+
];
|
|
471
|
+
function deriveBuiltinCrossCuttingDefaults(exists) {
|
|
472
|
+
return CROSS_CUTTING_CANDIDATES.filter((path) => exists(path));
|
|
473
|
+
}
|
|
395
474
|
// ../../packages/aeg-core/src/state-machine-model.ts
|
|
396
475
|
var FORGE_FACT_INPUTS = [
|
|
397
476
|
{
|
|
@@ -457,12 +536,30 @@ var DERIVED_STATUSES = [
|
|
|
457
536
|
"incoherent"
|
|
458
537
|
];
|
|
459
538
|
var DERIVABLE_STATUSES = DERIVED_STATUSES.filter((s) => s !== "backlog");
|
|
539
|
+
// ../../packages/aeg-core/src/file-classify.ts
|
|
540
|
+
function isCodeFile(p) {
|
|
541
|
+
if (p.endsWith(".md"))
|
|
542
|
+
return false;
|
|
543
|
+
return /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|sql|css)$/.test(p);
|
|
544
|
+
}
|
|
460
545
|
// ../../packages/aeg-core/src/waiver-label.ts
|
|
461
546
|
var WAIVER_LABEL = label("waiver-docs");
|
|
462
547
|
var WAIVER_LABEL_REVIEW = label("waiver-review");
|
|
548
|
+
var PRINCIPAL_ALLOWLIST = ["daniboomerang"];
|
|
549
|
+
function isPrincipal(login, principalAllowlist) {
|
|
550
|
+
if (login === null)
|
|
551
|
+
return false;
|
|
552
|
+
const normalized = login.toLowerCase();
|
|
553
|
+
return principalAllowlist.some((p) => p.toLowerCase() === normalized);
|
|
554
|
+
}
|
|
463
555
|
|
|
464
556
|
// ../../packages/aeg-core/src/doc-owners.ts
|
|
465
557
|
var DOC_OWNERS_PATH = ".vinaya/doc-owners";
|
|
558
|
+
function classifyDocOwnersManifest(content) {
|
|
559
|
+
if (content === null)
|
|
560
|
+
return "absent";
|
|
561
|
+
return content.trim() === "" ? "empty" : "present";
|
|
562
|
+
}
|
|
466
563
|
function parseDocOwners(content) {
|
|
467
564
|
const bindings = [];
|
|
468
565
|
const errors = [];
|
|
@@ -507,6 +604,13 @@ function globToRegex(pat) {
|
|
|
507
604
|
re += "$";
|
|
508
605
|
return new RegExp(re);
|
|
509
606
|
}
|
|
607
|
+
function isUrlPointer(p) {
|
|
608
|
+
return /^https?:\/\//.test(p);
|
|
609
|
+
}
|
|
610
|
+
function pointerToPath(p) {
|
|
611
|
+
const idx = p.indexOf("#");
|
|
612
|
+
return idx === -1 ? p : p.slice(0, idx);
|
|
613
|
+
}
|
|
510
614
|
var SEPARATOR = /(?:[ \t]*[—–][ \t]*|[ \t]+-[ \t]+)/.source;
|
|
511
615
|
// ../../packages/aeg-core/src/pr-tier.ts
|
|
512
616
|
function readTierFromPrBody(prBody) {
|
|
@@ -793,15 +897,25 @@ function isTaskIssueLabelSet(labels) {
|
|
|
793
897
|
// ../../packages/aeg-core/src/coherence-checks.ts
|
|
794
898
|
var R1_GRANDFATHERED_ISSUES = new Set([279, 280, 281, 282]);
|
|
795
899
|
// ../../packages/aeg-core/src/verdict-extraction.ts
|
|
900
|
+
var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
|
|
901
|
+
function extractHeadSha(comment) {
|
|
902
|
+
const m = comment.match(HEAD_SHA_PATTERN);
|
|
903
|
+
return m ? m[1].toLowerCase() : null;
|
|
904
|
+
}
|
|
796
905
|
function extractVerdict(comments, valuePattern, missingLabel) {
|
|
797
906
|
const clearHits = comments.filter((c) => valuePattern.test(c));
|
|
798
907
|
if (clearHits.length > 0) {
|
|
799
908
|
const latest = clearHits[clearHits.length - 1];
|
|
800
909
|
const m = latest.match(valuePattern);
|
|
801
|
-
return {
|
|
910
|
+
return {
|
|
911
|
+
value: m[1].toUpperCase().replace(/[_-]/g, " "),
|
|
912
|
+
headSha: extractHeadSha(latest),
|
|
913
|
+
danglingNote: null
|
|
914
|
+
};
|
|
802
915
|
}
|
|
803
916
|
return {
|
|
804
917
|
value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
|
|
918
|
+
headSha: null,
|
|
805
919
|
danglingNote: `no ${missingLabel} verdict comment found on this PR`
|
|
806
920
|
};
|
|
807
921
|
}
|
|
@@ -1536,6 +1650,14 @@ function starterDocOwners() {
|
|
|
1536
1650
|
# This file absent, OR no glob matches any changed code file → silent no-op.
|
|
1537
1651
|
# The gate has no opinion until you teach it one.
|
|
1538
1652
|
#
|
|
1653
|
+
# \`vinaya doctor\` (never \`vinaya check\`) separately reports — it does not
|
|
1654
|
+
# fail the check — any binding whose code glob matches zero tracked files
|
|
1655
|
+
# anywhere in the repo, or whose in-repo pointer doesn't exist on disk. This
|
|
1656
|
+
# catches what the dormancy rule above structurally cannot: a glob matching
|
|
1657
|
+
# nothing trivially satisfies "no changed file matched" on every diff,
|
|
1658
|
+
# forever, so a binding naming deleted or renamed code reads as healthy
|
|
1659
|
+
# indefinitely unless something walks the whole repo, not just one diff.
|
|
1660
|
+
#
|
|
1539
1661
|
# ── No-doc allow-list ───────────────────────────────────────────────────────
|
|
1540
1662
|
# Surfaces that legitimately need no bound doc are listed below. Format:
|
|
1541
1663
|
# # no-doc: <glob> — <reason>
|
|
@@ -1668,11 +1790,12 @@ function buildInitProductOps(name) {
|
|
|
1668
1790
|
}
|
|
1669
1791
|
|
|
1670
1792
|
// src/lib/ops.ts
|
|
1671
|
-
import { execFileSync as
|
|
1793
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
1672
1794
|
import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1673
1795
|
import { dirname as dirname3, isAbsolute, join as join4, resolve, sep } from "node:path";
|
|
1674
1796
|
|
|
1675
1797
|
// src/lib/config.ts
|
|
1798
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1676
1799
|
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
1677
1800
|
import { homedir } from "node:os";
|
|
1678
1801
|
import { dirname as dirname2, join as join3 } from "node:path";
|
|
@@ -1800,7 +1923,8 @@ var VinayaConfigSchema = z.object({
|
|
|
1800
1923
|
briefSchema: BriefSchemaSchema.optional(),
|
|
1801
1924
|
managed: ManagedManifestSchema.optional(),
|
|
1802
1925
|
principals: z.array(z.string()).min(1).optional(),
|
|
1803
|
-
ci: z.object({ setup: z.string().min(1) }).optional()
|
|
1926
|
+
ci: z.object({ setup: z.string().min(1) }).optional(),
|
|
1927
|
+
blastRadius: z.object({ extraDomains: z.array(z.string()).optional() }).optional()
|
|
1804
1928
|
});
|
|
1805
1929
|
var GLOBAL_VINAYA_HOME = join3(homedir(), ".vinaya");
|
|
1806
1930
|
var GLOBAL_CONFIG_PATH = join3(GLOBAL_VINAYA_HOME, "config.json");
|
|
@@ -1847,6 +1971,75 @@ function stripGlobalOnlyKeys(config, path) {
|
|
|
1847
1971
|
}
|
|
1848
1972
|
return result;
|
|
1849
1973
|
}
|
|
1974
|
+
function resolvePrincipalAllowlist(config) {
|
|
1975
|
+
return config?.principals ?? PRINCIPAL_ALLOWLIST;
|
|
1976
|
+
}
|
|
1977
|
+
function trustAnchorRepo() {
|
|
1978
|
+
const wellFormed = (slug) => /^[^/\s]+\/[^/\s]+$/.test(slug) ? slug : null;
|
|
1979
|
+
const fromRunner = process.env.GITHUB_REPOSITORY?.trim();
|
|
1980
|
+
if (fromRunner) {
|
|
1981
|
+
const validated = wellFormed(fromRunner);
|
|
1982
|
+
if (validated)
|
|
1983
|
+
return validated;
|
|
1984
|
+
}
|
|
1985
|
+
try {
|
|
1986
|
+
const url = execFileSync2("git", ["remote", "get-url", "origin"], {
|
|
1987
|
+
encoding: "utf-8",
|
|
1988
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1989
|
+
}).trim();
|
|
1990
|
+
const m = url.match(/github\.com[:/]([^/]+)\/(.+?)(?:\.git)?\/?$/);
|
|
1991
|
+
return m?.[1] && m[2] ? wellFormed(`${m[1]}/${m[2]}`) : null;
|
|
1992
|
+
} catch {
|
|
1993
|
+
return null;
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
function ghFetchTrustAnchorConfig() {
|
|
1997
|
+
const repo = trustAnchorRepo();
|
|
1998
|
+
if (!repo)
|
|
1999
|
+
throw new Error("could not resolve repo identity for the trust-anchor read");
|
|
2000
|
+
return execFileSync2("gh", ["api", `repos/${repo}/contents/${LOCAL_CONFIG_FILENAME}`, "--jq", ".content"], {
|
|
2001
|
+
encoding: "utf-8",
|
|
2002
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
function firstLine(err) {
|
|
2006
|
+
return err?.message?.split(`
|
|
2007
|
+
`)[0] ?? "unknown error";
|
|
2008
|
+
}
|
|
2009
|
+
function isMissingFileError(err) {
|
|
2010
|
+
const stderr = err?.stderr;
|
|
2011
|
+
const haystack = [
|
|
2012
|
+
err?.message ?? "",
|
|
2013
|
+
typeof stderr === "string" ? stderr : stderr?.toString() ?? ""
|
|
2014
|
+
].join(`
|
|
2015
|
+
`);
|
|
2016
|
+
return /\b404\b|not found/i.test(haystack);
|
|
2017
|
+
}
|
|
2018
|
+
function loadTrustAnchorConfig(fetcher = ghFetchTrustAnchorConfig) {
|
|
2019
|
+
const warn = (why) => process.stdout.write(`⚠ could not read \`principals\` from the default branch (${why}) — falling back to vinaya's built-in principal allowlist.
|
|
2020
|
+
`);
|
|
2021
|
+
let base64;
|
|
2022
|
+
try {
|
|
2023
|
+
base64 = fetcher().trim();
|
|
2024
|
+
} catch (err) {
|
|
2025
|
+
if (!isMissingFileError(err))
|
|
2026
|
+
warn(firstLine(err));
|
|
2027
|
+
return null;
|
|
2028
|
+
}
|
|
2029
|
+
if (!base64)
|
|
2030
|
+
return null;
|
|
2031
|
+
try {
|
|
2032
|
+
const raw = Buffer.from(base64, "base64").toString("utf-8");
|
|
2033
|
+
const parsed = VinayaConfigSchema.safeParse(JSON.parse(raw));
|
|
2034
|
+
if (parsed.success)
|
|
2035
|
+
return parsed.data;
|
|
2036
|
+
warn("it did not satisfy the config schema");
|
|
2037
|
+
return null;
|
|
2038
|
+
} catch {
|
|
2039
|
+
warn("it was not readable as JSON");
|
|
2040
|
+
return null;
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
1850
2043
|
function readRepoCiSetup(repoRoot) {
|
|
1851
2044
|
const p = join3(repoRoot, "vinaya.config.json");
|
|
1852
2045
|
if (!existsSync2(p))
|
|
@@ -1897,7 +2090,7 @@ function resolveManagedBlockPath(repoRoot, opPath) {
|
|
|
1897
2090
|
if (!opPath.startsWith(".git/"))
|
|
1898
2091
|
return join4(repoRoot, opPath);
|
|
1899
2092
|
try {
|
|
1900
|
-
const commonDir =
|
|
2093
|
+
const commonDir = execFileSync3("git", ["-C", repoRoot, "rev-parse", "--git-common-dir"], {
|
|
1901
2094
|
encoding: "utf8"
|
|
1902
2095
|
}).trim();
|
|
1903
2096
|
const gitDir = isAbsolute(commonDir) ? commonDir : join4(repoRoot, commonDir);
|
|
@@ -2463,7 +2656,7 @@ function realDeps() {
|
|
|
2463
2656
|
return { detectRepo: detectGitRepo };
|
|
2464
2657
|
}
|
|
2465
2658
|
function sh(args, input) {
|
|
2466
|
-
return
|
|
2659
|
+
return execFileSync4(args[0], args.slice(1), { encoding: "utf8", input }).trim();
|
|
2467
2660
|
}
|
|
2468
2661
|
function shJson(args) {
|
|
2469
2662
|
return JSON.parse(sh(args));
|
|
@@ -2639,7 +2832,7 @@ async function archiveTrancheCommand(args) {
|
|
|
2639
2832
|
}
|
|
2640
2833
|
|
|
2641
2834
|
// src/commands/audit.ts
|
|
2642
|
-
import { execFileSync as
|
|
2835
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
2643
2836
|
|
|
2644
2837
|
// src/lib/envelope.ts
|
|
2645
2838
|
var ENVELOPE_SCHEMA_VERSION = 1;
|
|
@@ -2667,7 +2860,7 @@ var DIRECT_PUSH_MARKER = `<!-- ${DIRECT_PUSH_LABEL} -->`;
|
|
|
2667
2860
|
var DIRECT_PUSH_LABEL_DESCRIPTION = "A commit landed on main with no associated merged PR (ring-2 detection)";
|
|
2668
2861
|
function shSoft(args, input) {
|
|
2669
2862
|
try {
|
|
2670
|
-
return
|
|
2863
|
+
return execFileSync5(args[0], args.slice(1), {
|
|
2671
2864
|
encoding: "utf8",
|
|
2672
2865
|
input,
|
|
2673
2866
|
timeout: COMMAND_TIMEOUT_MS
|
|
@@ -2691,7 +2884,7 @@ function shSoftJson(args) {
|
|
|
2691
2884
|
}
|
|
2692
2885
|
}
|
|
2693
2886
|
function sh2(args, input) {
|
|
2694
|
-
return
|
|
2887
|
+
return execFileSync5(args[0], args.slice(1), { encoding: "utf8", input }).trim();
|
|
2695
2888
|
}
|
|
2696
2889
|
function listTaskBranches() {
|
|
2697
2890
|
const out = shSoft(["git", "ls-remote", "--heads", "origin", "task/*"]);
|
|
@@ -2941,7 +3134,7 @@ async function auditCommand(args) {
|
|
|
2941
3134
|
}
|
|
2942
3135
|
|
|
2943
3136
|
// src/commands/check.ts
|
|
2944
|
-
import { execFileSync as
|
|
3137
|
+
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
2945
3138
|
|
|
2946
3139
|
// src/checks/contract.ts
|
|
2947
3140
|
var CHECK_SCHEMA_VERSION = 1;
|
|
@@ -3478,7 +3671,7 @@ async function runChecks(specs, opts) {
|
|
|
3478
3671
|
// src/commands/check.ts
|
|
3479
3672
|
function git(args) {
|
|
3480
3673
|
try {
|
|
3481
|
-
return
|
|
3674
|
+
return execFileSync6("git", args, { encoding: "utf8" }).trim();
|
|
3482
3675
|
} catch {
|
|
3483
3676
|
return "";
|
|
3484
3677
|
}
|
|
@@ -3611,11 +3804,13 @@ async function checkCommand(args) {
|
|
|
3611
3804
|
process.stdout.write(`${renderPlanTable(result)}
|
|
3612
3805
|
`);
|
|
3613
3806
|
}
|
|
3614
|
-
process.
|
|
3807
|
+
process.exitCode = result.failures.length > 0 ? 1 : 0;
|
|
3808
|
+
return;
|
|
3615
3809
|
}
|
|
3616
3810
|
if (!allRequested && !requestedName) {
|
|
3617
3811
|
console.error("Usage: vinaya check <name> | --all | --plan [--json] [--diff-only] [--local] [--parallel[=n]]");
|
|
3618
|
-
process.
|
|
3812
|
+
process.exitCode = 2;
|
|
3813
|
+
return;
|
|
3619
3814
|
}
|
|
3620
3815
|
if (refusals.length > 0) {
|
|
3621
3816
|
const outcome = {
|
|
@@ -3636,7 +3831,8 @@ async function checkCommand(args) {
|
|
|
3636
3831
|
process.stdout.write(` ${e.severity}: ${e.message}
|
|
3637
3832
|
`);
|
|
3638
3833
|
}
|
|
3639
|
-
process.
|
|
3834
|
+
process.exitCode = 1;
|
|
3835
|
+
return;
|
|
3640
3836
|
}
|
|
3641
3837
|
const allSpecs = result.resolved.map((entry2) => entry2.spec);
|
|
3642
3838
|
const notices = substitutionNotices(result.resolved);
|
|
@@ -3650,7 +3846,8 @@ async function checkCommand(args) {
|
|
|
3650
3846
|
const specsToRun = allRequested ? allSpecs.filter(runsUnderAll) : allSpecs.filter((s) => s.name === requestedName);
|
|
3651
3847
|
if (!allRequested && specsToRun.length === 0) {
|
|
3652
3848
|
console.error(`Unknown check: ${requestedName}`);
|
|
3653
|
-
process.
|
|
3849
|
+
process.exitCode = 2;
|
|
3850
|
+
return;
|
|
3654
3851
|
}
|
|
3655
3852
|
const changed = diffOnly ? changedFiles() : null;
|
|
3656
3853
|
const outcomes = specsToRun.length > 0 ? await runChecks(specsToRun, {
|
|
@@ -3677,18 +3874,18 @@ async function checkCommand(args) {
|
|
|
3677
3874
|
}
|
|
3678
3875
|
}
|
|
3679
3876
|
const failed = outcomes.some((o) => o.status === "fail" || o.status === "error" || o.status === "timeout");
|
|
3680
|
-
process.
|
|
3877
|
+
process.exitCode = failed ? 1 : 0;
|
|
3681
3878
|
}
|
|
3682
3879
|
|
|
3683
3880
|
// src/commands/demo.ts
|
|
3684
|
-
import { execFileSync as
|
|
3881
|
+
import { execFileSync as execFileSync7, spawnSync } from "node:child_process";
|
|
3685
3882
|
import { existsSync as existsSync6, readFileSync as readFileSync5, rmSync as rmSync2, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3686
3883
|
import { isAbsolute as isAbsolute2, join as join7 } from "node:path";
|
|
3687
3884
|
var DEMO_BRANCH_PREFIX = "vinaya/demo-break-";
|
|
3688
3885
|
var FIXTURE_PATH = ".vinaya-demo-brief.md";
|
|
3689
3886
|
var STATE_FILENAME = "vinaya-demo-state.json";
|
|
3690
3887
|
function gitCapture(repoRoot, args) {
|
|
3691
|
-
return
|
|
3888
|
+
return execFileSync7("git", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
3692
3889
|
}
|
|
3693
3890
|
function currentBranchName(repoRoot) {
|
|
3694
3891
|
try {
|
|
@@ -3699,7 +3896,7 @@ function currentBranchName(repoRoot) {
|
|
|
3699
3896
|
}
|
|
3700
3897
|
function branchExists(repoRoot, name) {
|
|
3701
3898
|
try {
|
|
3702
|
-
|
|
3899
|
+
execFileSync7("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${name}`], {
|
|
3703
3900
|
cwd: repoRoot,
|
|
3704
3901
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3705
3902
|
});
|
|
@@ -3901,7 +4098,8 @@ async function demoBreakCommand(args) {
|
|
|
3901
4098
|
}
|
|
3902
4099
|
|
|
3903
4100
|
// src/commands/doctor.ts
|
|
3904
|
-
import {
|
|
4101
|
+
import { execFileSync as execFileSync8 } from "node:child_process";
|
|
4102
|
+
import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync2 } from "node:fs";
|
|
3905
4103
|
import { join as join9 } from "node:path";
|
|
3906
4104
|
|
|
3907
4105
|
// src/lib/self-host.ts
|
|
@@ -4182,6 +4380,90 @@ function diagnoseCustomChecks(repoRoot, config) {
|
|
|
4182
4380
|
}
|
|
4183
4381
|
return findings;
|
|
4184
4382
|
}
|
|
4383
|
+
function listTrackedFiles(repoRoot) {
|
|
4384
|
+
try {
|
|
4385
|
+
return execFileSync8("git", ["ls-files"], { cwd: repoRoot, encoding: "utf-8" }).split(`
|
|
4386
|
+
`).map((s) => s.trim()).filter(Boolean);
|
|
4387
|
+
} catch {
|
|
4388
|
+
return [];
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
function diagnoseDocOwnersHealth(repoRoot) {
|
|
4392
|
+
const path = join9(repoRoot, DOC_OWNERS_PATH);
|
|
4393
|
+
const content = existsSync8(path) ? readFileSync8(path, "utf-8") : null;
|
|
4394
|
+
const state = classifyDocOwnersManifest(content);
|
|
4395
|
+
if (state === "absent" || state === "empty")
|
|
4396
|
+
return [];
|
|
4397
|
+
const { bindings } = parseDocOwners(content);
|
|
4398
|
+
if (bindings.length === 0)
|
|
4399
|
+
return [];
|
|
4400
|
+
const codeFiles = listTrackedFiles(repoRoot).filter(isCodeFile);
|
|
4401
|
+
const findings = [];
|
|
4402
|
+
for (const b of bindings) {
|
|
4403
|
+
let flagged = false;
|
|
4404
|
+
const re = globToRegex(b.glob);
|
|
4405
|
+
if (!codeFiles.some((f) => re.test(f))) {
|
|
4406
|
+
flagged = true;
|
|
4407
|
+
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."));
|
|
4408
|
+
}
|
|
4409
|
+
if (!isUrlPointer(b.pointer)) {
|
|
4410
|
+
const pointerPath = join9(repoRoot, pointerToPath(b.pointer));
|
|
4411
|
+
if (!existsSync8(pointerPath)) {
|
|
4412
|
+
flagged = true;
|
|
4413
|
+
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."));
|
|
4414
|
+
}
|
|
4415
|
+
}
|
|
4416
|
+
if (!flagged) {
|
|
4417
|
+
findings.push(ok("doc-owners", `${DOC_OWNERS_PATH}:${b.lineNum} — '${b.glob}' → ${b.pointer} is live.`));
|
|
4418
|
+
}
|
|
4419
|
+
}
|
|
4420
|
+
return findings;
|
|
4421
|
+
}
|
|
4422
|
+
var LEGACY_AEG_PACKAGES_PATH = ".aeg/packages";
|
|
4423
|
+
function readWorkspacesForDoctor(repoRoot) {
|
|
4424
|
+
const fromPackageJson = () => {
|
|
4425
|
+
try {
|
|
4426
|
+
const pkg = JSON.parse(readFileSync8(join9(repoRoot, "package.json"), "utf-8"));
|
|
4427
|
+
return Array.isArray(pkg.workspaces) ? pkg.workspaces.filter((w) => typeof w === "string") : [];
|
|
4428
|
+
} catch {
|
|
4429
|
+
return [];
|
|
4430
|
+
}
|
|
4431
|
+
};
|
|
4432
|
+
const fromPnpmWorkspaceYaml = () => {
|
|
4433
|
+
try {
|
|
4434
|
+
return parsePnpmWorkspaceYaml(readFileSync8(join9(repoRoot, "pnpm-workspace.yaml"), "utf-8"));
|
|
4435
|
+
} catch {
|
|
4436
|
+
return [];
|
|
4437
|
+
}
|
|
4438
|
+
};
|
|
4439
|
+
return [...fromPackageJson(), ...fromPnpmWorkspaceYaml()];
|
|
4440
|
+
}
|
|
4441
|
+
function listChildDirsForDoctor(dir, repoRoot) {
|
|
4442
|
+
try {
|
|
4443
|
+
return readdirSync3(join9(repoRoot, dir), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
4444
|
+
} catch {
|
|
4445
|
+
return [];
|
|
4446
|
+
}
|
|
4447
|
+
}
|
|
4448
|
+
function diagnoseBlastRadiusDeprecation(repoRoot, config) {
|
|
4449
|
+
const derived = deriveWorkspacePackageDomains(readWorkspacesForDoctor(repoRoot), (dir) => listChildDirsForDoctor(dir, repoRoot));
|
|
4450
|
+
const defaults = deriveBuiltinCrossCuttingDefaults((p) => existsSync8(join9(repoRoot, p)));
|
|
4451
|
+
const covered = new Set([...derived, ...defaults]);
|
|
4452
|
+
const configExtra = new Set(config?.blastRadius?.extraDomains ?? []);
|
|
4453
|
+
const legacyPath = join9(repoRoot, LEGACY_AEG_PACKAGES_PATH);
|
|
4454
|
+
if (!existsSync8(legacyPath)) {
|
|
4455
|
+
return [
|
|
4456
|
+
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.`)
|
|
4457
|
+
];
|
|
4458
|
+
}
|
|
4459
|
+
const legacyEntries = readFileSync8(legacyPath, "utf-8").split(`
|
|
4460
|
+
`).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
4461
|
+
const uncovered = legacyEntries.filter((e) => !covered.has(e) && !configExtra.has(e));
|
|
4462
|
+
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(", ")}.`;
|
|
4463
|
+
return [
|
|
4464
|
+
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}`)
|
|
4465
|
+
];
|
|
4466
|
+
}
|
|
4185
4467
|
function diagnoseEnvDeclarations(repoRoot, config) {
|
|
4186
4468
|
const resolved = resolveChecks(coreCheckRegistry(), config?.checks).resolved;
|
|
4187
4469
|
const specs = resolved.map((entry2) => entry2.source === "config" ? { ...entry2.spec, run: join9(repoRoot, entry2.spec.run) } : entry2.spec);
|
|
@@ -4242,6 +4524,29 @@ async function diagnoseBranchProtection(deps, owner, repo) {
|
|
|
4242
4524
|
}
|
|
4243
4525
|
return info("branch-protection", "main branch protection could not be determined (no gh auth, no remote, or a permission gap).");
|
|
4244
4526
|
}
|
|
4527
|
+
var TEST_INVOCATION_SUBSTRINGS = ["npm test", "npm run test", "bun test", "bunx turbo test"];
|
|
4528
|
+
function diagnoseTestCi(repoRoot) {
|
|
4529
|
+
let pkg;
|
|
4530
|
+
try {
|
|
4531
|
+
pkg = JSON.parse(readFileSync8(join9(repoRoot, "package.json"), "utf-8"));
|
|
4532
|
+
} catch {
|
|
4533
|
+
return [];
|
|
4534
|
+
}
|
|
4535
|
+
const testScript = pkg?.scripts?.test;
|
|
4536
|
+
if (typeof testScript !== "string" || testScript.trim() === "")
|
|
4537
|
+
return [];
|
|
4538
|
+
const workflowsDir = join9(repoRoot, ".github/workflows");
|
|
4539
|
+
const files = existsSync8(workflowsDir) ? readdirSync3(workflowsDir).filter((name) => statSync2(join9(workflowsDir, name)).isFile()) : [];
|
|
4540
|
+
const invoked = files.some((name) => {
|
|
4541
|
+
const content = readFileSync8(join9(workflowsDir, name), "utf-8");
|
|
4542
|
+
return TEST_INVOCATION_SUBSTRINGS.some((s) => content.includes(s));
|
|
4543
|
+
});
|
|
4544
|
+
if (invoked)
|
|
4545
|
+
return [];
|
|
4546
|
+
return [
|
|
4547
|
+
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.")
|
|
4548
|
+
];
|
|
4549
|
+
}
|
|
4245
4550
|
function symbolFor(severity) {
|
|
4246
4551
|
switch (severity) {
|
|
4247
4552
|
case "ok":
|
|
@@ -4295,12 +4600,15 @@ async function runDoctor(args, deps) {
|
|
|
4295
4600
|
hasDrift = install.hasDrift;
|
|
4296
4601
|
findings.push(...await diagnoseHookRouting(repo.repoRoot, hookDir, deps.readHooksPath));
|
|
4297
4602
|
findings.push(...diagnoseCustomChecks(repo.repoRoot, configRead.config));
|
|
4603
|
+
findings.push(...diagnoseDocOwnersHealth(repo.repoRoot));
|
|
4298
4604
|
}
|
|
4605
|
+
findings.push(...diagnoseBlastRadiusDeprecation(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
|
|
4299
4606
|
findings.push(...diagnoseEnvDeclarations(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
|
|
4300
4607
|
findings.push(...diagnoseCheckClassification(configRead.kind === "ok" ? configRead.config : null));
|
|
4301
4608
|
findings.push(...diagnoseGlobalConfigChecks());
|
|
4302
4609
|
findings.push(...await diagnoseEnvironment(deps, hasDrift));
|
|
4303
4610
|
findings.push(await diagnoseBranchProtection(deps, repo.owner, repo.repo));
|
|
4611
|
+
findings.push(...diagnoseTestCi(repo.repoRoot));
|
|
4304
4612
|
const healthy = findings.every((f) => f.severity === "ok" || f.severity === "info");
|
|
4305
4613
|
if (jsonOutput) {
|
|
4306
4614
|
printJson({ healthy, findings });
|
|
@@ -4314,7 +4622,7 @@ async function doctorCommand(args) {
|
|
|
4314
4622
|
}
|
|
4315
4623
|
|
|
4316
4624
|
// src/commands/doctrine.ts
|
|
4317
|
-
import { existsSync as existsSync9 } from "node:fs";
|
|
4625
|
+
import { existsSync as existsSync9, readdirSync as readdirSync4 } from "node:fs";
|
|
4318
4626
|
import { dirname as dirname4, join as join10, sep as sep3 } from "node:path";
|
|
4319
4627
|
var ENTRY_SEGMENTS = ["skills", "aeg", "SKILL.md"];
|
|
4320
4628
|
function resolveDoctrineRoot(pkg = packageRoot(import.meta.url)) {
|
|
@@ -4327,6 +4635,12 @@ function resolveDoctrineRoot(pkg = packageRoot(import.meta.url)) {
|
|
|
4327
4635
|
}
|
|
4328
4636
|
return null;
|
|
4329
4637
|
}
|
|
4638
|
+
function listRoleNames(root) {
|
|
4639
|
+
const rolesDir = join10(root, "roles");
|
|
4640
|
+
if (!existsSync9(rolesDir))
|
|
4641
|
+
return [];
|
|
4642
|
+
return readdirSync4(rolesDir).filter((name) => name.endsWith(".md")).map((name) => name.slice(0, -".md".length)).sort();
|
|
4643
|
+
}
|
|
4330
4644
|
function doctrineCommand(args) {
|
|
4331
4645
|
const root = resolveDoctrineRoot();
|
|
4332
4646
|
if (root === null) {
|
|
@@ -4334,6 +4648,24 @@ function doctrineCommand(args) {
|
|
|
4334
4648
|
`);
|
|
4335
4649
|
process.exit(1);
|
|
4336
4650
|
}
|
|
4651
|
+
const roleFlagIndex = args.indexOf("--role");
|
|
4652
|
+
if (roleFlagIndex !== -1) {
|
|
4653
|
+
const roleName = args[roleFlagIndex + 1];
|
|
4654
|
+
const validRoleNames = listRoleNames(root);
|
|
4655
|
+
if (roleName === undefined || roleName.startsWith("--") || !validRoleNames.includes(roleName)) {
|
|
4656
|
+
process.stderr.write(`vinaya doctrine --role: '${roleName ?? ""}' is not a known role. ` + `Valid role names: ${validRoleNames.join(", ")}
|
|
4657
|
+
`);
|
|
4658
|
+
process.exit(1);
|
|
4659
|
+
}
|
|
4660
|
+
const entry3 = join10(root, "roles", `${roleName}.md`);
|
|
4661
|
+
if (args.includes("--json")) {
|
|
4662
|
+
printJson({ root, entry: entry3 });
|
|
4663
|
+
return;
|
|
4664
|
+
}
|
|
4665
|
+
process.stdout.write(`${entry3}
|
|
4666
|
+
`);
|
|
4667
|
+
return;
|
|
4668
|
+
}
|
|
4337
4669
|
const entry2 = join10(root, ...ENTRY_SEGMENTS);
|
|
4338
4670
|
if (args.includes("--json")) {
|
|
4339
4671
|
printJson({ root, entry: entry2 });
|
|
@@ -4750,7 +5082,7 @@ async function initProductCommand(args) {
|
|
|
4750
5082
|
}
|
|
4751
5083
|
|
|
4752
5084
|
// src/commands/issue.ts
|
|
4753
|
-
import { execFileSync as
|
|
5085
|
+
import { execFileSync as execFileSync9 } from "node:child_process";
|
|
4754
5086
|
|
|
4755
5087
|
// src/lib/forge-write.ts
|
|
4756
5088
|
import { mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
|
|
@@ -4973,7 +5305,7 @@ function locateBodyOrRefuse(ghArgs, retryCommand) {
|
|
|
4973
5305
|
function fetchForgeLabels(issueRef) {
|
|
4974
5306
|
let out;
|
|
4975
5307
|
try {
|
|
4976
|
-
out =
|
|
5308
|
+
out = execFileSync9("gh", ["issue", "view", issueRef, "--json", "labels"], {
|
|
4977
5309
|
encoding: "utf8",
|
|
4978
5310
|
stdio: ["ignore", "pipe", "pipe"]
|
|
4979
5311
|
});
|
|
@@ -5001,7 +5333,7 @@ function reportPass(json, command) {
|
|
|
5001
5333
|
function runGhWrite(ghCmd, ghArgs, bodyResult, json) {
|
|
5002
5334
|
const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
|
|
5003
5335
|
try {
|
|
5004
|
-
const out =
|
|
5336
|
+
const out = execFileSync9("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
|
|
5005
5337
|
const url = out.trim();
|
|
5006
5338
|
if (json)
|
|
5007
5339
|
printJson({ validated: true, written: true, url });
|
|
@@ -5115,12 +5447,12 @@ ${registration}
|
|
|
5115
5447
|
}
|
|
5116
5448
|
|
|
5117
5449
|
// src/commands/pr.ts
|
|
5118
|
-
import { execFileSync as
|
|
5450
|
+
import { execFileSync as execFileSync10 } from "node:child_process";
|
|
5119
5451
|
var RETRY_CREATE2 = "vinaya pr create --validate-only …";
|
|
5120
5452
|
var RETRY_EDIT2 = "vinaya pr edit <n> --validate-only …";
|
|
5121
5453
|
function git2(args) {
|
|
5122
5454
|
try {
|
|
5123
|
-
return
|
|
5455
|
+
return execFileSync10("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
5124
5456
|
} catch {
|
|
5125
5457
|
return "";
|
|
5126
5458
|
}
|
|
@@ -5154,7 +5486,7 @@ function reportPass2(json, command) {
|
|
|
5154
5486
|
function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
|
|
5155
5487
|
const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
|
|
5156
5488
|
try {
|
|
5157
|
-
const out =
|
|
5489
|
+
const out = execFileSync10("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
|
|
5158
5490
|
const url = out.trim();
|
|
5159
5491
|
if (json)
|
|
5160
5492
|
printJson({ validated: true, written: true, url });
|
|
@@ -5168,7 +5500,7 @@ function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
|
|
|
5168
5500
|
function fetchPrForgeContext(prRef) {
|
|
5169
5501
|
let viewOut;
|
|
5170
5502
|
try {
|
|
5171
|
-
viewOut =
|
|
5503
|
+
viewOut = execFileSync10("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
|
|
5172
5504
|
encoding: "utf8",
|
|
5173
5505
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5174
5506
|
});
|
|
@@ -5270,14 +5602,14 @@ function prEditCommand(args) {
|
|
|
5270
5602
|
}
|
|
5271
5603
|
|
|
5272
5604
|
// src/commands/pr-report.ts
|
|
5273
|
-
import { execFileSync as
|
|
5605
|
+
import { execFileSync as execFileSync11, spawnSync as spawnSync2 } from "node:child_process";
|
|
5274
5606
|
import { existsSync as existsSync14, readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "node:fs";
|
|
5275
5607
|
import { join as join16 } from "node:path";
|
|
5276
5608
|
var EVIDENCE_START = "<!-- AEG:EVIDENCE:START -->";
|
|
5277
5609
|
var EVIDENCE_END = "<!-- AEG:EVIDENCE:END -->";
|
|
5278
5610
|
function git3(args) {
|
|
5279
5611
|
try {
|
|
5280
|
-
return
|
|
5612
|
+
return execFileSync11("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
5281
5613
|
} catch {
|
|
5282
5614
|
return "";
|
|
5283
5615
|
}
|
|
@@ -5295,7 +5627,7 @@ class GitCommandError extends Error {
|
|
|
5295
5627
|
}
|
|
5296
5628
|
function gitStrict(args) {
|
|
5297
5629
|
try {
|
|
5298
|
-
return
|
|
5630
|
+
return execFileSync11("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
5299
5631
|
} catch (err) {
|
|
5300
5632
|
const stderr = err.stderr;
|
|
5301
5633
|
throw new GitCommandError(args, String(stderr ?? err.message).trim() || "non-zero exit");
|
|
@@ -5449,7 +5781,7 @@ async function prReportCommand(args) {
|
|
|
5449
5781
|
}
|
|
5450
5782
|
|
|
5451
5783
|
// src/commands/quickstart.ts
|
|
5452
|
-
import { execFileSync as
|
|
5784
|
+
import { execFileSync as execFileSync12 } from "node:child_process";
|
|
5453
5785
|
import { existsSync as existsSync16, readFileSync as readFileSync16 } from "node:fs";
|
|
5454
5786
|
import { join as join18 } from "node:path";
|
|
5455
5787
|
|
|
@@ -5645,7 +5977,7 @@ function realDeps6() {
|
|
|
5645
5977
|
};
|
|
5646
5978
|
}
|
|
5647
5979
|
function execGit(repoRoot, args) {
|
|
5648
|
-
return
|
|
5980
|
+
return execFileSync12("git", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
5649
5981
|
}
|
|
5650
5982
|
function errorDetail(err) {
|
|
5651
5983
|
const stderr = err.stderr;
|
|
@@ -5761,11 +6093,331 @@ async function quickstartCommand(args) {
|
|
|
5761
6093
|
process.exit(await runQuickstart(args, realDeps6()));
|
|
5762
6094
|
}
|
|
5763
6095
|
|
|
6096
|
+
// src/commands/review-post.ts
|
|
6097
|
+
import { execFileSync as execFileSync13 } from "node:child_process";
|
|
6098
|
+
import { readFileSync as readFileSync17, rmSync as rmSync4, writeFileSync as writeFileSync10 } from "node:fs";
|
|
6099
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
6100
|
+
import { join as join19 } from "node:path";
|
|
6101
|
+
class FindingsParseError extends Error {
|
|
6102
|
+
}
|
|
6103
|
+
var CODE_REVIEW_SEVERITIES = ["BLOCKER", "MAJOR", "MINOR"];
|
|
6104
|
+
var SECURITY_SEVERITIES = ["CRITICAL", "HIGH", "MEDIUM", "LOW"];
|
|
6105
|
+
function parseFindingsFile(content, allowedSeverities) {
|
|
6106
|
+
const lines = content.split(`
|
|
6107
|
+
`).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
6108
|
+
return lines.map((line, idx) => {
|
|
6109
|
+
const parts = line.split("|");
|
|
6110
|
+
if (parts.length !== 3) {
|
|
6111
|
+
throw new FindingsParseError(`findings file line ${idx + 1}: expected exactly 3 \`|\`-delimited fields (SEVERITY|file:line|description), found ${parts.length}: ${line}`);
|
|
6112
|
+
}
|
|
6113
|
+
const severity = parts[0].trim().toUpperCase();
|
|
6114
|
+
const location = parts[1].trim();
|
|
6115
|
+
const description = parts[2].trim();
|
|
6116
|
+
if (!allowedSeverities.includes(severity)) {
|
|
6117
|
+
throw new FindingsParseError(`findings file line ${idx + 1}: severity "${parts[0]}" is not one of ${allowedSeverities.join("|")}: ${line}`);
|
|
6118
|
+
}
|
|
6119
|
+
if (!location || !description) {
|
|
6120
|
+
throw new FindingsParseError(`findings file line ${idx + 1}: file:line and description must both be non-empty: ${line}`);
|
|
6121
|
+
}
|
|
6122
|
+
return { severity, location, description };
|
|
6123
|
+
});
|
|
6124
|
+
}
|
|
6125
|
+
function sortBySeverity(findings, order) {
|
|
6126
|
+
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);
|
|
6127
|
+
}
|
|
6128
|
+
function renderFindingsSection(findings) {
|
|
6129
|
+
if (findings.length === 0)
|
|
6130
|
+
return "None.";
|
|
6131
|
+
return findings.map((f, i) => `${i + 1}. [${f.severity}] ${f.location} — ${f.description}`).join(`
|
|
6132
|
+
`);
|
|
6133
|
+
}
|
|
6134
|
+
function renderTokensLine(role, roleLabel, input) {
|
|
6135
|
+
return `Tokens: ${input.taskId}: ${role} — ${roleLabel} — ${input.model} — ${input.tokensIn}/${input.tokensOut}/${input.cost}`;
|
|
6136
|
+
}
|
|
6137
|
+
var CODE_REVIEW_VERDICT_TEXT = {
|
|
6138
|
+
APPROVE: "APPROVE",
|
|
6139
|
+
REQUEST_CHANGES: "REQUEST CHANGES"
|
|
6140
|
+
};
|
|
6141
|
+
function renderCodeReviewComment(input) {
|
|
6142
|
+
const sorted = sortBySeverity(input.findings, CODE_REVIEW_SEVERITIES);
|
|
6143
|
+
return [
|
|
6144
|
+
`VERDICT: ${CODE_REVIEW_VERDICT_TEXT[input.verdict]}`,
|
|
6145
|
+
"",
|
|
6146
|
+
`Judged head: ${input.headSha}`,
|
|
6147
|
+
"",
|
|
6148
|
+
`BRIEF CONFORMANCE: ${input.briefConformance}`,
|
|
6149
|
+
`SPEC CONFORMANCE: ${input.specConformance}`,
|
|
6150
|
+
"",
|
|
6151
|
+
"FINDINGS (ordered by severity):",
|
|
6152
|
+
renderFindingsSection(sorted),
|
|
6153
|
+
"",
|
|
6154
|
+
`SCOPE: ${input.scope}`,
|
|
6155
|
+
`TESTS: ${input.tests}`,
|
|
6156
|
+
`DOCS: ${input.docs}`,
|
|
6157
|
+
"",
|
|
6158
|
+
renderTokensLine("review", "Reviewer", input)
|
|
6159
|
+
].join(`
|
|
6160
|
+
`);
|
|
6161
|
+
}
|
|
6162
|
+
function isNoneFoundClaim(value) {
|
|
6163
|
+
return value.trim().toLowerCase().replace(/[-_]+/g, " ").replace(/\s+/g, " ") === "none found";
|
|
6164
|
+
}
|
|
6165
|
+
function renderSecurityComment(input) {
|
|
6166
|
+
const sorted = sortBySeverity(input.findings, SECURITY_SEVERITIES);
|
|
6167
|
+
const lines = [
|
|
6168
|
+
`VERDICT: ${input.verdict}`,
|
|
6169
|
+
"",
|
|
6170
|
+
`Judged head: ${input.headSha}`,
|
|
6171
|
+
"",
|
|
6172
|
+
"FINDINGS (ordered by severity):",
|
|
6173
|
+
renderFindingsSection(sorted),
|
|
6174
|
+
"",
|
|
6175
|
+
`CONFIG SCAN: ${input.configScan}`,
|
|
6176
|
+
""
|
|
6177
|
+
];
|
|
6178
|
+
if (input.secretsEvidence !== null) {
|
|
6179
|
+
lines.push("```", input.secretsEvidence, "```", "");
|
|
6180
|
+
}
|
|
6181
|
+
lines.push(`SECRETS: ${input.secrets}`, "", renderTokensLine("security", "Security", input));
|
|
6182
|
+
return lines.join(`
|
|
6183
|
+
`);
|
|
6184
|
+
}
|
|
6185
|
+
function isBoundToHead(extraction, headSha) {
|
|
6186
|
+
if (!extraction.headSha)
|
|
6187
|
+
return false;
|
|
6188
|
+
return headSha.toLowerCase().startsWith(extraction.headSha.toLowerCase());
|
|
6189
|
+
}
|
|
6190
|
+
function checkExtraction(extraction, expectedValue, headSha) {
|
|
6191
|
+
if (extraction.danglingNote) {
|
|
6192
|
+
return {
|
|
6193
|
+
ok: false,
|
|
6194
|
+
reason: `no clean VERDICT was found on re-fetch (${extraction.danglingNote}) — the posted comment does not match the gate's line-anchored \`VERDICT:\` pattern.`
|
|
6195
|
+
};
|
|
6196
|
+
}
|
|
6197
|
+
if (extraction.value !== expectedValue) {
|
|
6198
|
+
return {
|
|
6199
|
+
ok: false,
|
|
6200
|
+
reason: `re-extraction found VERDICT "${extraction.value}", expected "${expectedValue}" — the posted comment's VERDICT line does not match what this command rendered.`
|
|
6201
|
+
};
|
|
6202
|
+
}
|
|
6203
|
+
if (!extraction.headSha) {
|
|
6204
|
+
return { ok: false, reason: "the winning VERDICT comment carries no `Judged head:` line on re-fetch." };
|
|
6205
|
+
}
|
|
6206
|
+
if (!isBoundToHead(extraction, headSha)) {
|
|
6207
|
+
return {
|
|
6208
|
+
ok: false,
|
|
6209
|
+
reason: `re-extraction found \`Judged head: ${extraction.headSha}\`, which does not cover the resolved head ${headSha}.`
|
|
6210
|
+
};
|
|
6211
|
+
}
|
|
6212
|
+
return { ok: true, reason: "clean" };
|
|
6213
|
+
}
|
|
6214
|
+
function principalBodies(comments, principalAllowlist) {
|
|
6215
|
+
return comments.filter((c) => isPrincipal(c.author, principalAllowlist)).map((c) => c.body);
|
|
6216
|
+
}
|
|
6217
|
+
function verifyPostedCodeReview(comments, verdict, headSha, principalAllowlist) {
|
|
6218
|
+
return checkExtraction(extractCodeReviewVerdict(principalBodies(comments, principalAllowlist)), CODE_REVIEW_VERDICT_TEXT[verdict], headSha);
|
|
6219
|
+
}
|
|
6220
|
+
function verifyPostedSecurity(comments, verdict, headSha, principalAllowlist) {
|
|
6221
|
+
return checkExtraction(extractSecurityReviewVerdict(principalBodies(comments, principalAllowlist)), verdict, headSha);
|
|
6222
|
+
}
|
|
6223
|
+
function parseFlags(args) {
|
|
6224
|
+
const map = new Map;
|
|
6225
|
+
for (let i = 0;i < args.length; i++) {
|
|
6226
|
+
const a = args[i];
|
|
6227
|
+
if (a.startsWith("--")) {
|
|
6228
|
+
const value = args[i + 1];
|
|
6229
|
+
if (value === undefined || value.startsWith("--")) {
|
|
6230
|
+
map.set(a, "");
|
|
6231
|
+
continue;
|
|
6232
|
+
}
|
|
6233
|
+
map.set(a, value);
|
|
6234
|
+
i++;
|
|
6235
|
+
}
|
|
6236
|
+
}
|
|
6237
|
+
return map;
|
|
6238
|
+
}
|
|
6239
|
+
function refuseCmd(message, recovery) {
|
|
6240
|
+
refuse([makeCheckError("review-post", message, recovery)]);
|
|
6241
|
+
}
|
|
6242
|
+
function requireFlag(flags2, name) {
|
|
6243
|
+
const v = flags2.get(name);
|
|
6244
|
+
if (v === undefined || v === "") {
|
|
6245
|
+
refuseCmd(`Missing required \`${name}\` flag.`, `Pass \`${name} <value>\`, then re-run \`vinaya review post ...\`.`);
|
|
6246
|
+
}
|
|
6247
|
+
return v;
|
|
6248
|
+
}
|
|
6249
|
+
function requireTokenField(flags2, name) {
|
|
6250
|
+
const v = requireFlag(flags2, name);
|
|
6251
|
+
if (v !== "-" && !/^\d+$/.test(v)) {
|
|
6252
|
+
refuseCmd(`\`${name} ${v}\` is neither a non-negative integer nor \`-\` (unknown).`, `Pass a whole number or \`-\` for ${name}.`);
|
|
6253
|
+
}
|
|
6254
|
+
return v;
|
|
6255
|
+
}
|
|
6256
|
+
function readFindingsFile(path, allowedSeverities) {
|
|
6257
|
+
if (!path)
|
|
6258
|
+
return [];
|
|
6259
|
+
let content;
|
|
6260
|
+
try {
|
|
6261
|
+
content = readFileSync17(path, "utf8");
|
|
6262
|
+
} catch {
|
|
6263
|
+
refuseCmd(`Could not read findings file at ${path}.`, "Check the path and re-run.");
|
|
6264
|
+
}
|
|
6265
|
+
try {
|
|
6266
|
+
return parseFindingsFile(content, allowedSeverities);
|
|
6267
|
+
} catch (err) {
|
|
6268
|
+
if (err instanceof FindingsParseError) {
|
|
6269
|
+
refuseCmd(err.message, "Fix the malformed line in the findings file, then re-run.");
|
|
6270
|
+
}
|
|
6271
|
+
throw err;
|
|
6272
|
+
}
|
|
6273
|
+
}
|
|
6274
|
+
function normalizeCodeReviewVerdict(raw) {
|
|
6275
|
+
const v = raw.trim().toUpperCase().replace(/[-\s]+/g, "_");
|
|
6276
|
+
if (v === "APPROVE")
|
|
6277
|
+
return "APPROVE";
|
|
6278
|
+
if (v === "REQUEST_CHANGES")
|
|
6279
|
+
return "REQUEST_CHANGES";
|
|
6280
|
+
refuseCmd(`\`--verdict ${raw}\` is not APPROVE or REQUEST_CHANGES.`, "Pass `--verdict APPROVE` or `--verdict REQUEST_CHANGES`.");
|
|
6281
|
+
}
|
|
6282
|
+
function normalizeSecurityVerdict(raw) {
|
|
6283
|
+
const v = raw.trim().toUpperCase();
|
|
6284
|
+
if (v === "PASS")
|
|
6285
|
+
return "PASS";
|
|
6286
|
+
if (v === "FAIL")
|
|
6287
|
+
return "FAIL";
|
|
6288
|
+
refuseCmd(`\`--verdict ${raw}\` is not PASS or FAIL.`, "Pass `--verdict PASS` or `--verdict FAIL`.");
|
|
6289
|
+
}
|
|
6290
|
+
function gh(args) {
|
|
6291
|
+
try {
|
|
6292
|
+
return execFileSync13("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
6293
|
+
} catch (err) {
|
|
6294
|
+
const stderr = err.stderr;
|
|
6295
|
+
throw new Error(String(stderr ?? err.message).trim() || "gh command failed");
|
|
6296
|
+
}
|
|
6297
|
+
}
|
|
6298
|
+
function resolveHeadSha(pr) {
|
|
6299
|
+
let out;
|
|
6300
|
+
try {
|
|
6301
|
+
out = gh(["pr", "view", pr, "--json", "headRefOid", "-q", ".headRefOid"]);
|
|
6302
|
+
} catch (err) {
|
|
6303
|
+
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.");
|
|
6304
|
+
}
|
|
6305
|
+
if (!out) {
|
|
6306
|
+
refuseCmd(`\`gh pr view ${pr} --json headRefOid\` returned no head sha.`, "Confirm PR exists, then re-run.");
|
|
6307
|
+
}
|
|
6308
|
+
return out;
|
|
6309
|
+
}
|
|
6310
|
+
function postComment(pr, body) {
|
|
6311
|
+
const tmp = join19(tmpdir2(), `vinaya-review-post-${process.pid}-${Date.now()}.md`);
|
|
6312
|
+
writeFileSync10(tmp, body);
|
|
6313
|
+
try {
|
|
6314
|
+
return gh(["pr", "comment", pr, "--body-file", tmp]).trim();
|
|
6315
|
+
} catch (err) {
|
|
6316
|
+
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.");
|
|
6317
|
+
} finally {
|
|
6318
|
+
rmSync4(tmp, { force: true });
|
|
6319
|
+
}
|
|
6320
|
+
}
|
|
6321
|
+
function fetchComments(pr) {
|
|
6322
|
+
const out = gh(["pr", "view", pr, "--json", "comments"]);
|
|
6323
|
+
const parsed = JSON.parse(out);
|
|
6324
|
+
return parsed.comments.map((c) => ({ body: c.body, author: c.author?.login ?? null }));
|
|
6325
|
+
}
|
|
6326
|
+
async function reviewPostCommand(args) {
|
|
6327
|
+
const json = args.includes("--json");
|
|
6328
|
+
const flags2 = parseFlags(args.filter((a) => a !== "--json"));
|
|
6329
|
+
const role = flags2.get("--role");
|
|
6330
|
+
if (role !== "code-reviewer" && role !== "security") {
|
|
6331
|
+
refuseCmd(`\`--role ${role ?? "(missing)"}\` is not \`code-reviewer\` or \`security\`.`, "Pass `--role code-reviewer` or `--role security`.");
|
|
6332
|
+
}
|
|
6333
|
+
const pr = requireFlag(flags2, "--pr");
|
|
6334
|
+
const taskId = requireFlag(flags2, "--task-id");
|
|
6335
|
+
const model = requireFlag(flags2, "--model");
|
|
6336
|
+
const tokensIn = requireTokenField(flags2, "--tokens-in");
|
|
6337
|
+
const tokensOut = requireTokenField(flags2, "--tokens-out");
|
|
6338
|
+
const cost = requireFlag(flags2, "--cost");
|
|
6339
|
+
const tokens = { taskId, model, tokensIn, tokensOut, cost };
|
|
6340
|
+
const headSha = resolveHeadSha(pr);
|
|
6341
|
+
const principalAllowlist = resolvePrincipalAllowlist(loadTrustAnchorConfig());
|
|
6342
|
+
let body;
|
|
6343
|
+
let verify;
|
|
6344
|
+
if (role === "code-reviewer") {
|
|
6345
|
+
const verdict = normalizeCodeReviewVerdict(requireFlag(flags2, "--verdict"));
|
|
6346
|
+
const findings = readFindingsFile(flags2.get("--findings-file"), CODE_REVIEW_SEVERITIES);
|
|
6347
|
+
if (findings.some((f) => f.severity === "BLOCKER") && verdict === "APPROVE") {
|
|
6348
|
+
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.");
|
|
6349
|
+
}
|
|
6350
|
+
const input = {
|
|
6351
|
+
...tokens,
|
|
6352
|
+
headSha,
|
|
6353
|
+
verdict,
|
|
6354
|
+
briefConformance: requireFlag(flags2, "--brief-conformance"),
|
|
6355
|
+
specConformance: requireFlag(flags2, "--spec-conformance"),
|
|
6356
|
+
findings,
|
|
6357
|
+
scope: requireFlag(flags2, "--scope"),
|
|
6358
|
+
tests: requireFlag(flags2, "--tests"),
|
|
6359
|
+
docs: requireFlag(flags2, "--docs")
|
|
6360
|
+
};
|
|
6361
|
+
body = renderCodeReviewComment(input);
|
|
6362
|
+
verify = (comments2) => verifyPostedCodeReview(comments2, verdict, headSha, principalAllowlist);
|
|
6363
|
+
} else {
|
|
6364
|
+
const verdict = normalizeSecurityVerdict(requireFlag(flags2, "--verdict"));
|
|
6365
|
+
const findings = readFindingsFile(flags2.get("--findings-file"), SECURITY_SEVERITIES);
|
|
6366
|
+
if (findings.some((f) => f.severity === "CRITICAL" || f.severity === "HIGH") && verdict === "PASS") {
|
|
6367
|
+
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.");
|
|
6368
|
+
}
|
|
6369
|
+
const secrets = requireFlag(flags2, "--secrets");
|
|
6370
|
+
const secretsEvidenceFile = flags2.get("--secrets-evidence-file");
|
|
6371
|
+
if (isNoneFoundClaim(secrets) && !secretsEvidenceFile) {
|
|
6372
|
+
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.");
|
|
6373
|
+
}
|
|
6374
|
+
let secretsEvidence = null;
|
|
6375
|
+
if (secretsEvidenceFile) {
|
|
6376
|
+
try {
|
|
6377
|
+
secretsEvidence = readFileSync17(secretsEvidenceFile, "utf8");
|
|
6378
|
+
} catch {
|
|
6379
|
+
refuseCmd(`Could not read secrets evidence file at ${secretsEvidenceFile}.`, "Check the path and re-run.");
|
|
6380
|
+
}
|
|
6381
|
+
}
|
|
6382
|
+
const input = {
|
|
6383
|
+
...tokens,
|
|
6384
|
+
headSha,
|
|
6385
|
+
verdict,
|
|
6386
|
+
findings,
|
|
6387
|
+
configScan: requireFlag(flags2, "--config-scan"),
|
|
6388
|
+
secrets,
|
|
6389
|
+
secretsEvidence
|
|
6390
|
+
};
|
|
6391
|
+
body = renderSecurityComment(input);
|
|
6392
|
+
verify = (comments2) => verifyPostedSecurity(comments2, verdict, headSha, principalAllowlist);
|
|
6393
|
+
}
|
|
6394
|
+
const url = postComment(pr, body);
|
|
6395
|
+
let comments;
|
|
6396
|
+
try {
|
|
6397
|
+
comments = fetchComments(pr);
|
|
6398
|
+
} catch (err) {
|
|
6399
|
+
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.");
|
|
6400
|
+
}
|
|
6401
|
+
const result = verify(comments);
|
|
6402
|
+
if (!result.ok) {
|
|
6403
|
+
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.");
|
|
6404
|
+
}
|
|
6405
|
+
if (json) {
|
|
6406
|
+
printJson({ posted: true, url, role, headSha, selfVerified: true });
|
|
6407
|
+
} else {
|
|
6408
|
+
process.stdout.write(`${body}
|
|
6409
|
+
|
|
6410
|
+
Posted: ${url}
|
|
6411
|
+
Self-verification: clean — re-parsed VERDICT is bound to head ${headSha}.
|
|
6412
|
+
`);
|
|
6413
|
+
}
|
|
6414
|
+
}
|
|
6415
|
+
|
|
5764
6416
|
// src/commands/studio.ts
|
|
5765
6417
|
import { execFile as execFile5, spawn as spawn2 } from "node:child_process";
|
|
5766
|
-
import { existsSync as existsSync17, readFileSync as
|
|
6418
|
+
import { existsSync as existsSync17, readFileSync as readFileSync18, renameSync } from "node:fs";
|
|
5767
6419
|
import net from "node:net";
|
|
5768
|
-
import { dirname as dirname7, join as
|
|
6420
|
+
import { dirname as dirname7, join as join20 } from "node:path";
|
|
5769
6421
|
import { promisify as promisify5 } from "node:util";
|
|
5770
6422
|
|
|
5771
6423
|
// src/lib/studio-bundle.ts
|
|
@@ -5781,25 +6433,25 @@ var FALLBACK_PORT = 3108;
|
|
|
5781
6433
|
function resolveStudioTarget(cwd, moduleUrl = import.meta.url) {
|
|
5782
6434
|
let dir = cwd;
|
|
5783
6435
|
for (;; ) {
|
|
5784
|
-
const webDir =
|
|
5785
|
-
const pkgPath =
|
|
6436
|
+
const webDir = join20(dir, "apps", "vinaya-studio", "web");
|
|
6437
|
+
const pkgPath = join20(webDir, "package.json");
|
|
5786
6438
|
if (existsSync17(pkgPath)) {
|
|
5787
6439
|
try {
|
|
5788
|
-
const pkg = JSON.parse(
|
|
6440
|
+
const pkg = JSON.parse(readFileSync18(pkgPath, "utf-8"));
|
|
5789
6441
|
if (pkg.name === "@atta/vinaya-studio-web") {
|
|
5790
6442
|
return { kind: "workspace", webDir };
|
|
5791
6443
|
}
|
|
5792
6444
|
} catch {}
|
|
5793
6445
|
}
|
|
5794
|
-
if (existsSync17(
|
|
6446
|
+
if (existsSync17(join20(dir, ".git")))
|
|
5795
6447
|
break;
|
|
5796
6448
|
const parent = dirname7(dir);
|
|
5797
6449
|
if (parent === dir)
|
|
5798
6450
|
break;
|
|
5799
6451
|
dir = parent;
|
|
5800
6452
|
}
|
|
5801
|
-
const standaloneWebDir =
|
|
5802
|
-
if (existsSync17(
|
|
6453
|
+
const standaloneWebDir = join20(packageRoot(moduleUrl), "studio-standalone", "apps", "vinaya-studio", "web");
|
|
6454
|
+
if (existsSync17(join20(standaloneWebDir, "server.js"))) {
|
|
5803
6455
|
return { kind: "package", packageDir: standaloneWebDir };
|
|
5804
6456
|
}
|
|
5805
6457
|
return { kind: "missing" };
|
|
@@ -5819,8 +6471,8 @@ function isPortFree(port) {
|
|
|
5819
6471
|
});
|
|
5820
6472
|
}
|
|
5821
6473
|
function ensureStudioNodeModules(bundleRoot) {
|
|
5822
|
-
const real =
|
|
5823
|
-
const packed =
|
|
6474
|
+
const real = join20(bundleRoot, "node_modules");
|
|
6475
|
+
const packed = join20(bundleRoot, STUDIO_NODE_MODULES_PACKED_DIRNAME);
|
|
5824
6476
|
if (!existsSync17(real) && existsSync17(packed)) {
|
|
5825
6477
|
renameSync(packed, real);
|
|
5826
6478
|
}
|
|
@@ -5847,8 +6499,8 @@ async function runStudio(cwd, args, moduleUrl = import.meta.url) {
|
|
|
5847
6499
|
case "workspace":
|
|
5848
6500
|
return spawnDev(target.webDir, args);
|
|
5849
6501
|
case "package": {
|
|
5850
|
-
const bundleRoot =
|
|
5851
|
-
return spawnStandalone(cwd,
|
|
6502
|
+
const bundleRoot = join20(target.packageDir, "..", "..", "..");
|
|
6503
|
+
return spawnStandalone(cwd, join20(target.packageDir, "server.js"), bundleRoot);
|
|
5852
6504
|
}
|
|
5853
6505
|
case "missing":
|
|
5854
6506
|
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 +6509,8 @@ async function runStudio(cwd, args, moduleUrl = import.meta.url) {
|
|
|
5857
6509
|
}
|
|
5858
6510
|
|
|
5859
6511
|
// src/commands/upgrade.ts
|
|
5860
|
-
import { existsSync as existsSync18, readFileSync as
|
|
5861
|
-
import { join as
|
|
6512
|
+
import { existsSync as existsSync18, readFileSync as readFileSync19, rmSync as rmSync5, writeFileSync as writeFileSync11 } from "node:fs";
|
|
6513
|
+
import { join as join21 } from "node:path";
|
|
5862
6514
|
function realDeps7() {
|
|
5863
6515
|
return {
|
|
5864
6516
|
detectRepo: detectGitRepo,
|
|
@@ -5876,12 +6528,12 @@ function flags2(args) {
|
|
|
5876
6528
|
return { dryRun: args.includes("--dry-run"), yes: args.includes("--yes") };
|
|
5877
6529
|
}
|
|
5878
6530
|
function readManifest3(repoRoot) {
|
|
5879
|
-
const p =
|
|
6531
|
+
const p = join21(repoRoot, CONFIG_PATH);
|
|
5880
6532
|
if (!existsSync18(p))
|
|
5881
6533
|
return { kind: "missing" };
|
|
5882
6534
|
let raw;
|
|
5883
6535
|
try {
|
|
5884
|
-
raw = JSON.parse(
|
|
6536
|
+
raw = JSON.parse(readFileSync19(p, "utf-8"));
|
|
5885
6537
|
} catch (err) {
|
|
5886
6538
|
return { kind: "invalid", error: `invalid JSON: ${err.message}` };
|
|
5887
6539
|
}
|
|
@@ -5897,17 +6549,17 @@ function readManifest3(repoRoot) {
|
|
|
5897
6549
|
return { kind: "ok", manifest: parsed.data.managed };
|
|
5898
6550
|
}
|
|
5899
6551
|
function writeManifestVersion(repoRoot, manifest) {
|
|
5900
|
-
const configAbs =
|
|
5901
|
-
const seed = JSON.parse(
|
|
6552
|
+
const configAbs = join21(repoRoot, CONFIG_PATH);
|
|
6553
|
+
const seed = JSON.parse(readFileSync19(configAbs, "utf-8"));
|
|
5902
6554
|
const updated = { ...manifest, version: MANAGED_MANIFEST_VERSION };
|
|
5903
|
-
|
|
6555
|
+
writeFileSync11(configAbs, `${JSON.stringify({ ...seed, managed: updated }, null, 2)}
|
|
5904
6556
|
`, "utf-8");
|
|
5905
6557
|
}
|
|
5906
6558
|
function stripFor(repoRoot, path, marker, comment) {
|
|
5907
6559
|
const abs2 = resolveManagedBlockPath(repoRoot, path);
|
|
5908
6560
|
if (!existsSync18(abs2))
|
|
5909
6561
|
return { path, marker, comment, present: false, removesHost: false };
|
|
5910
|
-
const stripped = stripBlockFromContent(
|
|
6562
|
+
const stripped = stripBlockFromContent(readFileSync19(abs2, "utf-8"), marker, comment);
|
|
5911
6563
|
if (stripped === null)
|
|
5912
6564
|
return { path, marker, comment, present: false, removesHost: false };
|
|
5913
6565
|
return { path, marker, comment, present: true, removesHost: blockStripLeavesEmpty(stripped) };
|
|
@@ -5936,7 +6588,7 @@ function planHookRouting(repoRoot, manifest, recorded, hooksPathValue) {
|
|
|
5936
6588
|
const abs2 = resolveManagedBlockPath(repoRoot, b.path);
|
|
5937
6589
|
if (!existsSync18(abs2))
|
|
5938
6590
|
continue;
|
|
5939
|
-
const stripped = stripBlockFromContent(
|
|
6591
|
+
const stripped = stripBlockFromContent(readFileSync19(abs2, "utf-8"), b.marker, b.comment);
|
|
5940
6592
|
if (stripped === null)
|
|
5941
6593
|
blocked.push(`${b.path} exists without vinaya's managed block`);
|
|
5942
6594
|
else if (!blockStripLeavesEmpty(stripped))
|
|
@@ -5971,7 +6623,7 @@ function planUpgrade(ops, repoRoot, manifest, routing) {
|
|
|
5971
6623
|
const ownedBlocks = new Set(manifest.blocks.map((b) => blockKey(b.path, b.marker)));
|
|
5972
6624
|
for (const op of ops) {
|
|
5973
6625
|
if (op.kind === "create-file") {
|
|
5974
|
-
const abs2 =
|
|
6626
|
+
const abs2 = join21(repoRoot, op.path);
|
|
5975
6627
|
const exists = existsSync18(abs2);
|
|
5976
6628
|
const owned = ownedFiles.has(op.path);
|
|
5977
6629
|
let action;
|
|
@@ -5982,7 +6634,7 @@ function planUpgrade(ops, repoRoot, manifest, routing) {
|
|
|
5982
6634
|
} else if (!exists) {
|
|
5983
6635
|
action = "recreate";
|
|
5984
6636
|
hasChanges = true;
|
|
5985
|
-
} else if (
|
|
6637
|
+
} else if (readFileSync19(abs2, "utf-8") !== op.content) {
|
|
5986
6638
|
action = "regenerate";
|
|
5987
6639
|
hasChanges = true;
|
|
5988
6640
|
} else {
|
|
@@ -5999,7 +6651,7 @@ function planUpgrade(ops, repoRoot, manifest, routing) {
|
|
|
5999
6651
|
action = "recreate-host";
|
|
6000
6652
|
hasChanges = true;
|
|
6001
6653
|
} else {
|
|
6002
|
-
const content =
|
|
6654
|
+
const content = readFileSync19(abs2, "utf-8");
|
|
6003
6655
|
const { begin, end } = markerLines(op.marker, op.comment);
|
|
6004
6656
|
if (!(content.includes(begin) && content.includes(end))) {
|
|
6005
6657
|
action = "recreate-append";
|
|
@@ -6099,10 +6751,10 @@ function renderUpgradeDiff(plan) {
|
|
|
6099
6751
|
}
|
|
6100
6752
|
function regenerateBlock(repoRoot, op) {
|
|
6101
6753
|
const abs2 = resolveManagedBlockPath(repoRoot, op.path);
|
|
6102
|
-
const content =
|
|
6754
|
+
const content = readFileSync19(abs2, "utf-8");
|
|
6103
6755
|
const stripped = stripBlockFromContent(content, op.marker, op.comment);
|
|
6104
6756
|
if (stripped !== null) {
|
|
6105
|
-
|
|
6757
|
+
writeFileSync11(abs2, stripped.endsWith(`
|
|
6106
6758
|
`) ? stripped : `${stripped}
|
|
6107
6759
|
`, "utf-8");
|
|
6108
6760
|
}
|
|
@@ -6112,7 +6764,7 @@ function applyUpgrade(plan, repoRoot) {
|
|
|
6112
6764
|
for (const e of plan.entries) {
|
|
6113
6765
|
if (e.kind === "create-file") {
|
|
6114
6766
|
if (e.action === "regenerate" || e.action === "recreate") {
|
|
6115
|
-
writeFileWithDirs(
|
|
6767
|
+
writeFileWithDirs(join21(repoRoot, e.op.path), e.op.content, e.op.mode);
|
|
6116
6768
|
}
|
|
6117
6769
|
} else {
|
|
6118
6770
|
if (e.action === "recreate-host")
|
|
@@ -6129,13 +6781,13 @@ function applyUpgrade(plan, repoRoot) {
|
|
|
6129
6781
|
const abs2 = resolveManagedBlockPath(repoRoot, s.path);
|
|
6130
6782
|
if (!existsSync18(abs2))
|
|
6131
6783
|
continue;
|
|
6132
|
-
const stripped = stripBlockFromContent(
|
|
6784
|
+
const stripped = stripBlockFromContent(readFileSync19(abs2, "utf-8"), s.marker, s.comment);
|
|
6133
6785
|
if (stripped === null)
|
|
6134
6786
|
continue;
|
|
6135
6787
|
if (blockStripLeavesEmpty(stripped))
|
|
6136
|
-
|
|
6788
|
+
rmSync5(abs2, { force: true });
|
|
6137
6789
|
else
|
|
6138
|
-
|
|
6790
|
+
writeFileSync11(abs2, stripped.endsWith(`
|
|
6139
6791
|
`) ? stripped : `${stripped}
|
|
6140
6792
|
`, "utf-8");
|
|
6141
6793
|
}
|
|
@@ -6217,16 +6869,16 @@ async function upgradeCommand(args) {
|
|
|
6217
6869
|
}
|
|
6218
6870
|
|
|
6219
6871
|
// src/commands/waiver.ts
|
|
6220
|
-
import { execFileSync as
|
|
6872
|
+
import { execFileSync as execFileSync14 } from "node:child_process";
|
|
6221
6873
|
function labelFor(kind) {
|
|
6222
6874
|
return kind === "docs" ? WAIVER_LABEL : WAIVER_LABEL_REVIEW;
|
|
6223
6875
|
}
|
|
6224
|
-
function
|
|
6225
|
-
return
|
|
6876
|
+
function gh2(args) {
|
|
6877
|
+
return execFileSync14("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], env: process.env }).trim();
|
|
6226
6878
|
}
|
|
6227
6879
|
function detectPrNumber() {
|
|
6228
6880
|
try {
|
|
6229
|
-
const out =
|
|
6881
|
+
const out = gh2(["pr", "view", "--json", "number", "-q", ".number"]);
|
|
6230
6882
|
return /^\d+$/.test(out) ? out : null;
|
|
6231
6883
|
} catch {
|
|
6232
6884
|
return null;
|
|
@@ -6322,8 +6974,8 @@ Reason: ${reason}`];
|
|
|
6322
6974
|
}
|
|
6323
6975
|
process.stdout.write(`Applying \`${label2}\` to PR #${prNumber} under your authenticated \`gh\` identity — vinaya never fabricates an actor.
|
|
6324
6976
|
`);
|
|
6325
|
-
|
|
6326
|
-
|
|
6977
|
+
execFileSync14("gh", editArgs, { stdio: "inherit", env: process.env });
|
|
6978
|
+
execFileSync14("gh", commentArgs, { stdio: "inherit", env: process.env });
|
|
6327
6979
|
process.stdout.write(`
|
|
6328
6980
|
✓ Label applied and reason posted as a PR comment.
|
|
6329
6981
|
`);
|
|
@@ -6483,6 +7135,48 @@ var COMMANDS = [
|
|
|
6483
7135
|
],
|
|
6484
7136
|
status: "shipped"
|
|
6485
7137
|
},
|
|
7138
|
+
{
|
|
7139
|
+
name: "review post",
|
|
7140
|
+
description: "Render, post, and self-verify a code-reviewer or security-review verdict comment on a PR",
|
|
7141
|
+
flags: [
|
|
7142
|
+
{ flag: "--role", description: "`code-reviewer` or `security` — selects which role template is rendered" },
|
|
7143
|
+
{
|
|
7144
|
+
flag: "--pr",
|
|
7145
|
+
description: "Target PR number — its real head is resolved via `gh pr view`, never caller-supplied"
|
|
7146
|
+
},
|
|
7147
|
+
{
|
|
7148
|
+
flag: "--verdict",
|
|
7149
|
+
description: "code-reviewer: `APPROVE` | `REQUEST_CHANGES`. security: `PASS` | `FAIL`"
|
|
7150
|
+
},
|
|
7151
|
+
{
|
|
7152
|
+
flag: "--findings-file",
|
|
7153
|
+
description: "One finding per line: `SEVERITY|file:line|description` (`|`-delimited). Omit for zero findings."
|
|
7154
|
+
},
|
|
7155
|
+
{ flag: "--brief-conformance", description: "code-reviewer only: the BRIEF CONFORMANCE line" },
|
|
7156
|
+
{ flag: "--spec-conformance", description: "code-reviewer only: the SPEC CONFORMANCE line" },
|
|
7157
|
+
{ flag: "--scope", description: "code-reviewer only: the SCOPE line" },
|
|
7158
|
+
{ flag: "--tests", description: "code-reviewer only: the TESTS line" },
|
|
7159
|
+
{ flag: "--docs", description: "code-reviewer only: the DOCS line" },
|
|
7160
|
+
{ flag: "--config-scan", description: "security only: the CONFIG SCAN line" },
|
|
7161
|
+
{ flag: "--secrets", description: "security only: the SECRETS line" },
|
|
7162
|
+
{
|
|
7163
|
+
flag: "--secrets-evidence-file",
|
|
7164
|
+
description: 'security only: required whenever `--secrets` claims "none found" — the pasted scanner output backing that claim'
|
|
7165
|
+
},
|
|
7166
|
+
{ flag: "--task-id", description: "the closing `Tokens:` line's task id" },
|
|
7167
|
+
{ flag: "--model", description: "the closing `Tokens:` line's model name" },
|
|
7168
|
+
{ flag: "--tokens-in", description: "a non-negative integer, or `-` if unknown" },
|
|
7169
|
+
{ flag: "--tokens-out", description: "a non-negative integer, or `-` if unknown" },
|
|
7170
|
+
{ flag: "--cost", description: "free text, or `-` if unknown" },
|
|
7171
|
+
{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }
|
|
7172
|
+
],
|
|
7173
|
+
details: [
|
|
7174
|
+
"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.",
|
|
7175
|
+
"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.",
|
|
7176
|
+
"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."
|
|
7177
|
+
],
|
|
7178
|
+
status: "shipped"
|
|
7179
|
+
},
|
|
6486
7180
|
{
|
|
6487
7181
|
name: "doctor",
|
|
6488
7182
|
description: "Diagnose hook, workflow, and config health — report only, never mutates",
|
|
@@ -6628,9 +7322,9 @@ function printHelp() {
|
|
|
6628
7322
|
}
|
|
6629
7323
|
|
|
6630
7324
|
// src/index.ts
|
|
6631
|
-
var PACKAGE_ROOT =
|
|
7325
|
+
var PACKAGE_ROOT = join22(dirname8(fileURLToPath2(import.meta.url)), "..");
|
|
6632
7326
|
function readVersion2() {
|
|
6633
|
-
const pkg = JSON.parse(
|
|
7327
|
+
const pkg = JSON.parse(readFileSync20(join22(PACKAGE_ROOT, "package.json"), "utf-8"));
|
|
6634
7328
|
return pkg.version;
|
|
6635
7329
|
}
|
|
6636
7330
|
var [, , command, ...args] = process.argv;
|
|
@@ -6733,6 +7427,16 @@ try {
|
|
|
6733
7427
|
}
|
|
6734
7428
|
break;
|
|
6735
7429
|
}
|
|
7430
|
+
case "review": {
|
|
7431
|
+
const [subcommand, ...rest] = args;
|
|
7432
|
+
if (subcommand === "post") {
|
|
7433
|
+
await reviewPostCommand(rest);
|
|
7434
|
+
} else {
|
|
7435
|
+
console.error(`Unknown 'review' subcommand: ${subcommand ?? "(none)"} (expected 'post')`);
|
|
7436
|
+
process.exit(2);
|
|
7437
|
+
}
|
|
7438
|
+
break;
|
|
7439
|
+
}
|
|
6736
7440
|
case "demo": {
|
|
6737
7441
|
const [subcommand, ...rest] = args;
|
|
6738
7442
|
if (subcommand === "break") {
|