@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.
Files changed (60) hide show
  1. package/README.md +6 -4
  2. package/aeg-root/contracts/architect-planner.md +5 -5
  3. package/aeg-root/contracts/developer-reviewer.md +7 -7
  4. package/aeg-root/contracts/planner-developer.md +142 -0
  5. package/aeg-root/contracts/reviewer-archivist.md +2 -2
  6. package/aeg-root/contracts/tranche-archivist-planner.md +12 -11
  7. package/aeg-root/enforcement.md +17 -7
  8. package/aeg-root/milestone-model.md +2 -0
  9. package/aeg-root/process.md +53 -75
  10. package/aeg-root/roles/archivist.md +3 -3
  11. package/aeg-root/roles/developer.md +10 -10
  12. package/aeg-root/roles/planner.md +90 -23
  13. package/aeg-root/roles/principal.md +15 -13
  14. package/aeg-root/roles/reviewer.md +16 -12
  15. package/aeg-root/roles/security.md +13 -9
  16. package/aeg-root/roles/tranche-archivist.md +2 -2
  17. package/aeg-root/skills/aeg/SKILL.md +5 -5
  18. package/aeg-root/skills/aeg-roles/SKILL.md +7 -7
  19. package/aeg-root/state-machine.md +35 -34
  20. package/aeg-root/task-model.md +3 -3
  21. package/aeg-root/templates/brief-template.md +2 -2
  22. package/aeg-root/templates/issue-rationale-template.md +3 -3
  23. package/aeg-root/tranche-model.md +21 -21
  24. package/dist/checks/bin/check-body-bare-digits.js +233 -30
  25. package/dist/checks/bin/check-branch-topology.js +253 -32
  26. package/dist/checks/bin/check-brief-shape.js +291 -873
  27. package/dist/checks/bin/check-changeset-coverage.js +1012 -37
  28. package/dist/checks/bin/check-closes-n.js +253 -32
  29. package/dist/checks/bin/check-coherence.js +253 -32
  30. package/dist/checks/bin/check-dead-branch-push.js +215 -30
  31. package/dist/checks/bin/check-dispatch-readiness.js +256 -35
  32. package/dist/checks/bin/check-doc-coverage-push.js +1012 -37
  33. package/dist/checks/bin/check-doc-coverage.js +1014 -39
  34. package/dist/checks/bin/check-doctrine-no-procedures.js +215 -30
  35. package/dist/checks/bin/check-doctrine-portability.js +1012 -37
  36. package/dist/checks/bin/check-evidence-fresh.js +755 -71
  37. package/dist/checks/bin/check-exec-bits.js +1012 -37
  38. package/dist/checks/bin/check-first-push-dispatch.js +253 -32
  39. package/dist/checks/bin/check-issue-assignment.js +253 -32
  40. package/dist/checks/bin/check-main-branch-refusal.js +216 -31
  41. package/dist/checks/bin/check-no-disk-state.js +215 -30
  42. package/dist/checks/bin/check-pr-premise-reassert.js +5489 -0
  43. package/dist/checks/bin/check-pr-report-density.js +215 -30
  44. package/dist/checks/bin/check-quoted-command.js +1010 -35
  45. package/dist/checks/bin/check-reader-resolvable-prose.js +1010 -35
  46. package/dist/checks/bin/check-registry-gates.js +246 -32
  47. package/dist/checks/bin/check-retired-vocabulary.js +1010 -35
  48. package/dist/checks/bin/check-review-gate.js +274 -85
  49. package/dist/checks/bin/check-single-plan-pr.js +215 -30
  50. package/dist/checks/bin/check-surface-scope.js +5846 -0
  51. package/dist/checks/bin/check-test-plan.js +215 -30
  52. package/dist/checks/bin/check-token-collection-wired.js +215 -30
  53. package/dist/checks/bin/check-token-report.js +227 -37
  54. package/dist/checks/bin/check-workspace-escape.js +1010 -35
  55. package/dist/index.js +2407 -607
  56. package/package.json +1 -1
  57. package/aeg-root/contracts/brief-developer.md +0 -141
  58. package/aeg-root/contracts/planner-brief.md +0 -143
  59. package/aeg-root/roles/brief-author.md +0 -116
  60. package/aeg-root/skills/brief-authoring/SKILL.md +0 -509
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // src/checks/bin/check-review-gate.ts
6
- import { execFileSync as execFileSync3 } from "node:child_process";
6
+ import { execFileSync as execFileSync4 } from "node:child_process";
7
7
  // ../../packages/aeg-core/src/gate-audience.ts
8
8
  var GATE_AUDIENCE = {
9
9
  "check-branch-topology": { shippedAs: "branch-topology", ring: 0 },
@@ -27,6 +27,15 @@ var GATE_AUDIENCE = {
27
27
  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."
28
28
  }
29
29
  };
30
+ var NON_GATE_BINS = [
31
+ "archive-task",
32
+ "assign-task-issue",
33
+ "dead-branch-audit",
34
+ "eval-agent-compliance",
35
+ "open-issue",
36
+ "open-pr",
37
+ "report-tokens"
38
+ ];
30
39
  function isShipped(a) {
31
40
  return "shippedAs" in a;
32
41
  }
@@ -294,7 +303,7 @@ function issueListByLabelArgs(owner, repo, label) {
294
303
  "--state",
295
304
  "all",
296
305
  "--json",
297
- "number,title,body,state,labels,milestone",
306
+ "number,title,body,state,labels,milestone,stateReason",
298
307
  "--limit",
299
308
  "200"
300
309
  ];
