@attalabs/vinaya 0.25.0 → 0.27.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 +6 -4
- package/aeg-root/contracts/architect-planner.md +5 -5
- package/aeg-root/contracts/developer-reviewer.md +7 -7
- package/aeg-root/contracts/planner-developer.md +142 -0
- package/aeg-root/contracts/reviewer-archivist.md +2 -2
- package/aeg-root/contracts/tranche-archivist-planner.md +12 -11
- package/aeg-root/enforcement.md +17 -7
- package/aeg-root/milestone-model.md +2 -0
- package/aeg-root/process.md +53 -75
- package/aeg-root/roles/archivist.md +3 -3
- package/aeg-root/roles/developer.md +10 -10
- package/aeg-root/roles/planner.md +90 -23
- package/aeg-root/roles/principal.md +15 -13
- package/aeg-root/roles/reviewer.md +16 -12
- package/aeg-root/roles/security.md +13 -9
- package/aeg-root/roles/tranche-archivist.md +2 -2
- package/aeg-root/skills/aeg/SKILL.md +5 -5
- package/aeg-root/skills/aeg-roles/SKILL.md +7 -7
- package/aeg-root/state-machine.md +35 -34
- package/aeg-root/task-model.md +3 -3
- package/aeg-root/templates/brief-template.md +2 -2
- package/aeg-root/templates/issue-rationale-template.md +3 -3
- package/aeg-root/tranche-model.md +21 -21
- package/dist/checks/bin/check-body-bare-digits.js +233 -30
- package/dist/checks/bin/check-branch-topology.js +253 -32
- package/dist/checks/bin/check-brief-shape.js +291 -873
- package/dist/checks/bin/check-changeset-coverage.js +1012 -37
- package/dist/checks/bin/check-closes-n.js +253 -32
- package/dist/checks/bin/check-coherence.js +253 -32
- package/dist/checks/bin/check-dead-branch-push.js +215 -30
- package/dist/checks/bin/check-dispatch-readiness.js +256 -35
- package/dist/checks/bin/check-doc-coverage-push.js +1012 -37
- package/dist/checks/bin/check-doc-coverage.js +1014 -39
- package/dist/checks/bin/check-doctrine-no-procedures.js +215 -30
- package/dist/checks/bin/check-doctrine-portability.js +1012 -37
- package/dist/checks/bin/check-evidence-fresh.js +755 -71
- package/dist/checks/bin/check-exec-bits.js +1012 -37
- package/dist/checks/bin/check-first-push-dispatch.js +253 -32
- package/dist/checks/bin/check-issue-assignment.js +253 -32
- package/dist/checks/bin/check-main-branch-refusal.js +216 -31
- package/dist/checks/bin/check-no-disk-state.js +215 -30
- package/dist/checks/bin/check-pr-premise-reassert.js +5489 -0
- package/dist/checks/bin/check-pr-report-density.js +215 -30
- package/dist/checks/bin/check-quoted-command.js +1010 -35
- package/dist/checks/bin/check-reader-resolvable-prose.js +1010 -35
- package/dist/checks/bin/check-registry-gates.js +246 -32
- package/dist/checks/bin/check-retired-vocabulary.js +1010 -35
- package/dist/checks/bin/check-review-gate.js +274 -85
- package/dist/checks/bin/check-single-plan-pr.js +215 -30
- package/dist/checks/bin/check-surface-scope.js +5846 -0
- package/dist/checks/bin/check-test-plan.js +215 -30
- package/dist/checks/bin/check-token-collection-wired.js +215 -30
- package/dist/checks/bin/check-token-report.js +227 -37
- package/dist/checks/bin/check-workspace-escape.js +1010 -35
- package/dist/index.js +2407 -607
- package/package.json +1 -1
- package/aeg-root/contracts/brief-developer.md +0 -141
- package/aeg-root/contracts/planner-brief.md +0 -143
- package/aeg-root/roles/brief-author.md +0 -116
- package/aeg-root/skills/brief-authoring/SKILL.md +0 -509
|
@@ -30,6 +30,15 @@ var GATE_AUDIENCE = {
|
|
|
30
30
|
internal: "A composite that shells this repo's own `typecheck`/`lint`/`test`/`build` scripts by name. Those names are this monorepo's toolchain (bun + turbo + biome), not an adopter's; the shipped equivalent of its intent is `vinaya check --all`, which is portable by construction."
|
|
31
31
|
}
|
|
32
32
|
};
|
|
33
|
+
var NON_GATE_BINS = [
|
|
34
|
+
"archive-task",
|
|
35
|
+
"assign-task-issue",
|
|
36
|
+
"dead-branch-audit",
|
|
37
|
+
"eval-agent-compliance",
|
|
38
|
+
"open-issue",
|
|
39
|
+
"open-pr",
|
|
40
|
+
"report-tokens"
|
|
41
|
+
];
|
|
33
42
|
function isShipped(a) {
|
|
34
43
|
return "shippedAs" in a;
|
|
35
44
|
}
|
|
@@ -297,7 +306,7 @@ function issueListByLabelArgs(owner, repo, label) {
|
|
|
297
306
|
"--state",
|
|
298
307
|
"all",
|
|
299
308
|
"--json",
|
|
300
|
-
"number,title,body,state,labels,milestone",
|
|
309
|
+
"number,title,body,state,labels,milestone,stateReason",
|
|
301
310
|
"--limit",
|
|
302
311
|
"200"
|
|
303
312
|
];
|
|
@@ -390,7 +399,7 @@ var LABELS = [
|
|
|
390
399
|
id: "vinaya/needs:brief-correction",
|
|
391
400
|
category: "needs",
|
|
392
401
|
form: "literal",
|
|
393
|
-
carries: "Waiting on the
|
|
402
|
+
carries: "Waiting on the Planner — the brief contradicts the surface it describes."
|
|
394
403
|
},
|
|
395
404
|
{
|
|
396
405
|
key: "waiver-docs",
|
|
@@ -643,14 +652,19 @@ function resolveTaskIssueRef(title, labels) {
|
|
|
643
652
|
var INTENTS_HEADING = /^#{1,6}\s*Tranche intents\s*$/im;
|
|
644
653
|
var NEXT_HEADING = /^#{1,6}\s+\S/m;
|
|
645
654
|
var INTENT_BULLET = /^-\s+([a-z0-9][a-z0-9-]*)\s*:\s*(.+)$/i;
|
|
646
|
-
function
|
|
647
|
-
const text = stripCode(description, { inlineSpans: "keep" });
|
|
655
|
+
function intentsSection(text) {
|
|
648
656
|
const start = text.match(INTENTS_HEADING);
|
|
649
657
|
if (!start || start.index === undefined)
|
|
650
|
-
return
|
|
658
|
+
return null;
|
|
651
659
|
const rest = text.slice(start.index + start[0].length);
|
|
652
660
|
const next = rest.match(NEXT_HEADING);
|
|
653
|
-
|
|
661
|
+
return rest.slice(0, next && next.index !== undefined ? next.index : rest.length);
|
|
662
|
+
}
|
|
663
|
+
function intentGoalForSlug(description, slug) {
|
|
664
|
+
const text = stripCode(description, { inlineSpans: "keep" });
|
|
665
|
+
const section = intentsSection(text);
|
|
666
|
+
if (section === null)
|
|
667
|
+
return "";
|
|
654
668
|
for (const line of section.split(`
|
|
655
669
|
`)) {
|
|
656
670
|
const trimmed = line.trim();
|
|
@@ -1942,6 +1956,7 @@ function parseInlineFieldList(section) {
|
|
|
1942
1956
|
// ../../packages/aeg-core/src/verdict-extraction.ts
|
|
1943
1957
|
var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
|
|
1944
1958
|
var OBJECTIVES_VERSION_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Objectives version:\s*([0-9a-f]{64})(?![A-Za-z0-9])/im;
|
|
1959
|
+
var RULING_ORDINAL_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Ruling ordinal:\s*(\d+)(?!\d)/im;
|
|
1945
1960
|
function firstFiveLines(comment) {
|
|
1946
1961
|
return comment.split(`
|
|
1947
1962
|
`).slice(0, 5).join(`
|
|
@@ -1955,6 +1970,15 @@ function extractObjectivesVersion(comment) {
|
|
|
1955
1970
|
const m = firstFiveLines(comment).match(OBJECTIVES_VERSION_PATTERN);
|
|
1956
1971
|
return m ? m[1].toLowerCase() : null;
|
|
1957
1972
|
}
|
|
1973
|
+
function firstSevenLines(comment) {
|
|
1974
|
+
return comment.split(`
|
|
1975
|
+
`).slice(0, 7).join(`
|
|
1976
|
+
`);
|
|
1977
|
+
}
|
|
1978
|
+
function extractRulingOrdinal(comment) {
|
|
1979
|
+
const m = firstSevenLines(comment).match(RULING_ORDINAL_PATTERN);
|
|
1980
|
+
return m ? Number.parseInt(m[1], 10) : null;
|
|
1981
|
+
}
|
|
1958
1982
|
function extractVerdict(comments, valuePattern, missingLabel) {
|
|
1959
1983
|
const candidates = comments.filter((c) => valuePattern.test(c));
|
|
1960
1984
|
if (candidates.length === 0) {
|
|
@@ -1962,6 +1986,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
|
|
|
1962
1986
|
value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
|
|
1963
1987
|
headSha: null,
|
|
1964
1988
|
objectivesVersion: null,
|
|
1989
|
+
rulingOrdinal: null,
|
|
1965
1990
|
danglingNote: `no ${missingLabel} verdict comment found on this PR`
|
|
1966
1991
|
};
|
|
1967
1992
|
}
|
|
@@ -1972,6 +1997,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
|
|
|
1972
1997
|
value: `the most recent ${missingLabel} comment's VERDICT line is not within its first five lines — DANGLING, see below`,
|
|
1973
1998
|
headSha: null,
|
|
1974
1999
|
objectivesVersion: null,
|
|
2000
|
+
rulingOrdinal: null,
|
|
1975
2001
|
danglingNote: `the most recent ${missingLabel} verdict comment carries a VERDICT-shaped line outside the first-five-line read window`
|
|
1976
2002
|
};
|
|
1977
2003
|
}
|
|
@@ -1979,6 +2005,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
|
|
|
1979
2005
|
value: m[1].toUpperCase().replace(/[_-]/g, " "),
|
|
1980
2006
|
headSha: extractHeadSha(latest),
|
|
1981
2007
|
objectivesVersion: extractObjectivesVersion(latest),
|
|
2008
|
+
rulingOrdinal: extractRulingOrdinal(latest),
|
|
1982
2009
|
danglingNote: null
|
|
1983
2010
|
};
|
|
1984
2011
|
}
|
|
@@ -2340,6 +2367,9 @@ import { createHash as createHash2 } from "node:crypto";
|
|
|
2340
2367
|
var HEADING_RE = /^##[ \t]*Objectives[ \t]*$/im;
|
|
2341
2368
|
var NEXT_HEADING_RE = /^##[ \t]/m;
|
|
2342
2369
|
var OBJECTIVE_LINE_RE = /^O(\d+)\.[ \t]*(.*)$/;
|
|
2370
|
+
function maskedForHeadingSearch(body) {
|
|
2371
|
+
return maskDetailsBlocks(maskCode(body));
|
|
2372
|
+
}
|
|
2343
2373
|
function hasBacktickedPath(text) {
|
|
2344
2374
|
let i = 0;
|
|
2345
2375
|
while (i < text.length) {
|
|
@@ -2362,16 +2392,22 @@ function stripObjectiveBackticks(text) {
|
|
|
2362
2392
|
function wordCount(text) {
|
|
2363
2393
|
return text.split(/\s+/).filter((w) => /[a-z]/i.test(w)).length;
|
|
2364
2394
|
}
|
|
2365
|
-
function
|
|
2366
|
-
const
|
|
2395
|
+
function objectivesSectionBounds(body) {
|
|
2396
|
+
const masked = maskedForHeadingSearch(body);
|
|
2397
|
+
const heading = HEADING_RE.exec(masked);
|
|
2367
2398
|
if (!heading)
|
|
2368
2399
|
return null;
|
|
2369
|
-
const
|
|
2400
|
+
const start = heading.index + heading[0].length;
|
|
2401
|
+
const afterHeading = masked.slice(start);
|
|
2370
2402
|
const next = NEXT_HEADING_RE.exec(afterHeading);
|
|
2371
|
-
return next ?
|
|
2403
|
+
return { start, end: next ? start + next.index : body.length };
|
|
2404
|
+
}
|
|
2405
|
+
function objectivesSectionText(body) {
|
|
2406
|
+
const bounds = objectivesSectionBounds(body);
|
|
2407
|
+
return bounds === null ? null : body.slice(bounds.start, bounds.end);
|
|
2372
2408
|
}
|
|
2373
2409
|
function hasObjectivesHeading(body) {
|
|
2374
|
-
return HEADING_RE.test(body);
|
|
2410
|
+
return HEADING_RE.test(maskedForHeadingSearch(body));
|
|
2375
2411
|
}
|
|
2376
2412
|
function objectivesOf(body) {
|
|
2377
2413
|
const section = objectivesSectionText(body);
|
|
@@ -2397,7 +2433,7 @@ function objectivesOf(body) {
|
|
|
2397
2433
|
continue;
|
|
2398
2434
|
}
|
|
2399
2435
|
if (hasBacktickedPath(text) && wordCount(stripObjectiveBackticks(text)) < MIN_WORDS_OUTSIDE_BACKTICKS) {
|
|
2400
|
-
errors.push(`O${n} is little more than a file path — an objective states an observable outcome, never a bare path (the
|
|
2436
|
+
errors.push(`O${n} is little more than a file path — an objective states an observable outcome, never a bare path (the Planner maps it to files).`);
|
|
2401
2437
|
continue;
|
|
2402
2438
|
}
|
|
2403
2439
|
objectives.push({ id: `O${n}`, text });
|
|
@@ -2431,6 +2467,15 @@ function isIssueNotFoundError(err) {
|
|
|
2431
2467
|
`);
|
|
2432
2468
|
return /could not resolve to an (?:issue|pull request)|\b404\b|not found/i.test(haystack);
|
|
2433
2469
|
}
|
|
2470
|
+
function resolveObjectivesSource(prBody, issue, cutoverIssue) {
|
|
2471
|
+
if (issue !== null && issue < cutoverIssue)
|
|
2472
|
+
return { kind: "none" };
|
|
2473
|
+
if (issue !== null)
|
|
2474
|
+
return { kind: "issue", issue };
|
|
2475
|
+
if (hasObjectivesHeading(prBody))
|
|
2476
|
+
return { kind: "body" };
|
|
2477
|
+
return { kind: "none" };
|
|
2478
|
+
}
|
|
2434
2479
|
|
|
2435
2480
|
// ../../packages/aeg-core/src/premise-check.ts
|
|
2436
2481
|
import { createHash as createHash3 } from "node:crypto";
|
|
@@ -2609,18 +2654,14 @@ function checkTestPlanExclusivity(prBody) {
|
|
|
2609
2654
|
function checkPrincipalPlaceholder(prBody) {
|
|
2610
2655
|
const region = testPlanRegion(prBody);
|
|
2611
2656
|
const lineRe = /^-\s*\[[ xX]\]\s*\*{2}\[principal\]\*{2}(.*)$/gim;
|
|
2657
|
+
const errors = [];
|
|
2612
2658
|
for (const m of region.matchAll(lineRe)) {
|
|
2613
2659
|
const content = m[1] ?? "";
|
|
2614
2660
|
if (/^\s*None\b/i.test(content)) {
|
|
2615
|
-
|
|
2616
|
-
status: "fail",
|
|
2617
|
-
errors: [
|
|
2618
|
-
'brief-validation Test Plan shape: a `**[principal]**` checkbox item is a "None" placeholder — if there is no principal-runnable surface, omit the item entirely; an untickable placeholder box blocks the merge gate forever.'
|
|
2619
|
-
]
|
|
2620
|
-
};
|
|
2661
|
+
errors.push(`brief-validation Test Plan shape: a \`**[principal]**\` checkbox item is a "None" placeholder ("${content.trim()}") — if there is no principal-runnable surface, omit the item entirely; an untickable placeholder box blocks the merge gate forever.`);
|
|
2621
2662
|
}
|
|
2622
2663
|
}
|
|
2623
|
-
return { status: "pass", errors: [] };
|
|
2664
|
+
return errors.length > 0 ? { status: "fail", errors } : { status: "pass", errors: [] };
|
|
2624
2665
|
}
|
|
2625
2666
|
function checkSurfaceMap(prBody) {
|
|
2626
2667
|
return headingCheck(prBody, "(?:technical\\s+)?surface map", "Technical surface map");
|
|
@@ -2936,6 +2977,41 @@ function checkBriefSections(prBody, readTier, options = {}) {
|
|
|
2936
2977
|
];
|
|
2937
2978
|
return { errors: results.flatMap((r) => r.errors) };
|
|
2938
2979
|
}
|
|
2980
|
+
function contentAfterNLines(body, n) {
|
|
2981
|
+
let idx = -1;
|
|
2982
|
+
for (let i = 0;i < n; i++) {
|
|
2983
|
+
idx = body.indexOf(`
|
|
2984
|
+
`, idx + 1);
|
|
2985
|
+
if (idx === -1)
|
|
2986
|
+
return "";
|
|
2987
|
+
}
|
|
2988
|
+
return body.slice(idx + 1);
|
|
2989
|
+
}
|
|
2990
|
+
var BRIEF_MARKER_LINE_RE = /^<!-- aeg:brief:v(\d+) -->$/;
|
|
2991
|
+
function parseBriefMarkerVersion(firstLine) {
|
|
2992
|
+
const m = BRIEF_MARKER_LINE_RE.exec(firstLine.trim());
|
|
2993
|
+
if (!m)
|
|
2994
|
+
return null;
|
|
2995
|
+
const version = Number.parseInt(m[1], 10);
|
|
2996
|
+
return Number.isInteger(version) && version >= 1 ? version : null;
|
|
2997
|
+
}
|
|
2998
|
+
function frozenBriefContent(body, version) {
|
|
2999
|
+
return contentAfterNLines(body, version === 1 ? 2 : 3);
|
|
3000
|
+
}
|
|
3001
|
+
function resolveNewestFrozenBrief(comments, allowlist) {
|
|
3002
|
+
let best = null;
|
|
3003
|
+
for (const c of comments) {
|
|
3004
|
+
if (!isPrincipal(c.author, allowlist))
|
|
3005
|
+
continue;
|
|
3006
|
+
const version = parseBriefMarkerVersion(c.body.split(`
|
|
3007
|
+
`)[0] ?? "");
|
|
3008
|
+
if (version === null)
|
|
3009
|
+
continue;
|
|
3010
|
+
if (best === null || version > best.version)
|
|
3011
|
+
best = { ...c, version };
|
|
3012
|
+
}
|
|
3013
|
+
return best === null ? null : { ...best, content: frozenBriefContent(best.body, best.version) };
|
|
3014
|
+
}
|
|
2939
3015
|
// ../../packages/aeg-core/src/doctrine-portability.ts
|
|
2940
3016
|
var DEFAULT_SHIPS_PREFIX = "aeg-root/";
|
|
2941
3017
|
var STATIC_PORTABLE_PREFIXES = [
|
|
@@ -3178,6 +3254,66 @@ function checkIssueObjectives(body, issueNumber) {
|
|
|
3178
3254
|
return { status: "pass", errors: [] };
|
|
3179
3255
|
return { status: "fail", errors: result.errors.map((e) => `issue-validation objectives: ${e}`) };
|
|
3180
3256
|
}
|
|
3257
|
+
function topLevelSectionText(body, headingName) {
|
|
3258
|
+
const headingRe = new RegExp(`^##[ \\t]*${headingName}[ \\t]*$`, "im");
|
|
3259
|
+
const heading = headingRe.exec(body);
|
|
3260
|
+
if (!heading)
|
|
3261
|
+
return null;
|
|
3262
|
+
const afterHeading = body.slice(heading.index + heading[0].length);
|
|
3263
|
+
const next = /^##[ \t]/m.exec(afterHeading);
|
|
3264
|
+
return next ? afterHeading.slice(0, next.index) : afterHeading;
|
|
3265
|
+
}
|
|
3266
|
+
function looksLikeFilePath(entry2) {
|
|
3267
|
+
const stripped = entry2.replace(/\/\*\*?$/, "");
|
|
3268
|
+
const lastSegment = stripped.split("/").pop() ?? stripped;
|
|
3269
|
+
const extMatch = /\.[A-Za-z0-9]{1,6}$/.exec(lastSegment);
|
|
3270
|
+
return extMatch !== null && extMatch.index > 0;
|
|
3271
|
+
}
|
|
3272
|
+
function splitGlobList(raw) {
|
|
3273
|
+
return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
3274
|
+
}
|
|
3275
|
+
function parseIssueSurface(body) {
|
|
3276
|
+
const section = topLevelSectionText(body, "Surface");
|
|
3277
|
+
if (section === null)
|
|
3278
|
+
return { ok: false, errors: ["no `## Surface` heading found in the body."] };
|
|
3279
|
+
const inLine = /^in:\s*(.+)$/im.exec(section);
|
|
3280
|
+
const outLine = /^out:\s*(.+)$/im.exec(section);
|
|
3281
|
+
const errors = [];
|
|
3282
|
+
if (!inLine)
|
|
3283
|
+
errors.push("`## Surface` has no `in:` line.");
|
|
3284
|
+
if (!outLine)
|
|
3285
|
+
errors.push("`## Surface` has no `out:` line.");
|
|
3286
|
+
if (errors.length > 0)
|
|
3287
|
+
return { ok: false, errors };
|
|
3288
|
+
const inGlobs = splitGlobList(inLine[1]);
|
|
3289
|
+
const outGlobs = splitGlobList(outLine[1]);
|
|
3290
|
+
for (const g of [...inGlobs, ...outGlobs]) {
|
|
3291
|
+
if (looksLikeFilePath(g)) {
|
|
3292
|
+
errors.push(`\`${g}\` in \`## Surface\` looks like a file path — Surface entries are directory-level globs, never file paths.`);
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
if (inGlobs.length === 0)
|
|
3296
|
+
errors.push("`## Surface`'s `in:` line resolved to zero globs.");
|
|
3297
|
+
if (errors.length > 0)
|
|
3298
|
+
return { ok: false, errors };
|
|
3299
|
+
return { ok: true, value: { in: inGlobs, out: outGlobs } };
|
|
3300
|
+
}
|
|
3301
|
+
function globCoversPath(glob, path) {
|
|
3302
|
+
const g = glob.replace(/\/\*\*?$/, "").replace(/\/+$/, "");
|
|
3303
|
+
const p = path.replace(/\/+$/, "");
|
|
3304
|
+
return g === p || g.startsWith(`${p}/`) || p.startsWith(`${g}/`);
|
|
3305
|
+
}
|
|
3306
|
+
function checkSurfaceScope(changedFiles, outGlobs) {
|
|
3307
|
+
if (outGlobs.length === 0)
|
|
3308
|
+
return { ok: true };
|
|
3309
|
+
const violations = [];
|
|
3310
|
+
for (const file of changedFiles) {
|
|
3311
|
+
const glob = outGlobs.find((g) => globCoversPath(g, file));
|
|
3312
|
+
if (glob)
|
|
3313
|
+
violations.push({ file, glob });
|
|
3314
|
+
}
|
|
3315
|
+
return violations.length > 0 ? { ok: false, violations } : { ok: true };
|
|
3316
|
+
}
|
|
3181
3317
|
function isTaskIssueLabelSet(labels) {
|
|
3182
3318
|
return hasLabel("tranche", labels);
|
|
3183
3319
|
}
|
|
@@ -3242,6 +3378,8 @@ function checkProjectsRegistered(body, _labels, registeredNames) {
|
|
|
3242
3378
|
}
|
|
3243
3379
|
return errors.length > 0 ? { status: "fail", errors } : { status: "pass", errors: [] };
|
|
3244
3380
|
}
|
|
3381
|
+
var DOC_PATH_RE = /(?:(?:aeg-root|apps|packages|specs|docs|tools|\.claude|\.github)\/[\w./@-]*\.(?:md|mdx)|\.claude\/(?:skills|rules)\/[\w./-]+|\b(?:docs-index|decisions|projects|state-machine|enforcement|process|README|CLAUDE)\.md\b|\b[\w-]+-(?:spec|decisions|backlog)\.md\b)/i;
|
|
3382
|
+
var DOC_PATH_RE_GLOBAL = new RegExp(DOC_PATH_RE.source, "gi");
|
|
3245
3383
|
|
|
3246
3384
|
// ../../packages/aeg-core/src/coherence-checks.ts
|
|
3247
3385
|
var COHERENCE_ENFORCED_FROM = "2026-07-01";
|
|
@@ -3599,6 +3737,11 @@ function isBoundToObjectives(extraction, currentVersion) {
|
|
|
3599
3737
|
return true;
|
|
3600
3738
|
return extraction.objectivesVersion === currentVersion;
|
|
3601
3739
|
}
|
|
3740
|
+
function isBoundToRulings(extraction, currentOrdinal) {
|
|
3741
|
+
if (extraction.rulingOrdinal === null)
|
|
3742
|
+
return currentOrdinal === 0;
|
|
3743
|
+
return extraction.rulingOrdinal === currentOrdinal;
|
|
3744
|
+
}
|
|
3602
3745
|
function checkReviewGate(input) {
|
|
3603
3746
|
const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
|
|
3604
3747
|
const waived = isWaiverLabelActorVerified({
|
|
@@ -3627,7 +3770,9 @@ function checkReviewGate(input) {
|
|
|
3627
3770
|
const securityBound = isBoundToPatch(security, input.headSha, input.patchIdOf);
|
|
3628
3771
|
const codeReviewObjectivesBound = isBoundToObjectives(codeReview, input.objectivesVersion);
|
|
3629
3772
|
const securityObjectivesBound = isBoundToObjectives(security, input.objectivesVersion);
|
|
3630
|
-
|
|
3773
|
+
const codeReviewRulingsBound = isBoundToRulings(codeReview, input.rulingOrdinal);
|
|
3774
|
+
const securityRulingsBound = isBoundToRulings(security, input.rulingOrdinal);
|
|
3775
|
+
if (codeReviewClean && codeReviewBound && codeReviewObjectivesBound && codeReviewRulingsBound && securityClean && securityBound && securityObjectivesBound && securityRulingsBound && mechanicalChecksClean) {
|
|
3631
3776
|
return {
|
|
3632
3777
|
verdict: "pass",
|
|
3633
3778
|
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.`,
|
|
@@ -3641,6 +3786,8 @@ function checkReviewGate(input) {
|
|
|
3641
3786
|
problems.push(`the newest code-review verdict covers ${codeReview.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
|
|
3642
3787
|
} else if (!codeReviewObjectivesBound) {
|
|
3643
3788
|
problems.push(`the newest code-review verdict was cast against objectives version ${codeReview.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
|
|
3789
|
+
} else if (!codeReviewRulingsBound) {
|
|
3790
|
+
problems.push(`the newest code-review verdict was cast against ruling ordinal ${codeReview.rulingOrdinal ?? "none"}, a newer ruling (ruling ${input.rulingOrdinal}) is now posted on this PR`);
|
|
3644
3791
|
}
|
|
3645
3792
|
if (!securityClean) {
|
|
3646
3793
|
problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
|
|
@@ -3648,6 +3795,8 @@ function checkReviewGate(input) {
|
|
|
3648
3795
|
problems.push(`the newest security-review verdict covers ${security.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
|
|
3649
3796
|
} else if (!securityObjectivesBound) {
|
|
3650
3797
|
problems.push(`the newest security-review verdict was cast against objectives version ${security.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
|
|
3798
|
+
} else if (!securityRulingsBound) {
|
|
3799
|
+
problems.push(`the newest security-review verdict was cast against ruling ordinal ${security.rulingOrdinal ?? "none"}, a newer ruling (ruling ${input.rulingOrdinal}) is now posted on this PR`);
|
|
3651
3800
|
}
|
|
3652
3801
|
if (!mechanicalChecksClean) {
|
|
3653
3802
|
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(", ")}`);
|
|
@@ -3664,6 +3813,24 @@ var DEFAULT_RELEASE_ACTOR = "github-actions[bot]";
|
|
|
3664
3813
|
function isChangesetsReleasePr(branch, author, expectedAuthor) {
|
|
3665
3814
|
return branch === CHANGESET_RELEASE_BRANCH && author === expectedAuthor;
|
|
3666
3815
|
}
|
|
3816
|
+
// ../../packages/aeg-core/src/ruling-ordinal.ts
|
|
3817
|
+
var RULING_MARKER_ORDINAL = /^<!-- aeg:principal:ruling:\d+-(\d+) -->$/;
|
|
3818
|
+
function newestPrincipalRulingOrdinal(comments, allowlist) {
|
|
3819
|
+
let best = 0;
|
|
3820
|
+
for (const c of comments) {
|
|
3821
|
+
if (!isPrincipal(c.author, allowlist))
|
|
3822
|
+
continue;
|
|
3823
|
+
const firstLine = (c.body.split(`
|
|
3824
|
+
`)[0] ?? "").trim();
|
|
3825
|
+
const m = RULING_MARKER_ORDINAL.exec(firstLine);
|
|
3826
|
+
if (!m)
|
|
3827
|
+
continue;
|
|
3828
|
+
const k = Number.parseInt(m[1], 10);
|
|
3829
|
+
if (k > best)
|
|
3830
|
+
best = k;
|
|
3831
|
+
}
|
|
3832
|
+
return best;
|
|
3833
|
+
}
|
|
3667
3834
|
// ../../packages/aeg-core/src/markdown-table.ts
|
|
3668
3835
|
function splitRow(line) {
|
|
3669
3836
|
const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
|
|
@@ -3771,16 +3938,33 @@ function checkG1(rows, existsFn) {
|
|
|
3771
3938
|
return { check: "G1", status: findings.length > 0 ? "fail" : "pass", findings };
|
|
3772
3939
|
}
|
|
3773
3940
|
var SCAFFOLD_PLACEHOLDER = "[undocumented — fill in why]";
|
|
3941
|
+
var NON_GATE_BIN_NAMES = NON_GATE_BINS;
|
|
3942
|
+
var TWIN_CANDIDATE_EXCEPTIONS = {
|
|
3943
|
+
"apps/cli/src/checks/bin/check-dead-branch-push.ts": 'Ring 0 "Pushing to a branch whose pull request already resolved" (implementation: check-push-target.ts)',
|
|
3944
|
+
"apps/cli/src/checks/bin/check-issue-assignment.ts": `Ring 0 "A task branch's first push (Issue self-assignment)" (implementation: assign-task-issue.ts)`,
|
|
3945
|
+
"apps/cli/src/checks/bin/check-closes-n.ts": 'Ring 1 "Closes linkage" (implementation: verify-coherence.ts)'
|
|
3946
|
+
};
|
|
3947
|
+
var NON_GATE_HOOK_SCRIPTS = [".claude/hooks/track-transcript.sh"];
|
|
3774
3948
|
function checkG2(rows, candidateFiles) {
|
|
3775
3949
|
const implementations = new Set(rows.map((r) => r.implementation).filter((p) => p !== ""));
|
|
3950
|
+
const claimedNames = new Set(rows.flatMap((r) => claimedCheckNames(r.implementation)));
|
|
3776
3951
|
const findings = [];
|
|
3777
3952
|
for (const path of candidateFiles) {
|
|
3778
|
-
if (
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3953
|
+
if (implementations.has(path))
|
|
3954
|
+
continue;
|
|
3955
|
+
if (path.startsWith(AEG_CORE_BIN_PATH_PREFIX) && NON_GATE_BIN_NAMES.includes(basenameNoExt(path)))
|
|
3956
|
+
continue;
|
|
3957
|
+
if (NON_GATE_HOOK_SCRIPTS.includes(path))
|
|
3958
|
+
continue;
|
|
3959
|
+
if (path in TWIN_CANDIDATE_EXCEPTIONS)
|
|
3960
|
+
continue;
|
|
3961
|
+
const candidateNames = claimedCheckNames(path);
|
|
3962
|
+
if (candidateNames.length > 0 && candidateNames.some((n) => claimedNames.has(n)))
|
|
3963
|
+
continue;
|
|
3964
|
+
findings.push({
|
|
3965
|
+
path,
|
|
3966
|
+
reason: `"${path}" is not named as the implementation of any row in enforcement.md's ring tables`
|
|
3967
|
+
});
|
|
3784
3968
|
}
|
|
3785
3969
|
for (const row of rows) {
|
|
3786
3970
|
const carriesPlaceholder = [row.summary, row.description, row.spec].some((cell) => cell === SCAFFOLD_PLACEHOLDER);
|
|
@@ -3792,7 +3976,7 @@ function checkG2(rows, candidateFiles) {
|
|
|
3792
3976
|
});
|
|
3793
3977
|
}
|
|
3794
3978
|
}
|
|
3795
|
-
return { check: "G2", status: findings.length > 0 ? "
|
|
3979
|
+
return { check: "G2", status: findings.length > 0 ? "fail" : "pass", findings };
|
|
3796
3980
|
}
|
|
3797
3981
|
function checkG3(ring0Rows, crossingFiles) {
|
|
3798
3982
|
const ring0Implementations = new Set(ring0Rows.map((r) => r.implementation));
|
|
@@ -4668,7 +4852,6 @@ function findWorkspaceEscapes(files, knownPaths, workspaceDirs = DEFAULT_WORKSPA
|
|
|
4668
4852
|
import { z } from "zod";
|
|
4669
4853
|
var ROLE_VALUES = [
|
|
4670
4854
|
"planner",
|
|
4671
|
-
"brief-author",
|
|
4672
4855
|
"developer",
|
|
4673
4856
|
"code-reviewer",
|
|
4674
4857
|
"security",
|
|
@@ -4742,18 +4925,20 @@ var dispatchShared = {
|
|
|
4742
4925
|
round: z.number().int().optional(),
|
|
4743
4926
|
effect_id: z.string()
|
|
4744
4927
|
};
|
|
4928
|
+
var dispatchUsageField = z.object({ input: z.number().nonnegative(), output: z.number().nonnegative() }).strict().nullable();
|
|
4745
4929
|
var DispatchEventSchema = z.discriminatedUnion("event", [
|
|
4746
4930
|
z.object({ ...dispatchShared, event: z.literal("dispatched"), prompt_hash: z.string() }).strict(),
|
|
4747
4931
|
z.object({
|
|
4748
4932
|
...dispatchShared,
|
|
4749
4933
|
event: z.literal("outcome_received"),
|
|
4750
4934
|
outcome: DispatchOutcomeSchema,
|
|
4751
|
-
usage:
|
|
4935
|
+
usage: dispatchUsageField
|
|
4752
4936
|
}).strict(),
|
|
4753
4937
|
z.object({
|
|
4754
4938
|
...dispatchShared,
|
|
4755
4939
|
event: z.literal("dispatch_failed"),
|
|
4756
|
-
reason: z.enum(["timeout", "crash", "refused", "unattributed_write"])
|
|
4940
|
+
reason: z.enum(["timeout", "crash", "refused", "unattributed_write"]),
|
|
4941
|
+
usage: dispatchUsageField
|
|
4757
4942
|
}).strict()
|
|
4758
4943
|
]);
|
|
4759
4944
|
var loopShared = {
|
|
@@ -5000,14 +5185,41 @@ var COMMANDS = [
|
|
|
5000
5185
|
},
|
|
5001
5186
|
{
|
|
5002
5187
|
name: "task dispatch",
|
|
5003
|
-
description: "
|
|
5188
|
+
description: "Deprecated — render, pin, and post the brief on the Issue as the frozen original; start the developer",
|
|
5004
5189
|
flags: [
|
|
5005
5190
|
{ flag: "--agent <claude|codex|gemini>", description: "Start the developer through dispatchRole once posted" }
|
|
5006
5191
|
],
|
|
5007
5192
|
details: [
|
|
5008
5193
|
"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.",
|
|
5009
5194
|
"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.",
|
|
5010
|
-
"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."
|
|
5195
|
+
"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.",
|
|
5196
|
+
"Deprecated in favor of `task brief` (preparation only) and `task run` (the full unattended loop) — kept for a documented compatibility window while callers migrate."
|
|
5197
|
+
],
|
|
5198
|
+
status: "shipped"
|
|
5199
|
+
},
|
|
5200
|
+
{
|
|
5201
|
+
name: "task brief",
|
|
5202
|
+
description: "Render and freeze the brief as the Issue's original comment — preparation only, starts nobody",
|
|
5203
|
+
details: [
|
|
5204
|
+
"The preparation half of `task dispatch`, extracted so it is callable on its own: resolves the task's Issue, renders the brief, refuses on any gap, refuses when a frozen brief already exists, and posts it once as the same `aeg:brief:v1` Issue comment `task dispatch` posts. Starts no agent under any circumstances — there is no `--agent` flag here at all.",
|
|
5205
|
+
"Successor to `task dispatch` for the preparation step; the full unattended run (preparation, then the developer, then the review loop) is `task run`."
|
|
5206
|
+
],
|
|
5207
|
+
status: "shipped"
|
|
5208
|
+
},
|
|
5209
|
+
{
|
|
5210
|
+
name: "task run",
|
|
5211
|
+
description: "One command from a planned Issue to a reviewed pull request — exactly one developer started",
|
|
5212
|
+
flags: [
|
|
5213
|
+
{
|
|
5214
|
+
flag: "--agent <claude|codex|gemini>",
|
|
5215
|
+
description: "Vendor for the developer and both reviewers this run dispatches"
|
|
5216
|
+
}
|
|
5217
|
+
],
|
|
5218
|
+
details: [
|
|
5219
|
+
"Composes `task brief`'s own preparation (`prepareTask`) with `dev-review-loop` (`devReviewLoop`) — nothing else. Preparation starts no agent; the loop's own round 1 reads the frozen brief off the Issue and is the only place a developer is ever dispatched from a fresh task, so exactly one developer is started by construction.",
|
|
5220
|
+
"A brief already frozen on the Issue is reused, never re-posted — the second `task dispatch`/`task brief` call this composes around does not fail the whole run, it just skips straight to running the loop. A task whose Issue refuses preparation (a missing brief section, an unmet dispatch gate) is refused before any agent starts, with nothing posted.",
|
|
5221
|
+
"Refuses when the frozen brief's developer branch already has an open pull request — the old `task dispatch` followed by `task run` cannot start two developers this way.",
|
|
5222
|
+
"Exit and printed summary distinguish a published, reviewed pull request (exit `0`, the PR URL) from a pause (exit `1`, with the exact `vinaya dev-review-loop --resume <pr>` command to continue), a usage/argv error (exit `2`), and any other failure (exit `3`) — never sharing `1` with a pause, so an unattended host tells them apart from the exit code alone. A run that pauses is resumed with the loop's own existing `--resume <pr>` flag, never a flag on this command."
|
|
5011
5223
|
],
|
|
5012
5224
|
status: "shipped"
|
|
5013
5225
|
},
|
|
@@ -5215,6 +5427,15 @@ var COMMANDS = [
|
|
|
5215
5427
|
],
|
|
5216
5428
|
status: "shipped"
|
|
5217
5429
|
},
|
|
5430
|
+
{
|
|
5431
|
+
name: "milestone status",
|
|
5432
|
+
description: "Print each of a Milestone's declared tranche intents with its derived lifecycle and issue counts",
|
|
5433
|
+
flags: [{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }],
|
|
5434
|
+
details: [
|
|
5435
|
+
"Read-only — writes nothing. For each `- <slug>: …` line in the Milestone's `### Tranche intents` section, prints the tranche's lifecycle (`planned`/`active`/`complete`) and its labeled Issues' counts (merged, open, not planned), all derived from the forge. Refuses if `<n>` isn't a real Milestone in this repo, or if the forge is unreachable."
|
|
5436
|
+
],
|
|
5437
|
+
status: "shipped"
|
|
5438
|
+
},
|
|
5218
5439
|
{
|
|
5219
5440
|
name: "review status",
|
|
5220
5441
|
description: "Print the review loop's own state for a PR, and its branch's distance from the base",
|