@attalabs/vinaya 0.26.0 → 0.28.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 (50) hide show
  1. package/README.md +6 -3
  2. package/aeg-root/enforcement.md +5 -5
  3. package/aeg-root/milestone-model.md +2 -0
  4. package/aeg-root/process.md +1 -1
  5. package/aeg-root/roles/developer.md +23 -17
  6. package/aeg-root/roles/planner.md +4 -0
  7. package/aeg-root/roles/principal.md +8 -2
  8. package/aeg-root/roles/reviewer.md +9 -5
  9. package/aeg-root/roles/security.md +9 -5
  10. package/dist/checks/bin/check-body-bare-digits.js +1038 -257
  11. package/dist/checks/bin/check-branch-topology.js +963 -199
  12. package/dist/checks/bin/check-brief-shape.js +1276 -203
  13. package/dist/checks/bin/check-changeset-coverage.js +1677 -281
  14. package/dist/checks/bin/check-closes-n.js +967 -203
  15. package/dist/checks/bin/check-coherence.js +1127 -284
  16. package/dist/checks/bin/check-dead-branch-push.js +910 -193
  17. package/dist/checks/bin/check-dispatch-readiness.js +1189 -295
  18. package/dist/checks/bin/check-doc-coverage-push.js +1673 -277
  19. package/dist/checks/bin/check-doc-coverage.js +1675 -279
  20. package/dist/checks/bin/check-doctrine-no-procedures.js +1020 -257
  21. package/dist/checks/bin/check-doctrine-portability.js +1672 -276
  22. package/dist/checks/bin/check-evidence-fresh.js +2526 -322
  23. package/dist/checks/bin/check-exec-bits.js +1670 -274
  24. package/dist/checks/bin/check-first-push-dispatch.js +1075 -265
  25. package/dist/checks/bin/check-issue-assignment.js +965 -201
  26. package/dist/checks/bin/check-issue-milestone-attach.js +5734 -0
  27. package/dist/checks/bin/check-issue-objectives-numbering.js +5736 -0
  28. package/dist/checks/bin/check-issue-parts-coverage.js +5736 -0
  29. package/dist/checks/bin/check-issue-surface-globs.js +6910 -0
  30. package/dist/checks/bin/check-issue-title-grammar.js +5736 -0
  31. package/dist/checks/bin/check-issue-tranche-label.js +5736 -0
  32. package/dist/checks/bin/check-main-branch-refusal.js +910 -193
  33. package/dist/checks/bin/check-no-disk-state.js +910 -193
  34. package/dist/checks/bin/check-pr-premise-reassert.js +1020 -257
  35. package/dist/checks/bin/check-pr-report-density.js +910 -193
  36. package/dist/checks/bin/check-quoted-command.js +1651 -274
  37. package/dist/checks/bin/check-reader-resolvable-prose.js +1655 -278
  38. package/dist/checks/bin/check-registry-gates.js +997 -193
  39. package/dist/checks/bin/check-retired-vocabulary.js +1649 -272
  40. package/dist/checks/bin/check-review-gate.js +1163 -316
  41. package/dist/checks/bin/check-single-plan-pr.js +910 -193
  42. package/dist/checks/bin/check-surface-scope.js +973 -201
  43. package/dist/checks/bin/check-test-plan.js +939 -206
  44. package/dist/checks/bin/check-token-collection-wired.js +910 -193
  45. package/dist/checks/bin/check-token-report.js +910 -193
  46. package/dist/checks/bin/check-workspace-escape.js +1668 -272
  47. package/dist/index.js +10602 -5352
  48. package/dist/lib/pre-push-changed-files.js +57 -0
  49. package/dist/lib/pre-push-select-tests.js +1473 -0
  50. package/package.json +4 -2
@@ -303,7 +303,7 @@ function issueListByLabelArgs(owner, repo, label) {
303
303
  "--state",
304
304
  "all",
305
305
  "--json",
306
- "number,title,body,state,labels,milestone",
306
+ "number,title,body,state,labels,milestone,stateReason",
307
307
  "--limit",
308
308
  "200"
309
309
  ];