@@ -387,7 +396,7 @@ var LABELS = [
387
396
  id: "vinaya/needs:brief-correction",
388
397
  category: "needs",
389
398
  form: "literal",
390
- carries: "Waiting on the Brief Author — the brief contradicts the surface it describes."
399
+ carries: "Waiting on the Planner — the brief contradicts the surface it describes."
391
400
  },
392
401
  {
393
402
  key: "waiver-docs",
@@ -640,14 +649,19 @@ function resolveTaskIssueRef(title, labels) {
640
649
  var INTENTS_HEADING = /^#{1,6}\s*Tranche intents\s*$/im;
641
650
  var NEXT_HEADING = /^#{1,6}\s+\S/m;
642
651
  var INTENT_BULLET = /^-\s+([a-z0-9][a-z0-9-]*)\s*:\s*(.+)$/i;
643
- function intentGoalForSlug(description, slug) {
644
- const text = stripCode(description, { inlineSpans: "keep" });
652
+ function intentsSection(text) {
645
653
  const start = text.match(INTENTS_HEADING);
646
654
  if (!start || start.index === undefined)
647
- return "";
655
+ return null;
648
656
  const rest = text.slice(start.index + start[0].length);
649
657
  const next = rest.match(NEXT_HEADING);
650
- const section = rest.slice(0, next && next.index !== undefined ? next.index : rest.length);
658
+ return rest.slice(0, next && next.index !== undefined ? next.index : rest.length);
659
+ }
660
+ function intentGoalForSlug(description, slug) {
661
+ const text = stripCode(description, { inlineSpans: "keep" });
662
+ const section = intentsSection(text);
663
+ if (section === null)
664
+ return "";
651
665
  for (const line of section.split(`
652
666
  `)) {
653
667
  const trimmed = line.trim();
@@ -1939,6 +1953,7 @@ function parseInlineFieldList(section) {
1939
1953
  // ../../packages/aeg-core/src/verdict-extraction.ts
1940
1954
  var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
1941
1955
  var OBJECTIVES_VERSION_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Objectives version:\s*([0-9a-f]{64})(?![A-Za-z0-9])/im;
1956
+ var RULING_ORDINAL_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Ruling ordinal:\s*(\d+)(?!\d)/im;
1942
1957
  function firstFiveLines(comment) {
1943
1958
  return comment.split(`
1944
1959
  `).slice(0, 5).join(`
@@ -1952,6 +1967,15 @@ function extractObjectivesVersion(comment) {
1952
1967
  const m = firstFiveLines(comment).match(OBJECTIVES_VERSION_PATTERN);
1953
1968
  return m ? m[1].toLowerCase() : null;
1954
1969
  }
1970
+ function firstSevenLines(comment) {
1971
+ return comment.split(`
1972
+ `).slice(0, 7).join(`
1973
+ `);
1974
+ }
1975
+ function extractRulingOrdinal(comment) {
1976
+ const m = firstSevenLines(comment).match(RULING_ORDINAL_PATTERN);
1977
+ return m ? Number.parseInt(m[1], 10) : null;
1978
+ }
1955
1979
  function extractVerdict(comments, valuePattern, missingLabel) {
1956
1980
  const candidates = comments.filter((c) => valuePattern.test(c));
1957
1981
  if (candidates.length === 0) {
@@ -1959,6 +1983,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1959
1983
  value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
1960
1984
  headSha: null,
1961
1985
  objectivesVersion: null,
1986
+ rulingOrdinal: null,
1962
1987
  danglingNote: `no ${missingLabel} verdict comment found on this PR`
1963
1988
  };
1964
1989
  }
@@ -1969,6 +1994,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1969
1994
  value: `the most recent ${missingLabel} comment's VERDICT line is not within its first five lines — DANGLING, see below`,
1970
1995
  headSha: null,
1971
1996
  objectivesVersion: null,
1997
+ rulingOrdinal: null,
1972
1998
  danglingNote: `the most recent ${missingLabel} verdict comment carries a VERDICT-shaped line outside the first-five-line read window`
1973
1999
  };
1974
2000
  }
@@ -1976,6 +2002,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1976
2002
  value: m[1].toUpperCase().replace(/[_-]/g, " "),
1977
2003
  headSha: extractHeadSha(latest),
1978
2004
  objectivesVersion: extractObjectivesVersion(latest),
2005
+ rulingOrdinal: extractRulingOrdinal(latest),
1979
2006
  danglingNote: null
1980
2007
  };
1981
2008
  }
@@ -2337,6 +2364,9 @@ import { createHash as createHash2 } from "node:crypto";
2337
2364
  var HEADING_RE = /^##[ \t]*Objectives[ \t]*$/im;
2338
2365
  var NEXT_HEADING_RE = /^##[ \t]/m;
2339
2366
  var OBJECTIVE_LINE_RE = /^O(\d+)\.[ \t]*(.*)$/;
2367
+ function maskedForHeadingSearch(body) {
2368
+ return maskDetailsBlocks(maskCode(body));
2369
+ }
2340
2370
  function hasBacktickedPath(text) {
2341
2371
  let i = 0;
2342
2372
  while (i < text.length) {
@@ -2359,16 +2389,22 @@ function stripObjectiveBackticks(text) {
2359
2389
  function wordCount(text) {
2360
2390
  return text.split(/\s+/).filter((w) => /[a-z]/i.test(w)).length;
2361
2391
  }
2362
- function objectivesSectionText(body) {
2363
- const heading = HEADING_RE.exec(body);
2392
+ function objectivesSectionBounds(body) {
2393
+ const masked = maskedForHeadingSearch(body);
2394
+ const heading = HEADING_RE.exec(masked);
2364
2395
  if (!heading)
2365
2396
  return null;
2366
- const afterHeading = body.slice(heading.index + heading[0].length);
2397
+ const start = heading.index + heading[0].length;
2398
+ const afterHeading = masked.slice(start);
2367
2399
  const next = NEXT_HEADING_RE.exec(afterHeading);
2368
- return next ? afterHeading.slice(0, next.index) : afterHeading;
2400
+ return { start, end: next ? start + next.index : body.length };
2401
+ }
2402
+ function objectivesSectionText(body) {
2403
+ const bounds = objectivesSectionBounds(body);
2404
+ return bounds === null ? null : body.slice(bounds.start, bounds.end);
2369
2405
  }
2370
2406
  function hasObjectivesHeading(body) {
2371
- return HEADING_RE.test(body);
2407
+ return HEADING_RE.test(maskedForHeadingSearch(body));
2372
2408
  }
2373
2409
  function objectivesOf(body) {
2374
2410
  const section = objectivesSectionText(body);
@@ -2394,7 +2430,7 @@ function objectivesOf(body) {
2394
2430
  continue;
2395
2431
  }
2396
2432
  if (hasBacktickedPath(text) && wordCount(stripObjectiveBackticks(text)) < MIN_WORDS_OUTSIDE_BACKTICKS) {
2397
- 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).`);
2433
+ 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).`);
2398
2434
  continue;
2399
2435
  }
2400
2436
  objectives.push({ id: `O${n}`, text });
@@ -2428,6 +2464,15 @@ function isIssueNotFoundError(err) {
2428
2464
  `);
2429
2465
  return /could not resolve to an (?:issue|pull request)|\b404\b|not found/i.test(haystack);
2430
2466
  }
2467
+ function resolveObjectivesSource(prBody, issue, cutoverIssue) {
2468
+ if (issue !== null && issue < cutoverIssue)
2469
+ return { kind: "none" };
2470
+ if (issue !== null)
2471
+ return { kind: "issue", issue };
2472
+ if (hasObjectivesHeading(prBody))
2473
+ return { kind: "body" };
2474
+ return { kind: "none" };
2475
+ }
2431
2476
 
2432
2477
  // ../../packages/aeg-core/src/premise-check.ts
2433
2478
  import { createHash as createHash3 } from "node:crypto";
@@ -2606,18 +2651,14 @@ function checkTestPlanExclusivity(prBody) {
2606
2651
  function checkPrincipalPlaceholder(prBody) {
2607
2652
  const region = testPlanRegion(prBody);
2608
2653
  const lineRe = /^-\s*\[[ xX]\]\s*\*{2}\[principal\]\*{2}(.*)$/gim;
2654
+ const errors = [];
2609
2655
  for (const m of region.matchAll(lineRe)) {
2610
2656
  const content = m[1] ?? "";
2611
2657
  if (/^\s*None\b/i.test(content)) {
2612
- return {
2613
- status: "fail",
2614
- errors: [
2615
- '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.'
2616
- ]
2617
- };
2658
+ 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.`);
2618
2659
  }
2619
2660
  }
2620
- return { status: "pass", errors: [] };
2661
+ return errors.length > 0 ? { status: "fail", errors } : { status: "pass", errors: [] };
2621
2662
  }
2622
2663
  function checkSurfaceMap(prBody) {
2623
2664
  return headingCheck(prBody, "(?:technical\\s+)?surface map", "Technical surface map");
@@ -2933,6 +2974,41 @@ function checkBriefSections(prBody, readTier, options = {}) {
2933
2974
  ];
2934
2975
  return { errors: results.flatMap((r) => r.errors) };
2935
2976
  }
2977
+ function contentAfterNLines(body, n) {
2978
+ let idx = -1;
2979
+ for (let i = 0;i < n; i++) {
2980
+ idx = body.indexOf(`
2981
+ `, idx + 1);
2982
+ if (idx === -1)
2983
+ return "";
2984
+ }
2985
+ return body.slice(idx + 1);
2986
+ }
2987
+ var BRIEF_MARKER_LINE_RE = /^<!-- aeg:brief:v(\d+) -->$/;
2988
+ function parseBriefMarkerVersion(firstLine) {
2989
+ const m = BRIEF_MARKER_LINE_RE.exec(firstLine.trim());
2990
+ if (!m)
2991
+ return null;
2992
+ const version = Number.parseInt(m[1], 10);
2993
+ return Number.isInteger(version) && version >= 1 ? version : null;
2994
+ }
2995
+ function frozenBriefContent(body, version) {
2996
+ return contentAfterNLines(body, version === 1 ? 2 : 3);
2997
+ }
2998
+ function resolveNewestFrozenBrief(comments, allowlist) {
2999
+ let best = null;
3000
+ for (const c of comments) {
3001
+ if (!isPrincipal(c.author, allowlist))
3002
+ continue;
3003
+ const version = parseBriefMarkerVersion(c.body.split(`
3004
+ `)[0] ?? "");
3005
+ if (version === null)
3006
+ continue;
3007
+ if (best === null || version > best.version)
3008
+ best = { ...c, version };
3009
+ }
3010
+ return best === null ? null : { ...best, content: frozenBriefContent(best.body, best.version) };
3011
+ }
2936
3012
  // ../../packages/aeg-core/src/doctrine-portability.ts
2937
3013
  var DEFAULT_SHIPS_PREFIX = "aeg-root/";
2938
3014
  var STATIC_PORTABLE_PREFIXES = [
@@ -3175,6 +3251,66 @@ function checkIssueObjectives(body, issueNumber) {
3175
3251
  return { status: "pass", errors: [] };
3176
3252
  return { status: "fail", errors: result.errors.map((e) => `issue-validation objectives: ${e}`) };
3177
3253
  }
3254
+ function topLevelSectionText(body, headingName) {
3255
+ const headingRe = new RegExp(`^##[ \\t]*${headingName}[ \\t]*$`, "im");
3256
+ const heading = headingRe.exec(body);
3257
+ if (!heading)
3258
+ return null;
3259
+ const afterHeading = body.slice(heading.index + heading[0].length);
3260
+ const next = /^##[ \t]/m.exec(afterHeading);
3261
+ return next ? afterHeading.slice(0, next.index) : afterHeading;
3262
+ }
3263
+ function looksLikeFilePath(entry2) {
3264
+ const stripped = entry2.replace(/\/\*\*?$/, "");
3265
+ const lastSegment = stripped.split("/").pop() ?? stripped;
3266
+ const extMatch = /\.[A-Za-z0-9]{1,6}$/.exec(lastSegment);
3267
+ return extMatch !== null && extMatch.index > 0;
3268
+ }
3269
+ function splitGlobList(raw) {
3270
+ return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
3271
+ }
3272
+ function parseIssueSurface(body) {
3273
+ const section = topLevelSectionText(body, "Surface");
3274
+ if (section === null)
3275
+ return { ok: false, errors: ["no `## Surface` heading found in the body."] };
3276
+ const inLine = /^in:\s*(.+)$/im.exec(section);
3277
+ const outLine = /^out:\s*(.+)$/im.exec(section);
3278
+ const errors = [];
3279
+ if (!inLine)
3280
+ errors.push("`## Surface` has no `in:` line.");
3281
+ if (!outLine)
3282
+ errors.push("`## Surface` has no `out:` line.");
3283
+ if (errors.length > 0)
3284
+ return { ok: false, errors };
3285
+ const inGlobs = splitGlobList(inLine[1]);
3286
+ const outGlobs = splitGlobList(outLine[1]);
3287
+ for (const g of [...inGlobs, ...outGlobs]) {
3288
+ if (looksLikeFilePath(g)) {
3289
+ errors.push(`\`${g}\` in \`## Surface\` looks like a file path — Surface entries are directory-level globs, never file paths.`);
3290
+ }
3291
+ }
3292
+ if (inGlobs.length === 0)
3293
+ errors.push("`## Surface`'s `in:` line resolved to zero globs.");
3294
+ if (errors.length > 0)
3295
+ return { ok: false, errors };
3296
+ return { ok: true, value: { in: inGlobs, out: outGlobs } };
3297
+ }
3298
+ function globCoversPath(glob, path) {
3299
+ const g = glob.replace(/\/\*\*?$/, "").replace(/\/+$/, "");
3300
+ const p = path.replace(/\/+$/, "");
3301
+ return g === p || g.startsWith(`${p}/`) || p.startsWith(`${g}/`);
3302
+ }
3303
+ function checkSurfaceScope(changedFiles, outGlobs) {
3304
+ if (outGlobs.length === 0)
3305
+ return { ok: true };
3306
+ const violations = [];
3307
+ for (const file of changedFiles) {
3308
+ const glob = outGlobs.find((g) => globCoversPath(g, file));
3309
+ if (glob)
3310
+ violations.push({ file, glob });
3311
+ }
3312
+ return violations.length > 0 ? { ok: false, violations } : { ok: true };
3313
+ }
3178
3314
  function isTaskIssueLabelSet(labels) {
3179
3315
  return hasLabel("tranche", labels);
3180
3316
  }
@@ -3239,6 +3375,8 @@ function checkProjectsRegistered(body, _labels, registeredNames) {
3239
3375
  }
3240
3376
  return errors.length > 0 ? { status: "fail", errors } : { status: "pass", errors: [] };
3241
3377
  }
3378
+ 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;
3379
+ var DOC_PATH_RE_GLOBAL = new RegExp(DOC_PATH_RE.source, "gi");
3242
3380
 
3243
3381
  // ../../packages/aeg-core/src/coherence-checks.ts
3244
3382
  var COHERENCE_ENFORCED_FROM = "2026-07-01";
@@ -3596,6 +3734,11 @@ function isBoundToObjectives(extraction, currentVersion) {
3596
3734
  return true;
3597
3735
  return extraction.objectivesVersion === currentVersion;
3598
3736
  }
3737
+ function isBoundToRulings(extraction, currentOrdinal) {
3738
+ if (extraction.rulingOrdinal === null)
3739
+ return currentOrdinal === 0;
3740
+ return extraction.rulingOrdinal === currentOrdinal;
3741
+ }
3599
3742
  function checkReviewGate(input) {
3600
3743
  const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
3601
3744
  const waived = isWaiverLabelActorVerified({
@@ -3624,7 +3767,9 @@ function checkReviewGate(input) {
3624
3767
  const securityBound = isBoundToPatch(security, input.headSha, input.patchIdOf);
3625
3768
  const codeReviewObjectivesBound = isBoundToObjectives(codeReview, input.objectivesVersion);
3626
3769
  const securityObjectivesBound = isBoundToObjectives(security, input.objectivesVersion);
3627
- if (codeReviewClean && codeReviewBound && codeReviewObjectivesBound && securityClean && securityBound && securityObjectivesBound && mechanicalChecksClean) {
3770
+ const codeReviewRulingsBound = isBoundToRulings(codeReview, input.rulingOrdinal);
3771
+ const securityRulingsBound = isBoundToRulings(security, input.rulingOrdinal);
3772
+ if (codeReviewClean && codeReviewBound && codeReviewObjectivesBound && codeReviewRulingsBound && securityClean && securityBound && securityObjectivesBound && securityRulingsBound && mechanicalChecksClean) {
3628
3773
  return {
3629
3774
  verdict: "pass",
3630
3775
  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.`,
@@ -3638,6 +3783,8 @@ function checkReviewGate(input) {
3638
3783
  problems.push(`the newest code-review verdict covers ${codeReview.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
3639
3784
  } else if (!codeReviewObjectivesBound) {
3640
3785
  problems.push(`the newest code-review verdict was cast against objectives version ${codeReview.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
3786
+ } else if (!codeReviewRulingsBound) {
3787
+ 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`);
3641
3788
  }
3642
3789
  if (!securityClean) {
3643
3790
  problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
@@ -3645,6 +3792,8 @@ function checkReviewGate(input) {
3645
3792
  problems.push(`the newest security-review verdict covers ${security.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
3646
3793
  } else if (!securityObjectivesBound) {
3647
3794
  problems.push(`the newest security-review verdict was cast against objectives version ${security.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
3795
+ } else if (!securityRulingsBound) {
3796
+ 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`);
3648
3797
  }
3649
3798
  if (!mechanicalChecksClean) {
3650
3799
  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(", ")}`);
@@ -3661,6 +3810,24 @@ var DEFAULT_RELEASE_ACTOR = "github-actions[bot]";
3661
3810
  function isChangesetsReleasePr(branch, author, expectedAuthor) {
3662
3811
  return branch === CHANGESET_RELEASE_BRANCH && author === expectedAuthor;
3663
3812
  }
3813
+ // ../../packages/aeg-core/src/ruling-ordinal.ts
3814
+ var RULING_MARKER_ORDINAL = /^<!-- aeg:principal:ruling:\d+-(\d+) -->$/;
3815
+ function newestPrincipalRulingOrdinal(comments, allowlist) {
3816
+ let best = 0;
3817
+ for (const c of comments) {
3818
+ if (!isPrincipal(c.author, allowlist))
3819
+ continue;
3820
+ const firstLine = (c.body.split(`
3821
+ `)[0] ?? "").trim();
3822
+ const m = RULING_MARKER_ORDINAL.exec(firstLine);
3823
+ if (!m)
3824
+ continue;
3825
+ const k = Number.parseInt(m[1], 10);
3826
+ if (k > best)
3827
+ best = k;
3828
+ }
3829
+ return best;
3830
+ }
3664
3831
  // ../../packages/aeg-core/src/markdown-table.ts
3665
3832
  function splitRow(line) {
3666
3833
  const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
@@ -3768,16 +3935,33 @@ function checkG1(rows, existsFn) {
3768
3935
  return { check: "G1", status: findings.length > 0 ? "fail" : "pass", findings };
3769
3936
  }
3770
3937
  var SCAFFOLD_PLACEHOLDER = "[undocumented — fill in why]";
3938
+ var NON_GATE_BIN_NAMES = NON_GATE_BINS;
3939
+ var TWIN_CANDIDATE_EXCEPTIONS = {
3940
+ "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)',
3941
+ "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)`,
3942
+ "apps/cli/src/checks/bin/check-closes-n.ts": 'Ring 1 "Closes linkage" (implementation: verify-coherence.ts)'
3943
+ };
3944
+ var NON_GATE_HOOK_SCRIPTS = [".claude/hooks/track-transcript.sh"];
3771
3945
  function checkG2(rows, candidateFiles) {
3772
3946
  const implementations = new Set(rows.map((r) => r.implementation).filter((p) => p !== ""));
3947
+ const claimedNames = new Set(rows.flatMap((r) => claimedCheckNames(r.implementation)));
3773
3948
  const findings = [];
3774
3949
  for (const path of candidateFiles) {
3775
- if (!implementations.has(path)) {
3776
- findings.push({
3777
- path,
3778
- reason: `"${path}" is not named as the implementation of any row in enforcement.md's ring tables`
3779
- });
3780
- }
3950
+ if (implementations.has(path))
3951
+ continue;
3952
+ if (path.startsWith(AEG_CORE_BIN_PATH_PREFIX) && NON_GATE_BIN_NAMES.includes(basenameNoExt(path)))
3953
+ continue;
3954
+ if (NON_GATE_HOOK_SCRIPTS.includes(path))
3955
+ continue;
3956
+ if (path in TWIN_CANDIDATE_EXCEPTIONS)
3957
+ continue;
3958
+ const candidateNames = claimedCheckNames(path);
3959
+ if (candidateNames.length > 0 && candidateNames.some((n) => claimedNames.has(n)))
3960
+ continue;
3961
+ findings.push({
3962
+ path,
3963
+ reason: `"${path}" is not named as the implementation of any row in enforcement.md's ring tables`
3964
+ });
3781
3965
  }
3782
3966
  for (const row of rows) {
3783
3967
  const carriesPlaceholder = [row.summary, row.description, row.spec].some((cell) => cell === SCAFFOLD_PLACEHOLDER);
@@ -3789,7 +3973,7 @@ function checkG2(rows, candidateFiles) {
3789
3973
  });
3790
3974
  }
3791
3975
  }
3792
- return { check: "G2", status: findings.length > 0 ? "info" : "pass", findings };
3976
+ return { check: "G2", status: findings.length > 0 ? "fail" : "pass", findings };
3793
3977
  }
3794
3978
  function checkG3(ring0Rows, crossingFiles) {
3795
3979
  const ring0Implementations = new Set(ring0Rows.map((r) => r.implementation));
@@ -4665,7 +4849,6 @@ function findWorkspaceEscapes(files, knownPaths, workspaceDirs = DEFAULT_WORKSPA
4665
4849
  import { z } from "zod";
4666
4850
  var ROLE_VALUES = [
4667
4851
  "planner",
4668
- "brief-author",
4669
4852
  "developer",
4670
4853
  "code-reviewer",
4671
4854
  "security",
@@ -4739,18 +4922,20 @@ var dispatchShared = {
4739
4922
  round: z.number().int().optional(),
4740
4923
  effect_id: z.string()
4741
4924
  };
4925
+ var dispatchUsageField = z.object({ input: z.number().nonnegative(), output: z.number().nonnegative() }).strict().nullable();
4742
4926
  var DispatchEventSchema = z.discriminatedUnion("event", [
4743
4927
  z.object({ ...dispatchShared, event: z.literal("dispatched"), prompt_hash: z.string() }).strict(),
4744
4928
  z.object({
4745
4929
  ...dispatchShared,
4746
4930
  event: z.literal("outcome_received"),
4747
4931
  outcome: DispatchOutcomeSchema,
4748
- usage: z.object({ input: z.number().nonnegative(), output: z.number().nonnegative() }).strict().nullable()
4932
+ usage: dispatchUsageField
4749
4933
  }).strict(),
4750
4934
  z.object({
4751
4935
  ...dispatchShared,
4752
4936
  event: z.literal("dispatch_failed"),
4753
- reason: z.enum(["timeout", "crash", "refused", "unattributed_write"])
4937
+ reason: z.enum(["timeout", "crash", "refused", "unattributed_write"]),
4938
+ usage: dispatchUsageField
4754
4939
  }).strict()
4755
4940
  ]);
4756
4941
  var loopShared = {
@@ -5193,12 +5378,40 @@ function loadConfig() {
5193
5378
  }
5194
5379
  var TOKENS_COLLECT_TRUST_PATH = join(GLOBAL_VINAYA_HOME, "tokens-collect-trust.json");
5195
5380
 
5381
+ // src/lib/patch-id.ts
5382
+ import { execFileSync as execFileSync3 } from "node:child_process";
5383
+ function patchIdAt(base, sha) {
5384
+ try {
5385
+ execFileSync3("git", ["fetch", "--quiet", "origin", sha], { stdio: ["ignore", "ignore", "ignore"] });
5386
+ } catch {}
5387
+ try {
5388
+ const diff = execFileSync3("git", ["diff", `origin/${base}...${sha}`], {
5389
+ encoding: "utf8",
5390
+ maxBuffer: 64 * 1024 * 1024,
5391
+ stdio: ["ignore", "pipe", "pipe"]
5392
+ });
5393
+ if (diff === "")
5394
+ return null;
5395
+ const out = execFileSync3("git", ["patch-id", "--stable"], {
5396
+ input: diff,
5397
+ encoding: "utf8",
5398
+ stdio: ["pipe", "pipe", "pipe"]
5399
+ }).trim();
5400
+ const id = out.split(/\s+/)[0] ?? "";
5401
+ return id === "" ? null : id;
5402
+ } catch {
5403
+ return null;
5404
+ }
5405
+ }
5406
+
5407
+ // src/lib/review-gate-check-name.ts
5408
+ var REVIEW_GATE_CHECK_RUN_NAME = "vinaya review gate";
5409
+
5196
5410
  // src/checks/bin/check-review-gate.ts
5197
5411
  var CHECK_NAME = "review-gate";
5198
- var OWN_CHECK_RUN_NAME = "vinaya review gate";
5199
5412
  function fetchPr(prNumber) {
5200
5413
  try {
5201
- const out = execFileSync3("gh", ["pr", "view", String(prNumber), "--json", "number,comments,labels,headRefName,headRefOid,baseRefName,body"], {
5414
+ const out = execFileSync4("gh", ["pr", "view", String(prNumber), "--json", "number,comments,labels,headRefName,headRefOid,baseRefName,body"], {
5202
5415
  encoding: "utf8",
5203
5416
  stdio: ["ignore", "pipe", "pipe"]
5204
5417
  });
@@ -5208,27 +5421,28 @@ function fetchPr(prNumber) {
5208
5421
  }
5209
5422
  }
5210
5423
  function fetchIssueBodyForObjectives(issueNumber) {
5211
- return execFileSync3("gh", ["issue", "view", String(issueNumber), "--json", "body", "--jq", ".body"], {
5424
+ return execFileSync4("gh", ["issue", "view", String(issueNumber), "--json", "body", "--jq", ".body"], {
5212
5425
  encoding: "utf8",
5213
5426
  stdio: ["ignore", "pipe", "pipe"]
5214
5427
  });
5215
5428
  }
5216
5429
  function resolveObjectivesVersion(pr) {
5217
5430
  const { issue } = extractIssue(pr.body);
5218
- if (issue !== null && issue < OBJECTIVES_SINCE_ISSUE)
5431
+ const source = resolveObjectivesSource(pr.body, issue, OBJECTIVES_SINCE_ISSUE);
5432
+ if (source.kind === "none")
5219
5433
  return null;
5220
- if (issue !== null) {
5434
+ if (source.kind === "issue") {
5221
5435
  let issueBody;
5222
5436
  try {
5223
- issueBody = fetchIssueBodyForObjectives(issue);
5437
+ issueBody = fetchIssueBodyForObjectives(source.issue);
5224
5438
  } catch (err) {
5225
5439
  if (isIssueNotFoundError(err)) {
5226
5440
  emitCheckError({
5227
5441
  schema: CHECK_SCHEMA_VERSION,
5228
5442
  check: CHECK_NAME,
5229
5443
  severity: "error",
5230
- message: `review-gate severity:infra — Issue #${issue} does not resolve via \`gh issue view\` — cannot verify the objectives-version binding for a PR whose linked Issue is at/above the objectives cutover.`,
5231
- agent_recovery_prompt: `Restore Issue #${issue}, fix \`Closes #N\` to name a real Issue, or have a principal apply the \`vinaya/waiver:review\` label, then re-run \`vinaya check review-gate\`.`
5444
+ message: `review-gate severity:infra — Issue #${source.issue} does not resolve via \`gh issue view\` — cannot verify the objectives-version binding for a PR whose linked Issue is at/above the objectives cutover.`,
5445
+ agent_recovery_prompt: `Restore Issue #${source.issue}, fix \`Closes #N\` to name a real Issue, or have a principal apply the \`vinaya/waiver:review\` label, then re-run \`vinaya check review-gate\`.`
5232
5446
  });
5233
5447
  process.exit(1);
5234
5448
  }
@@ -5236,7 +5450,7 @@ function resolveObjectivesVersion(pr) {
5236
5450
  schema: CHECK_SCHEMA_VERSION,
5237
5451
  check: CHECK_NAME,
5238
5452
  severity: "error",
5239
- message: `review-gate severity:infra — could not fetch Issue #${issue}'s body via \`gh issue view\` to resolve its objectives version: ${err.message}`,
5453
+ message: `review-gate severity:infra — could not fetch Issue #${source.issue}'s body via \`gh issue view\` to resolve its objectives version: ${err.message}`,
5240
5454
  agent_recovery_prompt: "Confirm `gh auth status` passes and the Issue number is correct, then re-run `vinaya check review-gate`."
5241
5455
  });
5242
5456
  process.exit(1);
@@ -5247,32 +5461,29 @@ function resolveObjectivesVersion(pr) {
5247
5461
  schema: CHECK_SCHEMA_VERSION,
5248
5462
  check: CHECK_NAME,
5249
5463
  severity: "error",
5250
- message: `review-gate severity:infra — Issue #${issue}'s \`## Objectives\` section does not parse (${parsed.errors.join("; ")}) — cannot verify the objectives-version binding.`,
5251
- agent_recovery_prompt: `Fix Issue #${issue}'s \`## Objectives\` section, or have a principal apply the \`vinaya/waiver:review\` label, then re-run \`vinaya check review-gate\`.`
5464
+ message: `review-gate severity:infra — Issue #${source.issue}'s \`## Objectives\` section does not parse (${parsed.errors.join("; ")}) — cannot verify the objectives-version binding.`,
5465
+ agent_recovery_prompt: `Fix Issue #${source.issue}'s \`## Objectives\` section, or have a principal apply the \`vinaya/waiver:review\` label, then re-run \`vinaya check review-gate\`.`
5252
5466
  });
5253
5467
  process.exit(1);
5254
5468
  }
5255
5469
  return objectivesVersion(parsed.objectives);
5256
5470
  }
5257
- if (hasObjectivesHeading(pr.body)) {
5258
- const own = objectivesOf(pr.body);
5259
- if (!own.ok) {
5260
- emitCheckError({
5261
- schema: CHECK_SCHEMA_VERSION,
5262
- check: CHECK_NAME,
5263
- severity: "error",
5264
- message: `review-gate severity:infra — this PR body's own \`## Objectives\` section does not parse (${own.errors.join("; ")}) cannot verify the objectives-version binding.`,
5265
- agent_recovery_prompt: "Fix the PR body's `## Objectives` section, or have a principal apply the `vinaya/waiver:review` label, then re-run `vinaya check review-gate`."
5266
- });
5267
- process.exit(1);
5268
- }
5269
- return objectivesVersion(own.objectives);
5471
+ const own = objectivesOf(pr.body);
5472
+ if (!own.ok) {
5473
+ emitCheckError({
5474
+ schema: CHECK_SCHEMA_VERSION,
5475
+ check: CHECK_NAME,
5476
+ severity: "error",
5477
+ message: `review-gate severity:infra — this PR body's own \`## Objectives\` section does not parse (${own.errors.join("; ")}) — cannot verify the objectives-version binding.`,
5478
+ agent_recovery_prompt: "Fix the PR body's `## Objectives` section, or have a principal apply the `vinaya/waiver:review` label, then re-run `vinaya check review-gate`."
5479
+ });
5480
+ process.exit(1);
5270
5481
  }
5271
- return null;
5482
+ return objectivesVersion(own.objectives);
5272
5483
  }
5273
5484
  function shaFromLsRemote(branch) {
5274
5485
  try {
5275
- const out = execFileSync3("git", ["ls-remote", "origin", `refs/heads/${branch}`], {
5486
+ const out = execFileSync4("git", ["ls-remote", "origin", `refs/heads/${branch}`], {
5276
5487
  encoding: "utf8",
5277
5488
  stdio: ["ignore", "pipe", "pipe"]
5278
5489
  }).trim();
@@ -5284,7 +5495,7 @@ function shaFromLsRemote(branch) {
5284
5495
  }
5285
5496
  function shaFromGhApi(branch) {
5286
5497
  try {
5287
- const out = execFileSync3("gh", ["api", `repos/{owner}/{repo}/git/ref/heads/${branch}`, "--jq", ".object.sha"], {
5498
+ const out = execFileSync4("gh", ["api", `repos/{owner}/{repo}/git/ref/heads/${branch}`, "--jq", ".object.sha"], {
5288
5499
  encoding: "utf8",
5289
5500
  stdio: ["ignore", "pipe", "pipe"]
5290
5501
  }).trim();
@@ -5301,29 +5512,6 @@ function resolveTrueHeadSha(pr) {
5301
5512
  }
5302
5513
  return trueSha;
5303
5514
  }
5304
- function patchIdAt(base, sha) {
5305
- try {
5306
- execFileSync3("git", ["fetch", "--quiet", "origin", sha], { stdio: ["ignore", "ignore", "ignore"] });
5307
- } catch {}
5308
- try {
5309
- const diff = execFileSync3("git", ["diff", `origin/${base}...${sha}`], {
5310
- encoding: "utf8",
5311
- maxBuffer: 64 * 1024 * 1024,
5312
- stdio: ["ignore", "pipe", "pipe"]
5313
- });
5314
- if (diff === "")
5315
- return null;
5316
- const out = execFileSync3("git", ["patch-id", "--stable"], {
5317
- input: diff,
5318
- encoding: "utf8",
5319
- stdio: ["pipe", "pipe", "pipe"]
5320
- }).trim();
5321
- const id = out.split(/\s+/)[0] ?? "";
5322
- return id === "" ? null : id;
5323
- } catch {
5324
- return null;
5325
- }
5326
- }
5327
5515
  function bucketFor(run2) {
5328
5516
  if (run2.status !== "completed")
5329
5517
  return "pending";
@@ -5341,7 +5529,7 @@ function bucketFor(run2) {
5341
5529
  }
5342
5530
  function fetchMechanicalChecks(headSha) {
5343
5531
  try {
5344
- const out = execFileSync3("gh", [
5532
+ const out = execFileSync4("gh", [
5345
5533
  "api",
5346
5534
  `repos/{owner}/{repo}/commits/${headSha}/check-runs`,
5347
5535
  "--paginate",
@@ -5356,14 +5544,14 @@ function fetchMechanicalChecks(headSha) {
5356
5544
  if (!seen || run2.id > seen.id)
5357
5545
  latestByName.set(run2.name, run2);
5358
5546
  }
5359
- return Array.from(latestByName.values()).filter((r) => r.name !== OWN_CHECK_RUN_NAME).map((r) => ({ name: r.name, bucket: bucketFor(r) }));
5547
+ return Array.from(latestByName.values()).filter((r) => r.name !== REVIEW_GATE_CHECK_RUN_NAME).map((r) => ({ name: r.name, bucket: bucketFor(r) }));
5360
5548
  } catch {
5361
5549
  return null;
5362
5550
  }
5363
5551
  }
5364
5552
  function fetchWaiverLabelActor(prNumber, label2) {
5365
5553
  try {
5366
- const out = execFileSync3("gh", ["api", `repos/{owner}/{repo}/issues/${prNumber}/timeline`, "--paginate"], {
5554
+ const out = execFileSync4("gh", ["api", `repos/{owner}/{repo}/issues/${prNumber}/timeline`, "--paginate"], {
5367
5555
  encoding: "utf8",
5368
5556
  stdio: ["ignore", "pipe", "pipe"]
5369
5557
  });
@@ -5431,7 +5619,8 @@ function main() {
5431
5619
  mechanicalChecks,
5432
5620
  headSha,
5433
5621
  patchIdOf: (sha) => patchIdAt(pr.baseRefName, sha),
5434
- objectivesVersion: waived ? null : resolveObjectivesVersion(pr)
5622
+ objectivesVersion: waived ? null : resolveObjectivesVersion(pr),
5623
+ rulingOrdinal: newestPrincipalRulingOrdinal(pr.comments.map((c) => ({ body: c.body, author: c.author?.login ?? null })), principalAllowlist)
5435
5624
  });
5436
5625
  if (result.verdict === "fail") {
5437
5626
  emitCheckError({