@attalabs/vinaya 0.24.1 → 0.25.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 +25 -2
- package/aeg-root/contracts/developer-reviewer.md +3 -0
- package/aeg-root/contracts/planner-brief.md +13 -0
- package/aeg-root/enforcement.md +10 -6
- package/aeg-root/process.md +4 -4
- package/aeg-root/roles/brief-author.md +4 -2
- package/aeg-root/roles/developer.md +20 -14
- package/aeg-root/roles/planner.md +14 -0
- package/aeg-root/roles/principal.md +3 -0
- package/aeg-root/roles/reviewer.md +23 -7
- package/aeg-root/roles/security.md +21 -6
- package/aeg-root/skills/brief-authoring/SKILL.md +2 -2
- package/aeg-root/state-machine.md +2 -2
- package/aeg-root/templates/brief-template.md +20 -4
- package/aeg-root/templates/issue-rationale-template.md +34 -1
- package/aeg-root/templates/pr-report-template.md +5 -13
- package/dist/checks/bin/check-body-bare-digits.js +572 -127
- package/dist/checks/bin/check-branch-topology.js +629 -73
- package/dist/checks/bin/check-brief-shape.js +1675 -70
- package/dist/checks/bin/check-changeset-coverage.js +572 -127
- package/dist/checks/bin/check-closes-n.js +629 -73
- package/dist/checks/bin/check-coherence.js +696 -134
- package/dist/checks/bin/check-dead-branch-push.js +505 -66
- package/dist/checks/bin/check-dispatch-readiness.js +696 -134
- package/dist/checks/bin/check-doc-coverage-push.js +572 -127
- package/dist/checks/bin/check-doc-coverage.js +572 -127
- package/dist/checks/bin/check-doctrine-no-procedures.js +572 -127
- package/dist/checks/bin/check-doctrine-portability.js +572 -127
- package/dist/checks/bin/check-evidence-fresh.js +572 -127
- package/dist/checks/bin/check-exec-bits.js +572 -127
- package/dist/checks/bin/check-first-push-dispatch.js +696 -134
- package/dist/checks/bin/check-issue-assignment.js +629 -73
- package/dist/checks/bin/check-main-branch-refusal.js +505 -66
- package/dist/checks/bin/check-no-disk-state.js +505 -66
- package/dist/checks/bin/check-pr-report-density.js +505 -66
- package/dist/checks/bin/check-quoted-command.js +572 -127
- package/dist/checks/bin/check-reader-resolvable-prose.js +610 -133
- package/dist/checks/bin/check-registry-gates.js +516 -68
- package/dist/checks/bin/check-retired-vocabulary.js +572 -127
- package/dist/checks/bin/check-review-gate.js +646 -130
- package/dist/checks/bin/check-single-plan-pr.js +505 -66
- package/dist/checks/bin/check-test-plan.js +505 -66
- package/dist/checks/bin/check-token-collection-wired.js +505 -66
- package/dist/checks/bin/check-token-report.js +505 -66
- package/dist/checks/bin/check-workspace-escape.js +572 -127
- package/dist/index.js +7696 -4271
- package/package.json +1 -1
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/brief-render.ts +2 -2
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/main-branch-refusal.ts +26 -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/_head.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_index.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/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]__0053k9k._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0112h-k._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0o771t1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__1pndh3_._.js → [root-of-the-server]__0p8q38b._.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0puovz5._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1hs0dcu._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1wc4-ip._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_03x_w6q._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0gvm3og._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0lwxg63._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0wxycau._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1n0cnq-._.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_topbar_index_tsx_1h1gs1y._.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/{3m1kgax7j2vgs.js → 0bn8c8v5q429o.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0doqwpd81sjzn.js → 0keji7wvbe1d1.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1maoxbrl3pv5d.css +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1uogpj5w2n5ju.js → 3-6a3tinbdb-7.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0zebsmmk1bvnb.js → 3_9xytdmiv1xi.js} +4 -4
- package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/3qc67qhcvbg0o.css +0 -1
- /package/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/{aeg-core → vinaya/studio-standalone/_node_modules/@attalabs/aeg-core}/bin/verify-coherence.ts +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{QY2GhiI47-765D9DHGy1I → DMofV2-9FrHE7100GfrpI}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{QY2GhiI47-765D9DHGy1I → DMofV2-9FrHE7100GfrpI}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{QY2GhiI47-765D9DHGy1I → DMofV2-9FrHE7100GfrpI}/_ssgManifest.js +0 -0
|
@@ -3,7 +3,8 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
4
|
|
|
5
5
|
// src/checks/bin/check-brief-shape.ts
|
|
6
|
-
import {
|
|
6
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
7
|
+
import { readdirSync, readFileSync as readFileSync3 } from "node:fs";
|
|
7
8
|
// ../../packages/aeg-core/src/gate-audience.ts
|
|
8
9
|
var GATE_AUDIENCE = {
|
|
9
10
|
"check-branch-topology": { shippedAs: "branch-topology", ring: 0 },
|
|
@@ -1938,13 +1939,18 @@ function parseInlineFieldList(section) {
|
|
|
1938
1939
|
}
|
|
1939
1940
|
// ../../packages/aeg-core/src/verdict-extraction.ts
|
|
1940
1941
|
var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
|
|
1941
|
-
|
|
1942
|
+
var OBJECTIVES_VERSION_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Objectives version:\s*([0-9a-f]{64})(?![A-Za-z0-9])/im;
|
|
1943
|
+
function firstFiveLines(comment) {
|
|
1942
1944
|
return comment.split(`
|
|
1943
|
-
`).slice(0,
|
|
1945
|
+
`).slice(0, 5).join(`
|
|
1944
1946
|
`);
|
|
1945
1947
|
}
|
|
1946
1948
|
function extractHeadSha(comment) {
|
|
1947
|
-
const m =
|
|
1949
|
+
const m = firstFiveLines(comment).match(HEAD_SHA_PATTERN);
|
|
1950
|
+
return m ? m[1].toLowerCase() : null;
|
|
1951
|
+
}
|
|
1952
|
+
function extractObjectivesVersion(comment) {
|
|
1953
|
+
const m = firstFiveLines(comment).match(OBJECTIVES_VERSION_PATTERN);
|
|
1948
1954
|
return m ? m[1].toLowerCase() : null;
|
|
1949
1955
|
}
|
|
1950
1956
|
function extractVerdict(comments, valuePattern, missingLabel) {
|
|
@@ -1953,21 +1959,24 @@ function extractVerdict(comments, valuePattern, missingLabel) {
|
|
|
1953
1959
|
return {
|
|
1954
1960
|
value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
|
|
1955
1961
|
headSha: null,
|
|
1962
|
+
objectivesVersion: null,
|
|
1956
1963
|
danglingNote: `no ${missingLabel} verdict comment found on this PR`
|
|
1957
1964
|
};
|
|
1958
1965
|
}
|
|
1959
1966
|
const latest = candidates[candidates.length - 1];
|
|
1960
|
-
const m =
|
|
1967
|
+
const m = firstFiveLines(latest).match(valuePattern);
|
|
1961
1968
|
if (!m) {
|
|
1962
1969
|
return {
|
|
1963
|
-
value: `the most recent ${missingLabel} comment's VERDICT line is not within its first
|
|
1970
|
+
value: `the most recent ${missingLabel} comment's VERDICT line is not within its first five lines — DANGLING, see below`,
|
|
1964
1971
|
headSha: null,
|
|
1965
|
-
|
|
1972
|
+
objectivesVersion: null,
|
|
1973
|
+
danglingNote: `the most recent ${missingLabel} verdict comment carries a VERDICT-shaped line outside the first-five-line read window`
|
|
1966
1974
|
};
|
|
1967
1975
|
}
|
|
1968
1976
|
return {
|
|
1969
1977
|
value: m[1].toUpperCase().replace(/[_-]/g, " "),
|
|
1970
1978
|
headSha: extractHeadSha(latest),
|
|
1979
|
+
objectivesVersion: extractObjectivesVersion(latest),
|
|
1971
1980
|
danglingNote: null
|
|
1972
1981
|
};
|
|
1973
1982
|
}
|
|
@@ -2324,8 +2333,105 @@ function overrideActive(opts) {
|
|
|
2324
2333
|
return true;
|
|
2325
2334
|
return false;
|
|
2326
2335
|
}
|
|
2327
|
-
// ../../packages/aeg-core/src/
|
|
2336
|
+
// ../../packages/aeg-core/src/objectives.ts
|
|
2328
2337
|
import { createHash as createHash2 } from "node:crypto";
|
|
2338
|
+
var HEADING_RE = /^##[ \t]*Objectives[ \t]*$/im;
|
|
2339
|
+
var NEXT_HEADING_RE = /^##[ \t]/m;
|
|
2340
|
+
var OBJECTIVE_LINE_RE = /^O(\d+)\.[ \t]*(.*)$/;
|
|
2341
|
+
function hasBacktickedPath(text) {
|
|
2342
|
+
let i = 0;
|
|
2343
|
+
while (i < text.length) {
|
|
2344
|
+
const start = text.indexOf("`", i);
|
|
2345
|
+
if (start === -1)
|
|
2346
|
+
return false;
|
|
2347
|
+
const end = text.indexOf("`", start + 1);
|
|
2348
|
+
if (end === -1)
|
|
2349
|
+
return false;
|
|
2350
|
+
if (text.slice(start + 1, end).includes("/"))
|
|
2351
|
+
return true;
|
|
2352
|
+
i = end + 1;
|
|
2353
|
+
}
|
|
2354
|
+
return false;
|
|
2355
|
+
}
|
|
2356
|
+
var MIN_WORDS_OUTSIDE_BACKTICKS = 3;
|
|
2357
|
+
function stripObjectiveBackticks(text) {
|
|
2358
|
+
return text.replace(/`[^`\n]*`/g, " ");
|
|
2359
|
+
}
|
|
2360
|
+
function wordCount(text) {
|
|
2361
|
+
return text.split(/\s+/).filter((w) => /[a-z]/i.test(w)).length;
|
|
2362
|
+
}
|
|
2363
|
+
function objectivesSectionText(body) {
|
|
2364
|
+
const heading = HEADING_RE.exec(body);
|
|
2365
|
+
if (!heading)
|
|
2366
|
+
return null;
|
|
2367
|
+
const afterHeading = body.slice(heading.index + heading[0].length);
|
|
2368
|
+
const next = NEXT_HEADING_RE.exec(afterHeading);
|
|
2369
|
+
return next ? afterHeading.slice(0, next.index) : afterHeading;
|
|
2370
|
+
}
|
|
2371
|
+
function hasObjectivesHeading(body) {
|
|
2372
|
+
return HEADING_RE.test(body);
|
|
2373
|
+
}
|
|
2374
|
+
function objectivesOf(body) {
|
|
2375
|
+
const section = objectivesSectionText(body);
|
|
2376
|
+
if (section === null) {
|
|
2377
|
+
return { ok: false, errors: ["no `## Objectives` heading found in the body."] };
|
|
2378
|
+
}
|
|
2379
|
+
const lines = section.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
2380
|
+
if (lines.length === 0) {
|
|
2381
|
+
return { ok: false, errors: ["the `## Objectives` section has no `O<n>.` lines."] };
|
|
2382
|
+
}
|
|
2383
|
+
const objectives = [];
|
|
2384
|
+
const errors = [];
|
|
2385
|
+
for (const line of lines) {
|
|
2386
|
+
const m = OBJECTIVE_LINE_RE.exec(line);
|
|
2387
|
+
if (!m) {
|
|
2388
|
+
errors.push(`"${line}" is not a well-formed objective line — expected \`O<n>. <sentence>\`, e.g. \`O1. …\`.`);
|
|
2389
|
+
continue;
|
|
2390
|
+
}
|
|
2391
|
+
const n = m[1];
|
|
2392
|
+
const text = m[2].trim();
|
|
2393
|
+
if (text.length === 0) {
|
|
2394
|
+
errors.push(`O${n} has no sentence — every objective is one observable sentence.`);
|
|
2395
|
+
continue;
|
|
2396
|
+
}
|
|
2397
|
+
if (hasBacktickedPath(text) && wordCount(stripObjectiveBackticks(text)) < MIN_WORDS_OUTSIDE_BACKTICKS) {
|
|
2398
|
+
errors.push(`O${n} is little more than a file path — an objective states an observable outcome, never a bare path (the Brief Author maps it to files).`);
|
|
2399
|
+
continue;
|
|
2400
|
+
}
|
|
2401
|
+
objectives.push({ id: `O${n}`, text });
|
|
2402
|
+
}
|
|
2403
|
+
for (let i = 0;i < objectives.length; i++) {
|
|
2404
|
+
const expected = i + 1;
|
|
2405
|
+
const actual = Number.parseInt(objectives[i].id.slice(1), 10);
|
|
2406
|
+
if (actual !== expected) {
|
|
2407
|
+
errors.push(`numbering is not contiguous from O1 — expected O${expected}, found ${objectives[i].id}.`);
|
|
2408
|
+
break;
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
if (errors.length > 0)
|
|
2412
|
+
return { ok: false, errors };
|
|
2413
|
+
return { ok: true, objectives };
|
|
2414
|
+
}
|
|
2415
|
+
function normalizeText(text) {
|
|
2416
|
+
return text.trim().replace(/\s+/g, " ");
|
|
2417
|
+
}
|
|
2418
|
+
function objectivesVersion(objectives) {
|
|
2419
|
+
const normalized = objectives.map((o) => `${o.id} ${normalizeText(o.text)}`).join(`
|
|
2420
|
+
`);
|
|
2421
|
+
return createHash2("sha256").update(normalized).digest("hex");
|
|
2422
|
+
}
|
|
2423
|
+
function isIssueNotFoundError(err) {
|
|
2424
|
+
const stderr = err?.stderr;
|
|
2425
|
+
const haystack = [
|
|
2426
|
+
err?.message ?? "",
|
|
2427
|
+
typeof stderr === "string" ? stderr : stderr?.toString() ?? ""
|
|
2428
|
+
].join(`
|
|
2429
|
+
`);
|
|
2430
|
+
return /could not resolve to an (?:issue|pull request)|\b404\b|not found/i.test(haystack);
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
// ../../packages/aeg-core/src/premise-check.ts
|
|
2434
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2329
2435
|
var ASSERTION_KINDS = new Set(["contains", "absent", "sha256"]);
|
|
2330
2436
|
function isPremiseHeader(line) {
|
|
2331
2437
|
const stripped = line.replace(/[*#]/g, "").trim();
|
|
@@ -2396,7 +2502,7 @@ function checkPremises(assertions, fileReader) {
|
|
|
2396
2502
|
} else if (a.kind === "absent" && content.includes(a.value)) {
|
|
2397
2503
|
failures.push(`premise-check: ${a.path} now contains "${a.value}", but the brief pinned it absent — the premise has moved; re-dig before proceeding.`);
|
|
2398
2504
|
} else if (a.kind === "sha256") {
|
|
2399
|
-
const actual =
|
|
2505
|
+
const actual = createHash3("sha256").update(content).digest("hex");
|
|
2400
2506
|
if (actual !== a.value.toLowerCase()) {
|
|
2401
2507
|
failures.push(`premise-check: ${a.path} sha256 mismatch (brief pinned ${a.value}, file is now ${actual}) — the file changed since this brief was authored; re-dig before proceeding.`);
|
|
2402
2508
|
}
|
|
@@ -2757,8 +2863,55 @@ function checkDefeatCases(prBody) {
|
|
|
2757
2863
|
]
|
|
2758
2864
|
};
|
|
2759
2865
|
}
|
|
2866
|
+
var PART_CITATION_RE = /Part\s+\d+\s*\(([^)]*)\)/gi;
|
|
2867
|
+
var OBJECTIVE_REF_RE = /O(\d+)/g;
|
|
2868
|
+
function citedObjectiveIds(section6) {
|
|
2869
|
+
const ids = new Set;
|
|
2870
|
+
for (const m of section6.matchAll(PART_CITATION_RE)) {
|
|
2871
|
+
for (const r of m[1].matchAll(OBJECTIVE_REF_RE))
|
|
2872
|
+
ids.add(Number.parseInt(r[1], 10));
|
|
2873
|
+
}
|
|
2874
|
+
return ids;
|
|
2875
|
+
}
|
|
2876
|
+
function checkObjectivesCopy(prBody, issueObjectives) {
|
|
2877
|
+
const parsed = objectivesOf(prBody);
|
|
2878
|
+
if (!parsed.ok) {
|
|
2879
|
+
return { status: "fail", errors: parsed.errors.map((e) => `brief-validation objectives copy: ${e}`) };
|
|
2880
|
+
}
|
|
2881
|
+
if (objectivesVersion(parsed.objectives) !== objectivesVersion(issueObjectives)) {
|
|
2882
|
+
return {
|
|
2883
|
+
status: "fail",
|
|
2884
|
+
errors: [
|
|
2885
|
+
"brief-validation objectives copy: the brief's `## Objectives` section does not match the Issue's — copy the Issue's `## Objectives` section (aeg-root/roles/developer.md)."
|
|
2886
|
+
]
|
|
2887
|
+
};
|
|
2888
|
+
}
|
|
2889
|
+
return { status: "pass", errors: [] };
|
|
2890
|
+
}
|
|
2891
|
+
function checkObjectivesCoverage(prBody) {
|
|
2892
|
+
const parsed = objectivesOf(prBody);
|
|
2893
|
+
if (!parsed.ok) {
|
|
2894
|
+
return { status: "fail", errors: parsed.errors.map((e) => `brief-validation objectives coverage: ${e}`) };
|
|
2895
|
+
}
|
|
2896
|
+
const declaredIds = new Set(parsed.objectives.map((o) => Number.parseInt(o.id.slice(1), 10)));
|
|
2897
|
+
const section6 = extractNumberedSection(prBody, 6) ?? "";
|
|
2898
|
+
const cited = citedObjectiveIds(section6);
|
|
2899
|
+
const maxDeclared = Math.max(...declaredIds, 0);
|
|
2900
|
+
const errors = [];
|
|
2901
|
+
for (const id of declaredIds) {
|
|
2902
|
+
if (!cited.has(id)) {
|
|
2903
|
+
errors.push(`brief-validation objectives coverage: O${id} is not cited by any Part in §6 — every objective must be cited by at least one Part.`);
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
for (const id of cited) {
|
|
2907
|
+
if (!declaredIds.has(id)) {
|
|
2908
|
+
errors.push(`brief-validation objectives coverage: a Part in §6 cites O${id}, but the Objectives section ends at O${maxDeclared}.`);
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
return errors.length === 0 ? { status: "pass", errors: [] } : { status: "fail", errors };
|
|
2912
|
+
}
|
|
2760
2913
|
function checkBriefSections(prBody, readTier, options = {}) {
|
|
2761
|
-
const { requireClosesN = true, consumersOf = () => [] } = options;
|
|
2914
|
+
const { requireClosesN = true, consumersOf = () => [], issueObjectives } = options;
|
|
2762
2915
|
const results = [
|
|
2763
2916
|
checkTierField(prBody, readTier),
|
|
2764
2917
|
checkTestPlan(prBody),
|
|
@@ -2776,6 +2929,7 @@ function checkBriefSections(prBody, readTier, options = {}) {
|
|
|
2776
2929
|
checkCommandsCarryOutput(prBody),
|
|
2777
2930
|
checkConsumerTests(prBody, consumersOf),
|
|
2778
2931
|
checkDefeatCases(prBody),
|
|
2932
|
+
...issueObjectives !== undefined ? [checkObjectivesCopy(prBody, issueObjectives), checkObjectivesCoverage(prBody)] : [],
|
|
2779
2933
|
...requireClosesN ? [checkClosesN(prBody)] : []
|
|
2780
2934
|
];
|
|
2781
2935
|
return { errors: results.flatMap((r) => r.errors) };
|
|
@@ -2974,14 +3128,14 @@ function checkSectionDensity(prBody, heading) {
|
|
|
2974
3128
|
]
|
|
2975
3129
|
};
|
|
2976
3130
|
}
|
|
2977
|
-
function
|
|
2978
|
-
return checkSectionDensity(prBody, "
|
|
3131
|
+
function checkDecisionsDensity(prBody) {
|
|
3132
|
+
return checkSectionDensity(prBody, "Decisions");
|
|
2979
3133
|
}
|
|
2980
3134
|
function checkScopeDensity(prBody) {
|
|
2981
3135
|
return checkSectionDensity(prBody, "Scope");
|
|
2982
3136
|
}
|
|
2983
3137
|
function checkPrReportDensity(prBody) {
|
|
2984
|
-
const results = [
|
|
3138
|
+
const results = [checkDecisionsDensity(prBody), checkScopeDensity(prBody)];
|
|
2985
3139
|
return { errors: results.flatMap((r) => r.errors) };
|
|
2986
3140
|
}
|
|
2987
3141
|
// ../../packages/aeg-core/src/issue-validation.ts
|
|
@@ -3013,6 +3167,15 @@ function checkIssueRationale(body) {
|
|
|
3013
3167
|
}
|
|
3014
3168
|
return { status: errors.length > 0 ? "fail" : "pass", errors };
|
|
3015
3169
|
}
|
|
3170
|
+
var OBJECTIVES_SINCE_ISSUE = 404;
|
|
3171
|
+
function checkIssueObjectives(body, issueNumber) {
|
|
3172
|
+
if (issueNumber !== null && issueNumber < OBJECTIVES_SINCE_ISSUE)
|
|
3173
|
+
return { status: "pass", errors: [] };
|
|
3174
|
+
const result = objectivesOf(body);
|
|
3175
|
+
if (result.ok)
|
|
3176
|
+
return { status: "pass", errors: [] };
|
|
3177
|
+
return { status: "fail", errors: result.errors.map((e) => `issue-validation objectives: ${e}`) };
|
|
3178
|
+
}
|
|
3016
3179
|
function isTaskIssueLabelSet(labels) {
|
|
3017
3180
|
return hasLabel("tranche", labels);
|
|
3018
3181
|
}
|
|
@@ -3274,7 +3437,8 @@ function checkR1(issuesBySlug, grandfatheredIssues, registeredNames = []) {
|
|
|
3274
3437
|
continue;
|
|
3275
3438
|
const errors = [
|
|
3276
3439
|
...checkIssueRationale(issue.body).errors,
|
|
3277
|
-
...checkProjectsRegistered(issue.body, issue.labels, registeredNames).errors
|
|
3440
|
+
...checkProjectsRegistered(issue.body, issue.labels, registeredNames).errors,
|
|
3441
|
+
...checkIssueObjectives(issue.body, issue.number).errors
|
|
3278
3442
|
];
|
|
3279
3443
|
if (errors.length === 0)
|
|
3280
3444
|
continue;
|
|
@@ -3389,6 +3553,27 @@ function checkClosesN2(branch, prBody, trancheFiles, taskIssueRefs) {
|
|
|
3389
3553
|
}
|
|
3390
3554
|
return { ok: true, expectedIssue };
|
|
3391
3555
|
}
|
|
3556
|
+
// ../../packages/aeg-core/src/archive-task.ts
|
|
3557
|
+
function closesRefs(text) {
|
|
3558
|
+
return [...new Set([...text.matchAll(/Closes #(\d+)/gi)].map((m) => Number(m[1])))];
|
|
3559
|
+
}
|
|
3560
|
+
function extractIssue(body) {
|
|
3561
|
+
const stripped = stripCode(body);
|
|
3562
|
+
const bodyNums = closesRefs(stripped);
|
|
3563
|
+
const anchored = anchoredRegion(stripped, "CLOSES");
|
|
3564
|
+
if (anchored !== null) {
|
|
3565
|
+
const anchorNums = closesRefs(anchored);
|
|
3566
|
+
if (anchorNums.length === 0)
|
|
3567
|
+
return { issue: null, extraIssues: bodyNums, outsideHeader: false };
|
|
3568
|
+
const issue2 = anchorNums[0];
|
|
3569
|
+
return { issue: issue2, extraIssues: bodyNums.filter((n) => n !== issue2), outsideHeader: false };
|
|
3570
|
+
}
|
|
3571
|
+
const headerNums = closesRefs(headerRegion(stripped));
|
|
3572
|
+
if (bodyNums.length === 0)
|
|
3573
|
+
return { issue: null, extraIssues: [], outsideHeader: false };
|
|
3574
|
+
const issue = headerNums.length > 0 ? headerNums[0] : bodyNums[0];
|
|
3575
|
+
return { issue, extraIssues: bodyNums.filter((n) => n !== issue), outsideHeader: headerNums.length === 0 };
|
|
3576
|
+
}
|
|
3392
3577
|
// ../../packages/aeg-core/src/review-gate.ts
|
|
3393
3578
|
function isBoundToHead(extraction, headSha) {
|
|
3394
3579
|
if (!extraction.headSha)
|
|
@@ -3407,6 +3592,11 @@ function isBoundByPatchIdentity(extraction, headSha, patchIdOf) {
|
|
|
3407
3592
|
function isBoundToPatch(extraction, headSha, patchIdOf) {
|
|
3408
3593
|
return isBoundToHead(extraction, headSha) || isBoundByPatchIdentity(extraction, headSha, patchIdOf);
|
|
3409
3594
|
}
|
|
3595
|
+
function isBoundToObjectives(extraction, currentVersion) {
|
|
3596
|
+
if (currentVersion === null)
|
|
3597
|
+
return true;
|
|
3598
|
+
return extraction.objectivesVersion === currentVersion;
|
|
3599
|
+
}
|
|
3410
3600
|
function checkReviewGate(input) {
|
|
3411
3601
|
const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
|
|
3412
3602
|
const waived = isWaiverLabelActorVerified({
|
|
@@ -3433,7 +3623,9 @@ function checkReviewGate(input) {
|
|
|
3433
3623
|
const securityClean = security.value === "PASS";
|
|
3434
3624
|
const codeReviewBound = isBoundToPatch(codeReview, input.headSha, input.patchIdOf);
|
|
3435
3625
|
const securityBound = isBoundToPatch(security, input.headSha, input.patchIdOf);
|
|
3436
|
-
|
|
3626
|
+
const codeReviewObjectivesBound = isBoundToObjectives(codeReview, input.objectivesVersion);
|
|
3627
|
+
const securityObjectivesBound = isBoundToObjectives(security, input.objectivesVersion);
|
|
3628
|
+
if (codeReviewClean && codeReviewBound && codeReviewObjectivesBound && securityClean && securityBound && securityObjectivesBound && mechanicalChecksClean) {
|
|
3437
3629
|
return {
|
|
3438
3630
|
verdict: "pass",
|
|
3439
3631
|
reason: `code-reviewer verdict is a clean APPROVE and security-review verdict is a clean PASS, both covering head ${input.headSha}, and every reported mechanical check is green.`,
|
|
@@ -3445,11 +3637,15 @@ function checkReviewGate(input) {
|
|
|
3445
3637
|
problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
|
|
3446
3638
|
} else if (!codeReviewBound) {
|
|
3447
3639
|
problems.push(`the newest code-review verdict covers ${codeReview.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
|
|
3640
|
+
} else if (!codeReviewObjectivesBound) {
|
|
3641
|
+
problems.push(`the newest code-review verdict was cast against objectives version ${codeReview.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
|
|
3448
3642
|
}
|
|
3449
3643
|
if (!securityClean) {
|
|
3450
3644
|
problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
|
|
3451
3645
|
} else if (!securityBound) {
|
|
3452
3646
|
problems.push(`the newest security-review verdict covers ${security.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
|
|
3647
|
+
} else if (!securityObjectivesBound) {
|
|
3648
|
+
problems.push(`the newest security-review verdict was cast against objectives version ${security.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
|
|
3453
3649
|
}
|
|
3454
3650
|
if (!mechanicalChecksClean) {
|
|
3455
3651
|
problems.push(reportedMechanicalChecks.length === 0 ? "no mechanical checks have reported for this head yet" : `mechanical check(s) not green: ${reportedMechanicalChecks.filter((c) => c.bucket !== "pass").map((c) => `${c.name} (${c.bucket})`).join(", ")}`);
|
|
@@ -3685,55 +3881,20 @@ function checkG6(rows, registeredCheckNames) {
|
|
|
3685
3881
|
}
|
|
3686
3882
|
return { check: "G6", status: findings.length > 0 ? "fail" : "pass", findings };
|
|
3687
3883
|
}
|
|
3688
|
-
// ../../packages/aeg-core/src/
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
return {
|
|
3699
|
-
verdict: "refuse",
|
|
3700
|
-
reason: `Branch \`${branch}\` has a CLOSED PR (#${prNumber}) that was never merged — pushing here would resurrect abandoned/rejected work. Open a new task branch instead.`
|
|
3701
|
-
};
|
|
3702
|
-
}
|
|
3703
|
-
if (prState === "OPEN") {
|
|
3704
|
-
return {
|
|
3705
|
-
verdict: "allow",
|
|
3706
|
-
reason: `Branch \`${branch}\` has an OPEN PR (#${prNumber}) — pushing more commits to it is normal.`
|
|
3707
|
-
};
|
|
3708
|
-
}
|
|
3709
|
-
if (prState === "NONE") {
|
|
3710
|
-
return {
|
|
3711
|
-
verdict: "allow",
|
|
3712
|
-
reason: `Branch \`${branch}\` has no PR yet — a brand-new task branch legitimately has none.`
|
|
3713
|
-
};
|
|
3714
|
-
}
|
|
3715
|
-
return {
|
|
3716
|
-
verdict: "allow",
|
|
3717
|
-
reason: `Could not determine \`${branch}\`'s PR state (forge unreachable) — failing open rather than blocking the push on a transient issue.`
|
|
3718
|
-
};
|
|
3719
|
-
}
|
|
3720
|
-
// ../../packages/aeg-core/src/baseline-capture.ts
|
|
3721
|
-
function captureBaseline(counts, capturedAt) {
|
|
3722
|
-
return counts.map((c) => ({ tool: c.tool, findingCount: c.findingCount, capturedAt }));
|
|
3884
|
+
// ../../packages/aeg-core/src/reader-resolvable-prose.ts
|
|
3885
|
+
var PRODUCT_SLUG_SCOPE = [
|
|
3886
|
+
"apps/cli/src",
|
|
3887
|
+
".github/workflows",
|
|
3888
|
+
".vinaya",
|
|
3889
|
+
"apps/cli/README.md",
|
|
3890
|
+
"packages/sources/README.md"
|
|
3891
|
+
];
|
|
3892
|
+
function isUnderOrEqual(path, prefix) {
|
|
3893
|
+
return path === prefix || path.startsWith(`${prefix}/`);
|
|
3723
3894
|
}
|
|
3724
|
-
function
|
|
3725
|
-
|
|
3726
|
-
const baselineEntry = baseline.find((b) => b.tool === c.tool);
|
|
3727
|
-
const baselineCount = baselineEntry ? baselineEntry.findingCount : c.findingCount;
|
|
3728
|
-
return { tool: c.tool, baseline: baselineCount, current: c.findingCount, delta: c.findingCount - baselineCount };
|
|
3729
|
-
});
|
|
3730
|
-
return {
|
|
3731
|
-
withinBudget: perTool.every((t) => t.delta <= 0),
|
|
3732
|
-
delta: perTool.reduce((sum, t) => sum + t.delta, 0),
|
|
3733
|
-
perTool
|
|
3734
|
-
};
|
|
3895
|
+
function isProductScopeFile(path) {
|
|
3896
|
+
return PRODUCT_SLUG_SCOPE.some((prefix) => isUnderOrEqual(path, prefix));
|
|
3735
3897
|
}
|
|
3736
|
-
// ../../packages/aeg-core/src/reader-resolvable-prose.ts
|
|
3737
3898
|
var SHIPS_PREFIX = "aeg-root/";
|
|
3738
3899
|
function shipsArchivePrefix(shipsPrefix) {
|
|
3739
3900
|
return `${shipsPrefix}tranches/completed/`;
|
|
@@ -3753,6 +3914,8 @@ function classifyProseFile(path, readerFacingPrefix, readerFacingSuffix, shipsPr
|
|
|
3753
3914
|
}
|
|
3754
3915
|
if (isSpecFile2(path) || isClaudeMdFile(path))
|
|
3755
3916
|
return "internal";
|
|
3917
|
+
if (isProductScopeFile(path))
|
|
3918
|
+
return "product";
|
|
3756
3919
|
return null;
|
|
3757
3920
|
}
|
|
3758
3921
|
var SWEPT_CLASSES = new Set(["ships", "reader-facing"]);
|
|
@@ -3765,6 +3928,9 @@ function stripNonProse(path, content) {
|
|
|
3765
3928
|
}
|
|
3766
3929
|
return content;
|
|
3767
3930
|
}
|
|
3931
|
+
function stripNonProseForProduct(path, content) {
|
|
3932
|
+
return path.endsWith(".md") ? stripNonProse(path, content) : content;
|
|
3933
|
+
}
|
|
3768
3934
|
function lineAt(content, index) {
|
|
3769
3935
|
let line = 1;
|
|
3770
3936
|
for (let i = 0;i < index; i++) {
|
|
@@ -3789,12 +3955,19 @@ function checkUnresolvableReferences(files, readerFacingPrefix, readerFacingSuff
|
|
|
3789
3955
|
{ pattern: TRANCHE_SLUG_VN_PATTERN, what: "an internal tranche slug" },
|
|
3790
3956
|
...legacyPattern ? [{ pattern: legacyPattern, what: "an internal tranche slug", group: 2 }] : []
|
|
3791
3957
|
];
|
|
3958
|
+
const productPatterns = [
|
|
3959
|
+
{ pattern: TRANCHE_SLUG_VN_PATTERN, what: "an internal tranche slug in product code" }
|
|
3960
|
+
];
|
|
3792
3961
|
for (const file of files) {
|
|
3793
3962
|
const cls = classifyProseFile(file.path, readerFacingPrefix, readerFacingSuffix, shipsPrefix);
|
|
3794
|
-
if (!cls ||
|
|
3963
|
+
if (!cls || cls === "internal")
|
|
3795
3964
|
continue;
|
|
3796
|
-
|
|
3797
|
-
|
|
3965
|
+
if (cls !== "product" && !SWEPT_CLASSES.has(cls))
|
|
3966
|
+
continue;
|
|
3967
|
+
const blocking = cls === "product";
|
|
3968
|
+
const scrubbed = blocking ? stripNonProseForProduct(file.path, file.content) : stripNonProse(file.path, file.content);
|
|
3969
|
+
const patternsToRun = blocking ? productPatterns : patterns;
|
|
3970
|
+
for (const { pattern, what, group } of patternsToRun) {
|
|
3798
3971
|
pattern.lastIndex = 0;
|
|
3799
3972
|
let match = pattern.exec(scrubbed);
|
|
3800
3973
|
while (match !== null) {
|
|
@@ -3802,7 +3975,8 @@ function checkUnresolvableReferences(files, readerFacingPrefix, readerFacingSuff
|
|
|
3802
3975
|
findings.push({
|
|
3803
3976
|
file: file.path,
|
|
3804
3977
|
line: lineAt(scrubbed, match.index),
|
|
3805
|
-
message: `references ${what} ("${cited}") a reader outside this repo's tracker cannot resolve
|
|
3978
|
+
message: `references ${what} ("${cited}") a reader outside this repo's tracker cannot resolve`,
|
|
3979
|
+
blocking
|
|
3806
3980
|
});
|
|
3807
3981
|
match = pattern.exec(scrubbed);
|
|
3808
3982
|
}
|
|
@@ -3852,7 +4026,8 @@ function checkUndefinedVocabulary(files, glossaryTerms, readerFacingPrefix, read
|
|
|
3852
4026
|
findings.push({
|
|
3853
4027
|
file: file.path,
|
|
3854
4028
|
line,
|
|
3855
|
-
message: `uses coined term "${term}" without defining it inline or linking the glossary
|
|
4029
|
+
message: `uses coined term "${term}" without defining it inline or linking the glossary`,
|
|
4030
|
+
blocking: false
|
|
3856
4031
|
});
|
|
3857
4032
|
}
|
|
3858
4033
|
}
|
|
@@ -3875,6 +4050,7 @@ function parseGlossaryTerms(glossaryContent) {
|
|
|
3875
4050
|
}
|
|
3876
4051
|
return terms;
|
|
3877
4052
|
}
|
|
4053
|
+
|
|
3878
4054
|
// ../../packages/aeg-core/src/quoted-command.ts
|
|
3879
4055
|
var QUOTED_COMMAND_SHIPS_PREFIX = "aeg-root/";
|
|
3880
4056
|
var QUOTED_COMMAND_SWEPT_CLASSES = new Set(["ships", "reader-facing"]);
|
|
@@ -3969,6 +4145,54 @@ function evaluateCitedQuotes(citedQuotes, citedFileContents) {
|
|
|
3969
4145
|
}
|
|
3970
4146
|
return findings;
|
|
3971
4147
|
}
|
|
4148
|
+
// ../../packages/aeg-core/src/dead-branch-push-guard.ts
|
|
4149
|
+
function checkDeadBranchPush(input) {
|
|
4150
|
+
const { branch, prState, prNumber } = input;
|
|
4151
|
+
if (prState === "MERGED") {
|
|
4152
|
+
return {
|
|
4153
|
+
verdict: "refuse",
|
|
4154
|
+
reason: `Branch \`${branch}\` already has a MERGED PR (#${prNumber}) — this branch's work is done. Push a new task branch instead of pushing onto an already-resolved one.`
|
|
4155
|
+
};
|
|
4156
|
+
}
|
|
4157
|
+
if (prState === "CLOSED") {
|
|
4158
|
+
return {
|
|
4159
|
+
verdict: "refuse",
|
|
4160
|
+
reason: `Branch \`${branch}\` has a CLOSED PR (#${prNumber}) that was never merged — pushing here would resurrect abandoned/rejected work. Open a new task branch instead.`
|
|
4161
|
+
};
|
|
4162
|
+
}
|
|
4163
|
+
if (prState === "OPEN") {
|
|
4164
|
+
return {
|
|
4165
|
+
verdict: "allow",
|
|
4166
|
+
reason: `Branch \`${branch}\` has an OPEN PR (#${prNumber}) — pushing more commits to it is normal.`
|
|
4167
|
+
};
|
|
4168
|
+
}
|
|
4169
|
+
if (prState === "NONE") {
|
|
4170
|
+
return {
|
|
4171
|
+
verdict: "allow",
|
|
4172
|
+
reason: `Branch \`${branch}\` has no PR yet — a brand-new task branch legitimately has none.`
|
|
4173
|
+
};
|
|
4174
|
+
}
|
|
4175
|
+
return {
|
|
4176
|
+
verdict: "allow",
|
|
4177
|
+
reason: `Could not determine \`${branch}\`'s PR state (forge unreachable) — failing open rather than blocking the push on a transient issue.`
|
|
4178
|
+
};
|
|
4179
|
+
}
|
|
4180
|
+
// ../../packages/aeg-core/src/baseline-capture.ts
|
|
4181
|
+
function captureBaseline(counts, capturedAt) {
|
|
4182
|
+
return counts.map((c) => ({ tool: c.tool, findingCount: c.findingCount, capturedAt }));
|
|
4183
|
+
}
|
|
4184
|
+
function compareToBaseline(current, baseline) {
|
|
4185
|
+
const perTool = current.map((c) => {
|
|
4186
|
+
const baselineEntry = baseline.find((b) => b.tool === c.tool);
|
|
4187
|
+
const baselineCount = baselineEntry ? baselineEntry.findingCount : c.findingCount;
|
|
4188
|
+
return { tool: c.tool, baseline: baselineCount, current: c.findingCount, delta: c.findingCount - baselineCount };
|
|
4189
|
+
});
|
|
4190
|
+
return {
|
|
4191
|
+
withinBudget: perTool.every((t) => t.delta <= 0),
|
|
4192
|
+
delta: perTool.reduce((sum, t) => sum + t.delta, 0),
|
|
4193
|
+
perTool
|
|
4194
|
+
};
|
|
4195
|
+
}
|
|
3972
4196
|
// ../../packages/aeg-core/src/retired-vocabulary.ts
|
|
3973
4197
|
var RETIRED_PATTERNS = [
|
|
3974
4198
|
String.raw`\bD-[0-9]{3}\b`,
|
|
@@ -4438,6 +4662,222 @@ function findWorkspaceEscapes(files, knownPaths, workspaceDirs = DEFAULT_WORKSPA
|
|
|
4438
4662
|
}
|
|
4439
4663
|
return findings;
|
|
4440
4664
|
}
|
|
4665
|
+
// ../../packages/aeg-core/src/log/schema.ts
|
|
4666
|
+
import { z } from "zod";
|
|
4667
|
+
var ROLE_VALUES = [
|
|
4668
|
+
"planner",
|
|
4669
|
+
"brief-author",
|
|
4670
|
+
"developer",
|
|
4671
|
+
"code-reviewer",
|
|
4672
|
+
"security",
|
|
4673
|
+
"principal",
|
|
4674
|
+
"archivist",
|
|
4675
|
+
"architect"
|
|
4676
|
+
];
|
|
4677
|
+
var RoleSchema = z.enum(ROLE_VALUES);
|
|
4678
|
+
var HOST_VALUES = ["hook", "ci", "cli", "loop"];
|
|
4679
|
+
var HostSchema = z.enum(HOST_VALUES);
|
|
4680
|
+
var RUN_ID_PATTERN = /^[A-Za-z0-9_.-]{1,128}$/;
|
|
4681
|
+
var HeaderMetaSchema = z.object({
|
|
4682
|
+
schema: z.literal(1),
|
|
4683
|
+
ts: z.string(),
|
|
4684
|
+
run_id: z.string().regex(RUN_ID_PATTERN),
|
|
4685
|
+
seq: z.number().int().nonnegative(),
|
|
4686
|
+
repo: z.string().nullable(),
|
|
4687
|
+
vinaya: z.string(),
|
|
4688
|
+
doctrine: z.string(),
|
|
4689
|
+
host: HostSchema,
|
|
4690
|
+
machine: z.string()
|
|
4691
|
+
}).strict();
|
|
4692
|
+
var SubjectSchema = z.object({
|
|
4693
|
+
issue: z.number().int().nullable(),
|
|
4694
|
+
pr: z.number().int().optional(),
|
|
4695
|
+
sha: z.string().optional(),
|
|
4696
|
+
role: z.union([RoleSchema, z.literal("unattributed")]),
|
|
4697
|
+
round: z.number().int().optional(),
|
|
4698
|
+
objectives_version: z.string().optional()
|
|
4699
|
+
}).strict();
|
|
4700
|
+
var HeaderSchema = z.object({
|
|
4701
|
+
meta: HeaderMetaSchema,
|
|
4702
|
+
subject: SubjectSchema
|
|
4703
|
+
}).strict();
|
|
4704
|
+
var envelopeTail = {
|
|
4705
|
+
duration_ms: z.number().nonnegative().optional(),
|
|
4706
|
+
payload: z.object({}).strict()
|
|
4707
|
+
};
|
|
4708
|
+
var DispatchOutcomeSchema = z.discriminatedUnion("type", [
|
|
4709
|
+
z.object({ type: z.literal("pr_opened"), pr: z.number().int(), head: z.string() }).strict(),
|
|
4710
|
+
z.object({
|
|
4711
|
+
type: z.literal("round_pushed"),
|
|
4712
|
+
pr: z.number().int(),
|
|
4713
|
+
head: z.string(),
|
|
4714
|
+
comment_id: z.number().int()
|
|
4715
|
+
}).strict(),
|
|
4716
|
+
z.object({
|
|
4717
|
+
type: z.literal("verdict"),
|
|
4718
|
+
verdict: z.enum(["APPROVE", "REQUEST CHANGES", "PASS", "FAIL"]),
|
|
4719
|
+
head: z.string(),
|
|
4720
|
+
comment_id: z.number().int(),
|
|
4721
|
+
objectives: z.array(z.object({ id: z.string(), met: z.boolean() }).strict()),
|
|
4722
|
+
findings: z.array(z.object({ id: z.string(), severity: z.string(), state: z.string().optional() }).strict())
|
|
4723
|
+
}).strict(),
|
|
4724
|
+
z.object({
|
|
4725
|
+
type: z.literal("escalation"),
|
|
4726
|
+
class: z.enum(["authority", "strategy", "product"]),
|
|
4727
|
+
comment_id: z.number().int()
|
|
4728
|
+
}).strict(),
|
|
4729
|
+
z.object({ type: z.literal("brief"), comment_id: z.number().int(), hash: z.string() }).strict(),
|
|
4730
|
+
z.object({ type: z.literal("plan"), issues: z.array(z.number().int()) }).strict(),
|
|
4731
|
+
z.object({ type: z.literal("archive"), provenance_comment_id: z.number().int() }).strict()
|
|
4732
|
+
]);
|
|
4733
|
+
var dispatchShared = {
|
|
4734
|
+
meta: HeaderMetaSchema,
|
|
4735
|
+
subject: SubjectSchema,
|
|
4736
|
+
kind: z.literal("dispatch"),
|
|
4737
|
+
...envelopeTail,
|
|
4738
|
+
target_role: RoleSchema,
|
|
4739
|
+
model: z.string(),
|
|
4740
|
+
round: z.number().int().optional(),
|
|
4741
|
+
effect_id: z.string()
|
|
4742
|
+
};
|
|
4743
|
+
var DispatchEventSchema = z.discriminatedUnion("event", [
|
|
4744
|
+
z.object({ ...dispatchShared, event: z.literal("dispatched"), prompt_hash: z.string() }).strict(),
|
|
4745
|
+
z.object({
|
|
4746
|
+
...dispatchShared,
|
|
4747
|
+
event: z.literal("outcome_received"),
|
|
4748
|
+
outcome: DispatchOutcomeSchema,
|
|
4749
|
+
usage: z.object({ input: z.number().nonnegative(), output: z.number().nonnegative() }).strict().nullable()
|
|
4750
|
+
}).strict(),
|
|
4751
|
+
z.object({
|
|
4752
|
+
...dispatchShared,
|
|
4753
|
+
event: z.literal("dispatch_failed"),
|
|
4754
|
+
reason: z.enum(["timeout", "crash", "refused", "unattributed_write"])
|
|
4755
|
+
}).strict()
|
|
4756
|
+
]);
|
|
4757
|
+
var loopShared = {
|
|
4758
|
+
meta: HeaderMetaSchema,
|
|
4759
|
+
subject: SubjectSchema,
|
|
4760
|
+
kind: z.literal("dev_review_loop"),
|
|
4761
|
+
...envelopeTail,
|
|
4762
|
+
loop_id: z.string()
|
|
4763
|
+
};
|
|
4764
|
+
var DevReviewLoopEventSchema = z.discriminatedUnion("event", [
|
|
4765
|
+
z.object({
|
|
4766
|
+
...loopShared,
|
|
4767
|
+
event: z.literal("loop_started"),
|
|
4768
|
+
task: z.number().int(),
|
|
4769
|
+
policy: z.object({
|
|
4770
|
+
max_rounds: z.number().int().nonnegative(),
|
|
4771
|
+
reviewers: z.array(RoleSchema),
|
|
4772
|
+
models: z.record(RoleSchema, z.string())
|
|
4773
|
+
}).strict()
|
|
4774
|
+
}).strict(),
|
|
4775
|
+
z.object({ ...loopShared, event: z.literal("round_started"), round: z.number().int(), base_head: z.string() }).strict(),
|
|
4776
|
+
z.object({
|
|
4777
|
+
...loopShared,
|
|
4778
|
+
event: z.literal("gate_result_read"),
|
|
4779
|
+
round: z.number().int(),
|
|
4780
|
+
head: z.string(),
|
|
4781
|
+
green: z.boolean()
|
|
4782
|
+
}).strict(),
|
|
4783
|
+
z.object({
|
|
4784
|
+
...loopShared,
|
|
4785
|
+
event: z.literal("verdicts_read"),
|
|
4786
|
+
round: z.number().int(),
|
|
4787
|
+
head: z.string(),
|
|
4788
|
+
all_approve: z.boolean(),
|
|
4789
|
+
blockers: z.number().int().nonnegative()
|
|
4790
|
+
}).strict(),
|
|
4791
|
+
z.object({
|
|
4792
|
+
...loopShared,
|
|
4793
|
+
event: z.literal("findings_compared"),
|
|
4794
|
+
round: z.number().int(),
|
|
4795
|
+
open: z.array(z.string()),
|
|
4796
|
+
resolved: z.array(z.string()),
|
|
4797
|
+
new: z.array(z.string()),
|
|
4798
|
+
recurring: z.array(z.string())
|
|
4799
|
+
}).strict(),
|
|
4800
|
+
z.object({
|
|
4801
|
+
...loopShared,
|
|
4802
|
+
event: z.literal("stop_condition_met"),
|
|
4803
|
+
round: z.number().int(),
|
|
4804
|
+
condition: z.enum([
|
|
4805
|
+
"green",
|
|
4806
|
+
"max_rounds",
|
|
4807
|
+
"no_progress",
|
|
4808
|
+
"escalated",
|
|
4809
|
+
"principal_stop",
|
|
4810
|
+
"confidence",
|
|
4811
|
+
"reappearance"
|
|
4812
|
+
])
|
|
4813
|
+
}).strict(),
|
|
4814
|
+
z.object({
|
|
4815
|
+
...loopShared,
|
|
4816
|
+
event: z.literal("paused"),
|
|
4817
|
+
round: z.number().int(),
|
|
4818
|
+
reason: z.enum(["escalation", "principal_item", "refreeze_needed"])
|
|
4819
|
+
}).strict(),
|
|
4820
|
+
z.object({
|
|
4821
|
+
...loopShared,
|
|
4822
|
+
event: z.literal("resumed"),
|
|
4823
|
+
round: z.number().int(),
|
|
4824
|
+
by: z.literal("principal")
|
|
4825
|
+
}).strict(),
|
|
4826
|
+
z.object({
|
|
4827
|
+
...loopShared,
|
|
4828
|
+
event: z.literal("round_ended"),
|
|
4829
|
+
round: z.number().int(),
|
|
4830
|
+
base_head: z.string(),
|
|
4831
|
+
head: z.string(),
|
|
4832
|
+
files_changed: z.number().int().nonnegative(),
|
|
4833
|
+
insertions: z.number().int().nonnegative(),
|
|
4834
|
+
deletions: z.number().int().nonnegative(),
|
|
4835
|
+
wall_ms: z.number().nonnegative(),
|
|
4836
|
+
outcome: z.enum(["green", "changes_requested", "escalated"])
|
|
4837
|
+
}).strict(),
|
|
4838
|
+
z.object({
|
|
4839
|
+
...loopShared,
|
|
4840
|
+
event: z.literal("journal_finalized"),
|
|
4841
|
+
rounds: z.number().int().nonnegative(),
|
|
4842
|
+
total_wall_ms: z.number().nonnegative(),
|
|
4843
|
+
time_to_green_ms: z.number().nonnegative().nullable(),
|
|
4844
|
+
files_changed_total: z.number().int().nonnegative(),
|
|
4845
|
+
final_head: z.string(),
|
|
4846
|
+
result: z.enum(["merged_ready", "stopped"])
|
|
4847
|
+
}).strict()
|
|
4848
|
+
]);
|
|
4849
|
+
var ForgeOpSchema = z.enum([
|
|
4850
|
+
"pr.create",
|
|
4851
|
+
"pr.comment",
|
|
4852
|
+
"pr.body.replace",
|
|
4853
|
+
"pr.refreeze",
|
|
4854
|
+
"issue.create",
|
|
4855
|
+
"issue.edit",
|
|
4856
|
+
"issue.comment",
|
|
4857
|
+
"milestone.create",
|
|
4858
|
+
"milestone.edit",
|
|
4859
|
+
"milestone.close",
|
|
4860
|
+
"label.add",
|
|
4861
|
+
"label.remove"
|
|
4862
|
+
]);
|
|
4863
|
+
var ForgeWriteTargetSchema = z.object({
|
|
4864
|
+
issue: z.number().int().optional(),
|
|
4865
|
+
pr: z.number().int().optional()
|
|
4866
|
+
}).strict();
|
|
4867
|
+
var forgeWriteShared = {
|
|
4868
|
+
meta: HeaderMetaSchema,
|
|
4869
|
+
subject: SubjectSchema,
|
|
4870
|
+
kind: z.literal("forge_write"),
|
|
4871
|
+
...envelopeTail,
|
|
4872
|
+
op: ForgeOpSchema,
|
|
4873
|
+
target: ForgeWriteTargetSchema
|
|
4874
|
+
};
|
|
4875
|
+
var ForgeWriteEventSchema = z.discriminatedUnion("event", [
|
|
4876
|
+
z.object({ ...forgeWriteShared, event: z.literal("validated") }).strict(),
|
|
4877
|
+
z.object({ ...forgeWriteShared, event: z.literal("refused"), reason: z.string() }).strict(),
|
|
4878
|
+
z.object({ ...forgeWriteShared, event: z.literal("written"), comment_ids: z.array(z.string()) }).strict()
|
|
4879
|
+
]);
|
|
4880
|
+
var LogEventSchema = z.union([DispatchEventSchema, DevReviewLoopEventSchema, ForgeWriteEventSchema]);
|
|
4441
4881
|
// src/checks/contract.ts
|
|
4442
4882
|
var CHECK_SCHEMA_VERSION = 1;
|
|
4443
4883
|
function emitCheckError(error) {
|
|
@@ -4445,8 +4885,1118 @@ function emitCheckError(error) {
|
|
|
4445
4885
|
`);
|
|
4446
4886
|
}
|
|
4447
4887
|
|
|
4888
|
+
// ../../packages/sources/src/commands.ts
|
|
4889
|
+
var COMMANDS = [
|
|
4890
|
+
{
|
|
4891
|
+
name: "help",
|
|
4892
|
+
description: "Show this help text",
|
|
4893
|
+
status: "shipped"
|
|
4894
|
+
},
|
|
4895
|
+
{
|
|
4896
|
+
name: "version",
|
|
4897
|
+
description: "Print the CLI version",
|
|
4898
|
+
flags: [{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }],
|
|
4899
|
+
status: "shipped"
|
|
4900
|
+
},
|
|
4901
|
+
{
|
|
4902
|
+
name: "init",
|
|
4903
|
+
description: "Install Vinaya's git hooks, CI workflow, and starter config (diff-and-confirm, non-destructive)",
|
|
4904
|
+
flags: [
|
|
4905
|
+
{ flag: "--dry-run", description: "Print the full diff without installing anything" },
|
|
4906
|
+
{ flag: "--yes", description: "Skip the confirmation prompt" }
|
|
4907
|
+
],
|
|
4908
|
+
details: [
|
|
4909
|
+
"It detects your repo, prints the complete diff of every intended change, and waits for your confirmation before installing anything. `--dry-run` prints that same diff and installs nothing. Nothing ever runs automatically on package install.",
|
|
4910
|
+
"It installs one CI workflow that runs `vinaya check --all --diff-only`, alongside your existing workflows — refusing to overwrite rather than touching foreign content already at that path. Git hook stubs invoke the `vinaya` binary directly; if a hook already exists, it appends a delimited managed block, shown verbatim in the diff first, rather than overwriting it.",
|
|
4911
|
+
"`vinaya.config.json` is seeded with a starter ruleset extracted from Vinaya's own battle-tested gates, not invented defaults. Issue and PR templates carrying the brief schema are added alongside your own; tier and `needs:*-input` labels are created only if they don't already exist — your existing labels are never modified.",
|
|
4912
|
+
"Two empty folders — `vinaya/checks/` and `vinaya/roles/` — are scaffolded alongside the config, each held open by a placeholder file (git does not track empty directories). `vinaya new noop-check` and `vinaya new role` write their real output into these folders later; both placeholders are recorded in the ownership manifest, so `eject` removes them exactly like everything else `init` created.",
|
|
4913
|
+
"The generated workflows reach the `vinaya` binary through `npx @attalabs/vinaya`, with no build step — unless your repo vendors the CLI itself (a workspace member declaring the name `@attalabs/vinaya`), in which case `npx` would resolve to that unbuilt local member instead of the registry. `init` detects that at generation time and writes workflows that build and run your own copy by path instead, so your CI exercises the code in the pull request. `upgrade` and `doctor` make the same determination, so regenerating is stable.",
|
|
4914
|
+
"The recommended branch-protection command is printed for you to run yourself — it is never applied, and your PATH is never touched. `eject` removes exactly the managed block it owns, or a whole file only if `init` created it."
|
|
4915
|
+
],
|
|
4916
|
+
status: "shipped"
|
|
4917
|
+
},
|
|
4918
|
+
{
|
|
4919
|
+
name: "init product",
|
|
4920
|
+
description: "Register a project in .vinaya/projects.md in an already-initialized repo",
|
|
4921
|
+
flags: [
|
|
4922
|
+
{
|
|
4923
|
+
flag: "--path <path>",
|
|
4924
|
+
description: "The project's home folder — declared, not derived. Defaults to the repo root."
|
|
4925
|
+
}
|
|
4926
|
+
],
|
|
4927
|
+
details: [
|
|
4928
|
+
"Writes (or appends to) `.vinaya/projects.md` — the registry Vinaya Studio's tranche board resolves a project's board link against. Idempotent: re-running with the same name updates nothing.",
|
|
4929
|
+
"Reaches no forge and needs no GitHub remote or credentials: the registry row is a pure local file write, and it is deliberately NOT recorded in the ownership manifest, so `eject` does not reverse adopter-declared data.",
|
|
4930
|
+
"Creates no label. Project is a field, not a label: a task Issue declares its project in the body's `**Project:**` field, and a `project:*` label is ignored wherever one still exists."
|
|
4931
|
+
],
|
|
4932
|
+
status: "shipped"
|
|
4933
|
+
},
|
|
4934
|
+
{
|
|
4935
|
+
name: "check",
|
|
4936
|
+
description: "Run one check, or every registered check",
|
|
4937
|
+
flags: [
|
|
4938
|
+
{ flag: "--all", description: "Run every registered check instead of one named check" },
|
|
4939
|
+
{ flag: "--json", description: "Enveloped JSON output (schema: 1)" },
|
|
4940
|
+
{ flag: "--diff-only", description: "Scope diff-declared checks to changed files" },
|
|
4941
|
+
{
|
|
4942
|
+
flag: "--local",
|
|
4943
|
+
description: "Skip every requiresOpenPr check (closes-n, test-plan) — set by the generated hooks, never by CI"
|
|
4944
|
+
},
|
|
4945
|
+
{ flag: "--parallel[=n]", description: "Concurrency cap (default: cpu-derived)" },
|
|
4946
|
+
{
|
|
4947
|
+
flag: "--plan",
|
|
4948
|
+
description: "Print the resolved check registry and the resolved `roles` registry (default/overridden/additive) without running anything"
|
|
4949
|
+
}
|
|
4950
|
+
],
|
|
4951
|
+
details: [
|
|
4952
|
+
"Each spawned check's child process sees only a fixed baseline (`PATH`, `LANG`, `HOME`, `HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY`, `TMPDIR`) plus whatever its `CheckSpec['env']` declaration explicitly forwards — never the full parent environment. A required (`true`) or unsatisfied `anyOf` declaration missing from the caller's environment synthesizes a `CheckError` before the check ever spawns. Declare `env` (a core check's own registration, or `vinaya.config.json`'s `checks.<name>.env` for a custom one) for any check that reads `process.env`/`Bun.env`/`Deno.env` directly — `vinaya doctor` carries the permanent diagnostic for one that doesn't.",
|
|
4953
|
+
"The resolved registry IS what runs. A `checks` key that exactly matches a core check id REPLACES that core check (the core one does not run); any other key must be namespaced `<yourname>/<id>`. Anything the resolver cannot classify — a malformed entry, a bare un-namespaced key matching no core id, a duplicate id — makes `vinaya check` refuse the ENTIRE run: exit 1, nothing executes, never a partial ruleset and never a core-only fallback. `vinaya doctor` carries the permanent diagnostic for each rejected entry, so a refused config is still diagnosable.",
|
|
4954
|
+
"`--plan` composes with `--json`. It requires zero env vars and never prints an env value — only how each one resolves (passthrough, optional, literal, or anyOf). A `FAIL_CLOSED` entry always renders inline rather than being dropped, and exits non-zero. `--plan` and execution read the same resolution, so what the plan prints is what runs.",
|
|
4955
|
+
"`--local` exists because a `requiresOpenPr` check (the core `closes-n`/`test-plan`, or a custom check declaring the same field) can only evaluate for real once a pull request exists — the generated `pre-commit`/`pre-push` hooks pass it so the first commit on a fresh task branch is never asked to satisfy a PR-body field before a PR can possibly exist. CI's `vinaya-checks.yml` omits it, so these checks always run for real once a PR is open.",
|
|
4956
|
+
"`--plan`'s `roles` half resolves `vinaya.config.json`'s `roles` block against bundled doctrine — same override/additive shape as `checks`, with its own RENDERS AS column (the registry key and the role's own `role_id` differ for an additive entry) and a GATING column (`core` vs. `inert` — an additive role carries no core `ACTIONS` wiring). `roles.available` is `false` only when no bundled doctrine can be found next to this CLI install."
|
|
4957
|
+
],
|
|
4958
|
+
status: "shipped"
|
|
4959
|
+
},
|
|
4960
|
+
{
|
|
4961
|
+
name: "commit-msg",
|
|
4962
|
+
description: "The generated `commit-msg` hook's invocation target — validates a commit message's first line",
|
|
4963
|
+
details: [
|
|
4964
|
+
"Not meant to be run by hand day-to-day: the managed `commit-msg` hook calls `vinaya commit-msg <message-file> [source]` with the two arguments git itself passes a commit-msg hook (githooks(5)) — the message file path and, when known, the commit's source keyword.",
|
|
4965
|
+
"Validates the message file's first line against the same `Type(scope): Description` vocabulary `check`'s `forge-title`-shaped gates enforce on PR/Issue titles (`@attalabs/aeg-core`'s `COMMIT_TYPES` — the commitlint type set plus `Plan`). Exits 0 with no output on a conforming message; a `source` of `merge` is skipped outright, since a merge commit's message is written by git, not the person committing."
|
|
4966
|
+
],
|
|
4967
|
+
status: "shipped"
|
|
4968
|
+
},
|
|
4969
|
+
{
|
|
4970
|
+
name: "new check",
|
|
4971
|
+
description: "Scaffold a custom check into ./scripts/vinaya-checks/",
|
|
4972
|
+
details: [
|
|
4973
|
+
"Takes the REGISTRATION KEY, not a bare name: `vinaya new check <yourname>/<id>` writes `./scripts/vinaya-checks/<id>.ts` and prints the namespaced `checks` entry to paste. It refuses a bare, un-namespaced name — `vinaya check` refuses its entire run over a key it cannot resolve, so scaffolding one would brick every check invocation in the repo — and refuses a core check id, since registering one REPLACES that core gate and a scaffolded stub is never what an adopter means by that."
|
|
4974
|
+
],
|
|
4975
|
+
status: "shipped"
|
|
4976
|
+
},
|
|
4977
|
+
{
|
|
4978
|
+
name: "new noop-check",
|
|
4979
|
+
description: "Scaffold an explicit no-op into vinaya/checks/ that silences a core check",
|
|
4980
|
+
details: [
|
|
4981
|
+
"Takes a CORE check id — the opposite of what `new check` accepts, which refuses one. `vinaya new noop-check <core-check-id>` writes `vinaya/checks/<id>.ts`, an explicit, contract-satisfying no-op (always exits `0`, emits no findings, carries a comment marking the silencing as intentional) and prints the `checks` entry that REPLACES the named core check with it. This is the only sanctioned way to silence a core check."
|
|
4982
|
+
],
|
|
4983
|
+
status: "shipped"
|
|
4984
|
+
},
|
|
4985
|
+
{
|
|
4986
|
+
name: "new role",
|
|
4987
|
+
description: "Scaffold an additive role contract into vinaya/roles/",
|
|
4988
|
+
details: [
|
|
4989
|
+
'Takes the REGISTRATION KEY: `vinaya new role <yourname>/<id>` writes `vinaya/roles/<id>.md` — a structurally-valid role contract stub (the six frontmatter keys plus `title`/`order`, and a non-empty "## The short version" section) whose own `role_id` is set to `<id>` — and prints the `roles` entry to paste. It refuses a bare, un-namespaced key: that shape resolves as an OVERRIDE of a core role, a complete replacement of that role\'s contract and a real governance decision this scaffolder does not make for you.'
|
|
4990
|
+
],
|
|
4991
|
+
status: "shipped"
|
|
4992
|
+
},
|
|
4993
|
+
{
|
|
4994
|
+
name: "brief render",
|
|
4995
|
+
description: "Emit the twelve-section brief skeleton from the forge and the tree, every derivable section filled",
|
|
4996
|
+
flags: [
|
|
4997
|
+
{ flag: "--surfaces <glob1,glob2,...>", description: "Intended surface globs, expanded against tracked files" },
|
|
4998
|
+
{ flag: "--out <path>", description: "Write the rendered brief to a file instead of stdout" }
|
|
4999
|
+
],
|
|
5000
|
+
details: [
|
|
5001
|
+
"Reads the task Issue (`vinaya/tranche:<tranche>`-labeled, id `<n>`) and the tree, and fills every section a program can derive: the header `Project:`/`Tier:`/`Closes #N`, the Step 0 worktree line, the dispatch-gate status as the pre-flight line, §4's file list (with consumer packages and a `sha256` premise pin per file, and Out of surface from the Issue's own `## Surface` `out:` list), §6 from the Issue's `## Parts`, §7 from the `.vinaya/doc-owners` derivation, §9 from the Issue's `## Test plan`, §10 from the Issue's `## Stop conditions` plus the rationale's Stop-and-escalate field, and every remaining section from the Issue's eight-field Planner rationale. Refuses — naming the missing section — when it cannot be derived: no Issue, the dispatch gate not clear, a `--surfaces` glob matching no tracked file, or the Issue missing/malformed `## Surface`/`## Parts`/`## Test plan`/`## Stop conditions` — never a bracketed placeholder.",
|
|
5002
|
+
"Never writes under `aeg-root/` or to the Issue — a brief is pasted to the Developer, never committed. Reads `aeg-root/templates/brief-template.md` at run time for its one fixed sentence and the standing autonomy clause; every other byte is generated from the forge/tree facts."
|
|
5003
|
+
],
|
|
5004
|
+
status: "shipped"
|
|
5005
|
+
},
|
|
5006
|
+
{
|
|
5007
|
+
name: "task dispatch",
|
|
5008
|
+
description: "Render, pin, and post the brief on the Issue as the frozen original; start the developer",
|
|
5009
|
+
flags: [
|
|
5010
|
+
{ flag: "--agent <claude|codex|gemini>", description: "Start the developer through dispatchRole once posted" }
|
|
5011
|
+
],
|
|
5012
|
+
details: [
|
|
5013
|
+
"Renders the brief from the Issue and the tree (the same assembly `brief render` uses) and posts it once as an Issue comment whose first line is `<!-- aeg:brief:v1 -->` and whose second line is `Brief hash: <sha256>` — the hash covers only the body below those two lines, so any reader recomputes it. Refuses outright, naming the existing comment's URL, when a `v1` comment already exists on the Issue — the brief is frozen by design, never overwritten or silently reissued.",
|
|
5014
|
+
"With `--agent`, starts the Developer through `dispatchRole` when `apps/cli/src/lib/dispatch.ts` exports it; otherwise prints the rendered brief and the manual dispatch instruction and exits `0` — a soft dependency, never a hard block.",
|
|
5015
|
+
"Principal-only, with or without `--agent`: refuses before any render, forge read, or post when the authenticated `gh` identity is not on the Principal allowlist (or cannot be resolved at all) — dispatching is the `todo → in-flight` transition, not a general-purpose comment poster."
|
|
5016
|
+
],
|
|
5017
|
+
status: "shipped"
|
|
5018
|
+
},
|
|
5019
|
+
{
|
|
5020
|
+
name: "pr create",
|
|
5021
|
+
description: "Open a pull request after full brief-schema validation",
|
|
5022
|
+
flags: [
|
|
5023
|
+
{ flag: "--title", description: "PR title (validated against the forge-title grammar)" },
|
|
5024
|
+
{ flag: "--body-file", description: "Path to the PR body (stream-safe; the same bytes are validated and sent)" },
|
|
5025
|
+
{ flag: "--label", description: "Label(s) to apply (repeatable, comma-separated)" },
|
|
5026
|
+
{ flag: "--validate-only", description: "Run every gate and report PASS without opening the PR" },
|
|
5027
|
+
{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }
|
|
5028
|
+
],
|
|
5029
|
+
details: [
|
|
5030
|
+
"Runs the config-defined brief-schema gate (`briefSchema.pr` in `vinaya.config.json`) LOCALLY before any `gh` write — prevention, not detection. On any failure it refuses with the versioned CheckError contract (one JSON line per finding on stderr, exit 1) whose `agent_recovery_prompt` names the exact corrective command.",
|
|
5031
|
+
"Which sections bind depends on the branch, matching the `brief-shape` check that runs the same grammar in CI. On a `task/<tranche>/<n>` branch every configured section binds, `closesN` included. On any other branch a body that is not brief-shaped is exempt entirely — an ordinary one-line dependency bump is not made to grow a brief — while a brief-shaped body is still graded on every other section, since a standalone fix brief is a brief; only `closesN` is dropped, because such a branch has no task Issue to close. The title grammar sits outside all of this and binds on every branch.",
|
|
5032
|
+
"A branch counts as resolvable only when HEAD is a symbolic ref that also resolves to a commit. Neither git query answers that alone: `rev-parse --abbrev-ref HEAD` reports the literal string `HEAD` on a detached HEAD, and `symbolic-ref` reports the not-yet-created branch name when HEAD is unborn. Reading either as an ordinary branch would take the relaxed path, so both states — plus running outside a repo — resolve to nothing and the gate is fail-closed: every configured section is enforced."
|
|
5033
|
+
],
|
|
5034
|
+
status: "shipped"
|
|
5035
|
+
},
|
|
5036
|
+
{
|
|
5037
|
+
name: "pr edit",
|
|
5038
|
+
description: "Edit an existing pull request (<n>) after full brief-schema validation",
|
|
5039
|
+
flags: [
|
|
5040
|
+
{ flag: "--title", description: "New PR title (validated against the forge-title grammar)" },
|
|
5041
|
+
{ flag: "--body-file", description: "Path to the new PR body (stream-safe; same bytes validated and sent)" },
|
|
5042
|
+
{ flag: "--validate-only", description: "Run every gate and report PASS without editing the PR" },
|
|
5043
|
+
{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }
|
|
5044
|
+
],
|
|
5045
|
+
details: [
|
|
5046
|
+
"The target PR's real head branch and changed files are fetched from the forge to build the validation context — a failed fetch is a hard refusal, never a fall-back to the local checkout.",
|
|
5047
|
+
"That fetched head branch is what selects the section set, by the same branch grammar `pr create` uses — a property of the target PR, never of whatever the local checkout happens to have checked out."
|
|
5048
|
+
],
|
|
5049
|
+
status: "shipped"
|
|
5050
|
+
},
|
|
5051
|
+
{
|
|
5052
|
+
name: "pr report",
|
|
5053
|
+
description: "Emit the AEG:EVIDENCE block — a PR body's factual claims, from commands, never typed",
|
|
5054
|
+
flags: [
|
|
5055
|
+
{
|
|
5056
|
+
flag: "--write",
|
|
5057
|
+
description: "Path to the PR body file; replaces the content between the AEG:EVIDENCE anchors in place"
|
|
5058
|
+
},
|
|
5059
|
+
{
|
|
5060
|
+
flag: "--push",
|
|
5061
|
+
description: "PR number; splices the same content into that PR's LIVE body (fetched from the forge) and pushes it via `gh pr edit`, self-verifying nothing outside the AEG:EVIDENCE/AEG:TOKENS regions changed. Mutually exclusive with --write."
|
|
5062
|
+
}
|
|
5063
|
+
],
|
|
5064
|
+
details: [
|
|
5065
|
+
"Two groups: Group A (recomputable) is the head sha and a width-invariant `git diff --numstat` against the merge-base with `BASE_SHA` (else `origin/main`, then `main`) — `check-evidence-fresh` recomputes and byte-compares this exactly. Group B (attested) is the result of `vinaya check --all --diff-only`, this CLI's own portable gate suite — `check-evidence-fresh` can only check it for staleness (the block's recorded head still matches the PR's real head), never re-run it.",
|
|
5066
|
+
"With no `--write`/`--push`, prints the block to stdout instead of writing a file. Exits non-zero whenever the gate run failed, whether or not `--write`/`--push` was given — the block records a failing result rather than hiding one, and a non-zero exit stops a scripted `--write && open-pr` (or `--push`) from carrying a failing suite onto the forge.",
|
|
5067
|
+
"`--push` refuses before writing anything when the live body carries no real `AEG:EVIDENCE` pair (it never appends one, unlike `--write`), when the anchor resolves differently before and after normalisation, or when `<pr>` isn't a bare number. When the live body has no real `AEG:TOKENS` pair, the token splice is skipped (same reasoning, softer outcome) rather than creating one. After pushing, it re-reads the live body and restores the pre-edit body — exiting non-zero — if anything outside the two anchored regions changed."
|
|
5068
|
+
],
|
|
5069
|
+
status: "shipped"
|
|
5070
|
+
},
|
|
5071
|
+
{
|
|
5072
|
+
name: "pr rule",
|
|
5073
|
+
description: "Post a Principal ruling on a PR, marked and versioned — never mistaken for a review verdict",
|
|
5074
|
+
flags: [
|
|
5075
|
+
{ flag: "--file", description: "Path to the ruling file to post as a PR comment" },
|
|
5076
|
+
{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }
|
|
5077
|
+
],
|
|
5078
|
+
details: [
|
|
5079
|
+
"Refuses before posting when the file's first line reads as an escalation (`ESCALATE:`), or when the file carries verdict grammar anywhere `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` would treat as a candidate — a `VERDICT:` line past the extractor's own first-five-line window still counts, since the whole-body candidate test is what disqualifies the file, not merely a clean read.",
|
|
5080
|
+
"Marker numbers (`<!-- aeg:principal:ruling:<pr>-<k> -->`) are counted on the forge at post time from `gh pr view --json comments`, never derived from a local file — two rulings racing to the same number are a known, undocumented-lock case."
|
|
5081
|
+
],
|
|
5082
|
+
status: "shipped"
|
|
5083
|
+
},
|
|
5084
|
+
{
|
|
5085
|
+
name: "pr verify-evidence",
|
|
5086
|
+
description: "Prove a pull request's AEG:EVIDENCE region was machine-generated — regenerate it and compare",
|
|
5087
|
+
flags: [],
|
|
5088
|
+
details: [
|
|
5089
|
+
"Deliberately a command, not a check: `evidence-fresh` runs inside `vinaya check --all`, and `pr report` runs `vinaya check --all --diff-only`, so a check that regenerated the block would run the suite containing itself. That recursion is why `evidence-fresh` verifies Group A only and leaves Group B attested.",
|
|
5090
|
+
"Must be run from the repository ROOT of a CLEAN checkout at the pull request's head, and refuses otherwise. The regeneration inherits the working directory and several gates resolve their scan root from it, so a subdirectory silently narrows what the fresh run inspects. On cleanliness: Group B is regenerated by diff-scoped checks, so uncommitted or untracked files change which files are scanned and can produce a MATCH a clean checkout would not. Ignored paths are deliberately NOT inspected — `git diff` never reports one, so they cannot change the scope, and including them would refuse in every real checkout since `node_modules` is ignored and always present. The head is read from the forge and both a mismatch AND an unreadable head refuse — the verdict is bound to a commit, never merely attested. `BASE_SHA` is refused as well, since it steers the regeneration's merge-base.",
|
|
5091
|
+
"Exits 0 on MATCH; 1 on DIFFERS, HIDDEN, or no block; 2 on a refusal (dirty worktree, wrong head, usage error). HIDDEN means the anchor pair sits inside a collapsed `<details>` block, where `body-bare-digits` blanks every digit and nothing can verify what it claims.",
|
|
5092
|
+
"Comparison is a multiset of repo-relative, control-stripped lines: absolute paths (local AND foreign, so a block generated in CI compares against one generated on a laptop) and line order are ignored — a reorder is not a fabrication. Only the AEG:EVIDENCE region is read, since AEG:TOKENS appends by design. A moved merge-base is reported ALONGSIDE the differing lines, never instead of them."
|
|
5093
|
+
],
|
|
5094
|
+
status: "shipped"
|
|
5095
|
+
},
|
|
5096
|
+
{
|
|
5097
|
+
name: "issue create",
|
|
5098
|
+
description: "Open an issue after full brief-schema validation",
|
|
5099
|
+
flags: [
|
|
5100
|
+
{ flag: "--title", description: "Issue title (validated on task Issues)" },
|
|
5101
|
+
{ flag: "--body-file", description: "Path to the Issue body (stream-safe; same bytes validated and sent)" },
|
|
5102
|
+
{ flag: "--label", description: "Label(s) to apply; a `vinaya/tranche:*` label marks a task Issue" },
|
|
5103
|
+
{ flag: "--validate-only", description: "Run every gate and report PASS without opening the Issue" },
|
|
5104
|
+
{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }
|
|
5105
|
+
],
|
|
5106
|
+
details: [
|
|
5107
|
+
"A task Issue (any `vinaya/tranche:*` label) must carry the full Planner rationale (`briefSchema.issue`); non-task Issues pass through unvalidated."
|
|
5108
|
+
],
|
|
5109
|
+
status: "shipped"
|
|
5110
|
+
},
|
|
5111
|
+
{
|
|
5112
|
+
name: "issue edit",
|
|
5113
|
+
description: "Edit an existing issue (<n>) after full brief-schema validation",
|
|
5114
|
+
flags: [
|
|
5115
|
+
{ flag: "--title", description: "New Issue title (validated on task Issues)" },
|
|
5116
|
+
{ flag: "--body-file", description: "Path to the new Issue body (stream-safe; same bytes validated and sent)" },
|
|
5117
|
+
{ flag: "--validate-only", description: "Run every gate and report PASS without editing the Issue" },
|
|
5118
|
+
{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }
|
|
5119
|
+
],
|
|
5120
|
+
details: [
|
|
5121
|
+
"The target Issue's actual labels are fetched from the forge and unioned with argv to decide task-Issue applicability — a failed fetch is a hard refusal."
|
|
5122
|
+
],
|
|
5123
|
+
status: "shipped"
|
|
5124
|
+
},
|
|
5125
|
+
{
|
|
5126
|
+
name: "issue objectives edit",
|
|
5127
|
+
description: "Rewrite a task Issue's `## Objectives` section by command — versioned, findable on the forge",
|
|
5128
|
+
flags: [
|
|
5129
|
+
{ flag: "--add", description: "Append a new objective as `O<max+1>` with the given sentence" },
|
|
5130
|
+
{ flag: "--drop", description: "Remove an objective by id (`O<k>`) — never renumbers the survivors" },
|
|
5131
|
+
{
|
|
5132
|
+
flag: "--replace",
|
|
5133
|
+
description: 'Replace an objective\'s sentence in place, keeping its id (`O<k>` "<sentence>")'
|
|
5134
|
+
},
|
|
5135
|
+
{ flag: "--reason", description: "Required, non-empty: why this change is being made" },
|
|
5136
|
+
{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }
|
|
5137
|
+
],
|
|
5138
|
+
details: [
|
|
5139
|
+
"Exactly one of `--add`/`--drop`/`--replace` is required. Every edit goes through the same validated `issue edit` write path (`writeValidatedIssueEdit`) as `vinaya issue edit` itself — no second, unvalidated write path.",
|
|
5140
|
+
"A `--drop` that leaves the surviving objectives non-contiguous from `O1` is refused with `objectivesOf`'s own parser message: dropping never renumbers survivors, and task 1's contiguous-from-O1 grammar is the one parser everything else reads, so a live contradiction between the two stops here for a Principal ruling rather than silently renumbering.",
|
|
5141
|
+
"Splices the rendered section back in place (`## Objectives` heading through the next `##` heading, or end of body) — every other byte of the Issue body is untouched. Posts one comment marked `<!-- aeg:objectives:v<k> -->` carrying the previous list, the new list, the reason, and the new `objectivesVersion`; `k` is counted on the forge at post time, never derived from a local file."
|
|
5142
|
+
],
|
|
5143
|
+
status: "shipped"
|
|
5144
|
+
},
|
|
5145
|
+
{
|
|
5146
|
+
name: "log flush",
|
|
5147
|
+
description: "Post a target Issue or PR's outbox as one or more marked comments, then truncate what the forge confirmed",
|
|
5148
|
+
flags: [
|
|
5149
|
+
{ flag: "--issue", description: "Flush this Issue's outbox (`~/.vinaya/outbox/<owner>-<repo>/<n>.ndjson`)" },
|
|
5150
|
+
{
|
|
5151
|
+
flag: "--pr",
|
|
5152
|
+
description: "Flush the Issue named by this PR's `Closes #N` line, posting the comments on the PR"
|
|
5153
|
+
},
|
|
5154
|
+
{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }
|
|
5155
|
+
],
|
|
5156
|
+
details: [
|
|
5157
|
+
"Exactly one of `--issue`/`--pr` is required. `--pr` resolves the Issue from the PR body's `Closes #N` line — the same anchor every gate reads — and refuses with a check error naming the missing line when the body carries none.",
|
|
5158
|
+
"Splits the outbox first at `run_id` boundaries (a maximal run of consecutive lines sharing one `run_id`, never a global group-by, so an interleaved outbox never produces a range spanning a gap), then at `FORGE_COMMENT_MAX_CHARS` (65536) within each run. Each comment opens with `<!-- aeg:log:<run_id>:<seq_from>-<seq_to> -->` on its own line, then one fenced `ndjson` block, one outbox line per line, verbatim. A single line too large to fit alone is refused by name, never split across two comments.",
|
|
5159
|
+
"Logs its own `forge_write` line — `validated` before posting, `written` (with every returned comment id) after the last post succeeds, or `refused` (with gh's error) on any failure — through the same `log()` every other family uses, into the same outbox, before truncating. The outbox is truncated only to the lines confirmed posted; a failed chunk's lines, and anything appended to the outbox during the flush (including the flush's own `validated`/`written`/`refused` line), always survive to ride the next flush."
|
|
5160
|
+
],
|
|
5161
|
+
status: "shipped"
|
|
5162
|
+
},
|
|
5163
|
+
{
|
|
5164
|
+
name: "milestone create",
|
|
5165
|
+
description: "Create a GitHub Milestone from a validated body",
|
|
5166
|
+
flags: [
|
|
5167
|
+
{ flag: "--title", description: "Milestone title — free text, never parsed for a version" },
|
|
5168
|
+
{
|
|
5169
|
+
flag: "--body-file",
|
|
5170
|
+
description: "Path to the Milestone description (stream-safe; same bytes validated and sent)"
|
|
5171
|
+
},
|
|
5172
|
+
{ flag: "--validate-only", description: "Run every gate and report PASS without creating the Milestone" },
|
|
5173
|
+
{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }
|
|
5174
|
+
],
|
|
5175
|
+
details: [
|
|
5176
|
+
"Refuses before any `gh` call when the goal is absent, an optional `Release:` field is present but not a version, or an optional `### Tranche intents` section (`- <slug>: <intent text>`) doesn't parse. `Release:` is the sole authority for the milestone's version — the title is never parsed for one."
|
|
5177
|
+
],
|
|
5178
|
+
status: "shipped"
|
|
5179
|
+
},
|
|
5180
|
+
{
|
|
5181
|
+
name: "milestone adopt",
|
|
5182
|
+
description: "Move one or more existing tranches into a target Milestone",
|
|
5183
|
+
flags: [
|
|
5184
|
+
{ flag: "--target", description: "The Milestone every named slug is adopted into" },
|
|
5185
|
+
{ flag: "--slug", description: "A tranche slug to adopt — repeatable for a multi-slug move" },
|
|
5186
|
+
{ flag: "--validate-only", description: "Run every gate and report PASS without writing anything" },
|
|
5187
|
+
{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }
|
|
5188
|
+
],
|
|
5189
|
+
details: [
|
|
5190
|
+
"Reattaches every Issue carrying each named `vinaya/tranche:<slug>` label to `--target`, then closes (never deletes) each slug's old tranche-Milestone. Every fact for every named slug is gathered and checked in ONE call before any write, so a single unsafe slug — an unknown slug, a slug whose label carries no Issues, a target that does not exist or is closed, or a slug already adopted into a different Milestone — refuses the whole invocation, not just its own slug."
|
|
5191
|
+
],
|
|
5192
|
+
status: "shipped"
|
|
5193
|
+
},
|
|
5194
|
+
{
|
|
5195
|
+
name: "milestone edit",
|
|
5196
|
+
description: "Edit an existing Milestone (<n>) after full brief-schema validation",
|
|
5197
|
+
flags: [
|
|
5198
|
+
{
|
|
5199
|
+
flag: "--body-file",
|
|
5200
|
+
description: "Path to the new Milestone description (stream-safe; same bytes validated and sent)"
|
|
5201
|
+
},
|
|
5202
|
+
{ flag: "--validate-only", description: "Run every gate and report PASS without editing the Milestone" },
|
|
5203
|
+
{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }
|
|
5204
|
+
],
|
|
5205
|
+
details: [
|
|
5206
|
+
"Same `checkMilestoneShape` refusal `create` runs, before any `gh` call — the gated replacement for a raw `gh api PATCH` against a Milestone. Only the description changes; the title is untouched."
|
|
5207
|
+
],
|
|
5208
|
+
status: "shipped"
|
|
5209
|
+
},
|
|
5210
|
+
{
|
|
5211
|
+
name: "milestone close",
|
|
5212
|
+
description: "Close a tranche's Milestone after verifying every labeled Issue is actually attached",
|
|
5213
|
+
flags: [
|
|
5214
|
+
{ flag: "--slug", description: "The tranche whose Milestone is being closed" },
|
|
5215
|
+
{ flag: "--validate-only", description: "Run every gate and report PASS without closing the Milestone" },
|
|
5216
|
+
{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }
|
|
5217
|
+
],
|
|
5218
|
+
details: [
|
|
5219
|
+
"Resolves the target Milestone the same legacy-or-intent-declared way Issue create auto-attach does, then refuses to close on any mismatch between the label's Issues and the Milestone's natively attached Issues — naming each unattached or foreign Issue and its repair path (`gh issue edit <n> --milestone <title>`, or `vinaya milestone adopt`) — before the PATCH ever reaches the forge."
|
|
5220
|
+
],
|
|
5221
|
+
status: "shipped"
|
|
5222
|
+
},
|
|
5223
|
+
{
|
|
5224
|
+
name: "review status",
|
|
5225
|
+
description: "Print the review loop's own state for a PR, and its branch's distance from the base",
|
|
5226
|
+
details: [
|
|
5227
|
+
"Two lines at most. The first is `CONTINUE`, `PAUSE: <reason>[ <id>]` for `reappearance`, `zero-deaths` or `max-rounds` — or, for `stale`, the actionable fact itself: `push after verdict — re-review required`. The second reads `behind main by <n> — merge first` when the branch is behind its base, or `behind main: unknown — fetch origin/<base> first` when git cannot measure the distance; it is absent only when the branch is measurably not behind.",
|
|
5228
|
+
"Rounds are derived from the PR's own verdict comments — one round per `Judged head:` value, read through the same extractors the merge gate blocks on, and only from comments an allowlisted principal authored. A Developer round comment is recognised by its `<!-- aeg:developer:round-<n> -->` marker, at that fixed position, never by scanning its prose.",
|
|
5229
|
+
"Exit `0` only when the state is `CONTINUE` and the branch is not behind; `1` otherwise, so a script can gate on the exit code without parsing the text."
|
|
5230
|
+
],
|
|
5231
|
+
status: "shipped"
|
|
5232
|
+
},
|
|
5233
|
+
{
|
|
5234
|
+
name: "review post",
|
|
5235
|
+
description: "Render, post, and self-verify a code-reviewer or security-review verdict comment on a PR",
|
|
5236
|
+
flags: [
|
|
5237
|
+
{ flag: "--role", description: "`code-reviewer` or `security` — selects which role template is rendered" },
|
|
5238
|
+
{
|
|
5239
|
+
flag: "--pr",
|
|
5240
|
+
description: "Target PR number — its real head is resolved via `gh pr view`, never caller-supplied"
|
|
5241
|
+
},
|
|
5242
|
+
{
|
|
5243
|
+
flag: "--verdict",
|
|
5244
|
+
description: "Optional — derived from the findings file (code-reviewer: `APPROVE`/`REQUEST_CHANGES`; security: `PASS`/`FAIL`). Refused before posting if it disagrees with the derivation."
|
|
5245
|
+
},
|
|
5246
|
+
{
|
|
5247
|
+
flag: "--escalate",
|
|
5248
|
+
description: "`authority` | `strategy` | `product` — posts an `ESCALATE:` comment instead of a verdict. Refused together with `--verdict` or a blocking finding."
|
|
5249
|
+
},
|
|
5250
|
+
{ flag: "--summary", description: "Required with `--escalate` — the escalation body text" },
|
|
5251
|
+
{
|
|
5252
|
+
flag: "--findings-file",
|
|
5253
|
+
description: "One finding per line: `SEVERITY|file:line|description` (`|`-delimited). A re-review names a prior id in the description as `F<n> <class> <state>:`. Omit for zero findings."
|
|
5254
|
+
},
|
|
5255
|
+
{
|
|
5256
|
+
flag: "--objectives-file",
|
|
5257
|
+
description: "One line per objective: `O<n>|MET|<evidence>` or `O<n>|NOT MET|<evidence>` (`|`-delimited; evidence is the rest of the line). Required whenever the closed Issue (or the PR body's own `## Objectives`) has an objectives list to judge; its ids must cover that list exactly, and a re-review must restate every prior objective. Never together with `--escalate`."
|
|
5258
|
+
},
|
|
5259
|
+
{ flag: "--brief-conformance", description: "code-reviewer only: the BRIEF CONFORMANCE line" },
|
|
5260
|
+
{ flag: "--spec-conformance", description: "code-reviewer only: the SPEC CONFORMANCE line" },
|
|
5261
|
+
{ flag: "--scope", description: "code-reviewer only: the SCOPE line" },
|
|
5262
|
+
{
|
|
5263
|
+
flag: "--scope-evidence-file",
|
|
5264
|
+
description: "code-reviewer only: pasted diff-stat output, rendered as a fence directly below the verdict block"
|
|
5265
|
+
},
|
|
5266
|
+
{ flag: "--tests", description: "code-reviewer only: the TESTS line" },
|
|
5267
|
+
{ flag: "--docs", description: "code-reviewer only: the DOCS line" },
|
|
5268
|
+
{ flag: "--config-scan", description: "security only: the CONFIG SCAN line" },
|
|
5269
|
+
{ flag: "--secrets", description: "security only: the SECRETS line" },
|
|
5270
|
+
{
|
|
5271
|
+
flag: "--secrets-evidence-file",
|
|
5272
|
+
description: 'security only: required whenever `--secrets` claims "none found" — the pasted scanner output backing that claim'
|
|
5273
|
+
},
|
|
5274
|
+
{ flag: "--task-id", description: "the closing `Tokens:` line's task id" },
|
|
5275
|
+
{ flag: "--model", description: "the closing `Tokens:` line's model name" },
|
|
5276
|
+
{ flag: "--tokens-in", description: "a non-negative integer, or `-` if unknown" },
|
|
5277
|
+
{ flag: "--tokens-out", description: "a non-negative integer, or `-` if unknown" },
|
|
5278
|
+
{ flag: "--cost", description: "free text, or `-` if unknown" },
|
|
5279
|
+
{
|
|
5280
|
+
flag: "--print-only",
|
|
5281
|
+
description: "Render and self-check the comment, print it, and exit — never calls `gh pr comment`"
|
|
5282
|
+
},
|
|
5283
|
+
{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }
|
|
5284
|
+
],
|
|
5285
|
+
details: [
|
|
5286
|
+
"`--print-only` runs the exact same render-then-self-check path as a real post, then returns before `gh pr comment` — closes atta-labs/vinaya#184, where the old command silently posted a verdict anyway after a reviewer guessed this flag existed.",
|
|
5287
|
+
"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.",
|
|
5288
|
+
"The verdict is derived, not typed: a BLOCKER (or CRITICAL/HIGH) finding forces REQUEST_CHANGES/FAIL and its absence forces APPROVE/PASS, before posting anything — an explicit `--verdict` that disagrees is refused naming the derived value. When a same-role verdict comment already exists on the PR, a new findings file must carry every prior id with a state and no non-blocking finding outside the diff since that comment's judged head, or the post is refused.",
|
|
5289
|
+
"Renders an `OBJECTIVES:` block (one `O<n>: MET | NOT MET — <evidence>` line per objective) after `SPEC CONFORMANCE:`/before `CONFIG SCAN:`, and an `Objectives version:` line at line 5 — resolved from the closed Issue's `## Objectives` list, or the PR body's own section when it closes none. A clean verdict (`APPROVE`/`PASS`) is refused alongside any `NOT MET`; an Issue below the objectives cutover renders neither line at all, matching the merge gate's own skip.",
|
|
5290
|
+
"Before the post ever reaches the forge, runs the exact `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` functions `checkReviewGate` calls over its own rendered text and refuses (exit 2) unless exactly the intended verdict extracts and the other role extracts none — an escalation requires both to extract none. Both extractors read only a comment's first five lines; a code-review or security render's caller-supplied fields never open one of those lines, but an escalation's `--summary` can (pre-cutover, it becomes line 5 unprefixed) — this pre-post re-parse, not the render's construction, is what catches that case before it ever reaches the forge.",
|
|
5291
|
+
"After posting, re-fetches the PR's comments and runs them through the same extractors `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, bound to the resolved head and objectives version, and free of the other role's verdict. There is no `--skip-verify` escape."
|
|
5292
|
+
],
|
|
5293
|
+
status: "shipped"
|
|
5294
|
+
},
|
|
5295
|
+
{
|
|
5296
|
+
name: "doctor",
|
|
5297
|
+
description: "Diagnose hook, workflow, and config health — report only, never mutates",
|
|
5298
|
+
flags: [{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }],
|
|
5299
|
+
details: [
|
|
5300
|
+
'Carries the same env-declaration diagnostic `vinaya check` warns with — permanently, at `info` severity, not just ahead of the spawn-default flip — plus a `warn`-severity lint over suspicious `env` literal forms (a stray `"true"`/`"false"` string, or a high-entropy literal that reads like a leaked secret committed to config).',
|
|
5301
|
+
"Also carries the two permanent `checks`-classification diagnostics: a config key that REPLACES a core check (`warn`), and a bare un-namespaced key that is REJECTED (`error`, naming the rename requirement). These are the reason a config `vinaya check` now refuses outright is still diagnosable — the refusal runs nothing, so `doctor` is the surface that explains why."
|
|
5302
|
+
],
|
|
5303
|
+
status: "shipped"
|
|
5304
|
+
},
|
|
5305
|
+
{
|
|
5306
|
+
name: "tokens",
|
|
5307
|
+
description: "Print a role's `Tokens: …` report line — the portable front door over the collection adapter",
|
|
5308
|
+
flags: [
|
|
5309
|
+
{ flag: "--phase", description: 'e.g. `"<task-id>: develop"` — required' },
|
|
5310
|
+
{ flag: "--role", description: "e.g. `Developer` — required" },
|
|
5311
|
+
{ flag: "--model", description: "Overrides the model id the adapter derived, if given" },
|
|
5312
|
+
{
|
|
5313
|
+
flag: "--transcript",
|
|
5314
|
+
description: "Read this transcript directly. Supported primary route — use it whenever you know which transcript " + "is yours, and always in a repo with no track-transcript.sh hook. Omitted resolves via the Stop-hook " + "pointer file, if this repo installs that hook."
|
|
5315
|
+
},
|
|
5316
|
+
{
|
|
5317
|
+
flag: "--in / --out",
|
|
5318
|
+
description: "Manual entry: the exact token figures, for a host whose usage arrives by some other means than a " + "Claude Code transcript. Both required together; skips transcript resolution entirely."
|
|
5319
|
+
}
|
|
5320
|
+
],
|
|
5321
|
+
details: [
|
|
5322
|
+
"Resolves the Claude Code collection adapter (`resolveMeteringCapability`, `summarizeTranscript`, `formatTokensLine`) from the INSTALLED `@attalabs/aeg-core` package, never by a repo-relative path — the fix for `packages/aeg-core/bin/report-tokens.ts` not existing in an adopter repo with no local `packages/`.",
|
|
5323
|
+
"Refuses — never emits a `0/0/—` line — when no transcript resolves, a resolved transcript can't be read, or it summarizes to zero usage records (empty, unparseable, or not yet flushed to disk). Capability is probed by actually attempting resolution, never declared from the host being Claude Code."
|
|
5324
|
+
],
|
|
5325
|
+
status: "shipped"
|
|
5326
|
+
},
|
|
5327
|
+
{
|
|
5328
|
+
name: "doctrine",
|
|
5329
|
+
description: "Print the absolute path of the bundled doctrine's front door (aeg-root/skills/aeg/SKILL.md) on this machine",
|
|
5330
|
+
flags: [{ flag: "--json", description: "Enveloped JSON output (schema: 1) — `{ root, entry }`" }],
|
|
5331
|
+
details: [
|
|
5332
|
+
"The committed root `VINAYA.md` pointer names the `@attalabs/vinaya` package, never a filesystem path — where the package sits is a property of each machine, not of the repo, and the pointer is committed for every clone. This command is the read-time resolution step the pointer hands the reader: it resolves the installed package's own bundled `aeg-root/` wherever the CLI physically sits and prints the front door's absolute path, so `cat \"$(vinaya doctrine)\"` opens the doctrine on any machine at any version.",
|
|
5333
|
+
"In a repo that vendors the CLI, the bundled copy is a gitignored pack-time artifact, so the command falls back to the monorepo root's own `aeg-root/` — the same directory `bundle-doctrine` copies from. Exits 1 with a corrective message when neither location holds a doctrine."
|
|
5334
|
+
],
|
|
5335
|
+
status: "shipped"
|
|
5336
|
+
},
|
|
5337
|
+
{
|
|
5338
|
+
name: "upgrade",
|
|
5339
|
+
description: "Regenerate hooks, workflow, and config to the current contract version (diff-and-confirm)",
|
|
5340
|
+
flags: [
|
|
5341
|
+
{ flag: "--dry-run", description: "Print the full diff without regenerating anything" },
|
|
5342
|
+
{ flag: "--yes", description: "Skip the confirmation prompt" }
|
|
5343
|
+
],
|
|
5344
|
+
details: [
|
|
5345
|
+
"When `claude` is a selected agent vendor, also retrofits the Claude Code `Stop` hook (`.claude/hooks/track-transcript.sh`, `.claude/settings.json`) the same way `init` installs it on a fresh repo — for a repo that ran `init` before this hook existed, closing the gap that left token rows reading `—/—/—` with no operator ever told to re-run `init`. Never overwrites a foreign `.claude/settings.json` (refuse-if-foreign, same as `init`); appends to a foreign Stop-hook script rather than replacing it (managed-block, same discipline as the git hooks)."
|
|
5346
|
+
],
|
|
5347
|
+
status: "shipped"
|
|
5348
|
+
},
|
|
5349
|
+
{
|
|
5350
|
+
name: "archive",
|
|
5351
|
+
description: "Run the post-merge Archivist directly: provenance + Issue close-out for a merged task PR",
|
|
5352
|
+
flags: [{ flag: "--merge-sha", description: "The merge commit to resolve (defaults to the current HEAD)" }],
|
|
5353
|
+
details: [
|
|
5354
|
+
"Resolves the merge commit's associated PR via `gh`, and — if it's a task PR without a provenance comment yet — posts the provenance block and closes the linked Issue. Idempotent: re-running against an already-archived PR is a no-op.",
|
|
5355
|
+
"The same logic the generated `vinaya-archivist.yml` workflow's `post-merge` job runs on every push to `main` — callable directly for a one-off run or local verification.",
|
|
5356
|
+
"All progress output writes through `process.stdout.write`/`process.stderr.write` (never bare `console.*`), so piping this command into a log file or a CI step captures every line in order."
|
|
5357
|
+
],
|
|
5358
|
+
status: "shipped"
|
|
5359
|
+
},
|
|
5360
|
+
{
|
|
5361
|
+
name: "archive tranche",
|
|
5362
|
+
description: "Close a tranche — the tranche-level bookend to `init product`, closing the Milestone via the CLI",
|
|
5363
|
+
flags: [{ flag: "--yes", description: "Skip the confirmation prompt" }],
|
|
5364
|
+
details: [
|
|
5365
|
+
"Refuses if any task Issue attached to the named tranche is still open, naming each one — closing a tranche with unresolved work is never silently allowed.",
|
|
5366
|
+
"Once every task Issue is closed, prompts for confirmation (unless `--yes`) and closes the GitHub Milestone."
|
|
5367
|
+
],
|
|
5368
|
+
status: "shipped"
|
|
5369
|
+
},
|
|
5370
|
+
{
|
|
5371
|
+
name: "audit",
|
|
5372
|
+
description: "Run the ring-2 dead-branch-push and direct-main-push detection checks directly",
|
|
5373
|
+
flags: [
|
|
5374
|
+
{ flag: "--only", description: "Scope to one check: 'dead-branches' or 'direct-push'" },
|
|
5375
|
+
{ flag: "--sha", description: "The commit to check for direct-main-push (defaults to the current HEAD)" },
|
|
5376
|
+
{ flag: "--json", description: "Enveloped JSON output" }
|
|
5377
|
+
],
|
|
5378
|
+
details: [
|
|
5379
|
+
"Dead-branch-push is never-red — a notification channel that flags (label + PR comment) any `task/*` branch whose tip commit lands after its own PR already resolved. Direct-main-push is a real pass/fail — it polls the merge-association API for up to ~100s before deciding, then opens an incident Issue and exits 1 if a commit on `main` genuinely has no associated merged PR.",
|
|
5380
|
+
"The same logic the generated `vinaya-archivist.yml` workflow's `daily-drift` and `direct-main-push-detection` jobs run on schedule / on every push to `main` — callable directly for a one-off run or local verification.",
|
|
5381
|
+
"All progress output writes through `process.stdout.write`/`process.stderr.write` (never bare `console.*`), so piping this command into a log file or a CI step captures every line in order."
|
|
5382
|
+
],
|
|
5383
|
+
status: "shipped"
|
|
5384
|
+
},
|
|
5385
|
+
{
|
|
5386
|
+
name: "eject",
|
|
5387
|
+
description: "Remove every Vinaya-installed artifact, restoring the repo to stock",
|
|
5388
|
+
flags: [
|
|
5389
|
+
{ flag: "--dry-run", description: "Print the full removal diff without removing anything" },
|
|
5390
|
+
{ flag: "--yes", description: "Skip the confirmation prompt" }
|
|
5391
|
+
],
|
|
5392
|
+
status: "shipped"
|
|
5393
|
+
},
|
|
5394
|
+
{
|
|
5395
|
+
name: "demo break",
|
|
5396
|
+
description: "Run a guided refusal-then-fix demo on an isolated, discardable branch",
|
|
5397
|
+
flags: [{ flag: "--keep", description: "Skip cleanup and leave the demo branch checked out to inspect" }],
|
|
5398
|
+
details: [
|
|
5399
|
+
"Creates a collision-safe `vinaya/demo-break-<id>` branch off the current one, stages a deliberately incomplete draft brief, and attempts a real `git commit` — the repo's actually-installed pre-commit hook refuses it with its real output, not a scripted string. Applies the minimal fix, commits again, then switches back and deletes the demo branch.",
|
|
5400
|
+
"Safe to run twice: refuses on a dirty working tree, refuses from a detached HEAD, and recovers automatically from a prior crashed run before starting a fresh one — never leaves the original branch touched or a stray demo branch behind."
|
|
5401
|
+
],
|
|
5402
|
+
status: "shipped"
|
|
5403
|
+
},
|
|
5404
|
+
{
|
|
5405
|
+
name: "waiver",
|
|
5406
|
+
description: "Apply the actor-verified 'vinaya/waiver:docs' or 'vinaya/waiver:review' label after prompting for a reason",
|
|
5407
|
+
flags: [
|
|
5408
|
+
{ flag: "--reason", description: "The waiver rationale, posted as a PR comment (prompted for if omitted)" },
|
|
5409
|
+
{ flag: "--print-only", description: "Print the exact `gh` commands instead of running them" }
|
|
5410
|
+
],
|
|
5411
|
+
details: [
|
|
5412
|
+
"Applies the label via `gh pr edit --add-label`, under the invoking human's own authenticated `gh` identity — this command never fabricates an actor. A waiver is never an agent-emittable string: not a PR body field, not a commit trailer, not a comment — the label plus its own labeling-timeline actor is the only mechanism ring 1 honors.",
|
|
5413
|
+
"`--print-only` prints the exact `gh pr edit`/`gh pr comment` commands and runs nothing — for a human who wants to run them itself, or a CI/non-interactive context where an agent session should never be the one applying its own waiver."
|
|
5414
|
+
],
|
|
5415
|
+
status: "shipped"
|
|
5416
|
+
},
|
|
5417
|
+
{
|
|
5418
|
+
name: "studio",
|
|
5419
|
+
description: "Launch Vinaya Studio — runs the Studio dev app when its source (apps/vinaya-studio/web) is in a checkout above the current directory; a published install launches its bundled standalone server instead",
|
|
5420
|
+
flags: [
|
|
5421
|
+
{
|
|
5422
|
+
flag: "--port <n>",
|
|
5423
|
+
description: "Bind this exact port. Without the flag the default is unchanged — 3008, falling back to 3108 when it is taken. With it there is no fallback: a taken port is refused, so you always know which server answered. Accepts `--port 3208` and `--port=3208`. Applies to a published install; in a workspace checkout Studio's own dev script owns the port and the flag is refused."
|
|
5424
|
+
}
|
|
5425
|
+
],
|
|
5426
|
+
details: [
|
|
5427
|
+
"Resolution happens in this order: a workspace checkout carrying `apps/vinaya-studio/web` (Studio's source, which lives in the attalabs monorepo — not this repository) runs the dev app directly; a published install's `studio-standalone/` bundle (fetched from attalabs' release artifact at publish time, see `scripts/bundle-studio.ts`) runs that bundled server; anything else — a publish that shipped without the bundle — gets an explicit refusal and exit 1 rather than a silent no-op.",
|
|
5428
|
+
"Every published `@attalabs/vinaya` build ships the `studio-standalone/` bundle: `prepack` fetches attalabs’ latest CI-built standalone Studio artifact and assembles it into the tarball before publish."
|
|
5429
|
+
],
|
|
5430
|
+
status: "shipped"
|
|
5431
|
+
},
|
|
5432
|
+
{
|
|
5433
|
+
name: "quickstart",
|
|
5434
|
+
description: "Guided wizard: init, doc-owners, project, commit, demo break, doctor, push — one command",
|
|
5435
|
+
details: [
|
|
5436
|
+
"Calls `init`'s own diff-and-confirm flow unchanged (pausing on Enter before the diff prints, so its own step header isn't scrolled off by a long diff), then Y/n-prompts through the workarounds a guest used to run by hand: binding `.vinaya/doc-owners` pairs (bad input offers a retry instead of silently skipping, and a pointer that doesn't exist on disk is refused outright — both loop across as many pairs as the guest wants, not just one), registering tracked projects (`init product`, same retry/loop shape), committing the install, running `demo break` as proof the gates actually work (default yes — the one step this wizard makes hardest to skip), running `doctor`, and pushing. Each declined prompt skips only that step; the install commit itself is never prompt-gated — it just no-ops when there is genuinely nothing to commit.",
|
|
5437
|
+
"Never reimplements or edits `init`/`init product`/`demo break`/`doctor` — it only calls their existing, unmodified entry points in sequence."
|
|
5438
|
+
],
|
|
5439
|
+
status: "shipped"
|
|
5440
|
+
},
|
|
5441
|
+
{
|
|
5442
|
+
name: "release",
|
|
5443
|
+
description: "Run this repo's own publish sequence in one command, refusing to start unless every precondition holds",
|
|
5444
|
+
flags: [
|
|
5445
|
+
{
|
|
5446
|
+
flag: "--dry-run",
|
|
5447
|
+
description: "Run the preconditions only and print the plan; publishes nothing"
|
|
5448
|
+
},
|
|
5449
|
+
{
|
|
5450
|
+
flag: "--allow-any-commit",
|
|
5451
|
+
description: "Skip the check that HEAD's commit is a Version Packages commit"
|
|
5452
|
+
}
|
|
5453
|
+
],
|
|
5454
|
+
details: [
|
|
5455
|
+
"Refuses unless HEAD is the default branch, the working tree is clean, HEAD equals `origin/<default>` (after `git fetch origin`), HEAD's commit subject starts with `Chore(release): Version packages` (unless `--allow-any-commit`), and `npm whoami` exits `0` — each its own refusal naming the fix.",
|
|
5456
|
+
"Then streams `bun install --frozen-lockfile`, `bun run build`, `bun run changeset:publish`, and `git push origin --tags` — a real push, so the repo's own generated pre-push hook sees it exactly as any other push would. After it, prints `npm view <pkg> version` for every tag now on HEAD, noting registry lag on `@attalabs/vinaya` (observed ~20 minutes) when it still shows the previous version.",
|
|
5457
|
+
'This is the one procedure named in `apps/cli/specs/self-hosting.md`, "How the published version is produced" — publishing itself stays manual and human-triggered; this command only removes the hand-typed four-step recipe.'
|
|
5458
|
+
],
|
|
5459
|
+
status: "shipped"
|
|
5460
|
+
},
|
|
5461
|
+
{
|
|
5462
|
+
name: "dispatch",
|
|
5463
|
+
description: "Start a role's headless agent session (claude/codex/gemini) with attribution set on its environment, recording the outcome through the Vinaya Log",
|
|
5464
|
+
flags: [
|
|
5465
|
+
{
|
|
5466
|
+
flag: "--agent",
|
|
5467
|
+
description: "Vendor to start: `claude`, `codex`, or `gemini` (falls back to `dispatch.agent` in config)"
|
|
5468
|
+
},
|
|
5469
|
+
{ flag: "--prompt-file", description: "Path to the prompt text sent to the agent (never on argv)" },
|
|
5470
|
+
{ flag: "--task", description: "This dispatch's task Issue number — mutually exclusive with `--pr`" },
|
|
5471
|
+
{
|
|
5472
|
+
flag: "--pr",
|
|
5473
|
+
description: "Flush the outbox to this PR's Issue after the dispatch — mutually exclusive with `--task`"
|
|
5474
|
+
},
|
|
5475
|
+
{ flag: "--round", description: "Round number, for a dispatch inside a review loop" },
|
|
5476
|
+
{
|
|
5477
|
+
flag: "--resume <id>",
|
|
5478
|
+
description: "Resume the vendor's own session/thread from a prior dispatch's returned `resumeId`, instead of starting a fresh one"
|
|
5479
|
+
},
|
|
5480
|
+
{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }
|
|
5481
|
+
],
|
|
5482
|
+
details: [
|
|
5483
|
+
"Sets `VINAYA_RUN_ID`/`VINAYA_ROLE`/`VINAYA_TASK`/`VINAYA_ROUND` on the child only — never on this process's own environment — and refuses by name, before any spawn attempt, when the named vendor binary is absent from `PATH` or present but not executable.",
|
|
5484
|
+
"Records `dispatched` (with the prompt's sha256), `outcome_received` (duration, the vendor's own usage when its stdout prints a recognizable shape), or `dispatch_failed` (`timeout`, `crash`, or `refused`) through the Vinaya Log's one `dispatch` family writer, `dispatchRole`. A wall-time ceiling (`dispatch.timeoutMs` in config, default one hour) sends `SIGTERM` then, after a grace window, `SIGKILL`.",
|
|
5485
|
+
"When `--task` or `--pr` is given, flushes that outbox via `vinaya log flush` immediately after the child settles — `--task` and `--pr` are mutually exclusive here, matching `log flush`'s own single-target rule. Without either, the dispatch still runs and logs; nothing is flushed, and the lines ride to the next flush.",
|
|
5486
|
+
"A successful dispatch's `DispatchHandle` carries `resumeId` — the vendor's own session/thread identifier (claude/gemini: `session_id`; codex: `thread_id`), parsed from its stdout, `null` on any failure. Passing that value as `--resume <id>` on a later call swaps in that vendor's own resume invocation (`claude -p -r <id> ...`; `codex exec resume <id> ...`; `gemini ... --resume <id> ...`) in place of its first-dispatch args."
|
|
5487
|
+
],
|
|
5488
|
+
status: "shipped"
|
|
5489
|
+
},
|
|
5490
|
+
{
|
|
5491
|
+
name: "dev-review-loop",
|
|
5492
|
+
description: "Dispatch the developer, run review rounds against the forge, resume the same developer session every round, and hold every verdict until the policy says publish",
|
|
5493
|
+
flags: [
|
|
5494
|
+
{ flag: "--task <n>", description: "This task's Issue number — its frozen `aeg:brief:v1` comment is the brief" },
|
|
5495
|
+
{
|
|
5496
|
+
flag: "--agent <claude|codex|gemini>",
|
|
5497
|
+
description: "Vendor for every dispatch this loop makes (falls back to `dispatch.agent` in config)"
|
|
5498
|
+
},
|
|
5499
|
+
{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }
|
|
5500
|
+
],
|
|
5501
|
+
details: [
|
|
5502
|
+
"Dispatches the developer through `dispatchRole` with the brief read from the Issue, waits for the PR it opens, then runs `assessRound` (`@attalabs/aeg-core`) — the entire policy — against observations this command reads from the forge: the head via `git ls-remote` only (never `gh pr view headRefOid`, which can lag a push), CI conclusion via the check-runs API (never run locally), and ruling comments matching `<!-- aeg:principal:ruling:<pr>-<k> -->`.",
|
|
5503
|
+
"Nothing is posted to the PR before the policy decides `publish`: each round's reviewer and security verdicts are dispatched fresh (never a resumed session) through `dispatchRole`, rendered through `review post`'s own render functions, and written to a local file under the outbox — never `gh pr comment`/`gh pr review`. Posting the held verdicts is a separate, later task.",
|
|
5504
|
+
"The developer's session is resumed every round via `dispatchRole`'s `resumeId` — never a fresh session — for every vendor; a round whose resume fails for a vendor that resumed successfully the round before stops the loop rather than silently falling back to a fresh developer session.",
|
|
5505
|
+
"Every dispatch and round transition is a log line through the Vinaya Log, flushed to the forge at each round boundary."
|
|
5506
|
+
],
|
|
5507
|
+
status: "shipped"
|
|
5508
|
+
}
|
|
5509
|
+
];
|
|
5510
|
+
// ../../packages/sources/src/forge-adapter.ts
|
|
5511
|
+
function createForgeSource(config) {
|
|
5512
|
+
return {
|
|
5513
|
+
async getTranche(slug) {
|
|
5514
|
+
return deriveTrancheFromForge(config.owner, config.repo, slug);
|
|
5515
|
+
}
|
|
5516
|
+
};
|
|
5517
|
+
}
|
|
5518
|
+
// ../../packages/sources/src/select-source.ts
|
|
5519
|
+
import { z as z2 } from "zod";
|
|
5520
|
+
var StateSourceConfigSchema = z2.discriminatedUnion("kind", [
|
|
5521
|
+
z2.object({ kind: z2.literal("forge"), owner: z2.string(), repo: z2.string() }),
|
|
5522
|
+
z2.object({ kind: z2.literal("file"), root: z2.string().optional() })
|
|
5523
|
+
]);
|
|
5524
|
+
// src/checks/edge-resolve.ts
|
|
5525
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
5526
|
+
var DIRECT_ISSUE_REF = /#(\d+)/;
|
|
5527
|
+
var issueCache = new Map;
|
|
5528
|
+
function fetchIssueState(num, repo) {
|
|
5529
|
+
const key = `${repo.owner}/${repo.repo}#${num}`;
|
|
5530
|
+
if (issueCache.has(key))
|
|
5531
|
+
return issueCache.get(key) ?? null;
|
|
5532
|
+
let out = null;
|
|
5533
|
+
try {
|
|
5534
|
+
const raw = execFileSync2("gh", [
|
|
5535
|
+
"issue",
|
|
5536
|
+
"view",
|
|
5537
|
+
String(num),
|
|
5538
|
+
"-R",
|
|
5539
|
+
`${repo.owner}/${repo.repo}`,
|
|
5540
|
+
"--json",
|
|
5541
|
+
"state,stateReason,closedByPullRequestsReferences"
|
|
5542
|
+
], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
5543
|
+
out = JSON.parse(raw);
|
|
5544
|
+
} catch {
|
|
5545
|
+
out = null;
|
|
5546
|
+
}
|
|
5547
|
+
issueCache.set(key, out);
|
|
5548
|
+
return out;
|
|
5549
|
+
}
|
|
5550
|
+
var defaultResolveSiblingTranche = async (slug, repo) => {
|
|
5551
|
+
let tranche;
|
|
5552
|
+
try {
|
|
5553
|
+
tranche = await deriveTrancheFromForge(repo.owner, repo.repo, slug);
|
|
5554
|
+
} catch {
|
|
5555
|
+
return null;
|
|
5556
|
+
}
|
|
5557
|
+
const taskRefs = tranche.tasks.map((t) => ({ id: t.id, issue: t.issue }));
|
|
5558
|
+
const snapshot = await fetchForgeFacts({ owner: repo.owner, repo: repo.repo, tranche: slug, tasks: taskRefs });
|
|
5559
|
+
return {
|
|
5560
|
+
tasks: new Map(tranche.tasks.map((t) => [t.id, { id: t.id, issue: t.issue }])),
|
|
5561
|
+
facts: snapshot.facts
|
|
5562
|
+
};
|
|
5563
|
+
};
|
|
5564
|
+
var siblingTrancheCache = new Map;
|
|
5565
|
+
function edgeFromIssueJson(num, json) {
|
|
5566
|
+
if (json === null) {
|
|
5567
|
+
return {
|
|
5568
|
+
issue: num,
|
|
5569
|
+
merged: false,
|
|
5570
|
+
open: false,
|
|
5571
|
+
issueState: null,
|
|
5572
|
+
stateReason: null,
|
|
5573
|
+
closedByActor: null,
|
|
5574
|
+
resolved: true
|
|
5575
|
+
};
|
|
5576
|
+
}
|
|
5577
|
+
const state = json.state === "CLOSED" ? "closed" : json.state === "OPEN" ? "open" : null;
|
|
5578
|
+
const reason = json.stateReason === "COMPLETED" ? "completed" : json.stateReason === "NOT_PLANNED" ? "not_planned" : null;
|
|
5579
|
+
const closedByMergedPr = (json.closedByPullRequestsReferences ?? []).length > 0;
|
|
5580
|
+
return {
|
|
5581
|
+
issue: num,
|
|
5582
|
+
merged: state === "closed" && closedByMergedPr,
|
|
5583
|
+
open: false,
|
|
5584
|
+
issueState: state,
|
|
5585
|
+
stateReason: reason,
|
|
5586
|
+
closedByActor: null,
|
|
5587
|
+
resolved: true
|
|
5588
|
+
};
|
|
5589
|
+
}
|
|
5590
|
+
async function resolveEdge(id, taskById, factsByTaskId, repo, resolveSibling = defaultResolveSiblingTranche) {
|
|
5591
|
+
const target = taskById.get(id);
|
|
5592
|
+
if (target) {
|
|
5593
|
+
const facts = target.issue !== null ? factsByTaskId.get(target.id) : undefined;
|
|
5594
|
+
return {
|
|
5595
|
+
issue: target.issue,
|
|
5596
|
+
merged: facts?.prState === "merged",
|
|
5597
|
+
open: facts?.prState === "open",
|
|
5598
|
+
issueState: facts?.issueState ?? null,
|
|
5599
|
+
stateReason: facts?.stateReason ?? null,
|
|
5600
|
+
closedByActor: facts?.closedByActor ?? null,
|
|
5601
|
+
resolved: true
|
|
5602
|
+
};
|
|
5603
|
+
}
|
|
5604
|
+
const direct = id.match(DIRECT_ISSUE_REF);
|
|
5605
|
+
if (direct) {
|
|
5606
|
+
const num = Number(direct[1]);
|
|
5607
|
+
return edgeFromIssueJson(num, fetchIssueState(num, repo));
|
|
5608
|
+
}
|
|
5609
|
+
const qualified = splitSlugQualifiedEdge(id);
|
|
5610
|
+
if (qualified) {
|
|
5611
|
+
const cacheKey = `${repo.owner}/${repo.repo}:${qualified.slug}`;
|
|
5612
|
+
let sibling = siblingTrancheCache.get(cacheKey);
|
|
5613
|
+
if (sibling === undefined) {
|
|
5614
|
+
sibling = await resolveSibling(qualified.slug, repo);
|
|
5615
|
+
siblingTrancheCache.set(cacheKey, sibling);
|
|
5616
|
+
}
|
|
5617
|
+
const siblingTask = sibling?.tasks.get(qualified.bareId);
|
|
5618
|
+
if (sibling && siblingTask) {
|
|
5619
|
+
const facts = siblingTask.issue !== null ? sibling.facts.get(siblingTask.id) : undefined;
|
|
5620
|
+
return {
|
|
5621
|
+
issue: siblingTask.issue,
|
|
5622
|
+
merged: facts?.prState === "merged",
|
|
5623
|
+
open: facts?.prState === "open",
|
|
5624
|
+
issueState: facts?.issueState ?? null,
|
|
5625
|
+
stateReason: facts?.stateReason ?? null,
|
|
5626
|
+
closedByActor: facts?.closedByActor ?? null,
|
|
5627
|
+
resolved: true
|
|
5628
|
+
};
|
|
5629
|
+
}
|
|
5630
|
+
}
|
|
5631
|
+
return {
|
|
5632
|
+
issue: null,
|
|
5633
|
+
merged: false,
|
|
5634
|
+
open: false,
|
|
5635
|
+
issueState: null,
|
|
5636
|
+
stateReason: null,
|
|
5637
|
+
closedByActor: null,
|
|
5638
|
+
resolved: false
|
|
5639
|
+
};
|
|
5640
|
+
}
|
|
5641
|
+
|
|
5642
|
+
// src/lib/config.ts
|
|
5643
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
5644
|
+
import { existsSync, mkdirSync, readFileSync as readFileSync2, realpathSync, writeFileSync } from "node:fs";
|
|
5645
|
+
import { homedir } from "node:os";
|
|
5646
|
+
import { dirname, join, resolve } from "node:path";
|
|
5647
|
+
import { z as z3 } from "zod";
|
|
5648
|
+
|
|
5649
|
+
// src/lib/agent-vendors.ts
|
|
5650
|
+
var AGENT_VENDORS = ["skills", "claude", "gemini"];
|
|
5651
|
+
|
|
5652
|
+
// src/lib/config.ts
|
|
5653
|
+
var EnvEntrySchema = z3.union([
|
|
5654
|
+
z3.literal(true),
|
|
5655
|
+
z3.object({ optional: z3.literal(true) }),
|
|
5656
|
+
z3.object({ anyOf: z3.array(z3.string()).min(2) }),
|
|
5657
|
+
z3.string()
|
|
5658
|
+
]);
|
|
5659
|
+
var CheckEntrySchema = z3.object({
|
|
5660
|
+
run: z3.string(),
|
|
5661
|
+
scope: z3.enum(["diff", "full"]),
|
|
5662
|
+
include: z3.array(z3.string()).optional(),
|
|
5663
|
+
args: z3.array(z3.string()).optional(),
|
|
5664
|
+
timeoutMs: z3.number().optional(),
|
|
5665
|
+
env: z3.record(z3.string(), EnvEntrySchema).optional(),
|
|
5666
|
+
requiresOpenPr: z3.boolean().optional(),
|
|
5667
|
+
ownWorkflow: z3.boolean().optional()
|
|
5668
|
+
}).superRefine((entry2, ctx) => {
|
|
5669
|
+
if (!entry2.env)
|
|
5670
|
+
return;
|
|
5671
|
+
for (const [key, value] of Object.entries(entry2.env)) {
|
|
5672
|
+
if (typeof value !== "object" || value === null || !("anyOf" in value))
|
|
5673
|
+
continue;
|
|
5674
|
+
const members = value.anyOf;
|
|
5675
|
+
if (new Set(members).size !== members.length) {
|
|
5676
|
+
ctx.addIssue({
|
|
5677
|
+
code: z3.ZodIssueCode.custom,
|
|
5678
|
+
message: `env.${key}.anyOf has duplicate members`,
|
|
5679
|
+
path: ["env", key, "anyOf"]
|
|
5680
|
+
});
|
|
5681
|
+
}
|
|
5682
|
+
if (!members.includes(key)) {
|
|
5683
|
+
ctx.addIssue({
|
|
5684
|
+
code: z3.ZodIssueCode.custom,
|
|
5685
|
+
message: `env.${key}.anyOf must include "${key}" itself as a member`,
|
|
5686
|
+
path: ["env", key, "anyOf"]
|
|
5687
|
+
});
|
|
5688
|
+
}
|
|
5689
|
+
}
|
|
5690
|
+
});
|
|
5691
|
+
var RoleEntrySchema = z3.object({
|
|
5692
|
+
contract: z3.string().refine((p) => p.includes("/"), {
|
|
5693
|
+
message: 'must be a path (contain at least one "/"), not a bare filename'
|
|
5694
|
+
})
|
|
5695
|
+
});
|
|
5696
|
+
var BRIEF_BUILTINS = [
|
|
5697
|
+
"tier",
|
|
5698
|
+
"testPlan",
|
|
5699
|
+
"testPlanExclusivity",
|
|
5700
|
+
"principalPlaceholder",
|
|
5701
|
+
"surfaceMap",
|
|
5702
|
+
"docUpdateList",
|
|
5703
|
+
"worktreeStep0",
|
|
5704
|
+
"stopConditions",
|
|
5705
|
+
"autonomyClause",
|
|
5706
|
+
"project",
|
|
5707
|
+
"for",
|
|
5708
|
+
"closesN",
|
|
5709
|
+
"premiseCoverage",
|
|
5710
|
+
"issueRationale",
|
|
5711
|
+
"objectives",
|
|
5712
|
+
"briefSections",
|
|
5713
|
+
"milestoneShape"
|
|
5714
|
+
];
|
|
5715
|
+
var BriefSectionSchema = z3.union([
|
|
5716
|
+
z3.object({ builtin: z3.enum(BRIEF_BUILTINS) }),
|
|
5717
|
+
z3.object({ heading: z3.string().min(1), name: z3.string().optional() }),
|
|
5718
|
+
z3.object({ field: z3.string().min(1), name: z3.string().optional() }),
|
|
5719
|
+
z3.object({ phrase: z3.string().min(1), name: z3.string().optional() })
|
|
5720
|
+
]);
|
|
5721
|
+
var BriefSchemaSchema = z3.object({
|
|
5722
|
+
pr: z3.object({ sections: z3.array(BriefSectionSchema) }).optional(),
|
|
5723
|
+
issue: z3.object({ sections: z3.array(BriefSectionSchema) }).optional(),
|
|
5724
|
+
milestone: z3.object({ sections: z3.array(BriefSectionSchema) }).optional(),
|
|
5725
|
+
ack: z3.array(z3.enum(BRIEF_BUILTINS)).optional()
|
|
5726
|
+
});
|
|
5727
|
+
function isSafeRepoRelPath(p) {
|
|
5728
|
+
if (p.length === 0)
|
|
5729
|
+
return false;
|
|
5730
|
+
if (p.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(p))
|
|
5731
|
+
return false;
|
|
5732
|
+
return p.split(/[\\/]/).every((seg) => seg !== ".." && seg !== "");
|
|
5733
|
+
}
|
|
5734
|
+
var SafeRepoRelPath = z3.string().refine(isSafeRepoRelPath, {
|
|
5735
|
+
message: "must be a repo-root-relative path with no `..` segment or absolute root"
|
|
5736
|
+
});
|
|
5737
|
+
var CANONICAL_HOOK_BLOCK_PREFIXES = [".git/", ".husky/", ".vinaya/hooks/", ".claude/hooks/"];
|
|
5738
|
+
function isCanonicalHookBlockPath(p) {
|
|
5739
|
+
return CANONICAL_HOOK_BLOCK_PREFIXES.some((prefix) => p.startsWith(prefix) && p.length > prefix.length);
|
|
5740
|
+
}
|
|
5741
|
+
var ManagedHookBlockPath = SafeRepoRelPath.refine(isCanonicalHookBlockPath, {
|
|
5742
|
+
message: `must start with one of ${CANONICAL_HOOK_BLOCK_PREFIXES.join(", ")} (byte-exact, case-sensitive)`
|
|
5743
|
+
});
|
|
5744
|
+
var ManagedBlockRecordSchema = z3.object({
|
|
5745
|
+
path: ManagedHookBlockPath,
|
|
5746
|
+
marker: z3.string(),
|
|
5747
|
+
comment: z3.enum(["hash", "html"])
|
|
5748
|
+
});
|
|
5749
|
+
var ManagedManifestSchema = z3.object({
|
|
5750
|
+
version: z3.number().int().positive(),
|
|
5751
|
+
files: z3.array(SafeRepoRelPath),
|
|
5752
|
+
blocks: z3.array(ManagedBlockRecordSchema),
|
|
5753
|
+
labels: z3.array(z3.string()),
|
|
5754
|
+
agents: z3.array(z3.enum(AGENT_VENDORS)).optional()
|
|
5755
|
+
});
|
|
5756
|
+
var ProjectEntrySchema = z3.object({
|
|
5757
|
+
name: z3.string().min(1),
|
|
5758
|
+
description: z3.string().min(1).optional(),
|
|
5759
|
+
path: z3.string().min(1).optional()
|
|
5760
|
+
});
|
|
5761
|
+
function parseTokensCollectDeclaration(value) {
|
|
5762
|
+
const m = value.trim().match(/^(\S+)\s+(\S+)$/);
|
|
5763
|
+
if (!m)
|
|
5764
|
+
return null;
|
|
5765
|
+
const [, interpreter, script] = m;
|
|
5766
|
+
if (!isSafeRepoRelPath(script))
|
|
5767
|
+
return null;
|
|
5768
|
+
return { interpreter, script };
|
|
5769
|
+
}
|
|
5770
|
+
var VinayaConfigSchema = z3.object({
|
|
5771
|
+
rings: z3.object({
|
|
5772
|
+
ring1_forgeWriteInterception: z3.boolean(),
|
|
5773
|
+
ring2_asyncAudits: z3.boolean()
|
|
5774
|
+
}).optional(),
|
|
5775
|
+
checks: z3.record(z3.string(), CheckEntrySchema).optional(),
|
|
5776
|
+
roles: z3.record(z3.string(), RoleEntrySchema).optional(),
|
|
5777
|
+
briefSchema: BriefSchemaSchema.optional(),
|
|
5778
|
+
managed: ManagedManifestSchema.optional(),
|
|
5779
|
+
principals: z3.array(z3.string()).min(1).optional(),
|
|
5780
|
+
releaseActor: z3.string().min(1).optional(),
|
|
5781
|
+
ci: z3.object({ setup: z3.string().min(1) }).optional(),
|
|
5782
|
+
tokens: z3.object({
|
|
5783
|
+
collect: z3.string().min(1).refine((v) => parseTokensCollectDeclaration(v) !== null, {
|
|
5784
|
+
message: 'tokens.collect must be exactly "<interpreter> <repo-relative-script-path>" — two whitespace-separated tokens, no flags, no shell syntax, no quoting'
|
|
5785
|
+
})
|
|
5786
|
+
}).optional(),
|
|
5787
|
+
blastRadius: z3.object({ extraDomains: z3.array(z3.string()).optional() }).optional(),
|
|
5788
|
+
proseGates: z3.object({
|
|
5789
|
+
doctrineRoot: z3.string().min(1).optional(),
|
|
5790
|
+
readerFacingPrefix: z3.string().min(1).optional(),
|
|
5791
|
+
readerFacingSuffix: z3.string().min(1).optional(),
|
|
5792
|
+
legacySlugDir: z3.string().min(1).optional()
|
|
5793
|
+
}).optional(),
|
|
5794
|
+
projects: z3.array(ProjectEntrySchema).optional(),
|
|
5795
|
+
dispatch: z3.object({
|
|
5796
|
+
timeoutMs: z3.number().int().positive().optional(),
|
|
5797
|
+
agent: z3.enum(["claude", "codex", "gemini"]).optional()
|
|
5798
|
+
}).optional()
|
|
5799
|
+
});
|
|
5800
|
+
var GLOBAL_VINAYA_HOME = join(homedir(), ".vinaya");
|
|
5801
|
+
var GLOBAL_CONFIG_PATH = join(GLOBAL_VINAYA_HOME, "config.json");
|
|
5802
|
+
var LOCAL_CONFIG_FILENAME = "vinaya.config.json";
|
|
5803
|
+
function findLocalConfig() {
|
|
5804
|
+
let dir = process.cwd();
|
|
5805
|
+
while (true) {
|
|
5806
|
+
const candidate = join(dir, LOCAL_CONFIG_FILENAME);
|
|
5807
|
+
if (existsSync(candidate))
|
|
5808
|
+
return candidate;
|
|
5809
|
+
if (existsSync(join(dir, ".git")))
|
|
5810
|
+
return null;
|
|
5811
|
+
const parent = dirname(dir);
|
|
5812
|
+
if (parent === dir)
|
|
5813
|
+
return null;
|
|
5814
|
+
dir = parent;
|
|
5815
|
+
}
|
|
5816
|
+
}
|
|
5817
|
+
function configPath() {
|
|
5818
|
+
const local = findLocalConfig();
|
|
5819
|
+
if (local)
|
|
5820
|
+
return local;
|
|
5821
|
+
if (existsSync(GLOBAL_CONFIG_PATH))
|
|
5822
|
+
return GLOBAL_CONFIG_PATH;
|
|
5823
|
+
return null;
|
|
5824
|
+
}
|
|
5825
|
+
function globalChecksIgnoredWarning(path) {
|
|
5826
|
+
return `${path}: "checks" registration in the global config is ignored — checks may only be registered from a repo-local vinaya.config.json.`;
|
|
5827
|
+
}
|
|
5828
|
+
function globalPrincipalsIgnoredWarning(path) {
|
|
5829
|
+
return `${path}: "principals" in the global config is ignored — principals may only be declared from a repo-local vinaya.config.json.`;
|
|
5830
|
+
}
|
|
5831
|
+
function globalRolesIgnoredWarning(path) {
|
|
5832
|
+
return `${path}: "roles" registration in the global config is ignored — roles may only be registered from a repo-local vinaya.config.json.`;
|
|
5833
|
+
}
|
|
5834
|
+
function globalReleaseActorIgnoredWarning(path) {
|
|
5835
|
+
return `${path}: "releaseActor" in the global config is ignored — releaseActor may only be declared from a repo-local vinaya.config.json.`;
|
|
5836
|
+
}
|
|
5837
|
+
function globalTokensCollectIgnoredWarning(path) {
|
|
5838
|
+
return `${path}: "tokens" in the global config is ignored — tokens.collect may only be declared from a repo-local vinaya.config.json.`;
|
|
5839
|
+
}
|
|
5840
|
+
function stripGlobalOnlyKeys(config, path) {
|
|
5841
|
+
if (path !== GLOBAL_CONFIG_PATH)
|
|
5842
|
+
return config;
|
|
5843
|
+
let result = config;
|
|
5844
|
+
if (result.checks && Object.keys(result.checks).length > 0) {
|
|
5845
|
+
console.error(`⚠ ${globalChecksIgnoredWarning(path)}`);
|
|
5846
|
+
result = { ...result, checks: undefined };
|
|
5847
|
+
}
|
|
5848
|
+
if (result.roles && Object.keys(result.roles).length > 0) {
|
|
5849
|
+
console.error(`⚠ ${globalRolesIgnoredWarning(path)}`);
|
|
5850
|
+
result = { ...result, roles: undefined };
|
|
5851
|
+
}
|
|
5852
|
+
if (result.principals && result.principals.length > 0) {
|
|
5853
|
+
console.error(`⚠ ${globalPrincipalsIgnoredWarning(path)}`);
|
|
5854
|
+
result = { ...result, principals: undefined };
|
|
5855
|
+
}
|
|
5856
|
+
if (result.releaseActor) {
|
|
5857
|
+
console.error(`⚠ ${globalReleaseActorIgnoredWarning(path)}`);
|
|
5858
|
+
result = { ...result, releaseActor: undefined };
|
|
5859
|
+
}
|
|
5860
|
+
if (result.tokens) {
|
|
5861
|
+
console.error(`⚠ ${globalTokensCollectIgnoredWarning(path)}`);
|
|
5862
|
+
result = { ...result, tokens: undefined };
|
|
5863
|
+
}
|
|
5864
|
+
return result;
|
|
5865
|
+
}
|
|
5866
|
+
function resolvePrincipalAllowlist(config) {
|
|
5867
|
+
return config?.principals ?? PRINCIPAL_ALLOWLIST;
|
|
5868
|
+
}
|
|
5869
|
+
function resolveReleaseActor(config) {
|
|
5870
|
+
return config?.releaseActor ?? DEFAULT_RELEASE_ACTOR;
|
|
5871
|
+
}
|
|
5872
|
+
function trustAnchorRepo() {
|
|
5873
|
+
const wellFormed = (slug) => /^[^/\s]+\/[^/\s]+$/.test(slug) ? slug : null;
|
|
5874
|
+
const fromRunner = process.env.GITHUB_REPOSITORY?.trim();
|
|
5875
|
+
if (fromRunner) {
|
|
5876
|
+
const validated = wellFormed(fromRunner);
|
|
5877
|
+
if (validated)
|
|
5878
|
+
return validated;
|
|
5879
|
+
}
|
|
5880
|
+
try {
|
|
5881
|
+
const url = execFileSync3("git", ["remote", "get-url", "origin"], {
|
|
5882
|
+
encoding: "utf-8",
|
|
5883
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
5884
|
+
}).trim();
|
|
5885
|
+
const m = url.match(/github\.com[:/]([^/]+)\/(.+?)(?:\.git)?\/?$/);
|
|
5886
|
+
return m?.[1] && m[2] ? wellFormed(`${m[1]}/${m[2]}`) : null;
|
|
5887
|
+
} catch {
|
|
5888
|
+
return null;
|
|
5889
|
+
}
|
|
5890
|
+
}
|
|
5891
|
+
function ghFetchTrustAnchorConfig() {
|
|
5892
|
+
const repo = trustAnchorRepo();
|
|
5893
|
+
if (!repo)
|
|
5894
|
+
throw new Error("could not resolve repo identity for the trust-anchor read");
|
|
5895
|
+
return execFileSync3("gh", ["api", `repos/${repo}/contents/${LOCAL_CONFIG_FILENAME}`, "--jq", ".content"], {
|
|
5896
|
+
encoding: "utf-8",
|
|
5897
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
5898
|
+
});
|
|
5899
|
+
}
|
|
5900
|
+
function firstLine(err) {
|
|
5901
|
+
return err?.message?.split(`
|
|
5902
|
+
`)[0] ?? "unknown error";
|
|
5903
|
+
}
|
|
5904
|
+
function isMissingFileError(err) {
|
|
5905
|
+
const stderr = err?.stderr;
|
|
5906
|
+
const haystack = [
|
|
5907
|
+
err?.message ?? "",
|
|
5908
|
+
typeof stderr === "string" ? stderr : stderr?.toString() ?? ""
|
|
5909
|
+
].join(`
|
|
5910
|
+
`);
|
|
5911
|
+
return /\b404\b|not found/i.test(haystack);
|
|
5912
|
+
}
|
|
5913
|
+
function loadTrustAnchorConfig(fetcher = ghFetchTrustAnchorConfig) {
|
|
5914
|
+
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.
|
|
5915
|
+
`);
|
|
5916
|
+
let base64;
|
|
5917
|
+
try {
|
|
5918
|
+
base64 = fetcher().trim();
|
|
5919
|
+
} catch (err) {
|
|
5920
|
+
if (!isMissingFileError(err))
|
|
5921
|
+
warn(firstLine(err));
|
|
5922
|
+
return null;
|
|
5923
|
+
}
|
|
5924
|
+
if (!base64)
|
|
5925
|
+
return null;
|
|
5926
|
+
try {
|
|
5927
|
+
const raw = Buffer.from(base64, "base64").toString("utf-8");
|
|
5928
|
+
const parsed = VinayaConfigSchema.safeParse(JSON.parse(raw));
|
|
5929
|
+
if (parsed.success)
|
|
5930
|
+
return parsed.data;
|
|
5931
|
+
warn("it did not satisfy the config schema");
|
|
5932
|
+
return null;
|
|
5933
|
+
} catch {
|
|
5934
|
+
warn("it was not readable as JSON");
|
|
5935
|
+
return null;
|
|
5936
|
+
}
|
|
5937
|
+
}
|
|
5938
|
+
function loadConfig() {
|
|
5939
|
+
const path = configPath();
|
|
5940
|
+
if (!path)
|
|
5941
|
+
return null;
|
|
5942
|
+
try {
|
|
5943
|
+
const raw = JSON.parse(readFileSync2(path, "utf-8"));
|
|
5944
|
+
return stripGlobalOnlyKeys(VinayaConfigSchema.parse(raw), path);
|
|
5945
|
+
} catch {
|
|
5946
|
+
return null;
|
|
5947
|
+
}
|
|
5948
|
+
}
|
|
5949
|
+
var TOKENS_COLLECT_TRUST_PATH = join(GLOBAL_VINAYA_HOME, "tokens-collect-trust.json");
|
|
5950
|
+
|
|
5951
|
+
// src/lib/dispatch-task.ts
|
|
5952
|
+
function contentAfterTwoLines(body) {
|
|
5953
|
+
const first = body.indexOf(`
|
|
5954
|
+
`);
|
|
5955
|
+
if (first === -1)
|
|
5956
|
+
return "";
|
|
5957
|
+
const second = body.indexOf(`
|
|
5958
|
+
`, first + 1);
|
|
5959
|
+
if (second === -1)
|
|
5960
|
+
return "";
|
|
5961
|
+
return body.slice(second + 1);
|
|
5962
|
+
}
|
|
5963
|
+
|
|
4448
5964
|
// src/checks/bin/check-brief-shape.ts
|
|
4449
5965
|
var CHECK_NAME = "brief-shape";
|
|
5966
|
+
var AEG_BRIEF_V1_MARKER = "<!-- aeg:brief:v1 -->";
|
|
5967
|
+
function fetchIssueComments(issueNumber) {
|
|
5968
|
+
const out = execFileSync4("gh", ["issue", "view", String(issueNumber), "--json", "comments"], {
|
|
5969
|
+
encoding: "utf8",
|
|
5970
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
5971
|
+
});
|
|
5972
|
+
return JSON.parse(out);
|
|
5973
|
+
}
|
|
5974
|
+
function resolveGradedBody(prBody, taskBranch) {
|
|
5975
|
+
if (!taskBranch)
|
|
5976
|
+
return { ok: true, body: prBody };
|
|
5977
|
+
const { issue } = extractIssue(prBody);
|
|
5978
|
+
if (issue === null) {
|
|
5979
|
+
return { ok: false, message: "not dispatched — no `Closes #N` in the PR body to resolve the task Issue." };
|
|
5980
|
+
}
|
|
5981
|
+
let json;
|
|
5982
|
+
try {
|
|
5983
|
+
json = fetchIssueComments(issue);
|
|
5984
|
+
} catch (err) {
|
|
5985
|
+
if (isIssueNotFoundError(err)) {
|
|
5986
|
+
return { ok: true, body: prBody };
|
|
5987
|
+
}
|
|
5988
|
+
return {
|
|
5989
|
+
ok: false,
|
|
5990
|
+
message: `could not fetch Issue #${issue}'s comments (\`gh issue view\`) to grade the dispatched brief: ${err instanceof Error ? err.message : String(err)}`
|
|
5991
|
+
};
|
|
5992
|
+
}
|
|
5993
|
+
const comment = json.comments.find((c) => c.body.split(`
|
|
5994
|
+
`)[0] === AEG_BRIEF_V1_MARKER);
|
|
5995
|
+
if (!comment) {
|
|
5996
|
+
return { ok: false, message: `not dispatched — no \`aeg:brief:v1\` comment on Issue #${issue}.` };
|
|
5997
|
+
}
|
|
5998
|
+
return { ok: true, body: contentAfterTwoLines(comment.body) };
|
|
5999
|
+
}
|
|
4450
6000
|
function listDirs(dir) {
|
|
4451
6001
|
try {
|
|
4452
6002
|
return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
@@ -4456,7 +6006,7 @@ function listDirs(dir) {
|
|
|
4456
6006
|
}
|
|
4457
6007
|
function readJson(path) {
|
|
4458
6008
|
try {
|
|
4459
|
-
return JSON.parse(
|
|
6009
|
+
return JSON.parse(readFileSync3(path, "utf8"));
|
|
4460
6010
|
} catch {
|
|
4461
6011
|
return {};
|
|
4462
6012
|
}
|
|
@@ -4470,6 +6020,37 @@ function buildConsumersOf2() {
|
|
|
4470
6020
|
const workspaces = Array.isArray(root.workspaces) ? root.workspaces : [];
|
|
4471
6021
|
return buildConsumersOf(workspaces, listDirs, readManifest);
|
|
4472
6022
|
}
|
|
6023
|
+
function fetchIssueBody(issueNumber) {
|
|
6024
|
+
return execFileSync4("gh", ["issue", "view", String(issueNumber), "--json", "body", "--jq", ".body"], {
|
|
6025
|
+
encoding: "utf8",
|
|
6026
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
6027
|
+
});
|
|
6028
|
+
}
|
|
6029
|
+
function resolveObjectivesApplicability(prBody, taskBranch) {
|
|
6030
|
+
if (!taskBranch) {
|
|
6031
|
+
if (!hasObjectivesHeading(prBody))
|
|
6032
|
+
return { applies: false };
|
|
6033
|
+
const own = objectivesOf(prBody);
|
|
6034
|
+
return { applies: true, objectives: own.ok ? own.objectives : [] };
|
|
6035
|
+
}
|
|
6036
|
+
const { issue } = extractIssue(prBody);
|
|
6037
|
+
if (issue === null || issue < OBJECTIVES_SINCE_ISSUE)
|
|
6038
|
+
return { applies: false };
|
|
6039
|
+
try {
|
|
6040
|
+
const parsed = objectivesOf(fetchIssueBody(issue));
|
|
6041
|
+
return parsed.ok ? { applies: true, objectives: parsed.objectives } : { applies: false };
|
|
6042
|
+
} catch (err) {
|
|
6043
|
+
if (isIssueNotFoundError(err)) {
|
|
6044
|
+
process.stdout.write(`brief-shape: Issue #${issue} does not resolve (\`gh issue view\`) — skipping the objectives checks for this run.
|
|
6045
|
+
`);
|
|
6046
|
+
return { applies: false };
|
|
6047
|
+
}
|
|
6048
|
+
return {
|
|
6049
|
+
applies: true,
|
|
6050
|
+
fetchError: `could not fetch Issue #${issue}'s body (\`gh issue view\`) to compare Objectives: ${err.message}`
|
|
6051
|
+
};
|
|
6052
|
+
}
|
|
6053
|
+
}
|
|
4473
6054
|
function main() {
|
|
4474
6055
|
const prBody = process.env.PR_BODY ?? "";
|
|
4475
6056
|
if (!prBody) {
|
|
@@ -4480,9 +6061,33 @@ function main() {
|
|
|
4480
6061
|
if (branch && !taskBranch && !isBriefShaped(prBody)) {
|
|
4481
6062
|
process.exit(0);
|
|
4482
6063
|
}
|
|
4483
|
-
const
|
|
6064
|
+
const gradedBodyResolution = resolveGradedBody(prBody, taskBranch);
|
|
6065
|
+
if (!gradedBodyResolution.ok) {
|
|
6066
|
+
emitCheckError({
|
|
6067
|
+
schema: CHECK_SCHEMA_VERSION,
|
|
6068
|
+
check: CHECK_NAME,
|
|
6069
|
+
severity: "error",
|
|
6070
|
+
message: `brief-shape: ${gradedBodyResolution.message}`,
|
|
6071
|
+
agent_recovery_prompt: "Dispatch this task with `vinaya task dispatch <tranche> <n>` (posts the `aeg:brief:v1` Issue comment), or check `gh auth status`/network, then re-run `vinaya check brief-shape`."
|
|
6072
|
+
});
|
|
6073
|
+
process.exit(1);
|
|
6074
|
+
}
|
|
6075
|
+
const gradedBody = gradedBodyResolution.body;
|
|
6076
|
+
const objectivesResolution = resolveObjectivesApplicability(prBody, taskBranch);
|
|
6077
|
+
if (objectivesResolution.applies && "fetchError" in objectivesResolution) {
|
|
6078
|
+
emitCheckError({
|
|
6079
|
+
schema: CHECK_SCHEMA_VERSION,
|
|
6080
|
+
check: CHECK_NAME,
|
|
6081
|
+
severity: "error",
|
|
6082
|
+
message: `brief-validation objectives copy: ${objectivesResolution.fetchError}`,
|
|
6083
|
+
agent_recovery_prompt: "Check `gh auth status` and network, then re-run `vinaya check brief-shape`."
|
|
6084
|
+
});
|
|
6085
|
+
process.exit(1);
|
|
6086
|
+
}
|
|
6087
|
+
const { errors } = checkBriefSections(gradedBody, readTierFromPrBody, {
|
|
4484
6088
|
requireClosesN: taskBranch,
|
|
4485
|
-
consumersOf: buildConsumersOf2()
|
|
6089
|
+
consumersOf: buildConsumersOf2(),
|
|
6090
|
+
issueObjectives: objectivesResolution.applies ? objectivesResolution.objectives : undefined
|
|
4486
6091
|
});
|
|
4487
6092
|
const parsedPrNumber = Number.parseInt(process.env.PR_NUMBER ?? "", 10);
|
|
4488
6093
|
const prNumber = Number.isInteger(parsedPrNumber) ? parsedPrNumber : null;
|