@@ -649,14 +649,19 @@ function resolveTaskIssueRef(title, labels) {
649
649
  var INTENTS_HEADING = /^#{1,6}\s*Tranche intents\s*$/im;
650
650
  var NEXT_HEADING = /^#{1,6}\s+\S/m;
651
651
  var INTENT_BULLET = /^-\s+([a-z0-9][a-z0-9-]*)\s*:\s*(.+)$/i;
652
- function intentGoalForSlug(description, slug) {
653
- const text = stripCode(description, { inlineSpans: "keep" });
652
+ function intentsSection(text) {
654
653
  const start = text.match(INTENTS_HEADING);
655
654
  if (!start || start.index === undefined)
656
- return "";
655
+ return null;
657
656
  const rest = text.slice(start.index + start[0].length);
658
657
  const next = rest.match(NEXT_HEADING);
659
- 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 "";
660
665
  for (const line of section.split(`
661
666
  `)) {
662
667
  const trimmed = line.trim();
@@ -1948,6 +1953,9 @@ function parseInlineFieldList(section) {
1948
1953
  // ../../packages/aeg-core/src/verdict-extraction.ts
1949
1954
  var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
1950
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;
1957
+ var BRIEF_HASH_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Brief hash:\s*([0-9a-f]{64})(?![A-Za-z0-9])/im;
1958
+ var POLICY_DIGEST_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Policy digest:\s*([0-9a-f]{64})(?![A-Za-z0-9])/im;
1951
1959
  function firstFiveLines(comment) {
1952
1960
  return comment.split(`
1953
1961
  `).slice(0, 5).join(`
@@ -1961,6 +1969,35 @@ function extractObjectivesVersion(comment) {
1961
1969
  const m = firstFiveLines(comment).match(OBJECTIVES_VERSION_PATTERN);
1962
1970
  return m ? m[1].toLowerCase() : null;
1963
1971
  }
1972
+ function firstSevenLines(comment) {
1973
+ return comment.split(`
1974
+ `).slice(0, 7).join(`
1975
+ `);
1976
+ }
1977
+ function extractRulingOrdinal(comment) {
1978
+ const m = firstSevenLines(comment).match(RULING_ORDINAL_PATTERN);
1979
+ return m ? Number.parseInt(m[1], 10) : null;
1980
+ }
1981
+ function firstElevenLines(comment) {
1982
+ return comment.split(`
1983
+ `).slice(0, 11).join(`
1984
+ `);
1985
+ }
1986
+ function extractBriefHash(comment) {
1987
+ const m = firstElevenLines(comment).match(BRIEF_HASH_PATTERN);
1988
+ return m ? m[1].toLowerCase() : null;
1989
+ }
1990
+ function extractPolicyDigest(comment) {
1991
+ const m = firstElevenLines(comment).match(POLICY_DIGEST_PATTERN);
1992
+ return m ? m[1].toLowerCase() : null;
1993
+ }
1994
+ var FINDING_SEVERITY_LINE = /^\d+\.\s+\[([A-Z][A-Z]*)\]\s+(.+?)\s+—/gm;
1995
+ function extractFindingSeverities(comment) {
1996
+ return [...comment.matchAll(FINDING_SEVERITY_LINE)].map((m) => ({
1997
+ severity: m[1],
1998
+ location: m[2]
1999
+ }));
2000
+ }
1964
2001
  function extractVerdict(comments, valuePattern, missingLabel) {
1965
2002
  const candidates = comments.filter((c) => valuePattern.test(c));
1966
2003
  if (candidates.length === 0) {
@@ -1968,6 +2005,10 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1968
2005
  value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
1969
2006
  headSha: null,
1970
2007
  objectivesVersion: null,
2008
+ rulingOrdinal: null,
2009
+ briefHash: null,
2010
+ policyDigest: null,
2011
+ findingSeverities: [],
1971
2012
  danglingNote: `no ${missingLabel} verdict comment found on this PR`
1972
2013
  };
1973
2014
  }
@@ -1978,6 +2019,10 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1978
2019
  value: `the most recent ${missingLabel} comment's VERDICT line is not within its first five lines — DANGLING, see below`,
1979
2020
  headSha: null,
1980
2021
  objectivesVersion: null,
2022
+ rulingOrdinal: null,
2023
+ briefHash: null,
2024
+ policyDigest: null,
2025
+ findingSeverities: [],
1981
2026
  danglingNote: `the most recent ${missingLabel} verdict comment carries a VERDICT-shaped line outside the first-five-line read window`
1982
2027
  };
1983
2028
  }
@@ -1985,6 +2030,10 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1985
2030
  value: m[1].toUpperCase().replace(/[_-]/g, " "),
1986
2031
  headSha: extractHeadSha(latest),
1987
2032
  objectivesVersion: extractObjectivesVersion(latest),
2033
+ rulingOrdinal: extractRulingOrdinal(latest),
2034
+ briefHash: extractBriefHash(latest),
2035
+ policyDigest: extractPolicyDigest(latest),
2036
+ findingSeverities: extractFindingSeverities(latest),
1988
2037
  danglingNote: null
1989
2038
  };
1990
2039
  }
@@ -2320,6 +2369,53 @@ function evaluateC5(changed, docOwnersContent, prBody, fileExists, waiverActive,
2320
2369
  }
2321
2370
  return out;
2322
2371
  }
2372
+ // ../../packages/aeg-core/src/review-policy.ts
2373
+ var CODE_REVIEW_SEVERITY_ORDER = ["BLOCKER", "MAJOR", "MINOR"];
2374
+ var SECURITY_SEVERITY_ORDER = ["CRITICAL", "HIGH", "MEDIUM", "LOW"];
2375
+ var DEFAULT_MAX_ROUNDS = 3;
2376
+ var DEFAULT_REVIEW_POLICY = {
2377
+ codeReviewThreshold: "BLOCKER",
2378
+ securityThreshold: "HIGH",
2379
+ maxRounds: DEFAULT_MAX_ROUNDS
2380
+ };
2381
+ var FILE_SHAPED_LOCATION = /\.[a-zA-Z0-9]{1,10}(:\d+)?\s*$/;
2382
+ var PROSE_LOCATION_PATTERNS = [/\bpr\s*body\b/i, /\bcomment\b/i];
2383
+ var ROLE_FILE_LOCATION = /(^|\/)aeg-root\/roles\//i;
2384
+ function isProseLocation(location) {
2385
+ if (ROLE_FILE_LOCATION.test(location))
2386
+ return true;
2387
+ if (FILE_SHAPED_LOCATION.test(location))
2388
+ return false;
2389
+ return PROSE_LOCATION_PATTERNS.some((pattern) => pattern.test(location));
2390
+ }
2391
+ var PROSE_CAP_SEVERITY = "MINOR";
2392
+ function blockingSeverities(scale, threshold) {
2393
+ const idx = scale.indexOf(threshold);
2394
+ if (idx === -1) {
2395
+ throw new Error(`blockingSeverities: threshold "${threshold}" is not one of ${scale.join(" > ")}`);
2396
+ }
2397
+ return scale.slice(0, idx + 1);
2398
+ }
2399
+ function evaluateReviewFindings(findings, scale, threshold) {
2400
+ const blocking = new Set(blockingSeverities(scale, threshold));
2401
+ const blockingFindings = findings.filter((f) => {
2402
+ if (!scale.includes(f.severity)) {
2403
+ throw new Error(`evaluateReviewFindings: severity "${f.severity}" is not one of ${scale.join(" > ")}`);
2404
+ }
2405
+ const effectiveSeverity = f.location !== undefined && isProseLocation(f.location) ? PROSE_CAP_SEVERITY : f.severity;
2406
+ return blocking.has(effectiveSeverity);
2407
+ });
2408
+ return { outcome: blockingFindings.length > 0 ? "blocked" : "clean", blockingFindings };
2409
+ }
2410
+ function evaluateCodeReview(findings, policy) {
2411
+ return evaluateReviewFindings(findings, CODE_REVIEW_SEVERITY_ORDER, policy.codeReviewThreshold);
2412
+ }
2413
+ function evaluateSecurityReview(findings, policy) {
2414
+ return evaluateReviewFindings(findings, SECURITY_SEVERITY_ORDER, policy.securityThreshold);
2415
+ }
2416
+ function isKnownSeverity(scale, value) {
2417
+ return scale.includes(value);
2418
+ }
2323
2419
  // ../../packages/aeg-core/src/pr-tier.ts
2324
2420
  var TIER_FIELD = /(\*\*)?\s*Tier\s*(\*\*)?\s*:\s*(\*\*)?\s*([013])\b/i;
2325
2421
  function readTierFromPrBody(prBody) {
@@ -2346,6 +2442,9 @@ import { createHash as createHash2 } from "node:crypto";
2346
2442
  var HEADING_RE = /^##[ \t]*Objectives[ \t]*$/im;
2347
2443
  var NEXT_HEADING_RE = /^##[ \t]/m;
2348
2444
  var OBJECTIVE_LINE_RE = /^O(\d+)\.[ \t]*(.*)$/;
2445
+ function maskedForHeadingSearch(body) {
2446
+ return maskDetailsBlocks(maskCode(body));
2447
+ }
2349
2448
  function hasBacktickedPath(text) {
2350
2449
  let i = 0;
2351
2450
  while (i < text.length) {
@@ -2368,16 +2467,22 @@ function stripObjectiveBackticks(text) {
2368
2467
  function wordCount(text) {
2369
2468
  return text.split(/\s+/).filter((w) => /[a-z]/i.test(w)).length;
2370
2469
  }
2371
- function objectivesSectionText(body) {
2372
- const heading = HEADING_RE.exec(body);
2470
+ function objectivesSectionBounds(body) {
2471
+ const masked = maskedForHeadingSearch(body);
2472
+ const heading = HEADING_RE.exec(masked);
2373
2473
  if (!heading)
2374
2474
  return null;
2375
- const afterHeading = body.slice(heading.index + heading[0].length);
2475
+ const start = heading.index + heading[0].length;
2476
+ const afterHeading = masked.slice(start);
2376
2477
  const next = NEXT_HEADING_RE.exec(afterHeading);
2377
- return next ? afterHeading.slice(0, next.index) : afterHeading;
2478
+ return { start, end: next ? start + next.index : body.length };
2479
+ }
2480
+ function objectivesSectionText(body) {
2481
+ const bounds = objectivesSectionBounds(body);
2482
+ return bounds === null ? null : body.slice(bounds.start, bounds.end);
2378
2483
  }
2379
2484
  function hasObjectivesHeading(body) {
2380
- return HEADING_RE.test(body);
2485
+ return HEADING_RE.test(maskedForHeadingSearch(body));
2381
2486
  }
2382
2487
  function objectivesOf(body) {
2383
2488
  const section = objectivesSectionText(body);
@@ -2437,6 +2542,15 @@ function isIssueNotFoundError(err) {
2437
2542
  `);
2438
2543
  return /could not resolve to an (?:issue|pull request)|\b404\b|not found/i.test(haystack);
2439
2544
  }
2545
+ function resolveObjectivesSource(prBody, issue, cutoverIssue) {
2546
+ if (issue !== null && issue < cutoverIssue)
2547
+ return { kind: "none" };
2548
+ if (issue !== null)
2549
+ return { kind: "issue", issue };
2550
+ if (hasObjectivesHeading(prBody))
2551
+ return { kind: "body" };
2552
+ return { kind: "none" };
2553
+ }
2440
2554
 
2441
2555
  // ../../packages/aeg-core/src/premise-check.ts
2442
2556
  import { createHash as createHash3 } from "node:crypto";
@@ -2672,7 +2786,7 @@ function checkForField(prBody) {
2672
2786
  ]
2673
2787
  };
2674
2788
  }
2675
- function checkClosesN(prBody) {
2789
+ function checkClosesNPresence(prBody) {
2676
2790
  const closesPattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s{0,8}:?\s{0,8}#\d+/i;
2677
2791
  if (closesPattern.test(stripCode(prBody))) {
2678
2792
  return { status: "pass", errors: [] };
@@ -2704,10 +2818,22 @@ var COMMIT_TYPES = [
2704
2818
  "Test"
2705
2819
  ];
2706
2820
  var COMMIT_TYPE_STYLE = new RegExp(`^(${COMMIT_TYPES.join("|")})(\\([a-z0-9-]+\\))?: \\S`);
2821
+ function checkForgeTitle(title) {
2822
+ const taskStyle = /^\[[a-z0-9._-]+\] \S+ — \S/;
2823
+ if (COMMIT_TYPE_STYLE.test(title) || taskStyle.test(title))
2824
+ return { status: "pass", errors: [] };
2825
+ return {
2826
+ status: "fail",
2827
+ errors: [
2828
+ `brief-validation title: "${title}" matches neither title grammar — expected \`Type: description\` / \`Type(scope): description\` (commitlint types + Plan) or \`[tranche] id — description\` (task form).`
2829
+ ]
2830
+ };
2831
+ }
2707
2832
  var BRIEF_SHAPE_MARKERS = [checkSurfaceMap, checkDocUpdateList, checkStopConditions, checkAutonomyClause];
2708
2833
  var TASK_BRANCH_PATTERN = /^task\/[^/]+\/[^/]+$/;
2834
+ var TASK_ISSUE_BRANCH_PATTERN = /^task\/issue-\d+$/;
2709
2835
  function isTaskBranch(branch) {
2710
- return TASK_BRANCH_PATTERN.test(branch);
2836
+ return TASK_BRANCH_PATTERN.test(branch) || TASK_ISSUE_BRANCH_PATTERN.test(branch);
2711
2837
  }
2712
2838
  function isBriefShaped(prBody) {
2713
2839
  const stripped = stripCode(prBody);
@@ -2829,8 +2955,9 @@ function packagesNamedIn(text) {
2829
2955
  }
2830
2956
  function hasTestPathForConsumer(text, consumerDir) {
2831
2957
  const escaped = consumerDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2832
- const re = new RegExp(`${escaped}\\/[\\w./-]*\\.test\\.[A-Za-z0-9]+`);
2833
- return re.test(text);
2958
+ const filePathRe = new RegExp(`${escaped}\\/[\\w./-]*\\.test\\.[A-Za-z0-9]+`);
2959
+ const testDirRe = new RegExp(`${escaped}\\/(?:[\\w-]+\\/)*(?:tests|specs)(?:\\/|\\b)`);
2960
+ return filePathRe.test(text) || testDirRe.test(text);
2834
2961
  }
2835
2962
  function checkConsumerTests(prBody, consumersOf) {
2836
2963
  const section4 = extractNumberedSection(prBody, 4);
@@ -2934,21 +3061,44 @@ function checkBriefSections(prBody, readTier, options = {}) {
2934
3061
  checkConsumerTests(prBody, consumersOf),
2935
3062
  checkDefeatCases(prBody),
2936
3063
  ...issueObjectives !== undefined ? [checkObjectivesCopy(prBody, issueObjectives), checkObjectivesCoverage(prBody)] : [],
2937
- ...requireClosesN ? [checkClosesN(prBody)] : []
3064
+ ...requireClosesN ? [checkClosesNPresence(prBody)] : []
2938
3065
  ];
2939
3066
  return { errors: results.flatMap((r) => r.errors) };
2940
3067
  }
2941
- var AEG_BRIEF_V1_MARKER = "<!-- aeg:brief:v1 -->";
2942
- function contentAfterTwoLines(body) {
2943
- const first = body.indexOf(`
2944
- `);
2945
- if (first === -1)
2946
- return "";
2947
- const second = body.indexOf(`
2948
- `, first + 1);
2949
- if (second === -1)
2950
- return "";
2951
- return body.slice(second + 1);
3068
+ function contentAfterNLines(body, n) {
3069
+ let idx = -1;
3070
+ for (let i = 0;i < n; i++) {
3071
+ idx = body.indexOf(`
3072
+ `, idx + 1);
3073
+ if (idx === -1)
3074
+ return "";
3075
+ }
3076
+ return body.slice(idx + 1);
3077
+ }
3078
+ var BRIEF_MARKER_LINE_RE = /^<!-- aeg:brief:v(\d+) -->$/;
3079
+ function parseBriefMarkerVersion(firstLine) {
3080
+ const m = BRIEF_MARKER_LINE_RE.exec(firstLine.trim());
3081
+ if (!m)
3082
+ return null;
3083
+ const version = Number.parseInt(m[1], 10);
3084
+ return Number.isInteger(version) && version >= 1 ? version : null;
3085
+ }
3086
+ function frozenBriefContent(body, version) {
3087
+ return contentAfterNLines(body, version === 1 ? 2 : 3);
3088
+ }
3089
+ function resolveNewestFrozenBrief(comments, allowlist) {
3090
+ let best = null;
3091
+ for (const c of comments) {
3092
+ if (!isPrincipal(c.author, allowlist))
3093
+ continue;
3094
+ const version = parseBriefMarkerVersion(c.body.split(`
3095
+ `)[0] ?? "");
3096
+ if (version === null)
3097
+ continue;
3098
+ if (best === null || version > best.version)
3099
+ best = { ...c, version };
3100
+ }
3101
+ return best === null ? null : { ...best, content: frozenBriefContent(best.body, best.version) };
2952
3102
  }
2953
3103
  // ../../packages/aeg-core/src/doctrine-portability.ts
2954
3104
  var DEFAULT_SHIPS_PREFIX = "aeg-root/";
@@ -3201,6 +3351,28 @@ function topLevelSectionText(body, headingName) {
3201
3351
  const next = /^##[ \t]/m.exec(afterHeading);
3202
3352
  return next ? afterHeading.slice(0, next.index) : afterHeading;
3203
3353
  }
3354
+ function partHasBacktickedPath(text) {
3355
+ let i = 0;
3356
+ while (i < text.length) {
3357
+ const start = text.indexOf("`", i);
3358
+ if (start === -1)
3359
+ return false;
3360
+ const end = text.indexOf("`", start + 1);
3361
+ if (end === -1)
3362
+ return false;
3363
+ if (text.slice(start + 1, end).includes("/"))
3364
+ return true;
3365
+ i = end + 1;
3366
+ }
3367
+ return false;
3368
+ }
3369
+ var PART_MIN_WORDS_OUTSIDE_BACKTICKS = 3;
3370
+ function stripPartBackticks(text) {
3371
+ return text.replace(/`[^`\n]*`/g, " ");
3372
+ }
3373
+ function partWordCount(text) {
3374
+ return text.split(/\s+/).filter((w) => /[a-z]/i.test(w)).length;
3375
+ }
3204
3376
  function looksLikeFilePath(entry2) {
3205
3377
  const stripped = entry2.replace(/\/\*\*?$/, "");
3206
3378
  const lastSegment = stripped.split("/").pop() ?? stripped;
@@ -3241,6 +3413,18 @@ function globCoversPath(glob, path) {
3241
3413
  const p = path.replace(/\/+$/, "");
3242
3414
  return g === p || g.startsWith(`${p}/`) || p.startsWith(`${g}/`);
3243
3415
  }
3416
+ function checkSurfaceGlobsResolve(body, resolvesToFile) {
3417
+ const surface = parseIssueSurface(body);
3418
+ if (!surface.ok)
3419
+ return { status: "pass", errors: [] };
3420
+ const errors = [];
3421
+ for (const glob of surface.value.in) {
3422
+ if (!resolvesToFile(glob)) {
3423
+ errors.push(`issue-validation Surface: \`${glob}\` in \`## Surface\`'s \`in:\` list matches no tracked file — a Surface that cannot resolve cannot render a brief's file list.`);
3424
+ }
3425
+ }
3426
+ return { status: errors.length > 0 ? "fail" : "pass", errors };
3427
+ }
3244
3428
  function checkSurfaceScope(changedFiles, outGlobs) {
3245
3429
  if (outGlobs.length === 0)
3246
3430
  return { ok: true };
@@ -3252,9 +3436,77 @@ function checkSurfaceScope(changedFiles, outGlobs) {
3252
3436
  }
3253
3437
  return violations.length > 0 ? { ok: false, violations } : { ok: true };
3254
3438
  }
3439
+ var ISSUE_PART_LINE_RE = /^Part\s+(\d+)\s*\(([^)]*)\)\s*[-—–]\s*(.*)$/i;
3440
+ function parseIssueParts(body) {
3441
+ const section = topLevelSectionText(body, "Parts");
3442
+ if (section === null)
3443
+ return { ok: false, errors: ["no `## Parts` heading found in the body."] };
3444
+ const lines = section.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
3445
+ const parts = [];
3446
+ const errors = [];
3447
+ for (const line of lines) {
3448
+ const m = ISSUE_PART_LINE_RE.exec(line);
3449
+ if (!m) {
3450
+ errors.push(`"${line}" is not a well-formed Parts line — expected \`Part <n> (<refs>) — <outcome>\`.`);
3451
+ continue;
3452
+ }
3453
+ const n = Number.parseInt(m[1], 10);
3454
+ const refs = m[2];
3455
+ const text = m[3].trim();
3456
+ if (text.length === 0) {
3457
+ errors.push(`Part ${n} has no outcome text after the dash — every Part states one observable outcome.`);
3458
+ continue;
3459
+ }
3460
+ if (partHasBacktickedPath(text) && partWordCount(stripPartBackticks(text)) < PART_MIN_WORDS_OUTSIDE_BACKTICKS) {
3461
+ errors.push(`Part ${n} is little more than a file path — a Part names an outcome and symbols, never a bare path.`);
3462
+ continue;
3463
+ }
3464
+ const objectiveIds = [...refs.matchAll(/O(\d+)/g)].map((r) => Number.parseInt(r[1], 10));
3465
+ parts.push({ n, objectiveIds, text });
3466
+ }
3467
+ if (parts.length === 0) {
3468
+ errors.push("the `## Parts` section has no well-formed `Part <n> (<refs>) — <outcome>` lines.");
3469
+ }
3470
+ if (errors.length > 0)
3471
+ return { ok: false, errors };
3472
+ return { ok: true, value: parts };
3473
+ }
3474
+ function checkPartsCiteDefinedObjectives(body) {
3475
+ const parts = parseIssueParts(body);
3476
+ const objectives = objectivesOf(body);
3477
+ if (!parts.ok || !objectives.ok)
3478
+ return { status: "pass", errors: [] };
3479
+ const definedIds = new Set(objectives.objectives.map((o) => Number.parseInt(o.id.slice(1), 10)));
3480
+ const errors = [];
3481
+ for (const part of parts.value) {
3482
+ for (const objectiveId of part.objectiveIds) {
3483
+ if (!definedIds.has(objectiveId)) {
3484
+ errors.push(`issue-validation Parts: Part ${part.n} cites O${objectiveId}, which the Issue's own \`## Objectives\` section does not define.`);
3485
+ }
3486
+ }
3487
+ }
3488
+ return { status: errors.length > 0 ? "fail" : "pass", errors };
3489
+ }
3255
3490
  function isTaskIssueLabelSet(labels) {
3256
3491
  return hasLabel("tranche", labels);
3257
3492
  }
3493
+ function checkTrancheLabelPresence(_body, _labels) {
3494
+ return { status: "pass", errors: [] };
3495
+ }
3496
+ function checkMilestoneAttach(labels, currentMilestoneTitle, resolvedMilestoneTitle) {
3497
+ if (!isTaskIssueLabelSet(labels))
3498
+ return { status: "pass", errors: [] };
3499
+ if (resolvedMilestoneTitle === null)
3500
+ return { status: "pass", errors: [] };
3501
+ if (currentMilestoneTitle === resolvedMilestoneTitle)
3502
+ return { status: "pass", errors: [] };
3503
+ return {
3504
+ status: "fail",
3505
+ errors: [
3506
+ `issue-validation milestone attach: this task Issue's tranche label resolves to Milestone "${resolvedMilestoneTitle}", but its live Milestone is ${currentMilestoneTitle === null ? "unset" : `"${currentMilestoneTitle}"`} — attach it to "${resolvedMilestoneTitle}".`
3507
+ ]
3508
+ };
3509
+ }
3258
3510
  var TYPE_LABEL_IDS = LABELS.filter((l) => l.category === "type").map((l) => l.id);
3259
3511
  function isControlCodePoint(codePoint) {
3260
3512
  return codePoint <= 31 || codePoint >= 127 && codePoint <= 159;
@@ -3319,6 +3571,19 @@ function checkProjectsRegistered(body, _labels, registeredNames) {
3319
3571
  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;
3320
3572
  var DOC_PATH_RE_GLOBAL = new RegExp(DOC_PATH_RE.source, "gi");
3321
3573
 
3574
+ // ../../packages/aeg-core/src/task-branch-identity.ts
3575
+ var ISSUE_BRANCH_PATTERN = /^task\/issue-(\d+)$/;
3576
+ var TRANCHE_BRANCH_PATTERN = /^task\/([^/]+)\/([^/]+)$/;
3577
+ function parseTaskBranchIdentity(branch) {
3578
+ const issueMatch = ISSUE_BRANCH_PATTERN.exec(branch);
3579
+ if (issueMatch)
3580
+ return { kind: "issue", issueNumber: Number(issueMatch[1]) };
3581
+ const trancheMatch = TRANCHE_BRANCH_PATTERN.exec(branch);
3582
+ if (trancheMatch)
3583
+ return { kind: "tranche", tranche: trancheMatch[1], taskId: trancheMatch[2] };
3584
+ return null;
3585
+ }
3586
+
3322
3587
  // ../../packages/aeg-core/src/coherence-checks.ts
3323
3588
  var COHERENCE_ENFORCED_FROM = "2026-07-01";
3324
3589
  function isGrandfathered(isoDate) {
@@ -3339,6 +3604,7 @@ function checkA1(entries, principalAllowlist = PRINCIPAL_ALLOWLIST) {
3339
3604
  if (handClosed)
3340
3605
  continue;
3341
3606
  failures.push({
3607
+ code: "closed-without-merge",
3342
3608
  issue: e.task.issue,
3343
3609
  tranche: e.trancheSlug,
3344
3610
  task: e.task.id,
@@ -3363,6 +3629,7 @@ function checkA3(entries) {
3363
3629
  continue;
3364
3630
  if (e.facts.prState === "merged" && e.facts.issueState !== "closed") {
3365
3631
  failures.push({
3632
+ code: "auto-close-misfire",
3366
3633
  issue: e.task.issue,
3367
3634
  tranche: e.trancheSlug,
3368
3635
  task: e.task.id,
@@ -3387,6 +3654,7 @@ function checkT1(entries) {
3387
3654
  continue;
3388
3655
  if (e.facts === undefined) {
3389
3656
  failures.push({
3657
+ code: "phantom-issue-ref",
3390
3658
  issue: e.task.issue,
3391
3659
  tranche: e.trancheSlug,
3392
3660
  task: e.task.id,
@@ -3405,6 +3673,7 @@ function checkT2(openIssuesBySlug, topologyIssuesBySlug, ciTrancheSlug) {
3405
3673
  for (const num of openNums) {
3406
3674
  if (!topologySet.has(num)) {
3407
3675
  failures.push({
3676
+ code: "orphan-task",
3408
3677
  issue: num,
3409
3678
  tranche: slug,
3410
3679
  reason: `Issue #${num} is open and labeled ${trancheLabel(slug)} but does not appear in the topology file`
@@ -3439,6 +3708,7 @@ function checkT3(entries, ciTrancheSlug, enrichedEntries, forgeUnavailableSlugs)
3439
3708
  continue;
3440
3709
  if (forgeUnavailableSlugs?.has(e.trancheSlug)) {
3441
3710
  failures.push({
3711
+ code: "tbd-in-active-tranche",
3442
3712
  issue: null,
3443
3713
  tranche: e.trancheSlug,
3444
3714
  task: e.task.id,
@@ -3448,6 +3718,7 @@ function checkT3(entries, ciTrancheSlug, enrichedEntries, forgeUnavailableSlugs)
3448
3718
  continue;
3449
3719
  }
3450
3720
  failures.push({
3721
+ code: "tbd-in-active-tranche",
3451
3722
  issue: null,
3452
3723
  tranche: e.trancheSlug,
3453
3724
  task: e.task.id,
@@ -3480,6 +3751,7 @@ function checkD1(entries, issueToEntry, taskToEntry) {
3480
3751
  const sameIssue = depEntry.task.issue !== null && e.task.issue !== null && depEntry.task.issue === e.task.issue;
3481
3752
  if (sameTask || sameIssue) {
3482
3753
  failures.push({
3754
+ code: "d1-self-dependency",
3483
3755
  issue: e.task.issue,
3484
3756
  tranche: e.trancheSlug,
3485
3757
  task: e.task.id,
@@ -3491,6 +3763,7 @@ function checkD1(entries, issueToEntry, taskToEntry) {
3491
3763
  const depClosed = depFacts?.issueState === "closed";
3492
3764
  if (!depClosed) {
3493
3765
  failures.push({
3766
+ code: "dispatched-on-unmet-deps",
3494
3767
  issue: e.task.issue,
3495
3768
  tranche: e.trancheSlug,
3496
3769
  task: e.task.id,
@@ -3516,11 +3789,13 @@ function checkR1(issuesBySlug, grandfatheredIssues, registeredNames = []) {
3516
3789
  const errors = [
3517
3790
  ...checkIssueRationale(issue.body).errors,
3518
3791
  ...checkProjectsRegistered(issue.body, issue.labels, registeredNames).errors,
3519
- ...checkIssueObjectives(issue.body, issue.number).errors
3792
+ ...checkIssueObjectives(issue.body, issue.number).errors,
3793
+ ...checkPartsCiteDefinedObjectives(issue.body).errors
3520
3794
  ];
3521
3795
  if (errors.length === 0)
3522
3796
  continue;
3523
3797
  failures.push({
3798
+ code: "missing-rationale-field",
3524
3799
  issue: issue.number,
3525
3800
  tranche: slug,
3526
3801
  reason: `Issue #${issue.number} fails the rationale gate: ${errors.join(" | ")}`,
@@ -3549,6 +3824,7 @@ function checkL1(files, entriesBySlug) {
3549
3824
  const allClosed = withFacts.every((e) => e.facts?.issueState === "closed");
3550
3825
  if (allClosed) {
3551
3826
  failures.push({
3827
+ code: "archive-recommended",
3552
3828
  tranche: f.slug,
3553
3829
  reason: "Active tranche has no open task-Issues — consider archiving to completed/"
3554
3830
  });
@@ -3580,27 +3856,38 @@ function extractClosesReferences(prBody) {
3580
3856
  }
3581
3857
  return referenced;
3582
3858
  }
3583
- function checkClosesN2(branch, prBody, trancheFiles, taskIssueRefs) {
3859
+ function checkClosesNTopology(branch, prBody, trancheFiles, taskIssueRefs) {
3584
3860
  const referenced = extractClosesReferences(prBody);
3585
3861
  if (taskIssueRefs) {
3586
3862
  for (const n of referenced) {
3587
- const ref = taskIssueRefs.get(n);
3588
- if (!ref)
3863
+ const ref2 = taskIssueRefs.get(n);
3864
+ if (!ref2)
3589
3865
  continue;
3590
- const expectedBranch = `task/${ref.trancheSlug}/${ref.taskId}`;
3866
+ const expectedBranch = `task/${ref2.trancheSlug}/${ref2.taskId}`;
3591
3867
  if (branch !== expectedBranch) {
3592
3868
  return {
3593
3869
  ok: false,
3594
- message: `closes-n-reverse: branch "${branch}" closes #${n} (task ${ref.taskId} of tranche "${ref.trancheSlug}") but is not named "${expectedBranch}" — rename the branch and re-push, or if this work is intentionally outside AEG's dispatch flow, remove the Closes reference.`
3870
+ message: `closes-n-reverse: branch "${branch}" closes #${n} (task ${ref2.taskId} of tranche "${ref2.trancheSlug}") but is not named "${expectedBranch}" — rename the branch and re-push, or if this work is intentionally outside AEG's dispatch flow, remove the Closes reference.`
3595
3871
  };
3596
3872
  }
3597
3873
  }
3598
3874
  }
3599
- const m = branch.match(/^task\/([^/]+)\/([^/]+)$/);
3600
- if (!m)
3875
+ const ref = parseTaskBranchIdentity(branch);
3876
+ if (!ref)
3601
3877
  return { ok: true };
3602
- const trancheSlug = m[1];
3603
- const taskId = m[2];
3878
+ if (ref.kind === "issue") {
3879
+ const expectedIssue2 = ref.issueNumber;
3880
+ if (!referenced.has(expectedIssue2)) {
3881
+ return {
3882
+ ok: false,
3883
+ expectedIssue: expectedIssue2,
3884
+ message: `closes-n: PR body does not contain \`Closes #${expectedIssue2}\` (required for branch "${branch}"). Add it to the PR body Summary section.`
3885
+ };
3886
+ }
3887
+ return { ok: true, expectedIssue: expectedIssue2 };
3888
+ }
3889
+ const trancheSlug = ref.tranche;
3890
+ const taskId = ref.taskId;
3604
3891
  const trancheFile = trancheFiles.find((f) => f.slug === trancheSlug);
3605
3892
  if (!trancheFile) {
3606
3893
  return {
@@ -3652,29 +3939,67 @@ function extractIssue(body) {
3652
3939
  const issue = headerNums.length > 0 ? headerNums[0] : bodyNums[0];
3653
3940
  return { issue, extraIssues: bodyNums.filter((n) => n !== issue), outsideHeader: headerNums.length === 0 };
3654
3941
  }
3655
- // ../../packages/aeg-core/src/review-gate.ts
3656
- function isBoundToHead(extraction, headSha) {
3657
- if (!extraction.headSha)
3942
+ // ../../packages/aeg-core/src/review-input-manifest.ts
3943
+ import { createHash as createHash4 } from "node:crypto";
3944
+ function briefHash(brief) {
3945
+ return createHash4("sha256").update(`${brief}
3946
+ `).digest("hex");
3947
+ }
3948
+ function policyDigest(policy) {
3949
+ return createHash4("sha256").update(JSON.stringify({ codeReviewThreshold: policy.codeReviewThreshold, securityThreshold: policy.securityThreshold })).digest("hex");
3950
+ }
3951
+ function isBoundToHead(echoed, headSha) {
3952
+ if (!echoed.headSha)
3658
3953
  return false;
3659
- return headSha.toLowerCase().startsWith(extraction.headSha.toLowerCase());
3954
+ return headSha.toLowerCase().startsWith(echoed.headSha.toLowerCase());
3660
3955
  }
3661
- function isBoundByPatchIdentity(extraction, headSha, patchIdOf) {
3662
- if (patchIdOf === undefined || !extraction.headSha)
3956
+ function isBoundByPatchIdentity(echoed, headSha, patchIdOf) {
3957
+ if (patchIdOf === undefined || !echoed.headSha)
3663
3958
  return false;
3664
- const judged = patchIdOf(extraction.headSha);
3959
+ const judged = patchIdOf(echoed.headSha);
3665
3960
  const current = patchIdOf(headSha);
3666
3961
  if (judged === null || current === null)
3667
3962
  return false;
3668
3963
  return judged === current;
3669
3964
  }
3670
- function isBoundToPatch(extraction, headSha, patchIdOf) {
3671
- return isBoundToHead(extraction, headSha) || isBoundByPatchIdentity(extraction, headSha, patchIdOf);
3965
+ function isBoundToPatch(echoed, headSha, patchIdOf) {
3966
+ return isBoundToHead(echoed, headSha) || isBoundByPatchIdentity(echoed, headSha, patchIdOf);
3672
3967
  }
3673
- function isBoundToObjectives(extraction, currentVersion) {
3968
+ function isBoundToObjectives(echoed, currentVersion) {
3674
3969
  if (currentVersion === null)
3675
3970
  return true;
3676
- return extraction.objectivesVersion === currentVersion;
3971
+ return echoed.objectivesVersion === currentVersion;
3972
+ }
3973
+ function isBoundToRulings(echoed, currentOrdinal) {
3974
+ if (echoed.rulingOrdinal === null)
3975
+ return currentOrdinal === 0;
3976
+ return echoed.rulingOrdinal === currentOrdinal;
3977
+ }
3978
+ function isBoundToBriefHash(echoed, currentHash) {
3979
+ if (currentHash === null)
3980
+ return true;
3981
+ return echoed.briefHash === currentHash;
3982
+ }
3983
+ function isBoundToPolicy(echoed, currentDigest) {
3984
+ return echoed.policyDigest === currentDigest;
3677
3985
  }
3986
+ function compareManifest(echoed, current, patchIdOf) {
3987
+ const head = isBoundToPatch(echoed, current.headSha, patchIdOf);
3988
+ const briefHashBound = isBoundToBriefHash(echoed, current.briefHash);
3989
+ const objectivesVersion2 = isBoundToObjectives(echoed, current.objectivesVersion);
3990
+ const rulingOrdinal = isBoundToRulings(echoed, current.rulingOrdinal);
3991
+ const policyDigestBound = isBoundToPolicy(echoed, current.policyDigest);
3992
+ return {
3993
+ bound: head && briefHashBound && objectivesVersion2 && rulingOrdinal && policyDigestBound,
3994
+ head,
3995
+ briefHash: briefHashBound,
3996
+ objectivesVersion: objectivesVersion2,
3997
+ rulingOrdinal,
3998
+ policyDigest: policyDigestBound
3999
+ };
4000
+ }
4001
+
4002
+ // ../../packages/aeg-core/src/review-gate.ts
3678
4003
  function checkReviewGate(input) {
3679
4004
  const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
3680
4005
  const waived = isWaiverLabelActorVerified({
@@ -3697,13 +4022,55 @@ function checkReviewGate(input) {
3697
4022
  const verifiedBodies = verified.map((c) => c.body);
3698
4023
  const codeReview = extractCodeReviewVerdict(verifiedBodies);
3699
4024
  const security = extractSecurityReviewVerdict(verifiedBodies);
3700
- const codeReviewClean = codeReview.value === "APPROVE";
3701
- const securityClean = security.value === "PASS";
3702
- const codeReviewBound = isBoundToPatch(codeReview, input.headSha, input.patchIdOf);
3703
- const securityBound = isBoundToPatch(security, input.headSha, input.patchIdOf);
3704
- const codeReviewObjectivesBound = isBoundToObjectives(codeReview, input.objectivesVersion);
3705
- const securityObjectivesBound = isBoundToObjectives(security, input.objectivesVersion);
3706
- if (codeReviewClean && codeReviewBound && codeReviewObjectivesBound && securityClean && securityBound && securityObjectivesBound && mechanicalChecksClean) {
4025
+ const policy = input.policy ?? DEFAULT_REVIEW_POLICY;
4026
+ let codeReviewPolicyEvaluation;
4027
+ let securityPolicyEvaluation;
4028
+ try {
4029
+ codeReviewPolicyEvaluation = evaluateCodeReview(codeReview.findingSeverities, policy);
4030
+ securityPolicyEvaluation = evaluateSecurityReview(security.findingSeverities, policy);
4031
+ } catch (err) {
4032
+ return {
4033
+ verdict: "fail",
4034
+ reason: `a verdict comment carries a finding severity this repository's policy does not recognize: ${err instanceof Error ? err.message : String(err)}`,
4035
+ waived: false
4036
+ };
4037
+ }
4038
+ const codeReviewTextClean = codeReview.value === "APPROVE";
4039
+ const securityTextClean = security.value === "PASS";
4040
+ const codeReviewPolicyClean = codeReviewPolicyEvaluation.outcome === "clean";
4041
+ const securityPolicyClean = securityPolicyEvaluation.outcome === "clean";
4042
+ const codeReviewClean = codeReviewTextClean && codeReviewPolicyClean;
4043
+ const securityClean = securityTextClean && securityPolicyClean;
4044
+ const currentManifest = {
4045
+ headSha: input.headSha,
4046
+ briefHash: input.briefHash ?? null,
4047
+ objectivesVersion: input.objectivesVersion,
4048
+ rulingOrdinal: input.rulingOrdinal,
4049
+ policyDigest: policyDigest(policy)
4050
+ };
4051
+ const codeReviewEchoed = {
4052
+ headSha: codeReview.headSha,
4053
+ briefHash: codeReview.briefHash,
4054
+ objectivesVersion: codeReview.objectivesVersion,
4055
+ rulingOrdinal: codeReview.rulingOrdinal,
4056
+ policyDigest: codeReview.policyDigest
4057
+ };
4058
+ const securityEchoed = {
4059
+ headSha: security.headSha,
4060
+ briefHash: security.briefHash,
4061
+ objectivesVersion: security.objectivesVersion,
4062
+ rulingOrdinal: security.rulingOrdinal,
4063
+ policyDigest: security.policyDigest
4064
+ };
4065
+ const codeReviewBinding = compareManifest(codeReviewEchoed, currentManifest, input.patchIdOf);
4066
+ const securityBinding = compareManifest(securityEchoed, currentManifest, input.patchIdOf);
4067
+ const codeReviewBound = codeReviewBinding.head;
4068
+ const securityBound = securityBinding.head;
4069
+ const codeReviewObjectivesBound = codeReviewBinding.objectivesVersion;
4070
+ const securityObjectivesBound = securityBinding.objectivesVersion;
4071
+ const codeReviewRulingsBound = codeReviewBinding.rulingOrdinal;
4072
+ const securityRulingsBound = securityBinding.rulingOrdinal;
4073
+ if (codeReviewClean && codeReviewBinding.bound && securityClean && securityBinding.bound && mechanicalChecksClean) {
3707
4074
  return {
3708
4075
  verdict: "pass",
3709
4076
  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.`,
@@ -3711,19 +4078,35 @@ function checkReviewGate(input) {
3711
4078
  };
3712
4079
  }
3713
4080
  const problems = [];
3714
- if (!codeReviewClean) {
4081
+ if (!codeReviewTextClean) {
3715
4082
  problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
4083
+ } else if (!codeReviewPolicyClean) {
4084
+ problems.push(`code-reviewer verdict says APPROVE but carries a finding (${codeReviewPolicyEvaluation.blockingFindings.map((f) => f.severity).join(", ")}) at or above this repository's code-review policy threshold (${policy.codeReviewThreshold}) — a reviewer's own APPROVE never overrides policy`);
3716
4085
  } else if (!codeReviewBound) {
3717
4086
  problems.push(`the newest code-review verdict covers ${codeReview.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
3718
4087
  } else if (!codeReviewObjectivesBound) {
3719
4088
  problems.push(`the newest code-review verdict was cast against objectives version ${codeReview.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
3720
- }
3721
- if (!securityClean) {
4089
+ } else if (!codeReviewRulingsBound) {
4090
+ 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`);
4091
+ } else if (!codeReviewBinding.briefHash) {
4092
+ problems.push(`the newest code-review verdict was cast against brief hash ${codeReview.briefHash ?? "none"}, the frozen brief's current hash is ${currentManifest.briefHash ?? "none"}`);
4093
+ } else if (!codeReviewBinding.policyDigest) {
4094
+ problems.push(`the newest code-review verdict was cast against review policy digest ${codeReview.policyDigest ?? "none"}, this repository's current policy digest is ${currentManifest.policyDigest}`);
4095
+ }
4096
+ if (!securityTextClean) {
3722
4097
  problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
4098
+ } else if (!securityPolicyClean) {
4099
+ problems.push(`security-review verdict says PASS but carries a finding (${securityPolicyEvaluation.blockingFindings.map((f) => f.severity).join(", ")}) at or above this repository's security policy threshold (${policy.securityThreshold}) — a reviewer's own PASS never overrides policy`);
3723
4100
  } else if (!securityBound) {
3724
4101
  problems.push(`the newest security-review verdict covers ${security.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
3725
4102
  } else if (!securityObjectivesBound) {
3726
4103
  problems.push(`the newest security-review verdict was cast against objectives version ${security.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
4104
+ } else if (!securityRulingsBound) {
4105
+ 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`);
4106
+ } else if (!securityBinding.briefHash) {
4107
+ problems.push(`the newest security-review verdict was cast against brief hash ${security.briefHash ?? "none"}, the frozen brief's current hash is ${currentManifest.briefHash ?? "none"}`);
4108
+ } else if (!securityBinding.policyDigest) {
4109
+ problems.push(`the newest security-review verdict was cast against review policy digest ${security.policyDigest ?? "none"}, this repository's current policy digest is ${currentManifest.policyDigest}`);
3727
4110
  }
3728
4111
  if (!mechanicalChecksClean) {
3729
4112
  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(", ")}`);
@@ -3740,6 +4123,24 @@ var DEFAULT_RELEASE_ACTOR = "github-actions[bot]";
3740
4123
  function isChangesetsReleasePr(branch, author, expectedAuthor) {
3741
4124
  return branch === CHANGESET_RELEASE_BRANCH && author === expectedAuthor;
3742
4125
  }
4126
+ // ../../packages/aeg-core/src/ruling-ordinal.ts
4127
+ var RULING_MARKER_ORDINAL = /^<!-- aeg:principal:ruling:\d+-(\d+) -->$/;
4128
+ function newestPrincipalRulingOrdinal(comments, allowlist) {
4129
+ let best = 0;
4130
+ for (const c of comments) {
4131
+ if (!isPrincipal(c.author, allowlist))
4132
+ continue;
4133
+ const firstLine = (c.body.split(`
4134
+ `)[0] ?? "").trim();
4135
+ const m = RULING_MARKER_ORDINAL.exec(firstLine);
4136
+ if (!m)
4137
+ continue;
4138
+ const k = Number.parseInt(m[1], 10);
4139
+ if (k > best)
4140
+ best = k;
4141
+ }
4142
+ return best;
4143
+ }
3743
4144
  // ../../packages/aeg-core/src/markdown-table.ts
3744
4145
  function splitRow(line) {
3745
4146
  const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
@@ -4397,42 +4798,45 @@ function checkDispatchReadiness(input) {
4397
4798
  const { trancheSlug, task } = input;
4398
4799
  const taskLabel = `task ${task.id} (tranche ${trancheSlug})`;
4399
4800
  const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
4400
- const blockers = [];
4801
+ const blockerDetails = [];
4802
+ const push = (blockerClass, message) => {
4803
+ blockerDetails.push({ class: blockerClass, message });
4804
+ };
4401
4805
  if (task.issue === null) {
4402
- blockers.push(`dispatch-gate issue-existence: ${taskLabel} has no Issue (#TBD or blank) in the topology — not dispatchable until the Planner cuts the Issue.`);
4806
+ push("issue-existence", `dispatch-gate issue-existence: ${taskLabel} has no Issue (#TBD or blank) in the topology — not dispatchable until the Planner cuts the Issue.`);
4403
4807
  } else if (input.issue === null) {
4404
- blockers.push(`dispatch-gate issue-existence: ${taskLabel} names Issue #${task.issue}, but it does not resolve to a real GitHub Issue (phantom reference).`);
4808
+ push("issue-existence", `dispatch-gate issue-existence: ${taskLabel} names Issue #${task.issue}, but it does not resolve to a real GitHub Issue (phantom reference).`);
4405
4809
  }
4406
4810
  if (input.issue !== null && !input.issueRationalePass) {
4407
- blockers.push(`dispatch-gate rationale: Issue #${input.issue.number} for ${taskLabel} fails the rationale gate (checkIssueRationale) — the Planner must complete the eight-field rationale before this task is dispatchable.`);
4811
+ push("rationale", `dispatch-gate rationale: Issue #${input.issue.number} for ${taskLabel} fails the rationale gate (checkIssueRationale) — the Planner must complete the eight-field rationale before this task is dispatchable.`);
4408
4812
  }
4409
4813
  for (const dep of input.dependsOn) {
4410
4814
  if (isSelfDependency(dep, task, input.issue)) {
4411
4815
  const issueStr = input.issue !== null ? `#${input.issue.number}` : "?";
4412
- blockers.push(`dispatch-gate INTERNAL: parsed a self-dependency for task ${task.id} (${issueStr}) — this is a parser bug in parseRationaleDeps, not a real dependency. Please report it upstream. Re-run once the rationale is corrected or the fix ships.`);
4816
+ push("internal-self-dependency", `dispatch-gate INTERNAL: parsed a self-dependency for task ${task.id} (${issueStr}) — this is a parser bug in parseRationaleDeps, not a real dependency. Please report it upstream. Re-run once the rationale is corrected or the fix ships.`);
4413
4817
  continue;
4414
4818
  }
4415
4819
  if (dep.resolved === false) {
4416
- blockers.push(`dispatch-gate depends-on: ${taskLabel} depends on "${dep.id}", which is UNRESOLVABLE — the resolver could not find a matching tranche/task/Issue for this edge (not a claim about merge status). Not dispatchable until the edge is corrected.`);
4820
+ push("depends-on-unresolvable", `dispatch-gate depends-on: ${taskLabel} depends on "${dep.id}", which is UNRESOLVABLE — the resolver could not find a matching tranche/task/Issue for this edge (not a claim about merge status). Not dispatchable until the edge is corrected.`);
4417
4821
  continue;
4418
4822
  }
4419
4823
  if (!dep.merged && !isHandClosedByRecognizedPrincipal(dep, principalAllowlist)) {
4420
4824
  const issueStr = dep.issue !== null ? ` (#${dep.issue})` : "";
4421
- blockers.push(`dispatch-gate depends-on: ${taskLabel} depends on ${dep.id}${issueStr}, whose PR is not merged yet — not dispatchable, it serializes behind it.`);
4825
+ push("depends-on-not-merged", `dispatch-gate depends-on: ${taskLabel} depends on ${dep.id}${issueStr}, whose PR is not merged yet — not dispatchable, it serializes behind it.`);
4422
4826
  }
4423
4827
  }
4424
4828
  for (const c of input.conflictsWith) {
4425
4829
  if (c.openOrInFlight) {
4426
4830
  const issueStr = c.issue !== null ? ` (#${c.issue})` : "";
4427
- blockers.push(`dispatch-gate conflicts-with: ${taskLabel} conflicts with ${c.id}${issueStr}, whose PR is open or in-flight — not dispatchable until it merges.`);
4831
+ push("conflicts-with", `dispatch-gate conflicts-with: ${taskLabel} conflicts with ${c.id}${issueStr}, whose PR is open or in-flight — not dispatchable until it merges.`);
4428
4832
  }
4429
4833
  }
4430
4834
  for (const proj of input.priorTrancheArchival) {
4431
4835
  if (proj.priorTrancheSlug !== null && !proj.archived) {
4432
- blockers.push(`dispatch-gate prior-tranche-archival: project \`${proj.project}\`'s previous tranche \`${proj.priorTrancheSlug}\` is not archived — the Tranche Archivist must run before new work on this product.`);
4836
+ push("prior-tranche-archival", `dispatch-gate prior-tranche-archival: project \`${proj.project}\`'s previous tranche \`${proj.priorTrancheSlug}\` is not archived — the Tranche Archivist must run before new work on this product.`);
4433
4837
  }
4434
4838
  }
4435
- return { ready: blockers.length === 0, blockers };
4839
+ return { ready: blockerDetails.length === 0, blockers: blockerDetails.map((b) => b.message), blockerDetails };
4436
4840
  }
4437
4841
  // ../../packages/aeg-core/src/single-plan-pr.ts
4438
4842
  function trancheSlugFromTopologyPath(path) {
@@ -4534,6 +4938,91 @@ function checkBranchTopology(input) {
4534
4938
  reason: `Branch \`${branch}\` matches topology row \`${taskId}\` in ${topoPath}.`
4535
4939
  };
4536
4940
  }
4941
+ // ../../packages/aeg-core/src/task-tools.ts
4942
+ import { z } from "zod";
4943
+ var TASK_TOOL_ERROR_KINDS = [
4944
+ "validation",
4945
+ "authority",
4946
+ "precondition",
4947
+ "capability",
4948
+ "infrastructure",
4949
+ "cancellation",
4950
+ "timeout",
4951
+ "uncertain_effect"
4952
+ ];
4953
+ var TaskToolErrorSchema = z.object({
4954
+ kind: z.enum(TASK_TOOL_ERROR_KINDS),
4955
+ message: z.string().min(1),
4956
+ detail: z.string().optional()
4957
+ });
4958
+ var TaskToolRefSchema = z.union([
4959
+ z.object({ tranche: z.string().min(1), id: z.string().min(1) }),
4960
+ z.object({ issue: z.number().int().positive() })
4961
+ ]);
4962
+ var MAX_PAGE_LIMIT = 100;
4963
+ var PageRequestSchema = z.object({
4964
+ cursor: z.string().optional(),
4965
+ limit: z.number().int().positive().max(MAX_PAGE_LIMIT).optional()
4966
+ });
4967
+ var FreshnessSchema = z.enum(["fresh", "stale", "unknown"]);
4968
+ var ObservedSchema = z.object({
4969
+ observedAt: z.string(),
4970
+ freshness: FreshnessSchema
4971
+ });
4972
+ var TaskStatusInputSchema = z.object({
4973
+ task: TaskToolRefSchema.optional()
4974
+ }).merge(PageRequestSchema);
4975
+ var TaskStatusItemSchema = z.object({
4976
+ task: TaskToolRefSchema,
4977
+ issue: z.number().int().positive(),
4978
+ pr: z.number().int().positive().nullable(),
4979
+ state: z.string()
4980
+ }).merge(ObservedSchema);
4981
+ var TaskStatusResultSchema = z.object({
4982
+ items: z.array(TaskStatusItemSchema),
4983
+ nextCursor: z.string().nullable()
4984
+ });
4985
+ var RequestedAuthoritySchema = z.enum(["planner", "principal", "operator", "self"]);
4986
+ var EscalationInputsSchema = z.object({
4987
+ task: z.number().int().positive(),
4988
+ round: z.number().int().nonnegative(),
4989
+ head: z.string(),
4990
+ branch: z.string(),
4991
+ prNumber: z.number().int().positive()
4992
+ });
4993
+ var EscalationEvidenceSchema = z.object({
4994
+ round: z.number().int().nonnegative(),
4995
+ reviewer: z.string().nullable(),
4996
+ security: z.string().nullable()
4997
+ });
4998
+ var TaskEscalationReadInputSchema = z.object({
4999
+ task: TaskToolRefSchema
5000
+ }).merge(PageRequestSchema);
5001
+ var TaskEscalationPacketSchema = z.object({
5002
+ reason: z.string(),
5003
+ detail: z.string().nullable(),
5004
+ inputs: EscalationInputsSchema.nullable(),
5005
+ evidence: EscalationEvidenceSchema.nullable(),
5006
+ attemptedRecovery: z.string(),
5007
+ requestedAuthority: RequestedAuthoritySchema,
5008
+ permittedNextActions: z.array(z.string())
5009
+ }).merge(ObservedSchema);
5010
+ var TaskEscalationReadResultSchema = z.object({
5011
+ items: z.array(TaskEscalationPacketSchema),
5012
+ nextCursor: z.string().nullable()
5013
+ }).merge(ObservedSchema);
5014
+ var TaskStartInputSchema = z.object({
5015
+ tranche: z.string().min(1),
5016
+ id: z.string().min(1)
5017
+ });
5018
+ var TaskResumeInputSchema = z.object({
5019
+ task: TaskToolRefSchema
5020
+ });
5021
+ var TaskCancelInputSchema = z.object({
5022
+ task: TaskToolRefSchema,
5023
+ reason: z.string().min(1)
5024
+ });
5025
+ var NoResultSchema = z.never();
4537
5026
  // ../../packages/aeg-core/src/first-push-dispatch-gate.ts
4538
5027
  function parseTaskBranch(branch) {
4539
5028
  const m = /^task\/([^/]+)\/([^/]+)$/.exec(branch);
@@ -4617,7 +5106,6 @@ function decideIssueAssignment(input) {
4617
5106
  };
4618
5107
  }
4619
5108
  // ../../packages/aeg-core/src/test-plan-gate.ts
4620
- var TASK_BRANCH_PATTERN2 = /^task\/[^/]+\/[^/]+$/;
4621
5109
  function evaluateTestPlanGate(body, branch) {
4622
5110
  if (!body) {
4623
5111
  return {
@@ -4630,7 +5118,7 @@ function evaluateTestPlanGate(body, branch) {
4630
5118
  }
4631
5119
  const located = locateTestPlanSection(body);
4632
5120
  if (!located.found) {
4633
- if (TASK_BRANCH_PATTERN2.test(branch)) {
5121
+ if (parseTaskBranchIdentity(branch) !== null) {
4634
5122
  return {
4635
5123
  verdict: "fail",
4636
5124
  messages: [
@@ -4758,7 +5246,7 @@ function findWorkspaceEscapes(files, knownPaths, workspaceDirs = DEFAULT_WORKSPA
4758
5246
  return findings;
4759
5247
  }
4760
5248
  // ../../packages/aeg-core/src/log/schema.ts
4761
- import { z } from "zod";
5249
+ import { z as z2 } from "zod";
4762
5250
  var ROLE_VALUES = [
4763
5251
  "planner",
4764
5252
  "developer",
@@ -4768,136 +5256,172 @@ var ROLE_VALUES = [
4768
5256
  "archivist",
4769
5257
  "architect"
4770
5258
  ];
4771
- var RoleSchema = z.enum(ROLE_VALUES);
5259
+ var RoleSchema = z2.enum(ROLE_VALUES);
4772
5260
  var HOST_VALUES = ["hook", "ci", "cli", "loop"];
4773
- var HostSchema = z.enum(HOST_VALUES);
5261
+ var HostSchema = z2.enum(HOST_VALUES);
4774
5262
  var RUN_ID_PATTERN = /^[A-Za-z0-9_.-]{1,128}$/;
4775
- var HeaderMetaSchema = z.object({
4776
- schema: z.literal(1),
4777
- ts: z.string(),
4778
- run_id: z.string().regex(RUN_ID_PATTERN),
4779
- seq: z.number().int().nonnegative(),
4780
- repo: z.string().nullable(),
4781
- vinaya: z.string(),
4782
- doctrine: z.string(),
5263
+ var headerMetaCore = {
5264
+ ts: z2.string(),
5265
+ run_id: z2.string().regex(RUN_ID_PATTERN),
5266
+ seq: z2.number().int().nonnegative(),
5267
+ repo: z2.string().nullable(),
5268
+ vinaya: z2.string(),
5269
+ doctrine: z2.string(),
4783
5270
  host: HostSchema,
4784
- machine: z.string()
5271
+ machine: z2.string()
5272
+ };
5273
+ var HeaderMetaV1Schema = z2.object({
5274
+ schema: z2.literal(1),
5275
+ ...headerMetaCore
4785
5276
  }).strict();
4786
- var SubjectSchema = z.object({
4787
- issue: z.number().int().nullable(),
4788
- pr: z.number().int().optional(),
4789
- sha: z.string().optional(),
4790
- role: z.union([RoleSchema, z.literal("unattributed")]),
4791
- round: z.number().int().optional(),
4792
- objectives_version: z.string().optional()
5277
+ var LineageSchema = z2.object({
5278
+ run: z2.string().nullable(),
5279
+ attempt: z2.number().int().nullable(),
5280
+ parent: z2.string().nullable()
4793
5281
  }).strict();
4794
- var HeaderSchema = z.object({
5282
+ var InputVersionsSchema = z2.object({
5283
+ objectives_version: z2.string().nullable(),
5284
+ brief_hash: z2.string().nullable(),
5285
+ ruling_ordinal: z2.number().int().nullable(),
5286
+ policy_digest: z2.string().nullable()
5287
+ }).strict();
5288
+ var ProvenanceSchema = z2.enum(["parent_attributed", "env_correlated", "self_reported", "unavailable"]);
5289
+ var HeaderMetaV2Schema = z2.object({
5290
+ schema: z2.literal(2),
5291
+ ...headerMetaCore,
5292
+ event_id: z2.string().min(1),
5293
+ process_id: z2.string().min(1),
5294
+ actor_id: z2.string().nullable(),
5295
+ lineage: LineageSchema,
5296
+ input_versions: InputVersionsSchema,
5297
+ provenance: ProvenanceSchema
5298
+ }).strict();
5299
+ var HeaderMetaSchema = z2.discriminatedUnion("schema", [HeaderMetaV1Schema, HeaderMetaV2Schema]);
5300
+ var SubjectSchema = z2.object({
5301
+ issue: z2.number().int().nullable(),
5302
+ pr: z2.number().int().optional(),
5303
+ sha: z2.string().optional(),
5304
+ role: z2.union([RoleSchema, z2.literal("unattributed")]),
5305
+ round: z2.number().int().optional(),
5306
+ objectives_version: z2.string().optional()
5307
+ }).strict();
5308
+ var HeaderSchema = z2.object({
4795
5309
  meta: HeaderMetaSchema,
4796
5310
  subject: SubjectSchema
4797
5311
  }).strict();
4798
5312
  var envelopeTail = {
4799
- duration_ms: z.number().nonnegative().optional(),
4800
- payload: z.object({}).strict()
5313
+ duration_ms: z2.number().nonnegative().optional(),
5314
+ payload: z2.object({}).strict()
4801
5315
  };
4802
- var DispatchOutcomeSchema = z.discriminatedUnion("type", [
4803
- z.object({ type: z.literal("pr_opened"), pr: z.number().int(), head: z.string() }).strict(),
4804
- z.object({
4805
- type: z.literal("round_pushed"),
4806
- pr: z.number().int(),
4807
- head: z.string(),
4808
- comment_id: z.number().int()
5316
+ var ReviewFindingSchema = z2.object({
5317
+ id: z2.string(),
5318
+ severity: z2.string(),
5319
+ state: z2.string().optional(),
5320
+ severity_scale: z2.string().optional(),
5321
+ policy_treatment: z2.enum(["blocking", "non_blocking", "unavailable"]).optional(),
5322
+ confidence: z2.number().min(0).max(1).optional(),
5323
+ confidence_scale: z2.string().optional(),
5324
+ confidence_source: z2.string().optional()
5325
+ }).strict();
5326
+ var DispatchOutcomeSchema = z2.discriminatedUnion("type", [
5327
+ z2.object({ type: z2.literal("pr_opened"), pr: z2.number().int(), head: z2.string() }).strict(),
5328
+ z2.object({
5329
+ type: z2.literal("round_pushed"),
5330
+ pr: z2.number().int(),
5331
+ head: z2.string(),
5332
+ comment_id: z2.number().int()
4809
5333
  }).strict(),
4810
- z.object({
4811
- type: z.literal("verdict"),
4812
- verdict: z.enum(["APPROVE", "REQUEST CHANGES", "PASS", "FAIL"]),
4813
- head: z.string(),
4814
- comment_id: z.number().int(),
4815
- objectives: z.array(z.object({ id: z.string(), met: z.boolean() }).strict()),
4816
- findings: z.array(z.object({ id: z.string(), severity: z.string(), state: z.string().optional() }).strict())
5334
+ z2.object({
5335
+ type: z2.literal("verdict"),
5336
+ verdict: z2.enum(["APPROVE", "REQUEST CHANGES", "PASS", "FAIL"]),
5337
+ head: z2.string(),
5338
+ comment_id: z2.number().int(),
5339
+ objectives: z2.array(z2.object({ id: z2.string(), met: z2.boolean() }).strict()),
5340
+ findings: z2.array(ReviewFindingSchema)
4817
5341
  }).strict(),
4818
- z.object({
4819
- type: z.literal("escalation"),
4820
- class: z.enum(["authority", "strategy", "product"]),
4821
- comment_id: z.number().int()
5342
+ z2.object({
5343
+ type: z2.literal("escalation"),
5344
+ class: z2.enum(["authority", "strategy", "product"]),
5345
+ comment_id: z2.number().int()
4822
5346
  }).strict(),
4823
- z.object({ type: z.literal("brief"), comment_id: z.number().int(), hash: z.string() }).strict(),
4824
- z.object({ type: z.literal("plan"), issues: z.array(z.number().int()) }).strict(),
4825
- z.object({ type: z.literal("archive"), provenance_comment_id: z.number().int() }).strict()
5347
+ z2.object({ type: z2.literal("brief"), comment_id: z2.number().int(), hash: z2.string() }).strict(),
5348
+ z2.object({ type: z2.literal("plan"), issues: z2.array(z2.number().int()) }).strict(),
5349
+ z2.object({ type: z2.literal("archive"), provenance_comment_id: z2.number().int() }).strict()
4826
5350
  ]);
4827
5351
  var dispatchShared = {
4828
5352
  meta: HeaderMetaSchema,
4829
5353
  subject: SubjectSchema,
4830
- kind: z.literal("dispatch"),
5354
+ kind: z2.literal("dispatch"),
4831
5355
  ...envelopeTail,
4832
5356
  target_role: RoleSchema,
4833
- model: z.string(),
4834
- round: z.number().int().optional(),
4835
- effect_id: z.string()
5357
+ model: z2.string(),
5358
+ round: z2.number().int().optional(),
5359
+ effect_id: z2.string()
4836
5360
  };
4837
- var dispatchUsageField = z.object({ input: z.number().nonnegative(), output: z.number().nonnegative() }).strict().nullable();
4838
- var DispatchEventSchema = z.discriminatedUnion("event", [
4839
- z.object({ ...dispatchShared, event: z.literal("dispatched"), prompt_hash: z.string() }).strict(),
4840
- z.object({
5361
+ var dispatchUsageField = z2.object({ input: z2.number().nonnegative(), output: z2.number().nonnegative() }).strict().nullable();
5362
+ var DispatchEventSchema = z2.discriminatedUnion("event", [
5363
+ z2.object({ ...dispatchShared, event: z2.literal("dispatched"), prompt_hash: z2.string() }).strict(),
5364
+ z2.object({
4841
5365
  ...dispatchShared,
4842
- event: z.literal("outcome_received"),
5366
+ event: z2.literal("outcome_received"),
4843
5367
  outcome: DispatchOutcomeSchema,
4844
5368
  usage: dispatchUsageField
4845
5369
  }).strict(),
4846
- z.object({
5370
+ z2.object({
4847
5371
  ...dispatchShared,
4848
- event: z.literal("dispatch_failed"),
4849
- reason: z.enum(["timeout", "crash", "refused", "unattributed_write"]),
5372
+ event: z2.literal("dispatch_failed"),
5373
+ reason: z2.enum(["timeout", "crash", "refused", "unattributed_write"]),
4850
5374
  usage: dispatchUsageField
4851
5375
  }).strict()
4852
5376
  ]);
4853
5377
  var loopShared = {
4854
5378
  meta: HeaderMetaSchema,
4855
5379
  subject: SubjectSchema,
4856
- kind: z.literal("dev_review_loop"),
5380
+ kind: z2.literal("dev_review_loop"),
4857
5381
  ...envelopeTail,
4858
- loop_id: z.string()
5382
+ loop_id: z2.string()
4859
5383
  };
4860
- var DevReviewLoopEventSchema = z.discriminatedUnion("event", [
4861
- z.object({
5384
+ var DevReviewLoopEventSchema = z2.discriminatedUnion("event", [
5385
+ z2.object({
4862
5386
  ...loopShared,
4863
- event: z.literal("loop_started"),
4864
- task: z.number().int(),
4865
- policy: z.object({
4866
- max_rounds: z.number().int().nonnegative(),
4867
- reviewers: z.array(RoleSchema),
4868
- models: z.record(RoleSchema, z.string())
5387
+ event: z2.literal("loop_started"),
5388
+ task: z2.number().int(),
5389
+ policy: z2.object({
5390
+ max_rounds: z2.number().int().nonnegative(),
5391
+ reviewers: z2.array(RoleSchema),
5392
+ models: z2.record(RoleSchema, z2.string())
4869
5393
  }).strict()
4870
5394
  }).strict(),
4871
- z.object({ ...loopShared, event: z.literal("round_started"), round: z.number().int(), base_head: z.string() }).strict(),
4872
- z.object({
5395
+ z2.object({ ...loopShared, event: z2.literal("round_started"), round: z2.number().int(), base_head: z2.string() }).strict(),
5396
+ z2.object({
4873
5397
  ...loopShared,
4874
- event: z.literal("gate_result_read"),
4875
- round: z.number().int(),
4876
- head: z.string(),
4877
- green: z.boolean()
5398
+ event: z2.literal("gate_result_read"),
5399
+ round: z2.number().int(),
5400
+ head: z2.string(),
5401
+ green: z2.boolean()
4878
5402
  }).strict(),
4879
- z.object({
5403
+ z2.object({
4880
5404
  ...loopShared,
4881
- event: z.literal("verdicts_read"),
4882
- round: z.number().int(),
4883
- head: z.string(),
4884
- all_approve: z.boolean(),
4885
- blockers: z.number().int().nonnegative()
5405
+ event: z2.literal("verdicts_read"),
5406
+ round: z2.number().int(),
5407
+ head: z2.string(),
5408
+ all_approve: z2.boolean(),
5409
+ blockers: z2.number().int().nonnegative()
4886
5410
  }).strict(),
4887
- z.object({
5411
+ z2.object({
4888
5412
  ...loopShared,
4889
- event: z.literal("findings_compared"),
4890
- round: z.number().int(),
4891
- open: z.array(z.string()),
4892
- resolved: z.array(z.string()),
4893
- new: z.array(z.string()),
4894
- recurring: z.array(z.string())
5413
+ event: z2.literal("findings_compared"),
5414
+ round: z2.number().int(),
5415
+ open: z2.array(z2.string()),
5416
+ resolved: z2.array(z2.string()),
5417
+ new: z2.array(z2.string()),
5418
+ recurring: z2.array(z2.string())
4895
5419
  }).strict(),
4896
- z.object({
5420
+ z2.object({
4897
5421
  ...loopShared,
4898
- event: z.literal("stop_condition_met"),
4899
- round: z.number().int(),
4900
- condition: z.enum([
5422
+ event: z2.literal("stop_condition_met"),
5423
+ round: z2.number().int(),
5424
+ condition: z2.enum([
4901
5425
  "green",
4902
5426
  "max_rounds",
4903
5427
  "no_progress",
@@ -4907,42 +5431,49 @@ var DevReviewLoopEventSchema = z.discriminatedUnion("event", [
4907
5431
  "reappearance"
4908
5432
  ])
4909
5433
  }).strict(),
4910
- z.object({
5434
+ z2.object({
4911
5435
  ...loopShared,
4912
- event: z.literal("paused"),
4913
- round: z.number().int(),
4914
- reason: z.enum(["escalation", "principal_item", "refreeze_needed"])
5436
+ event: z2.literal("paused"),
5437
+ round: z2.number().int(),
5438
+ reason: z2.enum(["escalation", "principal_item", "refreeze_needed"])
4915
5439
  }).strict(),
4916
- z.object({
5440
+ z2.object({
4917
5441
  ...loopShared,
4918
- event: z.literal("resumed"),
4919
- round: z.number().int(),
4920
- by: z.literal("principal")
5442
+ event: z2.literal("resumed"),
5443
+ round: z2.number().int(),
5444
+ by: z2.literal("principal")
4921
5445
  }).strict(),
4922
- z.object({
5446
+ z2.object({
4923
5447
  ...loopShared,
4924
- event: z.literal("round_ended"),
4925
- round: z.number().int(),
4926
- base_head: z.string(),
4927
- head: z.string(),
4928
- files_changed: z.number().int().nonnegative(),
4929
- insertions: z.number().int().nonnegative(),
4930
- deletions: z.number().int().nonnegative(),
4931
- wall_ms: z.number().nonnegative(),
4932
- outcome: z.enum(["green", "changes_requested", "escalated"])
5448
+ event: z2.literal("unpushed_work_resume"),
5449
+ round: z2.number().int(),
5450
+ branch: z2.string(),
5451
+ detail: z2.string()
4933
5452
  }).strict(),
4934
- z.object({
5453
+ z2.object({
4935
5454
  ...loopShared,
4936
- event: z.literal("journal_finalized"),
4937
- rounds: z.number().int().nonnegative(),
4938
- total_wall_ms: z.number().nonnegative(),
4939
- time_to_green_ms: z.number().nonnegative().nullable(),
4940
- files_changed_total: z.number().int().nonnegative(),
4941
- final_head: z.string(),
4942
- result: z.enum(["merged_ready", "stopped"])
5455
+ event: z2.literal("round_ended"),
5456
+ round: z2.number().int(),
5457
+ base_head: z2.string(),
5458
+ head: z2.string(),
5459
+ files_changed: z2.number().int().nonnegative(),
5460
+ insertions: z2.number().int().nonnegative(),
5461
+ deletions: z2.number().int().nonnegative(),
5462
+ wall_ms: z2.number().nonnegative(),
5463
+ outcome: z2.enum(["green", "changes_requested", "escalated"])
5464
+ }).strict(),
5465
+ z2.object({
5466
+ ...loopShared,
5467
+ event: z2.literal("journal_finalized"),
5468
+ rounds: z2.number().int().nonnegative(),
5469
+ total_wall_ms: z2.number().nonnegative(),
5470
+ time_to_green_ms: z2.number().nonnegative().nullable(),
5471
+ files_changed_total: z2.number().int().nonnegative(),
5472
+ final_head: z2.string(),
5473
+ result: z2.enum(["merged_ready", "stopped"])
4943
5474
  }).strict()
4944
5475
  ]);
4945
- var ForgeOpSchema = z.enum([
5476
+ var ForgeOpSchema = z2.enum([
4946
5477
  "pr.create",
4947
5478
  "pr.comment",
4948
5479
  "pr.body.replace",
@@ -4956,24 +5487,210 @@ var ForgeOpSchema = z.enum([
4956
5487
  "label.add",
4957
5488
  "label.remove"
4958
5489
  ]);
4959
- var ForgeWriteTargetSchema = z.object({
4960
- issue: z.number().int().optional(),
4961
- pr: z.number().int().optional()
5490
+ var ForgeWriteTargetSchema = z2.object({
5491
+ issue: z2.number().int().optional(),
5492
+ pr: z2.number().int().optional()
4962
5493
  }).strict();
4963
5494
  var forgeWriteShared = {
4964
5495
  meta: HeaderMetaSchema,
4965
5496
  subject: SubjectSchema,
4966
- kind: z.literal("forge_write"),
5497
+ kind: z2.literal("forge_write"),
4967
5498
  ...envelopeTail,
4968
5499
  op: ForgeOpSchema,
4969
5500
  target: ForgeWriteTargetSchema
4970
5501
  };
4971
- var ForgeWriteEventSchema = z.discriminatedUnion("event", [
4972
- z.object({ ...forgeWriteShared, event: z.literal("validated") }).strict(),
4973
- z.object({ ...forgeWriteShared, event: z.literal("refused"), reason: z.string() }).strict(),
4974
- z.object({ ...forgeWriteShared, event: z.literal("written"), comment_ids: z.array(z.string()) }).strict()
5502
+ var ForgeWriteEventSchema = z2.discriminatedUnion("event", [
5503
+ z2.object({ ...forgeWriteShared, event: z2.literal("validated") }).strict(),
5504
+ z2.object({ ...forgeWriteShared, event: z2.literal("refused"), reason: z2.string() }).strict(),
5505
+ z2.object({ ...forgeWriteShared, event: z2.literal("written"), comment_ids: z2.array(z2.string()) }).strict()
5506
+ ]);
5507
+ var GateOutcomeSchema = z2.enum([
5508
+ "pass",
5509
+ "fail",
5510
+ "wait",
5511
+ "skip",
5512
+ "invalid_input",
5513
+ "unavailable_dependency",
5514
+ "timeout",
5515
+ "cancelled"
5516
+ ]);
5517
+ var gateShared = {
5518
+ meta: HeaderMetaSchema,
5519
+ subject: SubjectSchema,
5520
+ kind: z2.literal("gate"),
5521
+ ...envelopeTail,
5522
+ check: z2.string(),
5523
+ check_version: z2.string().nullable(),
5524
+ policy_version: z2.string().nullable(),
5525
+ input_fingerprint: z2.string().nullable()
5526
+ };
5527
+ var GateEventSchema = z2.discriminatedUnion("event", [
5528
+ z2.object({
5529
+ ...gateShared,
5530
+ event: z2.literal("checked"),
5531
+ outcome: GateOutcomeSchema,
5532
+ reason: z2.string().optional()
5533
+ }).strict()
5534
+ ]);
5535
+ var OperationResultSchema = z2.enum(["ok", "error", "refused", "timeout", "cancelled", "unavailable"]);
5536
+ var operationShared = {
5537
+ meta: HeaderMetaSchema,
5538
+ subject: SubjectSchema,
5539
+ kind: z2.literal("operation"),
5540
+ ...envelopeTail,
5541
+ operation: z2.string(),
5542
+ target: z2.string().nullable()
5543
+ };
5544
+ var OperationEventSchema = z2.discriminatedUnion("event", [
5545
+ z2.object({
5546
+ ...operationShared,
5547
+ event: z2.literal("completed"),
5548
+ result: OperationResultSchema,
5549
+ error_class: z2.string().nullable()
5550
+ }).strict()
5551
+ ]);
5552
+ var UsageUnitsSchema = z2.object({
5553
+ input: z2.number().nonnegative().nullable(),
5554
+ output: z2.number().nonnegative().nullable(),
5555
+ cache: z2.number().nonnegative().nullable()
5556
+ }).strict();
5557
+ var usageShared = {
5558
+ meta: HeaderMetaSchema,
5559
+ subject: SubjectSchema,
5560
+ kind: z2.literal("usage"),
5561
+ ...envelopeTail,
5562
+ model: z2.string().nullable(),
5563
+ source: z2.string(),
5564
+ semantics: z2.enum(["cumulative", "delta"])
5565
+ };
5566
+ var UsageEventSchema = z2.discriminatedUnion("event", [
5567
+ z2.object({
5568
+ ...usageShared,
5569
+ event: z2.literal("observed"),
5570
+ units: UsageUnitsSchema,
5571
+ unknown_reason: z2.string().nullable()
5572
+ }).strict()
5573
+ ]);
5574
+ var RoleAttemptOutcomeSchema = z2.enum([
5575
+ "completed",
5576
+ "incomplete",
5577
+ "infrastructure_failed",
5578
+ "cancelled",
5579
+ "timed_out",
5580
+ "capability_refused"
5581
+ ]);
5582
+ var roleAttemptShared = {
5583
+ meta: HeaderMetaSchema,
5584
+ subject: SubjectSchema,
5585
+ kind: z2.literal("role_attempt"),
5586
+ ...envelopeTail,
5587
+ actor: z2.string().nullable(),
5588
+ attempt: z2.number().int().nullable()
5589
+ };
5590
+ var RoleAttemptEventSchema = z2.discriminatedUnion("event", [
5591
+ z2.object({
5592
+ ...roleAttemptShared,
5593
+ event: z2.literal("attempted"),
5594
+ outcome: RoleAttemptOutcomeSchema,
5595
+ usage: dispatchUsageField
5596
+ }).strict()
5597
+ ]);
5598
+ var handoffShared = {
5599
+ meta: HeaderMetaSchema,
5600
+ subject: SubjectSchema,
5601
+ kind: z2.literal("handoff"),
5602
+ ...envelopeTail,
5603
+ class: z2.enum(["authority", "strategy", "product"]),
5604
+ reason: z2.string()
5605
+ };
5606
+ var HandoffEventSchema = z2.discriminatedUnion("event", [
5607
+ z2.object({
5608
+ ...handoffShared,
5609
+ event: z2.literal("raised"),
5610
+ requested_decision: z2.string().nullable()
5611
+ }).strict(),
5612
+ z2.object({
5613
+ ...handoffShared,
5614
+ event: z2.literal("resolved"),
5615
+ resolution: z2.string().nullable(),
5616
+ resolved_by: z2.string().nullable()
5617
+ }).strict()
5618
+ ]);
5619
+ var EffectTargetSchema = z2.object({
5620
+ kind: z2.string(),
5621
+ ref: z2.string()
5622
+ }).strict();
5623
+ var effectShared = {
5624
+ meta: HeaderMetaSchema,
5625
+ subject: SubjectSchema,
5626
+ kind: z2.literal("effect"),
5627
+ ...envelopeTail,
5628
+ effect_id: z2.string(),
5629
+ target: EffectTargetSchema
5630
+ };
5631
+ var EffectEventSchema = z2.discriminatedUnion("event", [
5632
+ z2.object({ ...effectShared, event: z2.literal("attempted") }).strict(),
5633
+ z2.object({ ...effectShared, event: z2.literal("observed"), outcome: z2.enum(["success", "failure", "uncertain"]) }).strict(),
5634
+ z2.object({ ...effectShared, event: z2.literal("verified"), outcome: z2.enum(["success", "failure", "uncertain"]) }).strict()
4975
5635
  ]);
4976
- var LogEventSchema = z.union([DispatchEventSchema, DevReviewLoopEventSchema, ForgeWriteEventSchema]);
5636
+ var LogEventSchema = z2.union([
5637
+ DispatchEventSchema,
5638
+ DevReviewLoopEventSchema,
5639
+ ForgeWriteEventSchema,
5640
+ GateEventSchema,
5641
+ OperationEventSchema,
5642
+ UsageEventSchema,
5643
+ RoleAttemptEventSchema,
5644
+ HandoffEventSchema,
5645
+ EffectEventSchema
5646
+ ]);
5647
+ // ../../packages/aeg-core/src/dev-review-loop/journal-reconstruction.ts
5648
+ var STOPPED_CONDITIONS = new Set(["confidence", "reappearance", "no_progress", "max_rounds"]);
5649
+ // ../../packages/aeg-core/src/control-store/records.ts
5650
+ import { z as z3 } from "zod";
5651
+ var isoTimestamp = z3.string().min(1);
5652
+ var taskId = z3.number().int().positive();
5653
+ var epochNumber = z3.number().int().nonnegative();
5654
+ var RunRecordSchema = z3.object({
5655
+ version: z3.literal(1),
5656
+ kind: z3.literal("run"),
5657
+ task: taskId,
5658
+ runId: z3.string().min(1),
5659
+ pid: z3.number().int().positive(),
5660
+ host: z3.string().min(1),
5661
+ startedAt: isoTimestamp
5662
+ }).strict();
5663
+ var InputRecordSchema = z3.object({
5664
+ version: z3.literal(1),
5665
+ kind: z3.literal("input"),
5666
+ task: taskId,
5667
+ runId: z3.string().min(1),
5668
+ source: z3.union([z3.literal("fresh"), z3.literal("resume")]),
5669
+ pr: z3.number().int().positive().nullable(),
5670
+ round: z3.number().int().nonnegative(),
5671
+ recordedAt: isoTimestamp
5672
+ }).strict();
5673
+ var OwnershipRecordSchema = z3.object({
5674
+ version: z3.literal(1),
5675
+ kind: z3.literal("ownership"),
5676
+ task: taskId,
5677
+ epoch: epochNumber,
5678
+ ownerId: z3.string().min(1),
5679
+ pid: z3.number().int().positive(),
5680
+ host: z3.string().min(1),
5681
+ acquiredAt: isoTimestamp
5682
+ }).strict();
5683
+ var TransitionRecordSchema = z3.object({
5684
+ version: z3.literal(1),
5685
+ kind: z3.literal("transition"),
5686
+ task: taskId,
5687
+ epoch: epochNumber,
5688
+ seq: z3.number().int().nonnegative(),
5689
+ from: z3.string().min(1),
5690
+ to: z3.string().min(1),
5691
+ detail: z3.string().optional(),
5692
+ at: isoTimestamp
5693
+ }).strict();
4977
5694
  // ../../packages/sources/src/commands.ts
4978
5695
  var COMMANDS = [
4979
5696
  {
@@ -5094,14 +5811,52 @@ var COMMANDS = [
5094
5811
  },
5095
5812
  {
5096
5813
  name: "task dispatch",
5097
- description: "Render, pin, and post the brief on the Issue as the frozen original; start the developer",
5814
+ description: "Deprecated — render, pin, and post the brief on the Issue as the frozen original; start the developer",
5098
5815
  flags: [
5099
5816
  { flag: "--agent <claude|codex|gemini>", description: "Start the developer through dispatchRole once posted" }
5100
5817
  ],
5101
5818
  details: [
5102
5819
  "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.",
5103
5820
  "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.",
5104
- "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."
5821
+ "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.",
5822
+ "Deprecated in favor of `task brief` (preparation only) and `task run` (the full unattended loop) — kept for a documented compatibility window while callers migrate."
5823
+ ],
5824
+ status: "shipped"
5825
+ },
5826
+ {
5827
+ name: "task brief",
5828
+ description: "Render and freeze the brief as the Issue's original comment — preparation only, starts nobody",
5829
+ details: [
5830
+ "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.",
5831
+ "Successor to `task dispatch` for the preparation step; the full unattended run (preparation, then the developer, then the review loop) is `task run`."
5832
+ ],
5833
+ status: "shipped"
5834
+ },
5835
+ {
5836
+ name: "task run",
5837
+ description: "One command from a planned Issue to a reviewed pull request — exactly one developer started",
5838
+ flags: [
5839
+ {
5840
+ flag: "--agent <claude|codex|gemini>",
5841
+ description: "Vendor for the developer and both reviewers this run dispatches"
5842
+ }
5843
+ ],
5844
+ details: [
5845
+ "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.",
5846
+ "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.",
5847
+ "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.",
5848
+ "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."
5849
+ ],
5850
+ status: "shipped"
5851
+ },
5852
+ {
5853
+ name: "task status",
5854
+ description: "Every open task with a frozen brief, its pull request, and whether its loop is running, paused, or published",
5855
+ flags: [{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }],
5856
+ details: [
5857
+ "Read-only: one `gh issue list` for every open task Issue across every tranche (title/label resolved through the same `resolveTaskIssueRef` `list-tasks.ts` already uses), the open pull request per branch, and the driver pid record / pause record / publish effect markers under `<outboxRoot>/dev-review-loop/<task>/` — never a `ps` scan, never a re-parse of posted verdict comments to decide `published`.",
5858
+ "`running` names the driver's pid (`review-validity-v1` task 7's pid record); `paused` names the reason from the pause record; `published` means the newest round's reviewer and security verdict effect markers both read `posted`; `no driver` is the fallback when none of the above holds.",
5859
+ "`vinaya task status <tranche> <n>` narrows to one task and adds the last round's held or published verdict lines (`round-<n>-reviewer.md`/`round-<n>-security.md`, whichever their outbox carries) plus the exact `vinaya dev-review-loop --resume <pr>` command when paused."
5105
5860
  ],
5106
5861
  status: "shipped"
5107
5862
  },
@@ -5309,6 +6064,15 @@ var COMMANDS = [
5309
6064
  ],
5310
6065
  status: "shipped"
5311
6066
  },
6067
+ {
6068
+ name: "milestone status",
6069
+ description: "Print each of a Milestone's declared tranche intents with its derived lifecycle and issue counts",
6070
+ flags: [{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }],
6071
+ details: [
6072
+ "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."
6073
+ ],
6074
+ status: "shipped"
6075
+ },
5312
6076
  {
5313
6077
  name: "review status",
5314
6078
  description: "Print the review loop's own state for a PR, and its branch's distance from the base",
@@ -5605,10 +6369,10 @@ function createForgeSource(config) {
5605
6369
  };
5606
6370
  }
5607
6371
  // ../../packages/sources/src/select-source.ts
5608
- import { z as z2 } from "zod";
5609
- var StateSourceConfigSchema = z2.discriminatedUnion("kind", [
5610
- z2.object({ kind: z2.literal("forge"), owner: z2.string(), repo: z2.string() }),
5611
- z2.object({ kind: z2.literal("file"), root: z2.string().optional() })
6372
+ import { z as z4 } from "zod";
6373
+ var StateSourceConfigSchema = z4.discriminatedUnion("kind", [
6374
+ z4.object({ kind: z4.literal("forge"), owner: z4.string(), repo: z4.string() }),
6375
+ z4.object({ kind: z4.literal("file"), root: z4.string().optional() })
5612
6376
  ]);
5613
6377
  // src/checks/contract.ts
5614
6378
  var CHECK_SCHEMA_VERSION = 1;
@@ -5669,9 +6433,14 @@ function refuse(message, agent_recovery_prompt) {
5669
6433
  }
5670
6434
  async function main() {
5671
6435
  const branch = process.env.BRANCH || git(["rev-parse", "--abbrev-ref", "HEAD"]);
5672
- const fields = taskBranchTopologyFields(branch);
5673
- if (!fields)
6436
+ const ref = parseTaskBranchIdentity(branch);
6437
+ if (!ref)
5674
6438
  process.exit(0);
6439
+ if (ref.kind === "issue") {
6440
+ await checkAgainstIssue(ref.issueNumber);
6441
+ return;
6442
+ }
6443
+ const fields = ref;
5675
6444
  const repo = resolveRepo2();
5676
6445
  if (!repo) {
5677
6446
  refuse(`branch \`${branch}\` names a task, but this repository's owner/repo could not be resolved from \`AEG_REPO\` or \`git remote get-url origin\` — cannot look up which Issue's Surface bounds this PR.`, "Set AEG_REPO=owner/repo, or confirm `git remote get-url origin` resolves to a GitHub URL, then re-run `vinaya check surface-scope`.");
@@ -5688,6 +6457,9 @@ async function main() {
5688
6457
  if (issue === null) {
5689
6458
  refuse(`no Issue number is recorded for task \`${fields.taskId}\` in tranche \`${fields.tranche}\`'s forge topology — cannot look up which Issue's Surface bounds this PR.`, "Confirm this task has a real forge Issue and that the topology row carries its number, then re-run `vinaya check surface-scope`.");
5690
6459
  }
6460
+ await checkAgainstIssue(issue);
6461
+ }
6462
+ async function checkAgainstIssue(issue) {
5691
6463
  const body = fetchIssueBody(issue);
5692
6464
  if (body === null) {
5693
6465
  refuse(`could not fetch Issue #${issue}'s body via \`gh issue view\` — cannot read its \`## Surface\` section.`, "Confirm `gh auth status` passes and that the Issue exists, then re-run `vinaya check surface-scope`.");