@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
@@ -306,7 +306,7 @@ function issueListByLabelArgs(owner, repo, label) {
306
306
  "--state",
307
307
  "all",
308
308
  "--json",
309
- "number,title,body,state,labels,milestone",
309
+ "number,title,body,state,labels,milestone,stateReason",
310
310
  "--limit",
311
311
  "200"
312
312
  ];
@@ -652,14 +652,19 @@ function resolveTaskIssueRef(title, labels) {
652
652
  var INTENTS_HEADING = /^#{1,6}\s*Tranche intents\s*$/im;
653
653
  var NEXT_HEADING = /^#{1,6}\s+\S/m;
654
654
  var INTENT_BULLET = /^-\s+([a-z0-9][a-z0-9-]*)\s*:\s*(.+)$/i;
655
- function intentGoalForSlug(description, slug) {
656
- const text = stripCode(description, { inlineSpans: "keep" });
655
+ function intentsSection(text) {
657
656
  const start = text.match(INTENTS_HEADING);
658
657
  if (!start || start.index === undefined)
659
- return "";
658
+ return null;
660
659
  const rest = text.slice(start.index + start[0].length);
661
660
  const next = rest.match(NEXT_HEADING);
662
- const section = rest.slice(0, next && next.index !== undefined ? next.index : rest.length);
661
+ return rest.slice(0, next && next.index !== undefined ? next.index : rest.length);
662
+ }
663
+ function intentGoalForSlug(description, slug) {
664
+ const text = stripCode(description, { inlineSpans: "keep" });
665
+ const section = intentsSection(text);
666
+ if (section === null)
667
+ return "";
663
668
  for (const line of section.split(`
664
669
  `)) {
665
670
  const trimmed = line.trim();
@@ -1951,6 +1956,9 @@ function parseInlineFieldList(section) {
1951
1956
  // ../../packages/aeg-core/src/verdict-extraction.ts
1952
1957
  var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
1953
1958
  var OBJECTIVES_VERSION_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Objectives version:\s*([0-9a-f]{64})(?![A-Za-z0-9])/im;
1959
+ var RULING_ORDINAL_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Ruling ordinal:\s*(\d+)(?!\d)/im;
1960
+ var BRIEF_HASH_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Brief hash:\s*([0-9a-f]{64})(?![A-Za-z0-9])/im;
1961
+ var POLICY_DIGEST_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Policy digest:\s*([0-9a-f]{64})(?![A-Za-z0-9])/im;
1954
1962
  function firstFiveLines(comment) {
1955
1963
  return comment.split(`
1956
1964
  `).slice(0, 5).join(`
@@ -1964,6 +1972,35 @@ function extractObjectivesVersion(comment) {
1964
1972
  const m = firstFiveLines(comment).match(OBJECTIVES_VERSION_PATTERN);
1965
1973
  return m ? m[1].toLowerCase() : null;
1966
1974
  }
1975
+ function firstSevenLines(comment) {
1976
+ return comment.split(`
1977
+ `).slice(0, 7).join(`
1978
+ `);
1979
+ }
1980
+ function extractRulingOrdinal(comment) {
1981
+ const m = firstSevenLines(comment).match(RULING_ORDINAL_PATTERN);
1982
+ return m ? Number.parseInt(m[1], 10) : null;
1983
+ }
1984
+ function firstElevenLines(comment) {
1985
+ return comment.split(`
1986
+ `).slice(0, 11).join(`
1987
+ `);
1988
+ }
1989
+ function extractBriefHash(comment) {
1990
+ const m = firstElevenLines(comment).match(BRIEF_HASH_PATTERN);
1991
+ return m ? m[1].toLowerCase() : null;
1992
+ }
1993
+ function extractPolicyDigest(comment) {
1994
+ const m = firstElevenLines(comment).match(POLICY_DIGEST_PATTERN);
1995
+ return m ? m[1].toLowerCase() : null;
1996
+ }
1997
+ var FINDING_SEVERITY_LINE = /^\d+\.\s+\[([A-Z][A-Z]*)\]\s+(.+?)\s+—/gm;
1998
+ function extractFindingSeverities(comment) {
1999
+ return [...comment.matchAll(FINDING_SEVERITY_LINE)].map((m) => ({
2000
+ severity: m[1],
2001
+ location: m[2]
2002
+ }));
2003
+ }
1967
2004
  function extractVerdict(comments, valuePattern, missingLabel) {
1968
2005
  const candidates = comments.filter((c) => valuePattern.test(c));
1969
2006
  if (candidates.length === 0) {
@@ -1971,6 +2008,10 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1971
2008
  value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
1972
2009
  headSha: null,
1973
2010
  objectivesVersion: null,
2011
+ rulingOrdinal: null,
2012
+ briefHash: null,
2013
+ policyDigest: null,
2014
+ findingSeverities: [],
1974
2015
  danglingNote: `no ${missingLabel} verdict comment found on this PR`
1975
2016
  };
1976
2017
  }
@@ -1981,6 +2022,10 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1981
2022
  value: `the most recent ${missingLabel} comment's VERDICT line is not within its first five lines — DANGLING, see below`,
1982
2023
  headSha: null,
1983
2024
  objectivesVersion: null,
2025
+ rulingOrdinal: null,
2026
+ briefHash: null,
2027
+ policyDigest: null,
2028
+ findingSeverities: [],
1984
2029
  danglingNote: `the most recent ${missingLabel} verdict comment carries a VERDICT-shaped line outside the first-five-line read window`
1985
2030
  };
1986
2031
  }
@@ -1988,6 +2033,10 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1988
2033
  value: m[1].toUpperCase().replace(/[_-]/g, " "),
1989
2034
  headSha: extractHeadSha(latest),
1990
2035
  objectivesVersion: extractObjectivesVersion(latest),
2036
+ rulingOrdinal: extractRulingOrdinal(latest),
2037
+ briefHash: extractBriefHash(latest),
2038
+ policyDigest: extractPolicyDigest(latest),
2039
+ findingSeverities: extractFindingSeverities(latest),
1991
2040
  danglingNote: null
1992
2041
  };
1993
2042
  }
@@ -2323,6 +2372,53 @@ function evaluateC5(changed, docOwnersContent, prBody, fileExists, waiverActive,
2323
2372
  }
2324
2373
  return out;
2325
2374
  }
2375
+ // ../../packages/aeg-core/src/review-policy.ts
2376
+ var CODE_REVIEW_SEVERITY_ORDER = ["BLOCKER", "MAJOR", "MINOR"];
2377
+ var SECURITY_SEVERITY_ORDER = ["CRITICAL", "HIGH", "MEDIUM", "LOW"];
2378
+ var DEFAULT_MAX_ROUNDS = 3;
2379
+ var DEFAULT_REVIEW_POLICY = {
2380
+ codeReviewThreshold: "BLOCKER",
2381
+ securityThreshold: "HIGH",
2382
+ maxRounds: DEFAULT_MAX_ROUNDS
2383
+ };
2384
+ var FILE_SHAPED_LOCATION = /\.[a-zA-Z0-9]{1,10}(:\d+)?\s*$/;
2385
+ var PROSE_LOCATION_PATTERNS = [/\bpr\s*body\b/i, /\bcomment\b/i];
2386
+ var ROLE_FILE_LOCATION = /(^|\/)aeg-root\/roles\//i;
2387
+ function isProseLocation(location) {
2388
+ if (ROLE_FILE_LOCATION.test(location))
2389
+ return true;
2390
+ if (FILE_SHAPED_LOCATION.test(location))
2391
+ return false;
2392
+ return PROSE_LOCATION_PATTERNS.some((pattern) => pattern.test(location));
2393
+ }
2394
+ var PROSE_CAP_SEVERITY = "MINOR";
2395
+ function blockingSeverities(scale, threshold) {
2396
+ const idx = scale.indexOf(threshold);
2397
+ if (idx === -1) {
2398
+ throw new Error(`blockingSeverities: threshold "${threshold}" is not one of ${scale.join(" > ")}`);
2399
+ }
2400
+ return scale.slice(0, idx + 1);
2401
+ }
2402
+ function evaluateReviewFindings(findings, scale, threshold) {
2403
+ const blocking = new Set(blockingSeverities(scale, threshold));
2404
+ const blockingFindings = findings.filter((f) => {
2405
+ if (!scale.includes(f.severity)) {
2406
+ throw new Error(`evaluateReviewFindings: severity "${f.severity}" is not one of ${scale.join(" > ")}`);
2407
+ }
2408
+ const effectiveSeverity = f.location !== undefined && isProseLocation(f.location) ? PROSE_CAP_SEVERITY : f.severity;
2409
+ return blocking.has(effectiveSeverity);
2410
+ });
2411
+ return { outcome: blockingFindings.length > 0 ? "blocked" : "clean", blockingFindings };
2412
+ }
2413
+ function evaluateCodeReview(findings, policy) {
2414
+ return evaluateReviewFindings(findings, CODE_REVIEW_SEVERITY_ORDER, policy.codeReviewThreshold);
2415
+ }
2416
+ function evaluateSecurityReview(findings, policy) {
2417
+ return evaluateReviewFindings(findings, SECURITY_SEVERITY_ORDER, policy.securityThreshold);
2418
+ }
2419
+ function isKnownSeverity(scale, value) {
2420
+ return scale.includes(value);
2421
+ }
2326
2422
  // ../../packages/aeg-core/src/pr-tier.ts
2327
2423
  var TIER_FIELD = /(\*\*)?\s*Tier\s*(\*\*)?\s*:\s*(\*\*)?\s*([013])\b/i;
2328
2424
  function readTierFromPrBody(prBody) {
@@ -2349,6 +2445,9 @@ import { createHash as createHash2 } from "node:crypto";
2349
2445
  var HEADING_RE = /^##[ \t]*Objectives[ \t]*$/im;
2350
2446
  var NEXT_HEADING_RE = /^##[ \t]/m;
2351
2447
  var OBJECTIVE_LINE_RE = /^O(\d+)\.[ \t]*(.*)$/;
2448
+ function maskedForHeadingSearch(body) {
2449
+ return maskDetailsBlocks(maskCode(body));
2450
+ }
2352
2451
  function hasBacktickedPath(text) {
2353
2452
  let i = 0;
2354
2453
  while (i < text.length) {
@@ -2371,16 +2470,22 @@ function stripObjectiveBackticks(text) {
2371
2470
  function wordCount(text) {
2372
2471
  return text.split(/\s+/).filter((w) => /[a-z]/i.test(w)).length;
2373
2472
  }
2374
- function objectivesSectionText(body) {
2375
- const heading = HEADING_RE.exec(body);
2473
+ function objectivesSectionBounds(body) {
2474
+ const masked = maskedForHeadingSearch(body);
2475
+ const heading = HEADING_RE.exec(masked);
2376
2476
  if (!heading)
2377
2477
  return null;
2378
- const afterHeading = body.slice(heading.index + heading[0].length);
2478
+ const start = heading.index + heading[0].length;
2479
+ const afterHeading = masked.slice(start);
2379
2480
  const next = NEXT_HEADING_RE.exec(afterHeading);
2380
- return next ? afterHeading.slice(0, next.index) : afterHeading;
2481
+ return { start, end: next ? start + next.index : body.length };
2482
+ }
2483
+ function objectivesSectionText(body) {
2484
+ const bounds = objectivesSectionBounds(body);
2485
+ return bounds === null ? null : body.slice(bounds.start, bounds.end);
2381
2486
  }
2382
2487
  function hasObjectivesHeading(body) {
2383
- return HEADING_RE.test(body);
2488
+ return HEADING_RE.test(maskedForHeadingSearch(body));
2384
2489
  }
2385
2490
  function objectivesOf(body) {
2386
2491
  const section = objectivesSectionText(body);
@@ -2440,6 +2545,15 @@ function isIssueNotFoundError(err) {
2440
2545
  `);
2441
2546
  return /could not resolve to an (?:issue|pull request)|\b404\b|not found/i.test(haystack);
2442
2547
  }
2548
+ function resolveObjectivesSource(prBody, issue, cutoverIssue) {
2549
+ if (issue !== null && issue < cutoverIssue)
2550
+ return { kind: "none" };
2551
+ if (issue !== null)
2552
+ return { kind: "issue", issue };
2553
+ if (hasObjectivesHeading(prBody))
2554
+ return { kind: "body" };
2555
+ return { kind: "none" };
2556
+ }
2443
2557
 
2444
2558
  // ../../packages/aeg-core/src/premise-check.ts
2445
2559
  import { createHash as createHash3 } from "node:crypto";
@@ -2675,7 +2789,7 @@ function checkForField(prBody) {
2675
2789
  ]
2676
2790
  };
2677
2791
  }
2678
- function checkClosesN(prBody) {
2792
+ function checkClosesNPresence(prBody) {
2679
2793
  const closesPattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s{0,8}:?\s{0,8}#\d+/i;
2680
2794
  if (closesPattern.test(stripCode(prBody))) {
2681
2795
  return { status: "pass", errors: [] };
@@ -2707,10 +2821,22 @@ var COMMIT_TYPES = [
2707
2821
  "Test"
2708
2822
  ];
2709
2823
  var COMMIT_TYPE_STYLE = new RegExp(`^(${COMMIT_TYPES.join("|")})(\\([a-z0-9-]+\\))?: \\S`);
2824
+ function checkForgeTitle(title) {
2825
+ const taskStyle = /^\[[a-z0-9._-]+\] \S+ — \S/;
2826
+ if (COMMIT_TYPE_STYLE.test(title) || taskStyle.test(title))
2827
+ return { status: "pass", errors: [] };
2828
+ return {
2829
+ status: "fail",
2830
+ errors: [
2831
+ `brief-validation title: "${title}" matches neither title grammar — expected \`Type: description\` / \`Type(scope): description\` (commitlint types + Plan) or \`[tranche] id — description\` (task form).`
2832
+ ]
2833
+ };
2834
+ }
2710
2835
  var BRIEF_SHAPE_MARKERS = [checkSurfaceMap, checkDocUpdateList, checkStopConditions, checkAutonomyClause];
2711
2836
  var TASK_BRANCH_PATTERN = /^task\/[^/]+\/[^/]+$/;
2837
+ var TASK_ISSUE_BRANCH_PATTERN = /^task\/issue-\d+$/;
2712
2838
  function isTaskBranch(branch) {
2713
- return TASK_BRANCH_PATTERN.test(branch);
2839
+ return TASK_BRANCH_PATTERN.test(branch) || TASK_ISSUE_BRANCH_PATTERN.test(branch);
2714
2840
  }
2715
2841
  function isBriefShaped(prBody) {
2716
2842
  const stripped = stripCode(prBody);
@@ -2832,8 +2958,9 @@ function packagesNamedIn(text) {
2832
2958
  }
2833
2959
  function hasTestPathForConsumer(text, consumerDir) {
2834
2960
  const escaped = consumerDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2835
- const re = new RegExp(`${escaped}\\/[\\w./-]*\\.test\\.[A-Za-z0-9]+`);
2836
- return re.test(text);
2961
+ const filePathRe = new RegExp(`${escaped}\\/[\\w./-]*\\.test\\.[A-Za-z0-9]+`);
2962
+ const testDirRe = new RegExp(`${escaped}\\/(?:[\\w-]+\\/)*(?:tests|specs)(?:\\/|\\b)`);
2963
+ return filePathRe.test(text) || testDirRe.test(text);
2837
2964
  }
2838
2965
  function checkConsumerTests(prBody, consumersOf) {
2839
2966
  const section4 = extractNumberedSection(prBody, 4);
@@ -2937,21 +3064,44 @@ function checkBriefSections(prBody, readTier, options = {}) {
2937
3064
  checkConsumerTests(prBody, consumersOf),
2938
3065
  checkDefeatCases(prBody),
2939
3066
  ...issueObjectives !== undefined ? [checkObjectivesCopy(prBody, issueObjectives), checkObjectivesCoverage(prBody)] : [],
2940
- ...requireClosesN ? [checkClosesN(prBody)] : []
3067
+ ...requireClosesN ? [checkClosesNPresence(prBody)] : []
2941
3068
  ];
2942
3069
  return { errors: results.flatMap((r) => r.errors) };
2943
3070
  }
2944
- var AEG_BRIEF_V1_MARKER = "<!-- aeg:brief:v1 -->";
2945
- function contentAfterTwoLines(body) {
2946
- const first = body.indexOf(`
2947
- `);
2948
- if (first === -1)
2949
- return "";
2950
- const second = body.indexOf(`
2951
- `, first + 1);
2952
- if (second === -1)
2953
- return "";
2954
- return body.slice(second + 1);
3071
+ function contentAfterNLines(body, n) {
3072
+ let idx = -1;
3073
+ for (let i = 0;i < n; i++) {
3074
+ idx = body.indexOf(`
3075
+ `, idx + 1);
3076
+ if (idx === -1)
3077
+ return "";
3078
+ }
3079
+ return body.slice(idx + 1);
3080
+ }
3081
+ var BRIEF_MARKER_LINE_RE = /^<!-- aeg:brief:v(\d+) -->$/;
3082
+ function parseBriefMarkerVersion(firstLine) {
3083
+ const m = BRIEF_MARKER_LINE_RE.exec(firstLine.trim());
3084
+ if (!m)
3085
+ return null;
3086
+ const version = Number.parseInt(m[1], 10);
3087
+ return Number.isInteger(version) && version >= 1 ? version : null;
3088
+ }
3089
+ function frozenBriefContent(body, version) {
3090
+ return contentAfterNLines(body, version === 1 ? 2 : 3);
3091
+ }
3092
+ function resolveNewestFrozenBrief(comments, allowlist) {
3093
+ let best = null;
3094
+ for (const c of comments) {
3095
+ if (!isPrincipal(c.author, allowlist))
3096
+ continue;
3097
+ const version = parseBriefMarkerVersion(c.body.split(`
3098
+ `)[0] ?? "");
3099
+ if (version === null)
3100
+ continue;
3101
+ if (best === null || version > best.version)
3102
+ best = { ...c, version };
3103
+ }
3104
+ return best === null ? null : { ...best, content: frozenBriefContent(best.body, best.version) };
2955
3105
  }
2956
3106
  // ../../packages/aeg-core/src/doctrine-portability.ts
2957
3107
  var DEFAULT_SHIPS_PREFIX = "aeg-root/";
@@ -3204,6 +3354,28 @@ function topLevelSectionText(body, headingName) {
3204
3354
  const next = /^##[ \t]/m.exec(afterHeading);
3205
3355
  return next ? afterHeading.slice(0, next.index) : afterHeading;
3206
3356
  }
3357
+ function partHasBacktickedPath(text) {
3358
+ let i = 0;
3359
+ while (i < text.length) {
3360
+ const start = text.indexOf("`", i);
3361
+ if (start === -1)
3362
+ return false;
3363
+ const end = text.indexOf("`", start + 1);
3364
+ if (end === -1)
3365
+ return false;
3366
+ if (text.slice(start + 1, end).includes("/"))
3367
+ return true;
3368
+ i = end + 1;
3369
+ }
3370
+ return false;
3371
+ }
3372
+ var PART_MIN_WORDS_OUTSIDE_BACKTICKS = 3;
3373
+ function stripPartBackticks(text) {
3374
+ return text.replace(/`[^`\n]*`/g, " ");
3375
+ }
3376
+ function partWordCount(text) {
3377
+ return text.split(/\s+/).filter((w) => /[a-z]/i.test(w)).length;
3378
+ }
3207
3379
  function looksLikeFilePath(entry2) {
3208
3380
  const stripped = entry2.replace(/\/\*\*?$/, "");
3209
3381
  const lastSegment = stripped.split("/").pop() ?? stripped;
@@ -3244,6 +3416,18 @@ function globCoversPath(glob, path) {
3244
3416
  const p = path.replace(/\/+$/, "");
3245
3417
  return g === p || g.startsWith(`${p}/`) || p.startsWith(`${g}/`);
3246
3418
  }
3419
+ function checkSurfaceGlobsResolve(body, resolvesToFile) {
3420
+ const surface = parseIssueSurface(body);
3421
+ if (!surface.ok)
3422
+ return { status: "pass", errors: [] };
3423
+ const errors = [];
3424
+ for (const glob of surface.value.in) {
3425
+ if (!resolvesToFile(glob)) {
3426
+ 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.`);
3427
+ }
3428
+ }
3429
+ return { status: errors.length > 0 ? "fail" : "pass", errors };
3430
+ }
3247
3431
  function checkSurfaceScope(changedFiles, outGlobs) {
3248
3432
  if (outGlobs.length === 0)
3249
3433
  return { ok: true };
@@ -3255,9 +3439,77 @@ function checkSurfaceScope(changedFiles, outGlobs) {
3255
3439
  }
3256
3440
  return violations.length > 0 ? { ok: false, violations } : { ok: true };
3257
3441
  }
3442
+ var ISSUE_PART_LINE_RE = /^Part\s+(\d+)\s*\(([^)]*)\)\s*[-—–]\s*(.*)$/i;
3443
+ function parseIssueParts(body) {
3444
+ const section = topLevelSectionText(body, "Parts");
3445
+ if (section === null)
3446
+ return { ok: false, errors: ["no `## Parts` heading found in the body."] };
3447
+ const lines = section.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
3448
+ const parts = [];
3449
+ const errors = [];
3450
+ for (const line of lines) {
3451
+ const m = ISSUE_PART_LINE_RE.exec(line);
3452
+ if (!m) {
3453
+ errors.push(`"${line}" is not a well-formed Parts line — expected \`Part <n> (<refs>) — <outcome>\`.`);
3454
+ continue;
3455
+ }
3456
+ const n = Number.parseInt(m[1], 10);
3457
+ const refs = m[2];
3458
+ const text = m[3].trim();
3459
+ if (text.length === 0) {
3460
+ errors.push(`Part ${n} has no outcome text after the dash — every Part states one observable outcome.`);
3461
+ continue;
3462
+ }
3463
+ if (partHasBacktickedPath(text) && partWordCount(stripPartBackticks(text)) < PART_MIN_WORDS_OUTSIDE_BACKTICKS) {
3464
+ errors.push(`Part ${n} is little more than a file path — a Part names an outcome and symbols, never a bare path.`);
3465
+ continue;
3466
+ }
3467
+ const objectiveIds = [...refs.matchAll(/O(\d+)/g)].map((r) => Number.parseInt(r[1], 10));
3468
+ parts.push({ n, objectiveIds, text });
3469
+ }
3470
+ if (parts.length === 0) {
3471
+ errors.push("the `## Parts` section has no well-formed `Part <n> (<refs>) — <outcome>` lines.");
3472
+ }
3473
+ if (errors.length > 0)
3474
+ return { ok: false, errors };
3475
+ return { ok: true, value: parts };
3476
+ }
3477
+ function checkPartsCiteDefinedObjectives(body) {
3478
+ const parts = parseIssueParts(body);
3479
+ const objectives = objectivesOf(body);
3480
+ if (!parts.ok || !objectives.ok)
3481
+ return { status: "pass", errors: [] };
3482
+ const definedIds = new Set(objectives.objectives.map((o) => Number.parseInt(o.id.slice(1), 10)));
3483
+ const errors = [];
3484
+ for (const part of parts.value) {
3485
+ for (const objectiveId of part.objectiveIds) {
3486
+ if (!definedIds.has(objectiveId)) {
3487
+ errors.push(`issue-validation Parts: Part ${part.n} cites O${objectiveId}, which the Issue's own \`## Objectives\` section does not define.`);
3488
+ }
3489
+ }
3490
+ }
3491
+ return { status: errors.length > 0 ? "fail" : "pass", errors };
3492
+ }
3258
3493
  function isTaskIssueLabelSet(labels) {
3259
3494
  return hasLabel("tranche", labels);
3260
3495
  }
3496
+ function checkTrancheLabelPresence(_body, _labels) {
3497
+ return { status: "pass", errors: [] };
3498
+ }
3499
+ function checkMilestoneAttach(labels, currentMilestoneTitle, resolvedMilestoneTitle) {
3500
+ if (!isTaskIssueLabelSet(labels))
3501
+ return { status: "pass", errors: [] };
3502
+ if (resolvedMilestoneTitle === null)
3503
+ return { status: "pass", errors: [] };
3504
+ if (currentMilestoneTitle === resolvedMilestoneTitle)
3505
+ return { status: "pass", errors: [] };
3506
+ return {
3507
+ status: "fail",
3508
+ errors: [
3509
+ `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}".`
3510
+ ]
3511
+ };
3512
+ }
3261
3513
  var TYPE_LABEL_IDS = LABELS.filter((l) => l.category === "type").map((l) => l.id);
3262
3514
  function isControlCodePoint(codePoint) {
3263
3515
  return codePoint <= 31 || codePoint >= 127 && codePoint <= 159;
@@ -3322,6 +3574,19 @@ function checkProjectsRegistered(body, _labels, registeredNames) {
3322
3574
  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;
3323
3575
  var DOC_PATH_RE_GLOBAL = new RegExp(DOC_PATH_RE.source, "gi");
3324
3576
 
3577
+ // ../../packages/aeg-core/src/task-branch-identity.ts
3578
+ var ISSUE_BRANCH_PATTERN = /^task\/issue-(\d+)$/;
3579
+ var TRANCHE_BRANCH_PATTERN = /^task\/([^/]+)\/([^/]+)$/;
3580
+ function parseTaskBranchIdentity(branch) {
3581
+ const issueMatch = ISSUE_BRANCH_PATTERN.exec(branch);
3582
+ if (issueMatch)
3583
+ return { kind: "issue", issueNumber: Number(issueMatch[1]) };
3584
+ const trancheMatch = TRANCHE_BRANCH_PATTERN.exec(branch);
3585
+ if (trancheMatch)
3586
+ return { kind: "tranche", tranche: trancheMatch[1], taskId: trancheMatch[2] };
3587
+ return null;
3588
+ }
3589
+
3325
3590
  // ../../packages/aeg-core/src/coherence-checks.ts
3326
3591
  var COHERENCE_ENFORCED_FROM = "2026-07-01";
3327
3592
  function isGrandfathered(isoDate) {
@@ -3342,6 +3607,7 @@ function checkA1(entries, principalAllowlist = PRINCIPAL_ALLOWLIST) {
3342
3607
  if (handClosed)
3343
3608
  continue;
3344
3609
  failures.push({
3610
+ code: "closed-without-merge",
3345
3611
  issue: e.task.issue,
3346
3612
  tranche: e.trancheSlug,
3347
3613
  task: e.task.id,
@@ -3366,6 +3632,7 @@ function checkA3(entries) {
3366
3632
  continue;
3367
3633
  if (e.facts.prState === "merged" && e.facts.issueState !== "closed") {
3368
3634
  failures.push({
3635
+ code: "auto-close-misfire",
3369
3636
  issue: e.task.issue,
3370
3637
  tranche: e.trancheSlug,
3371
3638
  task: e.task.id,
@@ -3390,6 +3657,7 @@ function checkT1(entries) {
3390
3657
  continue;
3391
3658
  if (e.facts === undefined) {
3392
3659
  failures.push({
3660
+ code: "phantom-issue-ref",
3393
3661
  issue: e.task.issue,
3394
3662
  tranche: e.trancheSlug,
3395
3663
  task: e.task.id,
@@ -3408,6 +3676,7 @@ function checkT2(openIssuesBySlug, topologyIssuesBySlug, ciTrancheSlug) {
3408
3676
  for (const num of openNums) {
3409
3677
  if (!topologySet.has(num)) {
3410
3678
  failures.push({
3679
+ code: "orphan-task",
3411
3680
  issue: num,
3412
3681
  tranche: slug,
3413
3682
  reason: `Issue #${num} is open and labeled ${trancheLabel(slug)} but does not appear in the topology file`
@@ -3442,6 +3711,7 @@ function checkT3(entries, ciTrancheSlug, enrichedEntries, forgeUnavailableSlugs)
3442
3711
  continue;
3443
3712
  if (forgeUnavailableSlugs?.has(e.trancheSlug)) {
3444
3713
  failures.push({
3714
+ code: "tbd-in-active-tranche",
3445
3715
  issue: null,
3446
3716
  tranche: e.trancheSlug,
3447
3717
  task: e.task.id,
@@ -3451,6 +3721,7 @@ function checkT3(entries, ciTrancheSlug, enrichedEntries, forgeUnavailableSlugs)
3451
3721
  continue;
3452
3722
  }
3453
3723
  failures.push({
3724
+ code: "tbd-in-active-tranche",
3454
3725
  issue: null,
3455
3726
  tranche: e.trancheSlug,
3456
3727
  task: e.task.id,
@@ -3483,6 +3754,7 @@ function checkD1(entries, issueToEntry, taskToEntry) {
3483
3754
  const sameIssue = depEntry.task.issue !== null && e.task.issue !== null && depEntry.task.issue === e.task.issue;
3484
3755
  if (sameTask || sameIssue) {
3485
3756
  failures.push({
3757
+ code: "d1-self-dependency",
3486
3758
  issue: e.task.issue,
3487
3759
  tranche: e.trancheSlug,
3488
3760
  task: e.task.id,
@@ -3494,6 +3766,7 @@ function checkD1(entries, issueToEntry, taskToEntry) {
3494
3766
  const depClosed = depFacts?.issueState === "closed";
3495
3767
  if (!depClosed) {
3496
3768
  failures.push({
3769
+ code: "dispatched-on-unmet-deps",
3497
3770
  issue: e.task.issue,
3498
3771
  tranche: e.trancheSlug,
3499
3772
  task: e.task.id,
@@ -3519,11 +3792,13 @@ function checkR1(issuesBySlug, grandfatheredIssues, registeredNames = []) {
3519
3792
  const errors = [
3520
3793
  ...checkIssueRationale(issue.body).errors,
3521
3794
  ...checkProjectsRegistered(issue.body, issue.labels, registeredNames).errors,
3522
- ...checkIssueObjectives(issue.body, issue.number).errors
3795
+ ...checkIssueObjectives(issue.body, issue.number).errors,
3796
+ ...checkPartsCiteDefinedObjectives(issue.body).errors
3523
3797
  ];
3524
3798
  if (errors.length === 0)
3525
3799
  continue;
3526
3800
  failures.push({
3801
+ code: "missing-rationale-field",
3527
3802
  issue: issue.number,
3528
3803
  tranche: slug,
3529
3804
  reason: `Issue #${issue.number} fails the rationale gate: ${errors.join(" | ")}`,
@@ -3552,6 +3827,7 @@ function checkL1(files, entriesBySlug) {
3552
3827
  const allClosed = withFacts.every((e) => e.facts?.issueState === "closed");
3553
3828
  if (allClosed) {
3554
3829
  failures.push({
3830
+ code: "archive-recommended",
3555
3831
  tranche: f.slug,
3556
3832
  reason: "Active tranche has no open task-Issues — consider archiving to completed/"
3557
3833
  });
@@ -3583,27 +3859,38 @@ function extractClosesReferences(prBody) {
3583
3859
  }
3584
3860
  return referenced;
3585
3861
  }
3586
- function checkClosesN2(branch, prBody, trancheFiles, taskIssueRefs) {
3862
+ function checkClosesNTopology(branch, prBody, trancheFiles, taskIssueRefs) {
3587
3863
  const referenced = extractClosesReferences(prBody);
3588
3864
  if (taskIssueRefs) {
3589
3865
  for (const n of referenced) {
3590
- const ref = taskIssueRefs.get(n);
3591
- if (!ref)
3866
+ const ref2 = taskIssueRefs.get(n);
3867
+ if (!ref2)
3592
3868
  continue;
3593
- const expectedBranch = `task/${ref.trancheSlug}/${ref.taskId}`;
3869
+ const expectedBranch = `task/${ref2.trancheSlug}/${ref2.taskId}`;
3594
3870
  if (branch !== expectedBranch) {
3595
3871
  return {
3596
3872
  ok: false,
3597
- 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.`
3873
+ 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.`
3598
3874
  };
3599
3875
  }
3600
3876
  }
3601
3877
  }
3602
- const m = branch.match(/^task\/([^/]+)\/([^/]+)$/);
3603
- if (!m)
3878
+ const ref = parseTaskBranchIdentity(branch);
3879
+ if (!ref)
3604
3880
  return { ok: true };
3605
- const trancheSlug = m[1];
3606
- const taskId = m[2];
3881
+ if (ref.kind === "issue") {
3882
+ const expectedIssue2 = ref.issueNumber;
3883
+ if (!referenced.has(expectedIssue2)) {
3884
+ return {
3885
+ ok: false,
3886
+ expectedIssue: expectedIssue2,
3887
+ message: `closes-n: PR body does not contain \`Closes #${expectedIssue2}\` (required for branch "${branch}"). Add it to the PR body Summary section.`
3888
+ };
3889
+ }
3890
+ return { ok: true, expectedIssue: expectedIssue2 };
3891
+ }
3892
+ const trancheSlug = ref.tranche;
3893
+ const taskId = ref.taskId;
3607
3894
  const trancheFile = trancheFiles.find((f) => f.slug === trancheSlug);
3608
3895
  if (!trancheFile) {
3609
3896
  return {
@@ -3655,29 +3942,67 @@ function extractIssue(body) {
3655
3942
  const issue = headerNums.length > 0 ? headerNums[0] : bodyNums[0];
3656
3943
  return { issue, extraIssues: bodyNums.filter((n) => n !== issue), outsideHeader: headerNums.length === 0 };
3657
3944
  }
3658
- // ../../packages/aeg-core/src/review-gate.ts
3659
- function isBoundToHead(extraction, headSha) {
3660
- if (!extraction.headSha)
3945
+ // ../../packages/aeg-core/src/review-input-manifest.ts
3946
+ import { createHash as createHash4 } from "node:crypto";
3947
+ function briefHash(brief) {
3948
+ return createHash4("sha256").update(`${brief}
3949
+ `).digest("hex");
3950
+ }
3951
+ function policyDigest(policy) {
3952
+ return createHash4("sha256").update(JSON.stringify({ codeReviewThreshold: policy.codeReviewThreshold, securityThreshold: policy.securityThreshold })).digest("hex");
3953
+ }
3954
+ function isBoundToHead(echoed, headSha) {
3955
+ if (!echoed.headSha)
3661
3956
  return false;
3662
- return headSha.toLowerCase().startsWith(extraction.headSha.toLowerCase());
3957
+ return headSha.toLowerCase().startsWith(echoed.headSha.toLowerCase());
3663
3958
  }
3664
- function isBoundByPatchIdentity(extraction, headSha, patchIdOf) {
3665
- if (patchIdOf === undefined || !extraction.headSha)
3959
+ function isBoundByPatchIdentity(echoed, headSha, patchIdOf) {
3960
+ if (patchIdOf === undefined || !echoed.headSha)
3666
3961
  return false;
3667
- const judged = patchIdOf(extraction.headSha);
3962
+ const judged = patchIdOf(echoed.headSha);
3668
3963
  const current = patchIdOf(headSha);
3669
3964
  if (judged === null || current === null)
3670
3965
  return false;
3671
3966
  return judged === current;
3672
3967
  }
3673
- function isBoundToPatch(extraction, headSha, patchIdOf) {
3674
- return isBoundToHead(extraction, headSha) || isBoundByPatchIdentity(extraction, headSha, patchIdOf);
3968
+ function isBoundToPatch(echoed, headSha, patchIdOf) {
3969
+ return isBoundToHead(echoed, headSha) || isBoundByPatchIdentity(echoed, headSha, patchIdOf);
3675
3970
  }
3676
- function isBoundToObjectives(extraction, currentVersion) {
3971
+ function isBoundToObjectives(echoed, currentVersion) {
3677
3972
  if (currentVersion === null)
3678
3973
  return true;
3679
- return extraction.objectivesVersion === currentVersion;
3974
+ return echoed.objectivesVersion === currentVersion;
3680
3975
  }
3976
+ function isBoundToRulings(echoed, currentOrdinal) {
3977
+ if (echoed.rulingOrdinal === null)
3978
+ return currentOrdinal === 0;
3979
+ return echoed.rulingOrdinal === currentOrdinal;
3980
+ }
3981
+ function isBoundToBriefHash(echoed, currentHash) {
3982
+ if (currentHash === null)
3983
+ return true;
3984
+ return echoed.briefHash === currentHash;
3985
+ }
3986
+ function isBoundToPolicy(echoed, currentDigest) {
3987
+ return echoed.policyDigest === currentDigest;
3988
+ }
3989
+ function compareManifest(echoed, current, patchIdOf) {
3990
+ const head = isBoundToPatch(echoed, current.headSha, patchIdOf);
3991
+ const briefHashBound = isBoundToBriefHash(echoed, current.briefHash);
3992
+ const objectivesVersion2 = isBoundToObjectives(echoed, current.objectivesVersion);
3993
+ const rulingOrdinal = isBoundToRulings(echoed, current.rulingOrdinal);
3994
+ const policyDigestBound = isBoundToPolicy(echoed, current.policyDigest);
3995
+ return {
3996
+ bound: head && briefHashBound && objectivesVersion2 && rulingOrdinal && policyDigestBound,
3997
+ head,
3998
+ briefHash: briefHashBound,
3999
+ objectivesVersion: objectivesVersion2,
4000
+ rulingOrdinal,
4001
+ policyDigest: policyDigestBound
4002
+ };
4003
+ }
4004
+
4005
+ // ../../packages/aeg-core/src/review-gate.ts
3681
4006
  function checkReviewGate(input) {
3682
4007
  const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
3683
4008
  const waived = isWaiverLabelActorVerified({
@@ -3700,13 +4025,55 @@ function checkReviewGate(input) {
3700
4025
  const verifiedBodies = verified.map((c) => c.body);
3701
4026
  const codeReview = extractCodeReviewVerdict(verifiedBodies);
3702
4027
  const security = extractSecurityReviewVerdict(verifiedBodies);
3703
- const codeReviewClean = codeReview.value === "APPROVE";
3704
- const securityClean = security.value === "PASS";
3705
- const codeReviewBound = isBoundToPatch(codeReview, input.headSha, input.patchIdOf);
3706
- const securityBound = isBoundToPatch(security, input.headSha, input.patchIdOf);
3707
- const codeReviewObjectivesBound = isBoundToObjectives(codeReview, input.objectivesVersion);
3708
- const securityObjectivesBound = isBoundToObjectives(security, input.objectivesVersion);
3709
- if (codeReviewClean && codeReviewBound && codeReviewObjectivesBound && securityClean && securityBound && securityObjectivesBound && mechanicalChecksClean) {
4028
+ const policy = input.policy ?? DEFAULT_REVIEW_POLICY;
4029
+ let codeReviewPolicyEvaluation;
4030
+ let securityPolicyEvaluation;
4031
+ try {
4032
+ codeReviewPolicyEvaluation = evaluateCodeReview(codeReview.findingSeverities, policy);
4033
+ securityPolicyEvaluation = evaluateSecurityReview(security.findingSeverities, policy);
4034
+ } catch (err) {
4035
+ return {
4036
+ verdict: "fail",
4037
+ reason: `a verdict comment carries a finding severity this repository's policy does not recognize: ${err instanceof Error ? err.message : String(err)}`,
4038
+ waived: false
4039
+ };
4040
+ }
4041
+ const codeReviewTextClean = codeReview.value === "APPROVE";
4042
+ const securityTextClean = security.value === "PASS";
4043
+ const codeReviewPolicyClean = codeReviewPolicyEvaluation.outcome === "clean";
4044
+ const securityPolicyClean = securityPolicyEvaluation.outcome === "clean";
4045
+ const codeReviewClean = codeReviewTextClean && codeReviewPolicyClean;
4046
+ const securityClean = securityTextClean && securityPolicyClean;
4047
+ const currentManifest = {
4048
+ headSha: input.headSha,
4049
+ briefHash: input.briefHash ?? null,
4050
+ objectivesVersion: input.objectivesVersion,
4051
+ rulingOrdinal: input.rulingOrdinal,
4052
+ policyDigest: policyDigest(policy)
4053
+ };
4054
+ const codeReviewEchoed = {
4055
+ headSha: codeReview.headSha,
4056
+ briefHash: codeReview.briefHash,
4057
+ objectivesVersion: codeReview.objectivesVersion,
4058
+ rulingOrdinal: codeReview.rulingOrdinal,
4059
+ policyDigest: codeReview.policyDigest
4060
+ };
4061
+ const securityEchoed = {
4062
+ headSha: security.headSha,
4063
+ briefHash: security.briefHash,
4064
+ objectivesVersion: security.objectivesVersion,
4065
+ rulingOrdinal: security.rulingOrdinal,
4066
+ policyDigest: security.policyDigest
4067
+ };
4068
+ const codeReviewBinding = compareManifest(codeReviewEchoed, currentManifest, input.patchIdOf);
4069
+ const securityBinding = compareManifest(securityEchoed, currentManifest, input.patchIdOf);
4070
+ const codeReviewBound = codeReviewBinding.head;
4071
+ const securityBound = securityBinding.head;
4072
+ const codeReviewObjectivesBound = codeReviewBinding.objectivesVersion;
4073
+ const securityObjectivesBound = securityBinding.objectivesVersion;
4074
+ const codeReviewRulingsBound = codeReviewBinding.rulingOrdinal;
4075
+ const securityRulingsBound = securityBinding.rulingOrdinal;
4076
+ if (codeReviewClean && codeReviewBinding.bound && securityClean && securityBinding.bound && mechanicalChecksClean) {
3710
4077
  return {
3711
4078
  verdict: "pass",
3712
4079
  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.`,
@@ -3714,19 +4081,35 @@ function checkReviewGate(input) {
3714
4081
  };
3715
4082
  }
3716
4083
  const problems = [];
3717
- if (!codeReviewClean) {
4084
+ if (!codeReviewTextClean) {
3718
4085
  problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
4086
+ } else if (!codeReviewPolicyClean) {
4087
+ 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`);
3719
4088
  } else if (!codeReviewBound) {
3720
4089
  problems.push(`the newest code-review verdict covers ${codeReview.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
3721
4090
  } else if (!codeReviewObjectivesBound) {
3722
4091
  problems.push(`the newest code-review verdict was cast against objectives version ${codeReview.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
3723
- }
3724
- if (!securityClean) {
4092
+ } else if (!codeReviewRulingsBound) {
4093
+ 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`);
4094
+ } else if (!codeReviewBinding.briefHash) {
4095
+ 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"}`);
4096
+ } else if (!codeReviewBinding.policyDigest) {
4097
+ 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}`);
4098
+ }
4099
+ if (!securityTextClean) {
3725
4100
  problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
4101
+ } else if (!securityPolicyClean) {
4102
+ 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`);
3726
4103
  } else if (!securityBound) {
3727
4104
  problems.push(`the newest security-review verdict covers ${security.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
3728
4105
  } else if (!securityObjectivesBound) {
3729
4106
  problems.push(`the newest security-review verdict was cast against objectives version ${security.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
4107
+ } else if (!securityRulingsBound) {
4108
+ 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`);
4109
+ } else if (!securityBinding.briefHash) {
4110
+ 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"}`);
4111
+ } else if (!securityBinding.policyDigest) {
4112
+ 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}`);
3730
4113
  }
3731
4114
  if (!mechanicalChecksClean) {
3732
4115
  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(", ")}`);
@@ -3743,6 +4126,24 @@ var DEFAULT_RELEASE_ACTOR = "github-actions[bot]";
3743
4126
  function isChangesetsReleasePr(branch, author, expectedAuthor) {
3744
4127
  return branch === CHANGESET_RELEASE_BRANCH && author === expectedAuthor;
3745
4128
  }
4129
+ // ../../packages/aeg-core/src/ruling-ordinal.ts
4130
+ var RULING_MARKER_ORDINAL = /^<!-- aeg:principal:ruling:\d+-(\d+) -->$/;
4131
+ function newestPrincipalRulingOrdinal(comments, allowlist) {
4132
+ let best = 0;
4133
+ for (const c of comments) {
4134
+ if (!isPrincipal(c.author, allowlist))
4135
+ continue;
4136
+ const firstLine = (c.body.split(`
4137
+ `)[0] ?? "").trim();
4138
+ const m = RULING_MARKER_ORDINAL.exec(firstLine);
4139
+ if (!m)
4140
+ continue;
4141
+ const k = Number.parseInt(m[1], 10);
4142
+ if (k > best)
4143
+ best = k;
4144
+ }
4145
+ return best;
4146
+ }
3746
4147
  // ../../packages/aeg-core/src/markdown-table.ts
3747
4148
  function splitRow(line) {
3748
4149
  const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
@@ -4400,42 +4801,45 @@ function checkDispatchReadiness(input) {
4400
4801
  const { trancheSlug, task } = input;
4401
4802
  const taskLabel = `task ${task.id} (tranche ${trancheSlug})`;
4402
4803
  const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
4403
- const blockers = [];
4804
+ const blockerDetails = [];
4805
+ const push = (blockerClass, message) => {
4806
+ blockerDetails.push({ class: blockerClass, message });
4807
+ };
4404
4808
  if (task.issue === null) {
4405
- blockers.push(`dispatch-gate issue-existence: ${taskLabel} has no Issue (#TBD or blank) in the topology — not dispatchable until the Planner cuts the Issue.`);
4809
+ 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.`);
4406
4810
  } else if (input.issue === null) {
4407
- blockers.push(`dispatch-gate issue-existence: ${taskLabel} names Issue #${task.issue}, but it does not resolve to a real GitHub Issue (phantom reference).`);
4811
+ push("issue-existence", `dispatch-gate issue-existence: ${taskLabel} names Issue #${task.issue}, but it does not resolve to a real GitHub Issue (phantom reference).`);
4408
4812
  }
4409
4813
  if (input.issue !== null && !input.issueRationalePass) {
4410
- 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.`);
4814
+ 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.`);
4411
4815
  }
4412
4816
  for (const dep of input.dependsOn) {
4413
4817
  if (isSelfDependency(dep, task, input.issue)) {
4414
4818
  const issueStr = input.issue !== null ? `#${input.issue.number}` : "?";
4415
- 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.`);
4819
+ 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.`);
4416
4820
  continue;
4417
4821
  }
4418
4822
  if (dep.resolved === false) {
4419
- 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.`);
4823
+ 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.`);
4420
4824
  continue;
4421
4825
  }
4422
4826
  if (!dep.merged && !isHandClosedByRecognizedPrincipal(dep, principalAllowlist)) {
4423
4827
  const issueStr = dep.issue !== null ? ` (#${dep.issue})` : "";
4424
- blockers.push(`dispatch-gate depends-on: ${taskLabel} depends on ${dep.id}${issueStr}, whose PR is not merged yet — not dispatchable, it serializes behind it.`);
4828
+ 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.`);
4425
4829
  }
4426
4830
  }
4427
4831
  for (const c of input.conflictsWith) {
4428
4832
  if (c.openOrInFlight) {
4429
4833
  const issueStr = c.issue !== null ? ` (#${c.issue})` : "";
4430
- blockers.push(`dispatch-gate conflicts-with: ${taskLabel} conflicts with ${c.id}${issueStr}, whose PR is open or in-flight — not dispatchable until it merges.`);
4834
+ 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.`);
4431
4835
  }
4432
4836
  }
4433
4837
  for (const proj of input.priorTrancheArchival) {
4434
4838
  if (proj.priorTrancheSlug !== null && !proj.archived) {
4435
- 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.`);
4839
+ 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.`);
4436
4840
  }
4437
4841
  }
4438
- return { ready: blockers.length === 0, blockers };
4842
+ return { ready: blockerDetails.length === 0, blockers: blockerDetails.map((b) => b.message), blockerDetails };
4439
4843
  }
4440
4844
  // ../../packages/aeg-core/src/single-plan-pr.ts
4441
4845
  function trancheSlugFromTopologyPath(path) {
@@ -4537,6 +4941,91 @@ function checkBranchTopology(input) {
4537
4941
  reason: `Branch \`${branch}\` matches topology row \`${taskId}\` in ${topoPath}.`
4538
4942
  };
4539
4943
  }
4944
+ // ../../packages/aeg-core/src/task-tools.ts
4945
+ import { z } from "zod";
4946
+ var TASK_TOOL_ERROR_KINDS = [
4947
+ "validation",
4948
+ "authority",
4949
+ "precondition",
4950
+ "capability",
4951
+ "infrastructure",
4952
+ "cancellation",
4953
+ "timeout",
4954
+ "uncertain_effect"
4955
+ ];
4956
+ var TaskToolErrorSchema = z.object({
4957
+ kind: z.enum(TASK_TOOL_ERROR_KINDS),
4958
+ message: z.string().min(1),
4959
+ detail: z.string().optional()
4960
+ });
4961
+ var TaskToolRefSchema = z.union([
4962
+ z.object({ tranche: z.string().min(1), id: z.string().min(1) }),
4963
+ z.object({ issue: z.number().int().positive() })
4964
+ ]);
4965
+ var MAX_PAGE_LIMIT = 100;
4966
+ var PageRequestSchema = z.object({
4967
+ cursor: z.string().optional(),
4968
+ limit: z.number().int().positive().max(MAX_PAGE_LIMIT).optional()
4969
+ });
4970
+ var FreshnessSchema = z.enum(["fresh", "stale", "unknown"]);
4971
+ var ObservedSchema = z.object({
4972
+ observedAt: z.string(),
4973
+ freshness: FreshnessSchema
4974
+ });
4975
+ var TaskStatusInputSchema = z.object({
4976
+ task: TaskToolRefSchema.optional()
4977
+ }).merge(PageRequestSchema);
4978
+ var TaskStatusItemSchema = z.object({
4979
+ task: TaskToolRefSchema,
4980
+ issue: z.number().int().positive(),
4981
+ pr: z.number().int().positive().nullable(),
4982
+ state: z.string()
4983
+ }).merge(ObservedSchema);
4984
+ var TaskStatusResultSchema = z.object({
4985
+ items: z.array(TaskStatusItemSchema),
4986
+ nextCursor: z.string().nullable()
4987
+ });
4988
+ var RequestedAuthoritySchema = z.enum(["planner", "principal", "operator", "self"]);
4989
+ var EscalationInputsSchema = z.object({
4990
+ task: z.number().int().positive(),
4991
+ round: z.number().int().nonnegative(),
4992
+ head: z.string(),
4993
+ branch: z.string(),
4994
+ prNumber: z.number().int().positive()
4995
+ });
4996
+ var EscalationEvidenceSchema = z.object({
4997
+ round: z.number().int().nonnegative(),
4998
+ reviewer: z.string().nullable(),
4999
+ security: z.string().nullable()
5000
+ });
5001
+ var TaskEscalationReadInputSchema = z.object({
5002
+ task: TaskToolRefSchema
5003
+ }).merge(PageRequestSchema);
5004
+ var TaskEscalationPacketSchema = z.object({
5005
+ reason: z.string(),
5006
+ detail: z.string().nullable(),
5007
+ inputs: EscalationInputsSchema.nullable(),
5008
+ evidence: EscalationEvidenceSchema.nullable(),
5009
+ attemptedRecovery: z.string(),
5010
+ requestedAuthority: RequestedAuthoritySchema,
5011
+ permittedNextActions: z.array(z.string())
5012
+ }).merge(ObservedSchema);
5013
+ var TaskEscalationReadResultSchema = z.object({
5014
+ items: z.array(TaskEscalationPacketSchema),
5015
+ nextCursor: z.string().nullable()
5016
+ }).merge(ObservedSchema);
5017
+ var TaskStartInputSchema = z.object({
5018
+ tranche: z.string().min(1),
5019
+ id: z.string().min(1)
5020
+ });
5021
+ var TaskResumeInputSchema = z.object({
5022
+ task: TaskToolRefSchema
5023
+ });
5024
+ var TaskCancelInputSchema = z.object({
5025
+ task: TaskToolRefSchema,
5026
+ reason: z.string().min(1)
5027
+ });
5028
+ var NoResultSchema = z.never();
4540
5029
  // ../../packages/aeg-core/src/first-push-dispatch-gate.ts
4541
5030
  function parseTaskBranch(branch) {
4542
5031
  const m = /^task\/([^/]+)\/([^/]+)$/.exec(branch);
@@ -4620,7 +5109,6 @@ function decideIssueAssignment(input) {
4620
5109
  };
4621
5110
  }
4622
5111
  // ../../packages/aeg-core/src/test-plan-gate.ts
4623
- var TASK_BRANCH_PATTERN2 = /^task\/[^/]+\/[^/]+$/;
4624
5112
  function evaluateTestPlanGate(body, branch) {
4625
5113
  if (!body) {
4626
5114
  return {
@@ -4633,7 +5121,7 @@ function evaluateTestPlanGate(body, branch) {
4633
5121
  }
4634
5122
  const located = locateTestPlanSection(body);
4635
5123
  if (!located.found) {
4636
- if (TASK_BRANCH_PATTERN2.test(branch)) {
5124
+ if (parseTaskBranchIdentity(branch) !== null) {
4637
5125
  return {
4638
5126
  verdict: "fail",
4639
5127
  messages: [
@@ -4761,7 +5249,7 @@ function findWorkspaceEscapes(files, knownPaths, workspaceDirs = DEFAULT_WORKSPA
4761
5249
  return findings;
4762
5250
  }
4763
5251
  // ../../packages/aeg-core/src/log/schema.ts
4764
- import { z } from "zod";
5252
+ import { z as z2 } from "zod";
4765
5253
  var ROLE_VALUES = [
4766
5254
  "planner",
4767
5255
  "developer",
@@ -4771,136 +5259,172 @@ var ROLE_VALUES = [
4771
5259
  "archivist",
4772
5260
  "architect"
4773
5261
  ];
4774
- var RoleSchema = z.enum(ROLE_VALUES);
5262
+ var RoleSchema = z2.enum(ROLE_VALUES);
4775
5263
  var HOST_VALUES = ["hook", "ci", "cli", "loop"];
4776
- var HostSchema = z.enum(HOST_VALUES);
5264
+ var HostSchema = z2.enum(HOST_VALUES);
4777
5265
  var RUN_ID_PATTERN = /^[A-Za-z0-9_.-]{1,128}$/;
4778
- var HeaderMetaSchema = z.object({
4779
- schema: z.literal(1),
4780
- ts: z.string(),
4781
- run_id: z.string().regex(RUN_ID_PATTERN),
4782
- seq: z.number().int().nonnegative(),
4783
- repo: z.string().nullable(),
4784
- vinaya: z.string(),
4785
- doctrine: z.string(),
5266
+ var headerMetaCore = {
5267
+ ts: z2.string(),
5268
+ run_id: z2.string().regex(RUN_ID_PATTERN),
5269
+ seq: z2.number().int().nonnegative(),
5270
+ repo: z2.string().nullable(),
5271
+ vinaya: z2.string(),
5272
+ doctrine: z2.string(),
4786
5273
  host: HostSchema,
4787
- machine: z.string()
5274
+ machine: z2.string()
5275
+ };
5276
+ var HeaderMetaV1Schema = z2.object({
5277
+ schema: z2.literal(1),
5278
+ ...headerMetaCore
5279
+ }).strict();
5280
+ var LineageSchema = z2.object({
5281
+ run: z2.string().nullable(),
5282
+ attempt: z2.number().int().nullable(),
5283
+ parent: z2.string().nullable()
4788
5284
  }).strict();
4789
- var SubjectSchema = z.object({
4790
- issue: z.number().int().nullable(),
4791
- pr: z.number().int().optional(),
4792
- sha: z.string().optional(),
4793
- role: z.union([RoleSchema, z.literal("unattributed")]),
4794
- round: z.number().int().optional(),
4795
- objectives_version: z.string().optional()
5285
+ var InputVersionsSchema = z2.object({
5286
+ objectives_version: z2.string().nullable(),
5287
+ brief_hash: z2.string().nullable(),
5288
+ ruling_ordinal: z2.number().int().nullable(),
5289
+ policy_digest: z2.string().nullable()
4796
5290
  }).strict();
4797
- var HeaderSchema = z.object({
5291
+ var ProvenanceSchema = z2.enum(["parent_attributed", "env_correlated", "self_reported", "unavailable"]);
5292
+ var HeaderMetaV2Schema = z2.object({
5293
+ schema: z2.literal(2),
5294
+ ...headerMetaCore,
5295
+ event_id: z2.string().min(1),
5296
+ process_id: z2.string().min(1),
5297
+ actor_id: z2.string().nullable(),
5298
+ lineage: LineageSchema,
5299
+ input_versions: InputVersionsSchema,
5300
+ provenance: ProvenanceSchema
5301
+ }).strict();
5302
+ var HeaderMetaSchema = z2.discriminatedUnion("schema", [HeaderMetaV1Schema, HeaderMetaV2Schema]);
5303
+ var SubjectSchema = z2.object({
5304
+ issue: z2.number().int().nullable(),
5305
+ pr: z2.number().int().optional(),
5306
+ sha: z2.string().optional(),
5307
+ role: z2.union([RoleSchema, z2.literal("unattributed")]),
5308
+ round: z2.number().int().optional(),
5309
+ objectives_version: z2.string().optional()
5310
+ }).strict();
5311
+ var HeaderSchema = z2.object({
4798
5312
  meta: HeaderMetaSchema,
4799
5313
  subject: SubjectSchema
4800
5314
  }).strict();
4801
5315
  var envelopeTail = {
4802
- duration_ms: z.number().nonnegative().optional(),
4803
- payload: z.object({}).strict()
5316
+ duration_ms: z2.number().nonnegative().optional(),
5317
+ payload: z2.object({}).strict()
4804
5318
  };
4805
- var DispatchOutcomeSchema = z.discriminatedUnion("type", [
4806
- z.object({ type: z.literal("pr_opened"), pr: z.number().int(), head: z.string() }).strict(),
4807
- z.object({
4808
- type: z.literal("round_pushed"),
4809
- pr: z.number().int(),
4810
- head: z.string(),
4811
- comment_id: z.number().int()
5319
+ var ReviewFindingSchema = z2.object({
5320
+ id: z2.string(),
5321
+ severity: z2.string(),
5322
+ state: z2.string().optional(),
5323
+ severity_scale: z2.string().optional(),
5324
+ policy_treatment: z2.enum(["blocking", "non_blocking", "unavailable"]).optional(),
5325
+ confidence: z2.number().min(0).max(1).optional(),
5326
+ confidence_scale: z2.string().optional(),
5327
+ confidence_source: z2.string().optional()
5328
+ }).strict();
5329
+ var DispatchOutcomeSchema = z2.discriminatedUnion("type", [
5330
+ z2.object({ type: z2.literal("pr_opened"), pr: z2.number().int(), head: z2.string() }).strict(),
5331
+ z2.object({
5332
+ type: z2.literal("round_pushed"),
5333
+ pr: z2.number().int(),
5334
+ head: z2.string(),
5335
+ comment_id: z2.number().int()
4812
5336
  }).strict(),
4813
- z.object({
4814
- type: z.literal("verdict"),
4815
- verdict: z.enum(["APPROVE", "REQUEST CHANGES", "PASS", "FAIL"]),
4816
- head: z.string(),
4817
- comment_id: z.number().int(),
4818
- objectives: z.array(z.object({ id: z.string(), met: z.boolean() }).strict()),
4819
- findings: z.array(z.object({ id: z.string(), severity: z.string(), state: z.string().optional() }).strict())
5337
+ z2.object({
5338
+ type: z2.literal("verdict"),
5339
+ verdict: z2.enum(["APPROVE", "REQUEST CHANGES", "PASS", "FAIL"]),
5340
+ head: z2.string(),
5341
+ comment_id: z2.number().int(),
5342
+ objectives: z2.array(z2.object({ id: z2.string(), met: z2.boolean() }).strict()),
5343
+ findings: z2.array(ReviewFindingSchema)
4820
5344
  }).strict(),
4821
- z.object({
4822
- type: z.literal("escalation"),
4823
- class: z.enum(["authority", "strategy", "product"]),
4824
- comment_id: z.number().int()
5345
+ z2.object({
5346
+ type: z2.literal("escalation"),
5347
+ class: z2.enum(["authority", "strategy", "product"]),
5348
+ comment_id: z2.number().int()
4825
5349
  }).strict(),
4826
- z.object({ type: z.literal("brief"), comment_id: z.number().int(), hash: z.string() }).strict(),
4827
- z.object({ type: z.literal("plan"), issues: z.array(z.number().int()) }).strict(),
4828
- z.object({ type: z.literal("archive"), provenance_comment_id: z.number().int() }).strict()
5350
+ z2.object({ type: z2.literal("brief"), comment_id: z2.number().int(), hash: z2.string() }).strict(),
5351
+ z2.object({ type: z2.literal("plan"), issues: z2.array(z2.number().int()) }).strict(),
5352
+ z2.object({ type: z2.literal("archive"), provenance_comment_id: z2.number().int() }).strict()
4829
5353
  ]);
4830
5354
  var dispatchShared = {
4831
5355
  meta: HeaderMetaSchema,
4832
5356
  subject: SubjectSchema,
4833
- kind: z.literal("dispatch"),
5357
+ kind: z2.literal("dispatch"),
4834
5358
  ...envelopeTail,
4835
5359
  target_role: RoleSchema,
4836
- model: z.string(),
4837
- round: z.number().int().optional(),
4838
- effect_id: z.string()
5360
+ model: z2.string(),
5361
+ round: z2.number().int().optional(),
5362
+ effect_id: z2.string()
4839
5363
  };
4840
- var dispatchUsageField = z.object({ input: z.number().nonnegative(), output: z.number().nonnegative() }).strict().nullable();
4841
- var DispatchEventSchema = z.discriminatedUnion("event", [
4842
- z.object({ ...dispatchShared, event: z.literal("dispatched"), prompt_hash: z.string() }).strict(),
4843
- z.object({
5364
+ var dispatchUsageField = z2.object({ input: z2.number().nonnegative(), output: z2.number().nonnegative() }).strict().nullable();
5365
+ var DispatchEventSchema = z2.discriminatedUnion("event", [
5366
+ z2.object({ ...dispatchShared, event: z2.literal("dispatched"), prompt_hash: z2.string() }).strict(),
5367
+ z2.object({
4844
5368
  ...dispatchShared,
4845
- event: z.literal("outcome_received"),
5369
+ event: z2.literal("outcome_received"),
4846
5370
  outcome: DispatchOutcomeSchema,
4847
5371
  usage: dispatchUsageField
4848
5372
  }).strict(),
4849
- z.object({
5373
+ z2.object({
4850
5374
  ...dispatchShared,
4851
- event: z.literal("dispatch_failed"),
4852
- reason: z.enum(["timeout", "crash", "refused", "unattributed_write"]),
5375
+ event: z2.literal("dispatch_failed"),
5376
+ reason: z2.enum(["timeout", "crash", "refused", "unattributed_write"]),
4853
5377
  usage: dispatchUsageField
4854
5378
  }).strict()
4855
5379
  ]);
4856
5380
  var loopShared = {
4857
5381
  meta: HeaderMetaSchema,
4858
5382
  subject: SubjectSchema,
4859
- kind: z.literal("dev_review_loop"),
5383
+ kind: z2.literal("dev_review_loop"),
4860
5384
  ...envelopeTail,
4861
- loop_id: z.string()
5385
+ loop_id: z2.string()
4862
5386
  };
4863
- var DevReviewLoopEventSchema = z.discriminatedUnion("event", [
4864
- z.object({
5387
+ var DevReviewLoopEventSchema = z2.discriminatedUnion("event", [
5388
+ z2.object({
4865
5389
  ...loopShared,
4866
- event: z.literal("loop_started"),
4867
- task: z.number().int(),
4868
- policy: z.object({
4869
- max_rounds: z.number().int().nonnegative(),
4870
- reviewers: z.array(RoleSchema),
4871
- models: z.record(RoleSchema, z.string())
5390
+ event: z2.literal("loop_started"),
5391
+ task: z2.number().int(),
5392
+ policy: z2.object({
5393
+ max_rounds: z2.number().int().nonnegative(),
5394
+ reviewers: z2.array(RoleSchema),
5395
+ models: z2.record(RoleSchema, z2.string())
4872
5396
  }).strict()
4873
5397
  }).strict(),
4874
- z.object({ ...loopShared, event: z.literal("round_started"), round: z.number().int(), base_head: z.string() }).strict(),
4875
- z.object({
5398
+ z2.object({ ...loopShared, event: z2.literal("round_started"), round: z2.number().int(), base_head: z2.string() }).strict(),
5399
+ z2.object({
4876
5400
  ...loopShared,
4877
- event: z.literal("gate_result_read"),
4878
- round: z.number().int(),
4879
- head: z.string(),
4880
- green: z.boolean()
5401
+ event: z2.literal("gate_result_read"),
5402
+ round: z2.number().int(),
5403
+ head: z2.string(),
5404
+ green: z2.boolean()
4881
5405
  }).strict(),
4882
- z.object({
5406
+ z2.object({
4883
5407
  ...loopShared,
4884
- event: z.literal("verdicts_read"),
4885
- round: z.number().int(),
4886
- head: z.string(),
4887
- all_approve: z.boolean(),
4888
- blockers: z.number().int().nonnegative()
5408
+ event: z2.literal("verdicts_read"),
5409
+ round: z2.number().int(),
5410
+ head: z2.string(),
5411
+ all_approve: z2.boolean(),
5412
+ blockers: z2.number().int().nonnegative()
4889
5413
  }).strict(),
4890
- z.object({
5414
+ z2.object({
4891
5415
  ...loopShared,
4892
- event: z.literal("findings_compared"),
4893
- round: z.number().int(),
4894
- open: z.array(z.string()),
4895
- resolved: z.array(z.string()),
4896
- new: z.array(z.string()),
4897
- recurring: z.array(z.string())
5416
+ event: z2.literal("findings_compared"),
5417
+ round: z2.number().int(),
5418
+ open: z2.array(z2.string()),
5419
+ resolved: z2.array(z2.string()),
5420
+ new: z2.array(z2.string()),
5421
+ recurring: z2.array(z2.string())
4898
5422
  }).strict(),
4899
- z.object({
5423
+ z2.object({
4900
5424
  ...loopShared,
4901
- event: z.literal("stop_condition_met"),
4902
- round: z.number().int(),
4903
- condition: z.enum([
5425
+ event: z2.literal("stop_condition_met"),
5426
+ round: z2.number().int(),
5427
+ condition: z2.enum([
4904
5428
  "green",
4905
5429
  "max_rounds",
4906
5430
  "no_progress",
@@ -4910,42 +5434,49 @@ var DevReviewLoopEventSchema = z.discriminatedUnion("event", [
4910
5434
  "reappearance"
4911
5435
  ])
4912
5436
  }).strict(),
4913
- z.object({
5437
+ z2.object({
5438
+ ...loopShared,
5439
+ event: z2.literal("paused"),
5440
+ round: z2.number().int(),
5441
+ reason: z2.enum(["escalation", "principal_item", "refreeze_needed"])
5442
+ }).strict(),
5443
+ z2.object({
4914
5444
  ...loopShared,
4915
- event: z.literal("paused"),
4916
- round: z.number().int(),
4917
- reason: z.enum(["escalation", "principal_item", "refreeze_needed"])
5445
+ event: z2.literal("resumed"),
5446
+ round: z2.number().int(),
5447
+ by: z2.literal("principal")
4918
5448
  }).strict(),
4919
- z.object({
5449
+ z2.object({
4920
5450
  ...loopShared,
4921
- event: z.literal("resumed"),
4922
- round: z.number().int(),
4923
- by: z.literal("principal")
5451
+ event: z2.literal("unpushed_work_resume"),
5452
+ round: z2.number().int(),
5453
+ branch: z2.string(),
5454
+ detail: z2.string()
4924
5455
  }).strict(),
4925
- z.object({
5456
+ z2.object({
4926
5457
  ...loopShared,
4927
- event: z.literal("round_ended"),
4928
- round: z.number().int(),
4929
- base_head: z.string(),
4930
- head: z.string(),
4931
- files_changed: z.number().int().nonnegative(),
4932
- insertions: z.number().int().nonnegative(),
4933
- deletions: z.number().int().nonnegative(),
4934
- wall_ms: z.number().nonnegative(),
4935
- outcome: z.enum(["green", "changes_requested", "escalated"])
5458
+ event: z2.literal("round_ended"),
5459
+ round: z2.number().int(),
5460
+ base_head: z2.string(),
5461
+ head: z2.string(),
5462
+ files_changed: z2.number().int().nonnegative(),
5463
+ insertions: z2.number().int().nonnegative(),
5464
+ deletions: z2.number().int().nonnegative(),
5465
+ wall_ms: z2.number().nonnegative(),
5466
+ outcome: z2.enum(["green", "changes_requested", "escalated"])
4936
5467
  }).strict(),
4937
- z.object({
5468
+ z2.object({
4938
5469
  ...loopShared,
4939
- event: z.literal("journal_finalized"),
4940
- rounds: z.number().int().nonnegative(),
4941
- total_wall_ms: z.number().nonnegative(),
4942
- time_to_green_ms: z.number().nonnegative().nullable(),
4943
- files_changed_total: z.number().int().nonnegative(),
4944
- final_head: z.string(),
4945
- result: z.enum(["merged_ready", "stopped"])
5470
+ event: z2.literal("journal_finalized"),
5471
+ rounds: z2.number().int().nonnegative(),
5472
+ total_wall_ms: z2.number().nonnegative(),
5473
+ time_to_green_ms: z2.number().nonnegative().nullable(),
5474
+ files_changed_total: z2.number().int().nonnegative(),
5475
+ final_head: z2.string(),
5476
+ result: z2.enum(["merged_ready", "stopped"])
4946
5477
  }).strict()
4947
5478
  ]);
4948
- var ForgeOpSchema = z.enum([
5479
+ var ForgeOpSchema = z2.enum([
4949
5480
  "pr.create",
4950
5481
  "pr.comment",
4951
5482
  "pr.body.replace",
@@ -4959,24 +5490,210 @@ var ForgeOpSchema = z.enum([
4959
5490
  "label.add",
4960
5491
  "label.remove"
4961
5492
  ]);
4962
- var ForgeWriteTargetSchema = z.object({
4963
- issue: z.number().int().optional(),
4964
- pr: z.number().int().optional()
5493
+ var ForgeWriteTargetSchema = z2.object({
5494
+ issue: z2.number().int().optional(),
5495
+ pr: z2.number().int().optional()
4965
5496
  }).strict();
4966
5497
  var forgeWriteShared = {
4967
5498
  meta: HeaderMetaSchema,
4968
5499
  subject: SubjectSchema,
4969
- kind: z.literal("forge_write"),
5500
+ kind: z2.literal("forge_write"),
4970
5501
  ...envelopeTail,
4971
5502
  op: ForgeOpSchema,
4972
5503
  target: ForgeWriteTargetSchema
4973
5504
  };
4974
- var ForgeWriteEventSchema = z.discriminatedUnion("event", [
4975
- z.object({ ...forgeWriteShared, event: z.literal("validated") }).strict(),
4976
- z.object({ ...forgeWriteShared, event: z.literal("refused"), reason: z.string() }).strict(),
4977
- z.object({ ...forgeWriteShared, event: z.literal("written"), comment_ids: z.array(z.string()) }).strict()
5505
+ var ForgeWriteEventSchema = z2.discriminatedUnion("event", [
5506
+ z2.object({ ...forgeWriteShared, event: z2.literal("validated") }).strict(),
5507
+ z2.object({ ...forgeWriteShared, event: z2.literal("refused"), reason: z2.string() }).strict(),
5508
+ z2.object({ ...forgeWriteShared, event: z2.literal("written"), comment_ids: z2.array(z2.string()) }).strict()
5509
+ ]);
5510
+ var GateOutcomeSchema = z2.enum([
5511
+ "pass",
5512
+ "fail",
5513
+ "wait",
5514
+ "skip",
5515
+ "invalid_input",
5516
+ "unavailable_dependency",
5517
+ "timeout",
5518
+ "cancelled"
5519
+ ]);
5520
+ var gateShared = {
5521
+ meta: HeaderMetaSchema,
5522
+ subject: SubjectSchema,
5523
+ kind: z2.literal("gate"),
5524
+ ...envelopeTail,
5525
+ check: z2.string(),
5526
+ check_version: z2.string().nullable(),
5527
+ policy_version: z2.string().nullable(),
5528
+ input_fingerprint: z2.string().nullable()
5529
+ };
5530
+ var GateEventSchema = z2.discriminatedUnion("event", [
5531
+ z2.object({
5532
+ ...gateShared,
5533
+ event: z2.literal("checked"),
5534
+ outcome: GateOutcomeSchema,
5535
+ reason: z2.string().optional()
5536
+ }).strict()
5537
+ ]);
5538
+ var OperationResultSchema = z2.enum(["ok", "error", "refused", "timeout", "cancelled", "unavailable"]);
5539
+ var operationShared = {
5540
+ meta: HeaderMetaSchema,
5541
+ subject: SubjectSchema,
5542
+ kind: z2.literal("operation"),
5543
+ ...envelopeTail,
5544
+ operation: z2.string(),
5545
+ target: z2.string().nullable()
5546
+ };
5547
+ var OperationEventSchema = z2.discriminatedUnion("event", [
5548
+ z2.object({
5549
+ ...operationShared,
5550
+ event: z2.literal("completed"),
5551
+ result: OperationResultSchema,
5552
+ error_class: z2.string().nullable()
5553
+ }).strict()
5554
+ ]);
5555
+ var UsageUnitsSchema = z2.object({
5556
+ input: z2.number().nonnegative().nullable(),
5557
+ output: z2.number().nonnegative().nullable(),
5558
+ cache: z2.number().nonnegative().nullable()
5559
+ }).strict();
5560
+ var usageShared = {
5561
+ meta: HeaderMetaSchema,
5562
+ subject: SubjectSchema,
5563
+ kind: z2.literal("usage"),
5564
+ ...envelopeTail,
5565
+ model: z2.string().nullable(),
5566
+ source: z2.string(),
5567
+ semantics: z2.enum(["cumulative", "delta"])
5568
+ };
5569
+ var UsageEventSchema = z2.discriminatedUnion("event", [
5570
+ z2.object({
5571
+ ...usageShared,
5572
+ event: z2.literal("observed"),
5573
+ units: UsageUnitsSchema,
5574
+ unknown_reason: z2.string().nullable()
5575
+ }).strict()
5576
+ ]);
5577
+ var RoleAttemptOutcomeSchema = z2.enum([
5578
+ "completed",
5579
+ "incomplete",
5580
+ "infrastructure_failed",
5581
+ "cancelled",
5582
+ "timed_out",
5583
+ "capability_refused"
5584
+ ]);
5585
+ var roleAttemptShared = {
5586
+ meta: HeaderMetaSchema,
5587
+ subject: SubjectSchema,
5588
+ kind: z2.literal("role_attempt"),
5589
+ ...envelopeTail,
5590
+ actor: z2.string().nullable(),
5591
+ attempt: z2.number().int().nullable()
5592
+ };
5593
+ var RoleAttemptEventSchema = z2.discriminatedUnion("event", [
5594
+ z2.object({
5595
+ ...roleAttemptShared,
5596
+ event: z2.literal("attempted"),
5597
+ outcome: RoleAttemptOutcomeSchema,
5598
+ usage: dispatchUsageField
5599
+ }).strict()
5600
+ ]);
5601
+ var handoffShared = {
5602
+ meta: HeaderMetaSchema,
5603
+ subject: SubjectSchema,
5604
+ kind: z2.literal("handoff"),
5605
+ ...envelopeTail,
5606
+ class: z2.enum(["authority", "strategy", "product"]),
5607
+ reason: z2.string()
5608
+ };
5609
+ var HandoffEventSchema = z2.discriminatedUnion("event", [
5610
+ z2.object({
5611
+ ...handoffShared,
5612
+ event: z2.literal("raised"),
5613
+ requested_decision: z2.string().nullable()
5614
+ }).strict(),
5615
+ z2.object({
5616
+ ...handoffShared,
5617
+ event: z2.literal("resolved"),
5618
+ resolution: z2.string().nullable(),
5619
+ resolved_by: z2.string().nullable()
5620
+ }).strict()
5621
+ ]);
5622
+ var EffectTargetSchema = z2.object({
5623
+ kind: z2.string(),
5624
+ ref: z2.string()
5625
+ }).strict();
5626
+ var effectShared = {
5627
+ meta: HeaderMetaSchema,
5628
+ subject: SubjectSchema,
5629
+ kind: z2.literal("effect"),
5630
+ ...envelopeTail,
5631
+ effect_id: z2.string(),
5632
+ target: EffectTargetSchema
5633
+ };
5634
+ var EffectEventSchema = z2.discriminatedUnion("event", [
5635
+ z2.object({ ...effectShared, event: z2.literal("attempted") }).strict(),
5636
+ z2.object({ ...effectShared, event: z2.literal("observed"), outcome: z2.enum(["success", "failure", "uncertain"]) }).strict(),
5637
+ z2.object({ ...effectShared, event: z2.literal("verified"), outcome: z2.enum(["success", "failure", "uncertain"]) }).strict()
4978
5638
  ]);
4979
- var LogEventSchema = z.union([DispatchEventSchema, DevReviewLoopEventSchema, ForgeWriteEventSchema]);
5639
+ var LogEventSchema = z2.union([
5640
+ DispatchEventSchema,
5641
+ DevReviewLoopEventSchema,
5642
+ ForgeWriteEventSchema,
5643
+ GateEventSchema,
5644
+ OperationEventSchema,
5645
+ UsageEventSchema,
5646
+ RoleAttemptEventSchema,
5647
+ HandoffEventSchema,
5648
+ EffectEventSchema
5649
+ ]);
5650
+ // ../../packages/aeg-core/src/dev-review-loop/journal-reconstruction.ts
5651
+ var STOPPED_CONDITIONS = new Set(["confidence", "reappearance", "no_progress", "max_rounds"]);
5652
+ // ../../packages/aeg-core/src/control-store/records.ts
5653
+ import { z as z3 } from "zod";
5654
+ var isoTimestamp = z3.string().min(1);
5655
+ var taskId = z3.number().int().positive();
5656
+ var epochNumber = z3.number().int().nonnegative();
5657
+ var RunRecordSchema = z3.object({
5658
+ version: z3.literal(1),
5659
+ kind: z3.literal("run"),
5660
+ task: taskId,
5661
+ runId: z3.string().min(1),
5662
+ pid: z3.number().int().positive(),
5663
+ host: z3.string().min(1),
5664
+ startedAt: isoTimestamp
5665
+ }).strict();
5666
+ var InputRecordSchema = z3.object({
5667
+ version: z3.literal(1),
5668
+ kind: z3.literal("input"),
5669
+ task: taskId,
5670
+ runId: z3.string().min(1),
5671
+ source: z3.union([z3.literal("fresh"), z3.literal("resume")]),
5672
+ pr: z3.number().int().positive().nullable(),
5673
+ round: z3.number().int().nonnegative(),
5674
+ recordedAt: isoTimestamp
5675
+ }).strict();
5676
+ var OwnershipRecordSchema = z3.object({
5677
+ version: z3.literal(1),
5678
+ kind: z3.literal("ownership"),
5679
+ task: taskId,
5680
+ epoch: epochNumber,
5681
+ ownerId: z3.string().min(1),
5682
+ pid: z3.number().int().positive(),
5683
+ host: z3.string().min(1),
5684
+ acquiredAt: isoTimestamp
5685
+ }).strict();
5686
+ var TransitionRecordSchema = z3.object({
5687
+ version: z3.literal(1),
5688
+ kind: z3.literal("transition"),
5689
+ task: taskId,
5690
+ epoch: epochNumber,
5691
+ seq: z3.number().int().nonnegative(),
5692
+ from: z3.string().min(1),
5693
+ to: z3.string().min(1),
5694
+ detail: z3.string().optional(),
5695
+ at: isoTimestamp
5696
+ }).strict();
4980
5697
  // ../../packages/sources/src/commands.ts
4981
5698
  var COMMANDS = [
4982
5699
  {
@@ -5097,14 +5814,52 @@ var COMMANDS = [
5097
5814
  },
5098
5815
  {
5099
5816
  name: "task dispatch",
5100
- description: "Render, pin, and post the brief on the Issue as the frozen original; start the developer",
5817
+ description: "Deprecated — render, pin, and post the brief on the Issue as the frozen original; start the developer",
5101
5818
  flags: [
5102
5819
  { flag: "--agent <claude|codex|gemini>", description: "Start the developer through dispatchRole once posted" }
5103
5820
  ],
5104
5821
  details: [
5105
5822
  "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.",
5106
5823
  "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.",
5107
- "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."
5824
+ "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.",
5825
+ "Deprecated in favor of `task brief` (preparation only) and `task run` (the full unattended loop) — kept for a documented compatibility window while callers migrate."
5826
+ ],
5827
+ status: "shipped"
5828
+ },
5829
+ {
5830
+ name: "task brief",
5831
+ description: "Render and freeze the brief as the Issue's original comment — preparation only, starts nobody",
5832
+ details: [
5833
+ "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.",
5834
+ "Successor to `task dispatch` for the preparation step; the full unattended run (preparation, then the developer, then the review loop) is `task run`."
5835
+ ],
5836
+ status: "shipped"
5837
+ },
5838
+ {
5839
+ name: "task run",
5840
+ description: "One command from a planned Issue to a reviewed pull request — exactly one developer started",
5841
+ flags: [
5842
+ {
5843
+ flag: "--agent <claude|codex|gemini>",
5844
+ description: "Vendor for the developer and both reviewers this run dispatches"
5845
+ }
5846
+ ],
5847
+ details: [
5848
+ "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.",
5849
+ "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.",
5850
+ "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.",
5851
+ "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."
5852
+ ],
5853
+ status: "shipped"
5854
+ },
5855
+ {
5856
+ name: "task status",
5857
+ description: "Every open task with a frozen brief, its pull request, and whether its loop is running, paused, or published",
5858
+ flags: [{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }],
5859
+ details: [
5860
+ "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`.",
5861
+ "`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.",
5862
+ "`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."
5108
5863
  ],
5109
5864
  status: "shipped"
5110
5865
  },
@@ -5312,6 +6067,15 @@ var COMMANDS = [
5312
6067
  ],
5313
6068
  status: "shipped"
5314
6069
  },
6070
+ {
6071
+ name: "milestone status",
6072
+ description: "Print each of a Milestone's declared tranche intents with its derived lifecycle and issue counts",
6073
+ flags: [{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }],
6074
+ details: [
6075
+ "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."
6076
+ ],
6077
+ status: "shipped"
6078
+ },
5315
6079
  {
5316
6080
  name: "review status",
5317
6081
  description: "Print the review loop's own state for a PR, and its branch's distance from the base",
@@ -5608,10 +6372,10 @@ function createForgeSource(config) {
5608
6372
  };
5609
6373
  }
5610
6374
  // ../../packages/sources/src/select-source.ts
5611
- import { z as z2 } from "zod";
5612
- var StateSourceConfigSchema = z2.discriminatedUnion("kind", [
5613
- z2.object({ kind: z2.literal("forge"), owner: z2.string(), repo: z2.string() }),
5614
- z2.object({ kind: z2.literal("file"), root: z2.string().optional() })
6375
+ import { z as z4 } from "zod";
6376
+ var StateSourceConfigSchema = z4.discriminatedUnion("kind", [
6377
+ z4.object({ kind: z4.literal("forge"), owner: z4.string(), repo: z4.string() }),
6378
+ z4.object({ kind: z4.literal("file"), root: z4.string().optional() })
5615
6379
  ]);
5616
6380
  // src/checks/contract.ts
5617
6381
  var CHECK_SCHEMA_VERSION = 1;
@@ -5625,27 +6389,29 @@ import { execFileSync as execFileSync2 } from "node:child_process";
5625
6389
  import { existsSync, mkdirSync, readFileSync as readFileSync2, realpathSync, writeFileSync } from "node:fs";
5626
6390
  import { homedir } from "node:os";
5627
6391
  import { dirname, join, resolve } from "node:path";
5628
- import { z as z3 } from "zod";
6392
+ import { z as z5 } from "zod";
5629
6393
 
5630
6394
  // src/lib/agent-vendors.ts
5631
6395
  var AGENT_VENDORS = ["skills", "claude", "gemini"];
5632
6396
 
5633
6397
  // src/lib/config.ts
5634
- var EnvEntrySchema = z3.union([
5635
- z3.literal(true),
5636
- z3.object({ optional: z3.literal(true) }),
5637
- z3.object({ anyOf: z3.array(z3.string()).min(2) }),
5638
- z3.string()
6398
+ var EnvEntrySchema = z5.union([
6399
+ z5.literal(true),
6400
+ z5.object({ optional: z5.literal(true) }),
6401
+ z5.object({ anyOf: z5.array(z5.string()).min(2) }),
6402
+ z5.string()
5639
6403
  ]);
5640
- var CheckEntrySchema = z3.object({
5641
- run: z3.string(),
5642
- scope: z3.enum(["diff", "full"]),
5643
- include: z3.array(z3.string()).optional(),
5644
- args: z3.array(z3.string()).optional(),
5645
- timeoutMs: z3.number().optional(),
5646
- env: z3.record(z3.string(), EnvEntrySchema).optional(),
5647
- requiresOpenPr: z3.boolean().optional(),
5648
- ownWorkflow: z3.boolean().optional()
6404
+ var CheckEntrySchema = z5.object({
6405
+ run: z5.string(),
6406
+ scope: z5.enum(["diff", "full"]),
6407
+ include: z5.array(z5.string()).optional(),
6408
+ args: z5.array(z5.string()).optional(),
6409
+ timeoutMs: z5.number().optional(),
6410
+ env: z5.record(z5.string(), EnvEntrySchema).optional(),
6411
+ requiresOpenPr: z5.boolean().optional(),
6412
+ ownWorkflow: z5.boolean().optional(),
6413
+ principalOwed: z5.literal(true).optional(),
6414
+ validates: z5.enum(["body", "issue"]).optional()
5649
6415
  }).superRefine((entry2, ctx) => {
5650
6416
  if (!entry2.env)
5651
6417
  return;
@@ -5655,25 +6421,26 @@ var CheckEntrySchema = z3.object({
5655
6421
  const members = value.anyOf;
5656
6422
  if (new Set(members).size !== members.length) {
5657
6423
  ctx.addIssue({
5658
- code: z3.ZodIssueCode.custom,
6424
+ code: z5.ZodIssueCode.custom,
5659
6425
  message: `env.${key}.anyOf has duplicate members`,
5660
6426
  path: ["env", key, "anyOf"]
5661
6427
  });
5662
6428
  }
5663
6429
  if (!members.includes(key)) {
5664
6430
  ctx.addIssue({
5665
- code: z3.ZodIssueCode.custom,
6431
+ code: z5.ZodIssueCode.custom,
5666
6432
  message: `env.${key}.anyOf must include "${key}" itself as a member`,
5667
6433
  path: ["env", key, "anyOf"]
5668
6434
  });
5669
6435
  }
5670
6436
  }
5671
6437
  });
5672
- var RoleEntrySchema = z3.object({
5673
- contract: z3.string().refine((p) => p.includes("/"), {
6438
+ var RoleEntrySchema = z5.object({
6439
+ contract: z5.string().refine((p) => p.includes("/"), {
5674
6440
  message: 'must be a path (contain at least one "/"), not a bare filename'
5675
6441
  })
5676
6442
  });
6443
+ var MAX_REPORT_COMMAND_TIMEOUT_MS = 3600000;
5677
6444
  var BRIEF_BUILTINS = [
5678
6445
  "tier",
5679
6446
  "testPlan",
@@ -5693,17 +6460,17 @@ var BRIEF_BUILTINS = [
5693
6460
  "briefSections",
5694
6461
  "milestoneShape"
5695
6462
  ];
5696
- var BriefSectionSchema = z3.union([
5697
- z3.object({ builtin: z3.enum(BRIEF_BUILTINS) }),
5698
- z3.object({ heading: z3.string().min(1), name: z3.string().optional() }),
5699
- z3.object({ field: z3.string().min(1), name: z3.string().optional() }),
5700
- z3.object({ phrase: z3.string().min(1), name: z3.string().optional() })
6463
+ var BriefSectionSchema = z5.union([
6464
+ z5.object({ builtin: z5.enum(BRIEF_BUILTINS) }),
6465
+ z5.object({ heading: z5.string().min(1), name: z5.string().optional() }),
6466
+ z5.object({ field: z5.string().min(1), name: z5.string().optional() }),
6467
+ z5.object({ phrase: z5.string().min(1), name: z5.string().optional() })
5701
6468
  ]);
5702
- var BriefSchemaSchema = z3.object({
5703
- pr: z3.object({ sections: z3.array(BriefSectionSchema) }).optional(),
5704
- issue: z3.object({ sections: z3.array(BriefSectionSchema) }).optional(),
5705
- milestone: z3.object({ sections: z3.array(BriefSectionSchema) }).optional(),
5706
- ack: z3.array(z3.enum(BRIEF_BUILTINS)).optional()
6469
+ var BriefSchemaSchema = z5.object({
6470
+ pr: z5.object({ sections: z5.array(BriefSectionSchema) }).optional(),
6471
+ issue: z5.object({ sections: z5.array(BriefSectionSchema) }).optional(),
6472
+ milestone: z5.object({ sections: z5.array(BriefSectionSchema) }).optional(),
6473
+ ack: z5.array(z5.enum(BRIEF_BUILTINS)).optional()
5707
6474
  });
5708
6475
  function isSafeRepoRelPath(p) {
5709
6476
  if (p.length === 0)
@@ -5712,7 +6479,7 @@ function isSafeRepoRelPath(p) {
5712
6479
  return false;
5713
6480
  return p.split(/[\\/]/).every((seg) => seg !== ".." && seg !== "");
5714
6481
  }
5715
- var SafeRepoRelPath = z3.string().refine(isSafeRepoRelPath, {
6482
+ var SafeRepoRelPath = z5.string().refine(isSafeRepoRelPath, {
5716
6483
  message: "must be a repo-root-relative path with no `..` segment or absolute root"
5717
6484
  });
5718
6485
  var CANONICAL_HOOK_BLOCK_PREFIXES = [".git/", ".husky/", ".vinaya/hooks/", ".claude/hooks/"];
@@ -5722,22 +6489,22 @@ function isCanonicalHookBlockPath(p) {
5722
6489
  var ManagedHookBlockPath = SafeRepoRelPath.refine(isCanonicalHookBlockPath, {
5723
6490
  message: `must start with one of ${CANONICAL_HOOK_BLOCK_PREFIXES.join(", ")} (byte-exact, case-sensitive)`
5724
6491
  });
5725
- var ManagedBlockRecordSchema = z3.object({
6492
+ var ManagedBlockRecordSchema = z5.object({
5726
6493
  path: ManagedHookBlockPath,
5727
- marker: z3.string(),
5728
- comment: z3.enum(["hash", "html"])
6494
+ marker: z5.string(),
6495
+ comment: z5.enum(["hash", "html"])
5729
6496
  });
5730
- var ManagedManifestSchema = z3.object({
5731
- version: z3.number().int().positive(),
5732
- files: z3.array(SafeRepoRelPath),
5733
- blocks: z3.array(ManagedBlockRecordSchema),
5734
- labels: z3.array(z3.string()),
5735
- agents: z3.array(z3.enum(AGENT_VENDORS)).optional()
6497
+ var ManagedManifestSchema = z5.object({
6498
+ version: z5.number().int().positive(),
6499
+ files: z5.array(SafeRepoRelPath),
6500
+ blocks: z5.array(ManagedBlockRecordSchema),
6501
+ labels: z5.array(z5.string()),
6502
+ agents: z5.array(z5.enum(AGENT_VENDORS)).optional()
5736
6503
  });
5737
- var ProjectEntrySchema = z3.object({
5738
- name: z3.string().min(1),
5739
- description: z3.string().min(1).optional(),
5740
- path: z3.string().min(1).optional()
6504
+ var ProjectEntrySchema = z5.object({
6505
+ name: z5.string().min(1),
6506
+ description: z5.string().min(1).optional(),
6507
+ path: z5.string().min(1).optional()
5741
6508
  });
5742
6509
  function parseTokensCollectDeclaration(value) {
5743
6510
  const m = value.trim().match(/^(\S+)\s+(\S+)$/);
@@ -5748,34 +6515,55 @@ function parseTokensCollectDeclaration(value) {
5748
6515
  return null;
5749
6516
  return { interpreter, script };
5750
6517
  }
5751
- var VinayaConfigSchema = z3.object({
5752
- rings: z3.object({
5753
- ring1_forgeWriteInterception: z3.boolean(),
5754
- ring2_asyncAudits: z3.boolean()
6518
+ var VinayaConfigSchema = z5.object({
6519
+ rings: z5.object({
6520
+ ring1_forgeWriteInterception: z5.boolean(),
6521
+ ring2_asyncAudits: z5.boolean()
5755
6522
  }).optional(),
5756
- checks: z3.record(z3.string(), CheckEntrySchema).optional(),
5757
- roles: z3.record(z3.string(), RoleEntrySchema).optional(),
6523
+ checks: z5.record(z5.string(), CheckEntrySchema).optional(),
6524
+ roles: z5.record(z5.string(), RoleEntrySchema).optional(),
5758
6525
  briefSchema: BriefSchemaSchema.optional(),
5759
6526
  managed: ManagedManifestSchema.optional(),
5760
- principals: z3.array(z3.string()).min(1).optional(),
5761
- releaseActor: z3.string().min(1).optional(),
5762
- ci: z3.object({ setup: z3.string().min(1) }).optional(),
5763
- tokens: z3.object({
5764
- collect: z3.string().min(1).refine((v) => parseTokensCollectDeclaration(v) !== null, {
6527
+ principals: z5.array(z5.string()).min(1).optional(),
6528
+ releaseActor: z5.string().min(1).optional(),
6529
+ ci: z5.object({ setup: z5.string().min(1) }).optional(),
6530
+ tokens: z5.object({
6531
+ collect: z5.string().min(1).refine((v) => parseTokensCollectDeclaration(v) !== null, {
5765
6532
  message: 'tokens.collect must be exactly "<interpreter> <repo-relative-script-path>" — two whitespace-separated tokens, no flags, no shell syntax, no quoting'
5766
6533
  })
5767
6534
  }).optional(),
5768
- blastRadius: z3.object({ extraDomains: z3.array(z3.string()).optional() }).optional(),
5769
- proseGates: z3.object({
5770
- doctrineRoot: z3.string().min(1).optional(),
5771
- readerFacingPrefix: z3.string().min(1).optional(),
5772
- readerFacingSuffix: z3.string().min(1).optional(),
5773
- legacySlugDir: z3.string().min(1).optional()
6535
+ blastRadius: z5.object({ extraDomains: z5.array(z5.string()).optional() }).optional(),
6536
+ proseGates: z5.object({
6537
+ doctrineRoot: z5.string().min(1).optional(),
6538
+ readerFacingPrefix: z5.string().min(1).optional(),
6539
+ readerFacingSuffix: z5.string().min(1).optional(),
6540
+ legacySlugDir: z5.string().min(1).optional()
6541
+ }).optional(),
6542
+ projects: z5.array(ProjectEntrySchema).optional(),
6543
+ dispatch: z5.object({
6544
+ timeoutMs: z5.number().int().positive().optional(),
6545
+ killGraceMs: z5.number().int().positive().optional(),
6546
+ agent: z5.enum(["claude", "codex", "gemini"]).optional()
6547
+ }).optional(),
6548
+ reviewPolicy: z5.object({
6549
+ codeReviewThreshold: z5.string().min(1).optional(),
6550
+ securityThreshold: z5.string().min(1).optional(),
6551
+ maxRounds: z5.number().optional()
5774
6552
  }).optional(),
5775
- projects: z3.array(ProjectEntrySchema).optional(),
5776
- dispatch: z3.object({
5777
- timeoutMs: z3.number().int().positive().optional(),
5778
- agent: z3.enum(["claude", "codex", "gemini"]).optional()
6553
+ prePush: z5.object({
6554
+ alwaysRun: z5.array(z5.string()).optional()
6555
+ }).optional(),
6556
+ report: z5.object({
6557
+ commandTimeoutMs: z5.number().int().positive().optional()
6558
+ }).superRefine((report, ctx) => {
6559
+ const commandTimeoutMs = report.commandTimeoutMs;
6560
+ if (commandTimeoutMs !== undefined && commandTimeoutMs > MAX_REPORT_COMMAND_TIMEOUT_MS) {
6561
+ ctx.addIssue({
6562
+ code: z5.ZodIssueCode.custom,
6563
+ path: ["commandTimeoutMs"],
6564
+ message: "must be at most 3600000 (1 hour)"
6565
+ });
6566
+ }
5779
6567
  }).optional()
5780
6568
  });
5781
6569
  var GLOBAL_VINAYA_HOME = join(homedir(), ".vinaya");
@@ -5850,6 +6638,28 @@ function resolvePrincipalAllowlist(config) {
5850
6638
  function resolveReleaseActor(config) {
5851
6639
  return config?.releaseActor ?? DEFAULT_RELEASE_ACTOR;
5852
6640
  }
6641
+ function resolveReviewPolicy(config) {
6642
+ const raw = config?.reviewPolicy;
6643
+ if (!raw)
6644
+ return DEFAULT_REVIEW_POLICY;
6645
+ const codeReviewThreshold = raw.codeReviewThreshold ?? DEFAULT_REVIEW_POLICY.codeReviewThreshold;
6646
+ if (!isKnownSeverity(CODE_REVIEW_SEVERITY_ORDER, codeReviewThreshold)) {
6647
+ throw new Error(`vinaya.config.json: reviewPolicy.codeReviewThreshold "${codeReviewThreshold}" is not one of ${CODE_REVIEW_SEVERITY_ORDER.join(" > ")} — fix the config, this never falls back to a default.`);
6648
+ }
6649
+ const securityThreshold = raw.securityThreshold ?? DEFAULT_REVIEW_POLICY.securityThreshold;
6650
+ if (!isKnownSeverity(SECURITY_SEVERITY_ORDER, securityThreshold)) {
6651
+ throw new Error(`vinaya.config.json: reviewPolicy.securityThreshold "${securityThreshold}" is not one of ${SECURITY_SEVERITY_ORDER.join(" > ")} — fix the config, this never falls back to a default.`);
6652
+ }
6653
+ const maxRounds = raw.maxRounds ?? DEFAULT_REVIEW_POLICY.maxRounds;
6654
+ if (!Number.isInteger(maxRounds) || maxRounds < 1) {
6655
+ throw new Error(`vinaya.config.json: reviewPolicy.maxRounds "${maxRounds}" is not a positive integer — fix the config, this never falls back to a default.`);
6656
+ }
6657
+ return {
6658
+ codeReviewThreshold,
6659
+ securityThreshold,
6660
+ maxRounds
6661
+ };
6662
+ }
5853
6663
  function trustAnchorRepo() {
5854
6664
  const wellFormed = (slug) => /^[^/\s]+\/[^/\s]+$/.test(slug) ? slug : null;
5855
6665
  const fromRunner = process.env.GITHUB_REPOSITORY?.trim();
@@ -5995,38 +6805,71 @@ function taskToEntry(entries, slug) {
5995
6805
  m.set(`${slug}/${e.task.id}`, e);
5996
6806
  return m;
5997
6807
  }
5998
- function recoveryPromptFor(checkCode, detail = "") {
5999
- if (detail.includes("INTERNAL:")) {
6000
- return "This is an INTERNAL parser-bug report, not a real coherence failure — a task cannot depend on itself, so there is nothing here to close or resolve. Do NOT work around it. Report it upstream, and re-run once the rationale text is corrected or the parser fix ships.";
6001
- }
6002
- switch (checkCode) {
6003
- case "A1":
6808
+ function recoveryPromptFor(code) {
6809
+ switch (code) {
6810
+ case "d1-self-dependency":
6811
+ return "This is an INTERNAL parser-bug report, not a real coherence failure — a task cannot depend on itself, so there is nothing here to close or resolve. Do NOT work around it. Report it upstream, and re-run once the rationale text is corrected or the parser fix ships.";
6812
+ case "closed-without-merge":
6004
6813
  return "The task's Issue is closed but its closing PR is not merged. Verify the PR actually merged (or reopen the Issue if it was closed in error), then re-run `vinaya check coherence`.";
6005
- case "A3":
6814
+ case "archived-without-provenance":
6815
+ return "The closing PR merged but carries no `### AEG provenance` comment. Ask the Archivist to post it, then re-run `vinaya check coherence`.";
6816
+ case "auto-close-misfire":
6006
6817
  return "The closing PR merged but the Issue is still open (a GitHub auto-close misfire). Manually close the Issue, then re-run `vinaya check coherence`.";
6007
- case "T1":
6818
+ case "phantom-issue-ref":
6008
6819
  return "The topology names an Issue number that doesn't resolve on the forge. Fix the Issue number in the topology, or ask the Planner to re-cut it, then re-run `vinaya check coherence`.";
6009
- case "T2":
6820
+ case "orphan-task":
6010
6821
  return "An open Issue under this tranche's label is missing from the topology. Add its row to the tranche's task list, then re-run `vinaya check coherence`.";
6011
- case "T3":
6822
+ case "tbd-in-active-tranche":
6012
6823
  return "A task in this active tranche has no Issue (#TBD). Ask the Planner to cut the Issue, then re-run `vinaya check coherence`.";
6013
- case "D1":
6824
+ case "dispatched-on-unmet-deps":
6014
6825
  return "This task has an open PR but a declared dependency isn't closed. Close the dependency first (or verify it truly is), then re-run `vinaya check coherence`.";
6015
- case "R1":
6826
+ case "missing-rationale-field":
6016
6827
  return "The Issue fails the rationale gate. Ask the Planner to complete the eight-field rationale on the Issue body, then re-run `vinaya check coherence`.";
6828
+ case "surface-excludes-bound-doc":
6829
+ return "The Issue's Surface `out:` list excludes a doc-owners-bound document its `in:` list otherwise covers. Fix the Surface split, or update the bound doc in the same PR, then re-run `vinaya check coherence`.";
6830
+ case "surface-overlap":
6831
+ return "This Issue overlaps another open task Issue sharing the same Milestone. Resolve the overlap (split scope or serialize the tasks), then re-run `vinaya check coherence`.";
6832
+ case "archive-recommended":
6833
+ return "This active tranche has no open task-Issues left (advisory). Archive it to `completed/`, or confirm it should stay open, then re-run `vinaya check coherence`.";
6834
+ case "premature-archive":
6835
+ return "This archived tranche still has an open task-Issue (advisory). Investigate whether the archive was premature, then re-run `vinaya check coherence`.";
6836
+ case "milestone-drift":
6837
+ return "This Issue's GitHub-native milestone doesn't match its `vinaya/tranche:` label (advisory, cosmetic). Re-attach the Issue to the correct Milestone, then re-run `vinaya check coherence`.";
6838
+ case "tranche-not-archived":
6839
+ return "Every task Issue in this tranche is closed but it was never archived. Ask the Tranche Archivist to run, then re-run `vinaya check coherence`.";
6840
+ case "doc-owners-dangling-pointer":
6841
+ return "A `.vinaya/doc-owners` entry points at a file that no longer exists in-repo. Fix or remove the dangling entry, then re-run `vinaya check coherence`.";
6842
+ case "doc-owners-duplicate-glob":
6843
+ return "A `.vinaya/doc-owners` glob is registered more than once. Remove the duplicate entry, then re-run `vinaya check coherence`.";
6844
+ case "forge-read-unavailable":
6845
+ return "A forge read failed while assembling coherence facts (severity:infra) — this is an outage, not a drift finding. Confirm `gh auth status` passes and the forge is reachable, then re-run `vinaya check coherence`.";
6017
6846
  default:
6018
- return "Read the named coherence failure and resolve the underlying forge/topology drift it names, then re-run `vinaya check coherence`.";
6847
+ return assertNeverCoherenceCode(code);
6019
6848
  }
6020
6849
  }
6850
+ function assertNeverCoherenceCode(x) {
6851
+ throw new Error(`Unhandled CoherenceFailureCode: ${JSON.stringify(x)}`);
6852
+ }
6021
6853
  function emitFailure(result) {
6022
- const detail = result.failures.map((f) => f.reason).join(" | ") || result.note || "see check output";
6023
- emitCheckError({
6024
- schema: CHECK_SCHEMA_VERSION,
6025
- check: CHECK_NAME,
6026
- severity: "error",
6027
- message: `${result.check}: ${detail}`,
6028
- agent_recovery_prompt: recoveryPromptFor(result.check, detail)
6029
- });
6854
+ if (result.failures.length === 0) {
6855
+ emitCheckError({
6856
+ schema: CHECK_SCHEMA_VERSION,
6857
+ check: CHECK_NAME,
6858
+ severity: "error",
6859
+ message: `${result.check}: ${result.note ?? "see check output"}`,
6860
+ agent_recovery_prompt: "Read the named coherence failure and resolve the underlying forge/topology drift it names, then re-run `vinaya check coherence`."
6861
+ });
6862
+ return;
6863
+ }
6864
+ for (const failure of result.failures) {
6865
+ emitCheckError({
6866
+ schema: CHECK_SCHEMA_VERSION,
6867
+ check: CHECK_NAME,
6868
+ severity: "error",
6869
+ message: `${result.check}: ${failure.reason}`,
6870
+ agent_recovery_prompt: recoveryPromptFor(failure.code)
6871
+ });
6872
+ }
6030
6873
  }
6031
6874
  async function main() {
6032
6875
  const slug = currentTrancheSlug();