@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
@@ -28,6 +28,15 @@ var GATE_AUDIENCE = {
28
28
  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."
29
29
  }
30
30
  };
31
+ var NON_GATE_BINS = [
32
+ "archive-task",
33
+ "assign-task-issue",
34
+ "dead-branch-audit",
35
+ "eval-agent-compliance",
36
+ "open-issue",
37
+ "open-pr",
38
+ "report-tokens"
39
+ ];
31
40
  function isShipped(a) {
32
41
  return "shippedAs" in a;
33
42
  }
@@ -295,7 +304,7 @@ function issueListByLabelArgs(owner, repo, label) {
295
304
  "--state",
296
305
  "all",
297
306
  "--json",
298
- "number,title,body,state,labels,milestone",
307
+ "number,title,body,state,labels,milestone,stateReason",
299
308
  "--limit",
300
309
  "200"
301
310
  ];
@@ -388,7 +397,7 @@ var LABELS = [
388
397
  id: "vinaya/needs:brief-correction",
389
398
  category: "needs",
390
399
  form: "literal",
391
- carries: "Waiting on the Brief Author — the brief contradicts the surface it describes."
400
+ carries: "Waiting on the Planner — the brief contradicts the surface it describes."
392
401
  },
393
402
  {
394
403
  key: "waiver-docs",
@@ -641,14 +650,19 @@ function resolveTaskIssueRef(title, labels) {
641
650
  var INTENTS_HEADING = /^#{1,6}\s*Tranche intents\s*$/im;
642
651
  var NEXT_HEADING = /^#{1,6}\s+\S/m;
643
652
  var INTENT_BULLET = /^-\s+([a-z0-9][a-z0-9-]*)\s*:\s*(.+)$/i;
644
- function intentGoalForSlug(description, slug) {
645
- const text = stripCode(description, { inlineSpans: "keep" });
653
+ function intentsSection(text) {
646
654
  const start = text.match(INTENTS_HEADING);
647
655
  if (!start || start.index === undefined)
648
- return "";
656
+ return null;
649
657
  const rest = text.slice(start.index + start[0].length);
650
658
  const next = rest.match(NEXT_HEADING);
651
- const section = rest.slice(0, next && next.index !== undefined ? next.index : rest.length);
659
+ return rest.slice(0, next && next.index !== undefined ? next.index : rest.length);
660
+ }
661
+ function intentGoalForSlug(description, slug) {
662
+ const text = stripCode(description, { inlineSpans: "keep" });
663
+ const section = intentsSection(text);
664
+ if (section === null)
665
+ return "";
652
666
  for (const line of section.split(`
653
667
  `)) {
654
668
  const trimmed = line.trim();
@@ -1940,6 +1954,7 @@ function parseInlineFieldList(section) {
1940
1954
  // ../../packages/aeg-core/src/verdict-extraction.ts
1941
1955
  var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
1942
1956
  var OBJECTIVES_VERSION_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Objectives version:\s*([0-9a-f]{64})(?![A-Za-z0-9])/im;
1957
+ var RULING_ORDINAL_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Ruling ordinal:\s*(\d+)(?!\d)/im;
1943
1958
  function firstFiveLines(comment) {
1944
1959
  return comment.split(`
1945
1960
  `).slice(0, 5).join(`
@@ -1953,6 +1968,15 @@ function extractObjectivesVersion(comment) {
1953
1968
  const m = firstFiveLines(comment).match(OBJECTIVES_VERSION_PATTERN);
1954
1969
  return m ? m[1].toLowerCase() : null;
1955
1970
  }
1971
+ function firstSevenLines(comment) {
1972
+ return comment.split(`
1973
+ `).slice(0, 7).join(`
1974
+ `);
1975
+ }
1976
+ function extractRulingOrdinal(comment) {
1977
+ const m = firstSevenLines(comment).match(RULING_ORDINAL_PATTERN);
1978
+ return m ? Number.parseInt(m[1], 10) : null;
1979
+ }
1956
1980
  function extractVerdict(comments, valuePattern, missingLabel) {
1957
1981
  const candidates = comments.filter((c) => valuePattern.test(c));
1958
1982
  if (candidates.length === 0) {
@@ -1960,6 +1984,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1960
1984
  value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
1961
1985
  headSha: null,
1962
1986
  objectivesVersion: null,
1987
+ rulingOrdinal: null,
1963
1988
  danglingNote: `no ${missingLabel} verdict comment found on this PR`
1964
1989
  };
1965
1990
  }
@@ -1970,6 +1995,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1970
1995
  value: `the most recent ${missingLabel} comment's VERDICT line is not within its first five lines — DANGLING, see below`,
1971
1996
  headSha: null,
1972
1997
  objectivesVersion: null,
1998
+ rulingOrdinal: null,
1973
1999
  danglingNote: `the most recent ${missingLabel} verdict comment carries a VERDICT-shaped line outside the first-five-line read window`
1974
2000
  };
1975
2001
  }
@@ -1977,6 +2003,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1977
2003
  value: m[1].toUpperCase().replace(/[_-]/g, " "),
1978
2004
  headSha: extractHeadSha(latest),
1979
2005
  objectivesVersion: extractObjectivesVersion(latest),
2006
+ rulingOrdinal: extractRulingOrdinal(latest),
1980
2007
  danglingNote: null
1981
2008
  };
1982
2009
  }
@@ -2338,6 +2365,9 @@ import { createHash as createHash2 } from "node:crypto";
2338
2365
  var HEADING_RE = /^##[ \t]*Objectives[ \t]*$/im;
2339
2366
  var NEXT_HEADING_RE = /^##[ \t]/m;
2340
2367
  var OBJECTIVE_LINE_RE = /^O(\d+)\.[ \t]*(.*)$/;
2368
+ function maskedForHeadingSearch(body) {
2369
+ return maskDetailsBlocks(maskCode(body));
2370
+ }
2341
2371
  function hasBacktickedPath(text) {
2342
2372
  let i = 0;
2343
2373
  while (i < text.length) {
@@ -2360,16 +2390,22 @@ function stripObjectiveBackticks(text) {
2360
2390
  function wordCount(text) {
2361
2391
  return text.split(/\s+/).filter((w) => /[a-z]/i.test(w)).length;
2362
2392
  }
2363
- function objectivesSectionText(body) {
2364
- const heading = HEADING_RE.exec(body);
2393
+ function objectivesSectionBounds(body) {
2394
+ const masked = maskedForHeadingSearch(body);
2395
+ const heading = HEADING_RE.exec(masked);
2365
2396
  if (!heading)
2366
2397
  return null;
2367
- const afterHeading = body.slice(heading.index + heading[0].length);
2398
+ const start = heading.index + heading[0].length;
2399
+ const afterHeading = masked.slice(start);
2368
2400
  const next = NEXT_HEADING_RE.exec(afterHeading);
2369
- return next ? afterHeading.slice(0, next.index) : afterHeading;
2401
+ return { start, end: next ? start + next.index : body.length };
2402
+ }
2403
+ function objectivesSectionText(body) {
2404
+ const bounds = objectivesSectionBounds(body);
2405
+ return bounds === null ? null : body.slice(bounds.start, bounds.end);
2370
2406
  }
2371
2407
  function hasObjectivesHeading(body) {
2372
- return HEADING_RE.test(body);
2408
+ return HEADING_RE.test(maskedForHeadingSearch(body));
2373
2409
  }
2374
2410
  function objectivesOf(body) {
2375
2411
  const section = objectivesSectionText(body);
@@ -2395,7 +2431,7 @@ function objectivesOf(body) {
2395
2431
  continue;
2396
2432
  }
2397
2433
  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).`);
2434
+ 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).`);
2399
2435
  continue;
2400
2436
  }
2401
2437
  objectives.push({ id: `O${n}`, text });
@@ -2429,6 +2465,15 @@ function isIssueNotFoundError(err) {
2429
2465
  `);
2430
2466
  return /could not resolve to an (?:issue|pull request)|\b404\b|not found/i.test(haystack);
2431
2467
  }
2468
+ function resolveObjectivesSource(prBody, issue, cutoverIssue) {
2469
+ if (issue !== null && issue < cutoverIssue)
2470
+ return { kind: "none" };
2471
+ if (issue !== null)
2472
+ return { kind: "issue", issue };
2473
+ if (hasObjectivesHeading(prBody))
2474
+ return { kind: "body" };
2475
+ return { kind: "none" };
2476
+ }
2432
2477
 
2433
2478
  // ../../packages/aeg-core/src/premise-check.ts
2434
2479
  import { createHash as createHash3 } from "node:crypto";
@@ -2607,18 +2652,14 @@ function checkTestPlanExclusivity(prBody) {
2607
2652
  function checkPrincipalPlaceholder(prBody) {
2608
2653
  const region = testPlanRegion(prBody);
2609
2654
  const lineRe = /^-\s*\[[ xX]\]\s*\*{2}\[principal\]\*{2}(.*)$/gim;
2655
+ const errors = [];
2610
2656
  for (const m of region.matchAll(lineRe)) {
2611
2657
  const content = m[1] ?? "";
2612
2658
  if (/^\s*None\b/i.test(content)) {
2613
- return {
2614
- status: "fail",
2615
- errors: [
2616
- '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.'
2617
- ]
2618
- };
2659
+ 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.`);
2619
2660
  }
2620
2661
  }
2621
- return { status: "pass", errors: [] };
2662
+ return errors.length > 0 ? { status: "fail", errors } : { status: "pass", errors: [] };
2622
2663
  }
2623
2664
  function checkSurfaceMap(prBody) {
2624
2665
  return headingCheck(prBody, "(?:technical\\s+)?surface map", "Technical surface map");
@@ -2934,6 +2975,41 @@ function checkBriefSections(prBody, readTier, options = {}) {
2934
2975
  ];
2935
2976
  return { errors: results.flatMap((r) => r.errors) };
2936
2977
  }
2978
+ function contentAfterNLines(body, n) {
2979
+ let idx = -1;
2980
+ for (let i = 0;i < n; i++) {
2981
+ idx = body.indexOf(`
2982
+ `, idx + 1);
2983
+ if (idx === -1)
2984
+ return "";
2985
+ }
2986
+ return body.slice(idx + 1);
2987
+ }
2988
+ var BRIEF_MARKER_LINE_RE = /^<!-- aeg:brief:v(\d+) -->$/;
2989
+ function parseBriefMarkerVersion(firstLine) {
2990
+ const m = BRIEF_MARKER_LINE_RE.exec(firstLine.trim());
2991
+ if (!m)
2992
+ return null;
2993
+ const version = Number.parseInt(m[1], 10);
2994
+ return Number.isInteger(version) && version >= 1 ? version : null;
2995
+ }
2996
+ function frozenBriefContent(body, version) {
2997
+ return contentAfterNLines(body, version === 1 ? 2 : 3);
2998
+ }
2999
+ function resolveNewestFrozenBrief(comments, allowlist) {
3000
+ let best = null;
3001
+ for (const c of comments) {
3002
+ if (!isPrincipal(c.author, allowlist))
3003
+ continue;
3004
+ const version = parseBriefMarkerVersion(c.body.split(`
3005
+ `)[0] ?? "");
3006
+ if (version === null)
3007
+ continue;
3008
+ if (best === null || version > best.version)
3009
+ best = { ...c, version };
3010
+ }
3011
+ return best === null ? null : { ...best, content: frozenBriefContent(best.body, best.version) };
3012
+ }
2937
3013
  // ../../packages/aeg-core/src/doctrine-portability.ts
2938
3014
  var DEFAULT_SHIPS_PREFIX = "aeg-root/";
2939
3015
  var STATIC_PORTABLE_PREFIXES = [
@@ -3176,6 +3252,66 @@ function checkIssueObjectives(body, issueNumber) {
3176
3252
  return { status: "pass", errors: [] };
3177
3253
  return { status: "fail", errors: result.errors.map((e) => `issue-validation objectives: ${e}`) };
3178
3254
  }
3255
+ function topLevelSectionText(body, headingName) {
3256
+ const headingRe = new RegExp(`^##[ \\t]*${headingName}[ \\t]*$`, "im");
3257
+ const heading = headingRe.exec(body);
3258
+ if (!heading)
3259
+ return null;
3260
+ const afterHeading = body.slice(heading.index + heading[0].length);
3261
+ const next = /^##[ \t]/m.exec(afterHeading);
3262
+ return next ? afterHeading.slice(0, next.index) : afterHeading;
3263
+ }
3264
+ function looksLikeFilePath(entry2) {
3265
+ const stripped = entry2.replace(/\/\*\*?$/, "");
3266
+ const lastSegment = stripped.split("/").pop() ?? stripped;
3267
+ const extMatch = /\.[A-Za-z0-9]{1,6}$/.exec(lastSegment);
3268
+ return extMatch !== null && extMatch.index > 0;
3269
+ }
3270
+ function splitGlobList(raw) {
3271
+ return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
3272
+ }
3273
+ function parseIssueSurface(body) {
3274
+ const section = topLevelSectionText(body, "Surface");
3275
+ if (section === null)
3276
+ return { ok: false, errors: ["no `## Surface` heading found in the body."] };
3277
+ const inLine = /^in:\s*(.+)$/im.exec(section);
3278
+ const outLine = /^out:\s*(.+)$/im.exec(section);
3279
+ const errors = [];
3280
+ if (!inLine)
3281
+ errors.push("`## Surface` has no `in:` line.");
3282
+ if (!outLine)
3283
+ errors.push("`## Surface` has no `out:` line.");
3284
+ if (errors.length > 0)
3285
+ return { ok: false, errors };
3286
+ const inGlobs = splitGlobList(inLine[1]);
3287
+ const outGlobs = splitGlobList(outLine[1]);
3288
+ for (const g of [...inGlobs, ...outGlobs]) {
3289
+ if (looksLikeFilePath(g)) {
3290
+ errors.push(`\`${g}\` in \`## Surface\` looks like a file path — Surface entries are directory-level globs, never file paths.`);
3291
+ }
3292
+ }
3293
+ if (inGlobs.length === 0)
3294
+ errors.push("`## Surface`'s `in:` line resolved to zero globs.");
3295
+ if (errors.length > 0)
3296
+ return { ok: false, errors };
3297
+ return { ok: true, value: { in: inGlobs, out: outGlobs } };
3298
+ }
3299
+ function globCoversPath(glob, path) {
3300
+ const g = glob.replace(/\/\*\*?$/, "").replace(/\/+$/, "");
3301
+ const p = path.replace(/\/+$/, "");
3302
+ return g === p || g.startsWith(`${p}/`) || p.startsWith(`${g}/`);
3303
+ }
3304
+ function checkSurfaceScope(changedFiles, outGlobs) {
3305
+ if (outGlobs.length === 0)
3306
+ return { ok: true };
3307
+ const violations = [];
3308
+ for (const file of changedFiles) {
3309
+ const glob = outGlobs.find((g) => globCoversPath(g, file));
3310
+ if (glob)
3311
+ violations.push({ file, glob });
3312
+ }
3313
+ return violations.length > 0 ? { ok: false, violations } : { ok: true };
3314
+ }
3179
3315
  function isTaskIssueLabelSet(labels) {
3180
3316
  return hasLabel("tranche", labels);
3181
3317
  }
@@ -3240,6 +3376,8 @@ function checkProjectsRegistered(body, _labels, registeredNames) {
3240
3376
  }
3241
3377
  return errors.length > 0 ? { status: "fail", errors } : { status: "pass", errors: [] };
3242
3378
  }
3379
+ 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;
3380
+ var DOC_PATH_RE_GLOBAL = new RegExp(DOC_PATH_RE.source, "gi");
3243
3381
 
3244
3382
  // ../../packages/aeg-core/src/coherence-checks.ts
3245
3383
  var COHERENCE_ENFORCED_FROM = "2026-07-01";
@@ -3597,6 +3735,11 @@ function isBoundToObjectives(extraction, currentVersion) {
3597
3735
  return true;
3598
3736
  return extraction.objectivesVersion === currentVersion;
3599
3737
  }
3738
+ function isBoundToRulings(extraction, currentOrdinal) {
3739
+ if (extraction.rulingOrdinal === null)
3740
+ return currentOrdinal === 0;
3741
+ return extraction.rulingOrdinal === currentOrdinal;
3742
+ }
3600
3743
  function checkReviewGate(input) {
3601
3744
  const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
3602
3745
  const waived = isWaiverLabelActorVerified({
@@ -3625,7 +3768,9 @@ function checkReviewGate(input) {
3625
3768
  const securityBound = isBoundToPatch(security, input.headSha, input.patchIdOf);
3626
3769
  const codeReviewObjectivesBound = isBoundToObjectives(codeReview, input.objectivesVersion);
3627
3770
  const securityObjectivesBound = isBoundToObjectives(security, input.objectivesVersion);
3628
- if (codeReviewClean && codeReviewBound && codeReviewObjectivesBound && securityClean && securityBound && securityObjectivesBound && mechanicalChecksClean) {
3771
+ const codeReviewRulingsBound = isBoundToRulings(codeReview, input.rulingOrdinal);
3772
+ const securityRulingsBound = isBoundToRulings(security, input.rulingOrdinal);
3773
+ if (codeReviewClean && codeReviewBound && codeReviewObjectivesBound && codeReviewRulingsBound && securityClean && securityBound && securityObjectivesBound && securityRulingsBound && mechanicalChecksClean) {
3629
3774
  return {
3630
3775
  verdict: "pass",
3631
3776
  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.`,
@@ -3639,6 +3784,8 @@ function checkReviewGate(input) {
3639
3784
  problems.push(`the newest code-review verdict covers ${codeReview.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
3640
3785
  } else if (!codeReviewObjectivesBound) {
3641
3786
  problems.push(`the newest code-review verdict was cast against objectives version ${codeReview.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
3787
+ } else if (!codeReviewRulingsBound) {
3788
+ 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`);
3642
3789
  }
3643
3790
  if (!securityClean) {
3644
3791
  problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
@@ -3646,6 +3793,8 @@ function checkReviewGate(input) {
3646
3793
  problems.push(`the newest security-review verdict covers ${security.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
3647
3794
  } else if (!securityObjectivesBound) {
3648
3795
  problems.push(`the newest security-review verdict was cast against objectives version ${security.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
3796
+ } else if (!securityRulingsBound) {
3797
+ 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`);
3649
3798
  }
3650
3799
  if (!mechanicalChecksClean) {
3651
3800
  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(", ")}`);
@@ -3662,6 +3811,24 @@ var DEFAULT_RELEASE_ACTOR = "github-actions[bot]";
3662
3811
  function isChangesetsReleasePr(branch, author, expectedAuthor) {
3663
3812
  return branch === CHANGESET_RELEASE_BRANCH && author === expectedAuthor;
3664
3813
  }
3814
+ // ../../packages/aeg-core/src/ruling-ordinal.ts
3815
+ var RULING_MARKER_ORDINAL = /^<!-- aeg:principal:ruling:\d+-(\d+) -->$/;
3816
+ function newestPrincipalRulingOrdinal(comments, allowlist) {
3817
+ let best = 0;
3818
+ for (const c of comments) {
3819
+ if (!isPrincipal(c.author, allowlist))
3820
+ continue;
3821
+ const firstLine = (c.body.split(`
3822
+ `)[0] ?? "").trim();
3823
+ const m = RULING_MARKER_ORDINAL.exec(firstLine);
3824
+ if (!m)
3825
+ continue;
3826
+ const k = Number.parseInt(m[1], 10);
3827
+ if (k > best)
3828
+ best = k;
3829
+ }
3830
+ return best;
3831
+ }
3665
3832
  // ../../packages/aeg-core/src/markdown-table.ts
3666
3833
  function splitRow(line) {
3667
3834
  const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
@@ -3769,16 +3936,33 @@ function checkG1(rows, existsFn) {
3769
3936
  return { check: "G1", status: findings.length > 0 ? "fail" : "pass", findings };
3770
3937
  }
3771
3938
  var SCAFFOLD_PLACEHOLDER = "[undocumented — fill in why]";
3939
+ var NON_GATE_BIN_NAMES = NON_GATE_BINS;
3940
+ var TWIN_CANDIDATE_EXCEPTIONS = {
3941
+ "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)',
3942
+ "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)`,
3943
+ "apps/cli/src/checks/bin/check-closes-n.ts": 'Ring 1 "Closes linkage" (implementation: verify-coherence.ts)'
3944
+ };
3945
+ var NON_GATE_HOOK_SCRIPTS = [".claude/hooks/track-transcript.sh"];
3772
3946
  function checkG2(rows, candidateFiles) {
3773
3947
  const implementations = new Set(rows.map((r) => r.implementation).filter((p) => p !== ""));
3948
+ const claimedNames = new Set(rows.flatMap((r) => claimedCheckNames(r.implementation)));
3774
3949
  const findings = [];
3775
3950
  for (const path of candidateFiles) {
3776
- if (!implementations.has(path)) {
3777
- findings.push({
3778
- path,
3779
- reason: `"${path}" is not named as the implementation of any row in enforcement.md's ring tables`
3780
- });
3781
- }
3951
+ if (implementations.has(path))
3952
+ continue;
3953
+ if (path.startsWith(AEG_CORE_BIN_PATH_PREFIX) && NON_GATE_BIN_NAMES.includes(basenameNoExt(path)))
3954
+ continue;
3955
+ if (NON_GATE_HOOK_SCRIPTS.includes(path))
3956
+ continue;
3957
+ if (path in TWIN_CANDIDATE_EXCEPTIONS)
3958
+ continue;
3959
+ const candidateNames = claimedCheckNames(path);
3960
+ if (candidateNames.length > 0 && candidateNames.some((n) => claimedNames.has(n)))
3961
+ continue;
3962
+ findings.push({
3963
+ path,
3964
+ reason: `"${path}" is not named as the implementation of any row in enforcement.md's ring tables`
3965
+ });
3782
3966
  }
3783
3967
  for (const row of rows) {
3784
3968
  const carriesPlaceholder = [row.summary, row.description, row.spec].some((cell) => cell === SCAFFOLD_PLACEHOLDER);
@@ -3790,7 +3974,7 @@ function checkG2(rows, candidateFiles) {
3790
3974
  });
3791
3975
  }
3792
3976
  }
3793
- return { check: "G2", status: findings.length > 0 ? "info" : "pass", findings };
3977
+ return { check: "G2", status: findings.length > 0 ? "fail" : "pass", findings };
3794
3978
  }
3795
3979
  function checkG3(ring0Rows, crossingFiles) {
3796
3980
  const ring0Implementations = new Set(ring0Rows.map((r) => r.implementation));
@@ -4666,7 +4850,6 @@ function findWorkspaceEscapes(files, knownPaths, workspaceDirs = DEFAULT_WORKSPA
4666
4850
  import { z } from "zod";
4667
4851
  var ROLE_VALUES = [
4668
4852
  "planner",
4669
- "brief-author",
4670
4853
  "developer",
4671
4854
  "code-reviewer",
4672
4855
  "security",
@@ -4740,18 +4923,20 @@ var dispatchShared = {
4740
4923
  round: z.number().int().optional(),
4741
4924
  effect_id: z.string()
4742
4925
  };
4926
+ var dispatchUsageField = z.object({ input: z.number().nonnegative(), output: z.number().nonnegative() }).strict().nullable();
4743
4927
  var DispatchEventSchema = z.discriminatedUnion("event", [
4744
4928
  z.object({ ...dispatchShared, event: z.literal("dispatched"), prompt_hash: z.string() }).strict(),
4745
4929
  z.object({
4746
4930
  ...dispatchShared,
4747
4931
  event: z.literal("outcome_received"),
4748
4932
  outcome: DispatchOutcomeSchema,
4749
- usage: z.object({ input: z.number().nonnegative(), output: z.number().nonnegative() }).strict().nullable()
4933
+ usage: dispatchUsageField
4750
4934
  }).strict(),
4751
4935
  z.object({
4752
4936
  ...dispatchShared,
4753
4937
  event: z.literal("dispatch_failed"),
4754
- reason: z.enum(["timeout", "crash", "refused", "unattributed_write"])
4938
+ reason: z.enum(["timeout", "crash", "refused", "unattributed_write"]),
4939
+ usage: dispatchUsageField
4755
4940
  }).strict()
4756
4941
  ]);
4757
4942
  var loopShared = {