@attalabs/vinaya 0.27.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 (48) hide show
  1. package/README.md +4 -3
  2. package/aeg-root/enforcement.md +4 -4
  3. package/aeg-root/roles/developer.md +23 -17
  4. package/aeg-root/roles/planner.md +2 -0
  5. package/aeg-root/roles/principal.md +4 -0
  6. package/aeg-root/roles/reviewer.md +3 -3
  7. package/aeg-root/roles/security.md +3 -3
  8. package/dist/checks/bin/check-body-bare-digits.js +915 -240
  9. package/dist/checks/bin/check-branch-topology.js +820 -180
  10. package/dist/checks/bin/check-brief-shape.js +915 -240
  11. package/dist/checks/bin/check-changeset-coverage.js +1534 -262
  12. package/dist/checks/bin/check-closes-n.js +824 -184
  13. package/dist/checks/bin/check-coherence.js +984 -265
  14. package/dist/checks/bin/check-dead-branch-push.js +805 -176
  15. package/dist/checks/bin/check-dispatch-readiness.js +1046 -276
  16. package/dist/checks/bin/check-doc-coverage-push.js +1530 -258
  17. package/dist/checks/bin/check-doc-coverage.js +1532 -260
  18. package/dist/checks/bin/check-doctrine-no-procedures.js +915 -240
  19. package/dist/checks/bin/check-doctrine-portability.js +1529 -257
  20. package/dist/checks/bin/check-evidence-fresh.js +1875 -258
  21. package/dist/checks/bin/check-exec-bits.js +1527 -255
  22. package/dist/checks/bin/check-first-push-dispatch.js +932 -246
  23. package/dist/checks/bin/check-issue-assignment.js +822 -182
  24. package/dist/checks/bin/check-issue-milestone-attach.js +5734 -0
  25. package/dist/checks/bin/check-issue-objectives-numbering.js +5736 -0
  26. package/dist/checks/bin/check-issue-parts-coverage.js +5736 -0
  27. package/dist/checks/bin/check-issue-surface-globs.js +6910 -0
  28. package/dist/checks/bin/check-issue-title-grammar.js +5736 -0
  29. package/dist/checks/bin/check-issue-tranche-label.js +5736 -0
  30. package/dist/checks/bin/check-main-branch-refusal.js +805 -176
  31. package/dist/checks/bin/check-no-disk-state.js +805 -176
  32. package/dist/checks/bin/check-pr-premise-reassert.js +915 -240
  33. package/dist/checks/bin/check-pr-report-density.js +805 -176
  34. package/dist/checks/bin/check-quoted-command.js +1508 -255
  35. package/dist/checks/bin/check-reader-resolvable-prose.js +1512 -259
  36. package/dist/checks/bin/check-registry-gates.js +892 -176
  37. package/dist/checks/bin/check-retired-vocabulary.js +1506 -253
  38. package/dist/checks/bin/check-review-gate.js +1000 -245
  39. package/dist/checks/bin/check-single-plan-pr.js +805 -176
  40. package/dist/checks/bin/check-surface-scope.js +830 -182
  41. package/dist/checks/bin/check-test-plan.js +834 -189
  42. package/dist/checks/bin/check-token-collection-wired.js +805 -176
  43. package/dist/checks/bin/check-token-report.js +805 -176
  44. package/dist/checks/bin/check-workspace-escape.js +1525 -253
  45. package/dist/index.js +9835 -5597
  46. package/dist/lib/pre-push-changed-files.js +57 -0
  47. package/dist/lib/pre-push-select-tests.js +1473 -0
  48. package/package.json +4 -2
@@ -1954,6 +1954,8 @@ function parseInlineFieldList(section) {
1954
1954
  var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
1955
1955
  var OBJECTIVES_VERSION_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Objectives version:\s*([0-9a-f]{64})(?![A-Za-z0-9])/im;
1956
1956
  var RULING_ORDINAL_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Ruling ordinal:\s*(\d+)(?!\d)/im;
1957
+ var BRIEF_HASH_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Brief hash:\s*([0-9a-f]{64})(?![A-Za-z0-9])/im;
1958
+ var POLICY_DIGEST_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Policy digest:\s*([0-9a-f]{64})(?![A-Za-z0-9])/im;
1957
1959
  function firstFiveLines(comment) {
1958
1960
  return comment.split(`
1959
1961
  `).slice(0, 5).join(`
@@ -1976,6 +1978,26 @@ function extractRulingOrdinal(comment) {
1976
1978
  const m = firstSevenLines(comment).match(RULING_ORDINAL_PATTERN);
1977
1979
  return m ? Number.parseInt(m[1], 10) : null;
1978
1980
  }
1981
+ function firstElevenLines(comment) {
1982
+ return comment.split(`
1983
+ `).slice(0, 11).join(`
1984
+ `);
1985
+ }
1986
+ function extractBriefHash(comment) {
1987
+ const m = firstElevenLines(comment).match(BRIEF_HASH_PATTERN);
1988
+ return m ? m[1].toLowerCase() : null;
1989
+ }
1990
+ function extractPolicyDigest(comment) {
1991
+ const m = firstElevenLines(comment).match(POLICY_DIGEST_PATTERN);
1992
+ return m ? m[1].toLowerCase() : null;
1993
+ }
1994
+ var FINDING_SEVERITY_LINE = /^\d+\.\s+\[([A-Z][A-Z]*)\]\s+(.+?)\s+—/gm;
1995
+ function extractFindingSeverities(comment) {
1996
+ return [...comment.matchAll(FINDING_SEVERITY_LINE)].map((m) => ({
1997
+ severity: m[1],
1998
+ location: m[2]
1999
+ }));
2000
+ }
1979
2001
  function extractVerdict(comments, valuePattern, missingLabel) {
1980
2002
  const candidates = comments.filter((c) => valuePattern.test(c));
1981
2003
  if (candidates.length === 0) {
@@ -1984,6 +2006,9 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1984
2006
  headSha: null,
1985
2007
  objectivesVersion: null,
1986
2008
  rulingOrdinal: null,
2009
+ briefHash: null,
2010
+ policyDigest: null,
2011
+ findingSeverities: [],
1987
2012
  danglingNote: `no ${missingLabel} verdict comment found on this PR`
1988
2013
  };
1989
2014
  }
@@ -1995,6 +2020,9 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1995
2020
  headSha: null,
1996
2021
  objectivesVersion: null,
1997
2022
  rulingOrdinal: null,
2023
+ briefHash: null,
2024
+ policyDigest: null,
2025
+ findingSeverities: [],
1998
2026
  danglingNote: `the most recent ${missingLabel} verdict comment carries a VERDICT-shaped line outside the first-five-line read window`
1999
2027
  };
2000
2028
  }
@@ -2003,6 +2031,9 @@ function extractVerdict(comments, valuePattern, missingLabel) {
2003
2031
  headSha: extractHeadSha(latest),
2004
2032
  objectivesVersion: extractObjectivesVersion(latest),
2005
2033
  rulingOrdinal: extractRulingOrdinal(latest),
2034
+ briefHash: extractBriefHash(latest),
2035
+ policyDigest: extractPolicyDigest(latest),
2036
+ findingSeverities: extractFindingSeverities(latest),
2006
2037
  danglingNote: null
2007
2038
  };
2008
2039
  }
@@ -2338,6 +2369,53 @@ function evaluateC5(changed, docOwnersContent, prBody, fileExists, waiverActive,
2338
2369
  }
2339
2370
  return out;
2340
2371
  }
2372
+ // ../../packages/aeg-core/src/review-policy.ts
2373
+ var CODE_REVIEW_SEVERITY_ORDER = ["BLOCKER", "MAJOR", "MINOR"];
2374
+ var SECURITY_SEVERITY_ORDER = ["CRITICAL", "HIGH", "MEDIUM", "LOW"];
2375
+ var DEFAULT_MAX_ROUNDS = 3;
2376
+ var DEFAULT_REVIEW_POLICY = {
2377
+ codeReviewThreshold: "BLOCKER",
2378
+ securityThreshold: "HIGH",
2379
+ maxRounds: DEFAULT_MAX_ROUNDS
2380
+ };
2381
+ var FILE_SHAPED_LOCATION = /\.[a-zA-Z0-9]{1,10}(:\d+)?\s*$/;
2382
+ var PROSE_LOCATION_PATTERNS = [/\bpr\s*body\b/i, /\bcomment\b/i];
2383
+ var ROLE_FILE_LOCATION = /(^|\/)aeg-root\/roles\//i;
2384
+ function isProseLocation(location) {
2385
+ if (ROLE_FILE_LOCATION.test(location))
2386
+ return true;
2387
+ if (FILE_SHAPED_LOCATION.test(location))
2388
+ return false;
2389
+ return PROSE_LOCATION_PATTERNS.some((pattern) => pattern.test(location));
2390
+ }
2391
+ var PROSE_CAP_SEVERITY = "MINOR";
2392
+ function blockingSeverities(scale, threshold) {
2393
+ const idx = scale.indexOf(threshold);
2394
+ if (idx === -1) {
2395
+ throw new Error(`blockingSeverities: threshold "${threshold}" is not one of ${scale.join(" > ")}`);
2396
+ }
2397
+ return scale.slice(0, idx + 1);
2398
+ }
2399
+ function evaluateReviewFindings(findings, scale, threshold) {
2400
+ const blocking = new Set(blockingSeverities(scale, threshold));
2401
+ const blockingFindings = findings.filter((f) => {
2402
+ if (!scale.includes(f.severity)) {
2403
+ throw new Error(`evaluateReviewFindings: severity "${f.severity}" is not one of ${scale.join(" > ")}`);
2404
+ }
2405
+ const effectiveSeverity = f.location !== undefined && isProseLocation(f.location) ? PROSE_CAP_SEVERITY : f.severity;
2406
+ return blocking.has(effectiveSeverity);
2407
+ });
2408
+ return { outcome: blockingFindings.length > 0 ? "blocked" : "clean", blockingFindings };
2409
+ }
2410
+ function evaluateCodeReview(findings, policy) {
2411
+ return evaluateReviewFindings(findings, CODE_REVIEW_SEVERITY_ORDER, policy.codeReviewThreshold);
2412
+ }
2413
+ function evaluateSecurityReview(findings, policy) {
2414
+ return evaluateReviewFindings(findings, SECURITY_SEVERITY_ORDER, policy.securityThreshold);
2415
+ }
2416
+ function isKnownSeverity(scale, value) {
2417
+ return scale.includes(value);
2418
+ }
2341
2419
  // ../../packages/aeg-core/src/pr-tier.ts
2342
2420
  var TIER_FIELD = /(\*\*)?\s*Tier\s*(\*\*)?\s*:\s*(\*\*)?\s*([013])\b/i;
2343
2421
  function readTierFromPrBody(prBody) {
@@ -2708,7 +2786,7 @@ function checkForField(prBody) {
2708
2786
  ]
2709
2787
  };
2710
2788
  }
2711
- function checkClosesN(prBody) {
2789
+ function checkClosesNPresence(prBody) {
2712
2790
  const closesPattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s{0,8}:?\s{0,8}#\d+/i;
2713
2791
  if (closesPattern.test(stripCode(prBody))) {
2714
2792
  return { status: "pass", errors: [] };
@@ -2740,10 +2818,22 @@ var COMMIT_TYPES = [
2740
2818
  "Test"
2741
2819
  ];
2742
2820
  var COMMIT_TYPE_STYLE = new RegExp(`^(${COMMIT_TYPES.join("|")})(\\([a-z0-9-]+\\))?: \\S`);
2821
+ function checkForgeTitle(title) {
2822
+ const taskStyle = /^\[[a-z0-9._-]+\] \S+ — \S/;
2823
+ if (COMMIT_TYPE_STYLE.test(title) || taskStyle.test(title))
2824
+ return { status: "pass", errors: [] };
2825
+ return {
2826
+ status: "fail",
2827
+ errors: [
2828
+ `brief-validation title: "${title}" matches neither title grammar — expected \`Type: description\` / \`Type(scope): description\` (commitlint types + Plan) or \`[tranche] id — description\` (task form).`
2829
+ ]
2830
+ };
2831
+ }
2743
2832
  var BRIEF_SHAPE_MARKERS = [checkSurfaceMap, checkDocUpdateList, checkStopConditions, checkAutonomyClause];
2744
2833
  var TASK_BRANCH_PATTERN = /^task\/[^/]+\/[^/]+$/;
2834
+ var TASK_ISSUE_BRANCH_PATTERN = /^task\/issue-\d+$/;
2745
2835
  function isTaskBranch(branch) {
2746
- return TASK_BRANCH_PATTERN.test(branch);
2836
+ return TASK_BRANCH_PATTERN.test(branch) || TASK_ISSUE_BRANCH_PATTERN.test(branch);
2747
2837
  }
2748
2838
  function isBriefShaped(prBody) {
2749
2839
  const stripped = stripCode(prBody);
@@ -2865,8 +2955,9 @@ function packagesNamedIn(text) {
2865
2955
  }
2866
2956
  function hasTestPathForConsumer(text, consumerDir) {
2867
2957
  const escaped = consumerDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2868
- const re = new RegExp(`${escaped}\\/[\\w./-]*\\.test\\.[A-Za-z0-9]+`);
2869
- return re.test(text);
2958
+ const filePathRe = new RegExp(`${escaped}\\/[\\w./-]*\\.test\\.[A-Za-z0-9]+`);
2959
+ const testDirRe = new RegExp(`${escaped}\\/(?:[\\w-]+\\/)*(?:tests|specs)(?:\\/|\\b)`);
2960
+ return filePathRe.test(text) || testDirRe.test(text);
2870
2961
  }
2871
2962
  function checkConsumerTests(prBody, consumersOf) {
2872
2963
  const section4 = extractNumberedSection(prBody, 4);
@@ -2970,7 +3061,7 @@ function checkBriefSections(prBody, readTier, options = {}) {
2970
3061
  checkConsumerTests(prBody, consumersOf),
2971
3062
  checkDefeatCases(prBody),
2972
3063
  ...issueObjectives !== undefined ? [checkObjectivesCopy(prBody, issueObjectives), checkObjectivesCoverage(prBody)] : [],
2973
- ...requireClosesN ? [checkClosesN(prBody)] : []
3064
+ ...requireClosesN ? [checkClosesNPresence(prBody)] : []
2974
3065
  ];
2975
3066
  return { errors: results.flatMap((r) => r.errors) };
2976
3067
  }
@@ -3260,6 +3351,28 @@ function topLevelSectionText(body, headingName) {
3260
3351
  const next = /^##[ \t]/m.exec(afterHeading);
3261
3352
  return next ? afterHeading.slice(0, next.index) : afterHeading;
3262
3353
  }
3354
+ function partHasBacktickedPath(text) {
3355
+ let i = 0;
3356
+ while (i < text.length) {
3357
+ const start = text.indexOf("`", i);
3358
+ if (start === -1)
3359
+ return false;
3360
+ const end = text.indexOf("`", start + 1);
3361
+ if (end === -1)
3362
+ return false;
3363
+ if (text.slice(start + 1, end).includes("/"))
3364
+ return true;
3365
+ i = end + 1;
3366
+ }
3367
+ return false;
3368
+ }
3369
+ var PART_MIN_WORDS_OUTSIDE_BACKTICKS = 3;
3370
+ function stripPartBackticks(text) {
3371
+ return text.replace(/`[^`\n]*`/g, " ");
3372
+ }
3373
+ function partWordCount(text) {
3374
+ return text.split(/\s+/).filter((w) => /[a-z]/i.test(w)).length;
3375
+ }
3263
3376
  function looksLikeFilePath(entry2) {
3264
3377
  const stripped = entry2.replace(/\/\*\*?$/, "");
3265
3378
  const lastSegment = stripped.split("/").pop() ?? stripped;
@@ -3300,6 +3413,18 @@ function globCoversPath(glob, path) {
3300
3413
  const p = path.replace(/\/+$/, "");
3301
3414
  return g === p || g.startsWith(`${p}/`) || p.startsWith(`${g}/`);
3302
3415
  }
3416
+ function checkSurfaceGlobsResolve(body, resolvesToFile) {
3417
+ const surface = parseIssueSurface(body);
3418
+ if (!surface.ok)
3419
+ return { status: "pass", errors: [] };
3420
+ const errors = [];
3421
+ for (const glob of surface.value.in) {
3422
+ if (!resolvesToFile(glob)) {
3423
+ errors.push(`issue-validation Surface: \`${glob}\` in \`## Surface\`'s \`in:\` list matches no tracked file — a Surface that cannot resolve cannot render a brief's file list.`);
3424
+ }
3425
+ }
3426
+ return { status: errors.length > 0 ? "fail" : "pass", errors };
3427
+ }
3303
3428
  function checkSurfaceScope(changedFiles, outGlobs) {
3304
3429
  if (outGlobs.length === 0)
3305
3430
  return { ok: true };
@@ -3311,9 +3436,77 @@ function checkSurfaceScope(changedFiles, outGlobs) {
3311
3436
  }
3312
3437
  return violations.length > 0 ? { ok: false, violations } : { ok: true };
3313
3438
  }
3439
+ var ISSUE_PART_LINE_RE = /^Part\s+(\d+)\s*\(([^)]*)\)\s*[-—–]\s*(.*)$/i;
3440
+ function parseIssueParts(body) {
3441
+ const section = topLevelSectionText(body, "Parts");
3442
+ if (section === null)
3443
+ return { ok: false, errors: ["no `## Parts` heading found in the body."] };
3444
+ const lines = section.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
3445
+ const parts = [];
3446
+ const errors = [];
3447
+ for (const line of lines) {
3448
+ const m = ISSUE_PART_LINE_RE.exec(line);
3449
+ if (!m) {
3450
+ errors.push(`"${line}" is not a well-formed Parts line — expected \`Part <n> (<refs>) — <outcome>\`.`);
3451
+ continue;
3452
+ }
3453
+ const n = Number.parseInt(m[1], 10);
3454
+ const refs = m[2];
3455
+ const text = m[3].trim();
3456
+ if (text.length === 0) {
3457
+ errors.push(`Part ${n} has no outcome text after the dash — every Part states one observable outcome.`);
3458
+ continue;
3459
+ }
3460
+ if (partHasBacktickedPath(text) && partWordCount(stripPartBackticks(text)) < PART_MIN_WORDS_OUTSIDE_BACKTICKS) {
3461
+ errors.push(`Part ${n} is little more than a file path — a Part names an outcome and symbols, never a bare path.`);
3462
+ continue;
3463
+ }
3464
+ const objectiveIds = [...refs.matchAll(/O(\d+)/g)].map((r) => Number.parseInt(r[1], 10));
3465
+ parts.push({ n, objectiveIds, text });
3466
+ }
3467
+ if (parts.length === 0) {
3468
+ errors.push("the `## Parts` section has no well-formed `Part <n> (<refs>) — <outcome>` lines.");
3469
+ }
3470
+ if (errors.length > 0)
3471
+ return { ok: false, errors };
3472
+ return { ok: true, value: parts };
3473
+ }
3474
+ function checkPartsCiteDefinedObjectives(body) {
3475
+ const parts = parseIssueParts(body);
3476
+ const objectives = objectivesOf(body);
3477
+ if (!parts.ok || !objectives.ok)
3478
+ return { status: "pass", errors: [] };
3479
+ const definedIds = new Set(objectives.objectives.map((o) => Number.parseInt(o.id.slice(1), 10)));
3480
+ const errors = [];
3481
+ for (const part of parts.value) {
3482
+ for (const objectiveId of part.objectiveIds) {
3483
+ if (!definedIds.has(objectiveId)) {
3484
+ errors.push(`issue-validation Parts: Part ${part.n} cites O${objectiveId}, which the Issue's own \`## Objectives\` section does not define.`);
3485
+ }
3486
+ }
3487
+ }
3488
+ return { status: errors.length > 0 ? "fail" : "pass", errors };
3489
+ }
3314
3490
  function isTaskIssueLabelSet(labels) {
3315
3491
  return hasLabel("tranche", labels);
3316
3492
  }
3493
+ function checkTrancheLabelPresence(_body, _labels) {
3494
+ return { status: "pass", errors: [] };
3495
+ }
3496
+ function checkMilestoneAttach(labels, currentMilestoneTitle, resolvedMilestoneTitle) {
3497
+ if (!isTaskIssueLabelSet(labels))
3498
+ return { status: "pass", errors: [] };
3499
+ if (resolvedMilestoneTitle === null)
3500
+ return { status: "pass", errors: [] };
3501
+ if (currentMilestoneTitle === resolvedMilestoneTitle)
3502
+ return { status: "pass", errors: [] };
3503
+ return {
3504
+ status: "fail",
3505
+ errors: [
3506
+ `issue-validation milestone attach: this task Issue's tranche label resolves to Milestone "${resolvedMilestoneTitle}", but its live Milestone is ${currentMilestoneTitle === null ? "unset" : `"${currentMilestoneTitle}"`} — attach it to "${resolvedMilestoneTitle}".`
3507
+ ]
3508
+ };
3509
+ }
3317
3510
  var TYPE_LABEL_IDS = LABELS.filter((l) => l.category === "type").map((l) => l.id);
3318
3511
  function isControlCodePoint(codePoint) {
3319
3512
  return codePoint <= 31 || codePoint >= 127 && codePoint <= 159;
@@ -3378,6 +3571,19 @@ function checkProjectsRegistered(body, _labels, registeredNames) {
3378
3571
  var DOC_PATH_RE = /(?:(?:aeg-root|apps|packages|specs|docs|tools|\.claude|\.github)\/[\w./@-]*\.(?:md|mdx)|\.claude\/(?:skills|rules)\/[\w./-]+|\b(?:docs-index|decisions|projects|state-machine|enforcement|process|README|CLAUDE)\.md\b|\b[\w-]+-(?:spec|decisions|backlog)\.md\b)/i;
3379
3572
  var DOC_PATH_RE_GLOBAL = new RegExp(DOC_PATH_RE.source, "gi");
3380
3573
 
3574
+ // ../../packages/aeg-core/src/task-branch-identity.ts
3575
+ var ISSUE_BRANCH_PATTERN = /^task\/issue-(\d+)$/;
3576
+ var TRANCHE_BRANCH_PATTERN = /^task\/([^/]+)\/([^/]+)$/;
3577
+ function parseTaskBranchIdentity(branch) {
3578
+ const issueMatch = ISSUE_BRANCH_PATTERN.exec(branch);
3579
+ if (issueMatch)
3580
+ return { kind: "issue", issueNumber: Number(issueMatch[1]) };
3581
+ const trancheMatch = TRANCHE_BRANCH_PATTERN.exec(branch);
3582
+ if (trancheMatch)
3583
+ return { kind: "tranche", tranche: trancheMatch[1], taskId: trancheMatch[2] };
3584
+ return null;
3585
+ }
3586
+
3381
3587
  // ../../packages/aeg-core/src/coherence-checks.ts
3382
3588
  var COHERENCE_ENFORCED_FROM = "2026-07-01";
3383
3589
  function isGrandfathered(isoDate) {
@@ -3398,6 +3604,7 @@ function checkA1(entries, principalAllowlist = PRINCIPAL_ALLOWLIST) {
3398
3604
  if (handClosed)
3399
3605
  continue;
3400
3606
  failures.push({
3607
+ code: "closed-without-merge",
3401
3608
  issue: e.task.issue,
3402
3609
  tranche: e.trancheSlug,
3403
3610
  task: e.task.id,
@@ -3422,6 +3629,7 @@ function checkA3(entries) {
3422
3629
  continue;
3423
3630
  if (e.facts.prState === "merged" && e.facts.issueState !== "closed") {
3424
3631
  failures.push({
3632
+ code: "auto-close-misfire",
3425
3633
  issue: e.task.issue,
3426
3634
  tranche: e.trancheSlug,
3427
3635
  task: e.task.id,
@@ -3446,6 +3654,7 @@ function checkT1(entries) {
3446
3654
  continue;
3447
3655
  if (e.facts === undefined) {
3448
3656
  failures.push({
3657
+ code: "phantom-issue-ref",
3449
3658
  issue: e.task.issue,
3450
3659
  tranche: e.trancheSlug,
3451
3660
  task: e.task.id,
@@ -3464,6 +3673,7 @@ function checkT2(openIssuesBySlug, topologyIssuesBySlug, ciTrancheSlug) {
3464
3673
  for (const num of openNums) {
3465
3674
  if (!topologySet.has(num)) {
3466
3675
  failures.push({
3676
+ code: "orphan-task",
3467
3677
  issue: num,
3468
3678
  tranche: slug,
3469
3679
  reason: `Issue #${num} is open and labeled ${trancheLabel(slug)} but does not appear in the topology file`
@@ -3498,6 +3708,7 @@ function checkT3(entries, ciTrancheSlug, enrichedEntries, forgeUnavailableSlugs)
3498
3708
  continue;
3499
3709
  if (forgeUnavailableSlugs?.has(e.trancheSlug)) {
3500
3710
  failures.push({
3711
+ code: "tbd-in-active-tranche",
3501
3712
  issue: null,
3502
3713
  tranche: e.trancheSlug,
3503
3714
  task: e.task.id,
@@ -3507,6 +3718,7 @@ function checkT3(entries, ciTrancheSlug, enrichedEntries, forgeUnavailableSlugs)
3507
3718
  continue;
3508
3719
  }
3509
3720
  failures.push({
3721
+ code: "tbd-in-active-tranche",
3510
3722
  issue: null,
3511
3723
  tranche: e.trancheSlug,
3512
3724
  task: e.task.id,
@@ -3539,6 +3751,7 @@ function checkD1(entries, issueToEntry, taskToEntry) {
3539
3751
  const sameIssue = depEntry.task.issue !== null && e.task.issue !== null && depEntry.task.issue === e.task.issue;
3540
3752
  if (sameTask || sameIssue) {
3541
3753
  failures.push({
3754
+ code: "d1-self-dependency",
3542
3755
  issue: e.task.issue,
3543
3756
  tranche: e.trancheSlug,
3544
3757
  task: e.task.id,
@@ -3550,6 +3763,7 @@ function checkD1(entries, issueToEntry, taskToEntry) {
3550
3763
  const depClosed = depFacts?.issueState === "closed";
3551
3764
  if (!depClosed) {
3552
3765
  failures.push({
3766
+ code: "dispatched-on-unmet-deps",
3553
3767
  issue: e.task.issue,
3554
3768
  tranche: e.trancheSlug,
3555
3769
  task: e.task.id,
@@ -3575,11 +3789,13 @@ function checkR1(issuesBySlug, grandfatheredIssues, registeredNames = []) {
3575
3789
  const errors = [
3576
3790
  ...checkIssueRationale(issue.body).errors,
3577
3791
  ...checkProjectsRegistered(issue.body, issue.labels, registeredNames).errors,
3578
- ...checkIssueObjectives(issue.body, issue.number).errors
3792
+ ...checkIssueObjectives(issue.body, issue.number).errors,
3793
+ ...checkPartsCiteDefinedObjectives(issue.body).errors
3579
3794
  ];
3580
3795
  if (errors.length === 0)
3581
3796
  continue;
3582
3797
  failures.push({
3798
+ code: "missing-rationale-field",
3583
3799
  issue: issue.number,
3584
3800
  tranche: slug,
3585
3801
  reason: `Issue #${issue.number} fails the rationale gate: ${errors.join(" | ")}`,
@@ -3608,6 +3824,7 @@ function checkL1(files, entriesBySlug) {
3608
3824
  const allClosed = withFacts.every((e) => e.facts?.issueState === "closed");
3609
3825
  if (allClosed) {
3610
3826
  failures.push({
3827
+ code: "archive-recommended",
3611
3828
  tranche: f.slug,
3612
3829
  reason: "Active tranche has no open task-Issues — consider archiving to completed/"
3613
3830
  });
@@ -3639,27 +3856,38 @@ function extractClosesReferences(prBody) {
3639
3856
  }
3640
3857
  return referenced;
3641
3858
  }
3642
- function checkClosesN2(branch, prBody, trancheFiles, taskIssueRefs) {
3859
+ function checkClosesNTopology(branch, prBody, trancheFiles, taskIssueRefs) {
3643
3860
  const referenced = extractClosesReferences(prBody);
3644
3861
  if (taskIssueRefs) {
3645
3862
  for (const n of referenced) {
3646
- const ref = taskIssueRefs.get(n);
3647
- if (!ref)
3863
+ const ref2 = taskIssueRefs.get(n);
3864
+ if (!ref2)
3648
3865
  continue;
3649
- const expectedBranch = `task/${ref.trancheSlug}/${ref.taskId}`;
3866
+ const expectedBranch = `task/${ref2.trancheSlug}/${ref2.taskId}`;
3650
3867
  if (branch !== expectedBranch) {
3651
3868
  return {
3652
3869
  ok: false,
3653
- message: `closes-n-reverse: branch "${branch}" closes #${n} (task ${ref.taskId} of tranche "${ref.trancheSlug}") but is not named "${expectedBranch}" — rename the branch and re-push, or if this work is intentionally outside AEG's dispatch flow, remove the Closes reference.`
3870
+ message: `closes-n-reverse: branch "${branch}" closes #${n} (task ${ref2.taskId} of tranche "${ref2.trancheSlug}") but is not named "${expectedBranch}" — rename the branch and re-push, or if this work is intentionally outside AEG's dispatch flow, remove the Closes reference.`
3654
3871
  };
3655
3872
  }
3656
3873
  }
3657
3874
  }
3658
- const m = branch.match(/^task\/([^/]+)\/([^/]+)$/);
3659
- if (!m)
3875
+ const ref = parseTaskBranchIdentity(branch);
3876
+ if (!ref)
3660
3877
  return { ok: true };
3661
- const trancheSlug = m[1];
3662
- const taskId = m[2];
3878
+ if (ref.kind === "issue") {
3879
+ const expectedIssue2 = ref.issueNumber;
3880
+ if (!referenced.has(expectedIssue2)) {
3881
+ return {
3882
+ ok: false,
3883
+ expectedIssue: expectedIssue2,
3884
+ message: `closes-n: PR body does not contain \`Closes #${expectedIssue2}\` (required for branch "${branch}"). Add it to the PR body Summary section.`
3885
+ };
3886
+ }
3887
+ return { ok: true, expectedIssue: expectedIssue2 };
3888
+ }
3889
+ const trancheSlug = ref.tranche;
3890
+ const taskId = ref.taskId;
3663
3891
  const trancheFile = trancheFiles.find((f) => f.slug === trancheSlug);
3664
3892
  if (!trancheFile) {
3665
3893
  return {
@@ -3711,34 +3939,67 @@ function extractIssue(body) {
3711
3939
  const issue = headerNums.length > 0 ? headerNums[0] : bodyNums[0];
3712
3940
  return { issue, extraIssues: bodyNums.filter((n) => n !== issue), outsideHeader: headerNums.length === 0 };
3713
3941
  }
3714
- // ../../packages/aeg-core/src/review-gate.ts
3715
- function isBoundToHead(extraction, headSha) {
3716
- if (!extraction.headSha)
3942
+ // ../../packages/aeg-core/src/review-input-manifest.ts
3943
+ import { createHash as createHash4 } from "node:crypto";
3944
+ function briefHash(brief) {
3945
+ return createHash4("sha256").update(`${brief}
3946
+ `).digest("hex");
3947
+ }
3948
+ function policyDigest(policy) {
3949
+ return createHash4("sha256").update(JSON.stringify({ codeReviewThreshold: policy.codeReviewThreshold, securityThreshold: policy.securityThreshold })).digest("hex");
3950
+ }
3951
+ function isBoundToHead(echoed, headSha) {
3952
+ if (!echoed.headSha)
3717
3953
  return false;
3718
- return headSha.toLowerCase().startsWith(extraction.headSha.toLowerCase());
3954
+ return headSha.toLowerCase().startsWith(echoed.headSha.toLowerCase());
3719
3955
  }
3720
- function isBoundByPatchIdentity(extraction, headSha, patchIdOf) {
3721
- if (patchIdOf === undefined || !extraction.headSha)
3956
+ function isBoundByPatchIdentity(echoed, headSha, patchIdOf) {
3957
+ if (patchIdOf === undefined || !echoed.headSha)
3722
3958
  return false;
3723
- const judged = patchIdOf(extraction.headSha);
3959
+ const judged = patchIdOf(echoed.headSha);
3724
3960
  const current = patchIdOf(headSha);
3725
3961
  if (judged === null || current === null)
3726
3962
  return false;
3727
3963
  return judged === current;
3728
3964
  }
3729
- function isBoundToPatch(extraction, headSha, patchIdOf) {
3730
- return isBoundToHead(extraction, headSha) || isBoundByPatchIdentity(extraction, headSha, patchIdOf);
3965
+ function isBoundToPatch(echoed, headSha, patchIdOf) {
3966
+ return isBoundToHead(echoed, headSha) || isBoundByPatchIdentity(echoed, headSha, patchIdOf);
3731
3967
  }
3732
- function isBoundToObjectives(extraction, currentVersion) {
3968
+ function isBoundToObjectives(echoed, currentVersion) {
3733
3969
  if (currentVersion === null)
3734
3970
  return true;
3735
- return extraction.objectivesVersion === currentVersion;
3971
+ return echoed.objectivesVersion === currentVersion;
3736
3972
  }
3737
- function isBoundToRulings(extraction, currentOrdinal) {
3738
- if (extraction.rulingOrdinal === null)
3973
+ function isBoundToRulings(echoed, currentOrdinal) {
3974
+ if (echoed.rulingOrdinal === null)
3739
3975
  return currentOrdinal === 0;
3740
- return extraction.rulingOrdinal === currentOrdinal;
3976
+ return echoed.rulingOrdinal === currentOrdinal;
3741
3977
  }
3978
+ function isBoundToBriefHash(echoed, currentHash) {
3979
+ if (currentHash === null)
3980
+ return true;
3981
+ return echoed.briefHash === currentHash;
3982
+ }
3983
+ function isBoundToPolicy(echoed, currentDigest) {
3984
+ return echoed.policyDigest === currentDigest;
3985
+ }
3986
+ function compareManifest(echoed, current, patchIdOf) {
3987
+ const head = isBoundToPatch(echoed, current.headSha, patchIdOf);
3988
+ const briefHashBound = isBoundToBriefHash(echoed, current.briefHash);
3989
+ const objectivesVersion2 = isBoundToObjectives(echoed, current.objectivesVersion);
3990
+ const rulingOrdinal = isBoundToRulings(echoed, current.rulingOrdinal);
3991
+ const policyDigestBound = isBoundToPolicy(echoed, current.policyDigest);
3992
+ return {
3993
+ bound: head && briefHashBound && objectivesVersion2 && rulingOrdinal && policyDigestBound,
3994
+ head,
3995
+ briefHash: briefHashBound,
3996
+ objectivesVersion: objectivesVersion2,
3997
+ rulingOrdinal,
3998
+ policyDigest: policyDigestBound
3999
+ };
4000
+ }
4001
+
4002
+ // ../../packages/aeg-core/src/review-gate.ts
3742
4003
  function checkReviewGate(input) {
3743
4004
  const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
3744
4005
  const waived = isWaiverLabelActorVerified({
@@ -3761,15 +4022,55 @@ function checkReviewGate(input) {
3761
4022
  const verifiedBodies = verified.map((c) => c.body);
3762
4023
  const codeReview = extractCodeReviewVerdict(verifiedBodies);
3763
4024
  const security = extractSecurityReviewVerdict(verifiedBodies);
3764
- const codeReviewClean = codeReview.value === "APPROVE";
3765
- const securityClean = security.value === "PASS";
3766
- const codeReviewBound = isBoundToPatch(codeReview, input.headSha, input.patchIdOf);
3767
- const securityBound = isBoundToPatch(security, input.headSha, input.patchIdOf);
3768
- const codeReviewObjectivesBound = isBoundToObjectives(codeReview, input.objectivesVersion);
3769
- const securityObjectivesBound = isBoundToObjectives(security, input.objectivesVersion);
3770
- const codeReviewRulingsBound = isBoundToRulings(codeReview, input.rulingOrdinal);
3771
- const securityRulingsBound = isBoundToRulings(security, input.rulingOrdinal);
3772
- if (codeReviewClean && codeReviewBound && codeReviewObjectivesBound && codeReviewRulingsBound && securityClean && securityBound && securityObjectivesBound && securityRulingsBound && mechanicalChecksClean) {
4025
+ const policy = input.policy ?? DEFAULT_REVIEW_POLICY;
4026
+ let codeReviewPolicyEvaluation;
4027
+ let securityPolicyEvaluation;
4028
+ try {
4029
+ codeReviewPolicyEvaluation = evaluateCodeReview(codeReview.findingSeverities, policy);
4030
+ securityPolicyEvaluation = evaluateSecurityReview(security.findingSeverities, policy);
4031
+ } catch (err) {
4032
+ return {
4033
+ verdict: "fail",
4034
+ reason: `a verdict comment carries a finding severity this repository's policy does not recognize: ${err instanceof Error ? err.message : String(err)}`,
4035
+ waived: false
4036
+ };
4037
+ }
4038
+ const codeReviewTextClean = codeReview.value === "APPROVE";
4039
+ const securityTextClean = security.value === "PASS";
4040
+ const codeReviewPolicyClean = codeReviewPolicyEvaluation.outcome === "clean";
4041
+ const securityPolicyClean = securityPolicyEvaluation.outcome === "clean";
4042
+ const codeReviewClean = codeReviewTextClean && codeReviewPolicyClean;
4043
+ const securityClean = securityTextClean && securityPolicyClean;
4044
+ const currentManifest = {
4045
+ headSha: input.headSha,
4046
+ briefHash: input.briefHash ?? null,
4047
+ objectivesVersion: input.objectivesVersion,
4048
+ rulingOrdinal: input.rulingOrdinal,
4049
+ policyDigest: policyDigest(policy)
4050
+ };
4051
+ const codeReviewEchoed = {
4052
+ headSha: codeReview.headSha,
4053
+ briefHash: codeReview.briefHash,
4054
+ objectivesVersion: codeReview.objectivesVersion,
4055
+ rulingOrdinal: codeReview.rulingOrdinal,
4056
+ policyDigest: codeReview.policyDigest
4057
+ };
4058
+ const securityEchoed = {
4059
+ headSha: security.headSha,
4060
+ briefHash: security.briefHash,
4061
+ objectivesVersion: security.objectivesVersion,
4062
+ rulingOrdinal: security.rulingOrdinal,
4063
+ policyDigest: security.policyDigest
4064
+ };
4065
+ const codeReviewBinding = compareManifest(codeReviewEchoed, currentManifest, input.patchIdOf);
4066
+ const securityBinding = compareManifest(securityEchoed, currentManifest, input.patchIdOf);
4067
+ const codeReviewBound = codeReviewBinding.head;
4068
+ const securityBound = securityBinding.head;
4069
+ const codeReviewObjectivesBound = codeReviewBinding.objectivesVersion;
4070
+ const securityObjectivesBound = securityBinding.objectivesVersion;
4071
+ const codeReviewRulingsBound = codeReviewBinding.rulingOrdinal;
4072
+ const securityRulingsBound = securityBinding.rulingOrdinal;
4073
+ if (codeReviewClean && codeReviewBinding.bound && securityClean && securityBinding.bound && mechanicalChecksClean) {
3773
4074
  return {
3774
4075
  verdict: "pass",
3775
4076
  reason: `code-reviewer verdict is a clean APPROVE and security-review verdict is a clean PASS, both covering head ${input.headSha}, and every reported mechanical check is green.`,
@@ -3777,23 +4078,35 @@ function checkReviewGate(input) {
3777
4078
  };
3778
4079
  }
3779
4080
  const problems = [];
3780
- if (!codeReviewClean) {
4081
+ if (!codeReviewTextClean) {
3781
4082
  problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
4083
+ } else if (!codeReviewPolicyClean) {
4084
+ problems.push(`code-reviewer verdict says APPROVE but carries a finding (${codeReviewPolicyEvaluation.blockingFindings.map((f) => f.severity).join(", ")}) at or above this repository's code-review policy threshold (${policy.codeReviewThreshold}) — a reviewer's own APPROVE never overrides policy`);
3782
4085
  } else if (!codeReviewBound) {
3783
4086
  problems.push(`the newest code-review verdict covers ${codeReview.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
3784
4087
  } else if (!codeReviewObjectivesBound) {
3785
4088
  problems.push(`the newest code-review verdict was cast against objectives version ${codeReview.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
3786
4089
  } else if (!codeReviewRulingsBound) {
3787
4090
  problems.push(`the newest code-review verdict was cast against ruling ordinal ${codeReview.rulingOrdinal ?? "none"}, a newer ruling (ruling ${input.rulingOrdinal}) is now posted on this PR`);
4091
+ } else if (!codeReviewBinding.briefHash) {
4092
+ problems.push(`the newest code-review verdict was cast against brief hash ${codeReview.briefHash ?? "none"}, the frozen brief's current hash is ${currentManifest.briefHash ?? "none"}`);
4093
+ } else if (!codeReviewBinding.policyDigest) {
4094
+ problems.push(`the newest code-review verdict was cast against review policy digest ${codeReview.policyDigest ?? "none"}, this repository's current policy digest is ${currentManifest.policyDigest}`);
3788
4095
  }
3789
- if (!securityClean) {
4096
+ if (!securityTextClean) {
3790
4097
  problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
4098
+ } else if (!securityPolicyClean) {
4099
+ problems.push(`security-review verdict says PASS but carries a finding (${securityPolicyEvaluation.blockingFindings.map((f) => f.severity).join(", ")}) at or above this repository's security policy threshold (${policy.securityThreshold}) — a reviewer's own PASS never overrides policy`);
3791
4100
  } else if (!securityBound) {
3792
4101
  problems.push(`the newest security-review verdict covers ${security.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
3793
4102
  } else if (!securityObjectivesBound) {
3794
4103
  problems.push(`the newest security-review verdict was cast against objectives version ${security.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
3795
4104
  } else if (!securityRulingsBound) {
3796
4105
  problems.push(`the newest security-review verdict was cast against ruling ordinal ${security.rulingOrdinal ?? "none"}, a newer ruling (ruling ${input.rulingOrdinal}) is now posted on this PR`);
4106
+ } else if (!securityBinding.briefHash) {
4107
+ problems.push(`the newest security-review verdict was cast against brief hash ${security.briefHash ?? "none"}, the frozen brief's current hash is ${currentManifest.briefHash ?? "none"}`);
4108
+ } else if (!securityBinding.policyDigest) {
4109
+ problems.push(`the newest security-review verdict was cast against review policy digest ${security.policyDigest ?? "none"}, this repository's current policy digest is ${currentManifest.policyDigest}`);
3797
4110
  }
3798
4111
  if (!mechanicalChecksClean) {
3799
4112
  problems.push(reportedMechanicalChecks.length === 0 ? "no mechanical checks have reported for this head yet" : `mechanical check(s) not green: ${reportedMechanicalChecks.filter((c) => c.bucket !== "pass").map((c) => `${c.name} (${c.bucket})`).join(", ")}`);
@@ -4485,42 +4798,45 @@ function checkDispatchReadiness(input) {
4485
4798
  const { trancheSlug, task } = input;
4486
4799
  const taskLabel = `task ${task.id} (tranche ${trancheSlug})`;
4487
4800
  const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
4488
- const blockers = [];
4801
+ const blockerDetails = [];
4802
+ const push = (blockerClass, message) => {
4803
+ blockerDetails.push({ class: blockerClass, message });
4804
+ };
4489
4805
  if (task.issue === null) {
4490
- blockers.push(`dispatch-gate issue-existence: ${taskLabel} has no Issue (#TBD or blank) in the topology — not dispatchable until the Planner cuts the Issue.`);
4806
+ push("issue-existence", `dispatch-gate issue-existence: ${taskLabel} has no Issue (#TBD or blank) in the topology — not dispatchable until the Planner cuts the Issue.`);
4491
4807
  } else if (input.issue === null) {
4492
- blockers.push(`dispatch-gate issue-existence: ${taskLabel} names Issue #${task.issue}, but it does not resolve to a real GitHub Issue (phantom reference).`);
4808
+ push("issue-existence", `dispatch-gate issue-existence: ${taskLabel} names Issue #${task.issue}, but it does not resolve to a real GitHub Issue (phantom reference).`);
4493
4809
  }
4494
4810
  if (input.issue !== null && !input.issueRationalePass) {
4495
- blockers.push(`dispatch-gate rationale: Issue #${input.issue.number} for ${taskLabel} fails the rationale gate (checkIssueRationale) — the Planner must complete the eight-field rationale before this task is dispatchable.`);
4811
+ push("rationale", `dispatch-gate rationale: Issue #${input.issue.number} for ${taskLabel} fails the rationale gate (checkIssueRationale) — the Planner must complete the eight-field rationale before this task is dispatchable.`);
4496
4812
  }
4497
4813
  for (const dep of input.dependsOn) {
4498
4814
  if (isSelfDependency(dep, task, input.issue)) {
4499
4815
  const issueStr = input.issue !== null ? `#${input.issue.number}` : "?";
4500
- blockers.push(`dispatch-gate INTERNAL: parsed a self-dependency for task ${task.id} (${issueStr}) — this is a parser bug in parseRationaleDeps, not a real dependency. Please report it upstream. Re-run once the rationale is corrected or the fix ships.`);
4816
+ push("internal-self-dependency", `dispatch-gate INTERNAL: parsed a self-dependency for task ${task.id} (${issueStr}) — this is a parser bug in parseRationaleDeps, not a real dependency. Please report it upstream. Re-run once the rationale is corrected or the fix ships.`);
4501
4817
  continue;
4502
4818
  }
4503
4819
  if (dep.resolved === false) {
4504
- blockers.push(`dispatch-gate depends-on: ${taskLabel} depends on "${dep.id}", which is UNRESOLVABLE — the resolver could not find a matching tranche/task/Issue for this edge (not a claim about merge status). Not dispatchable until the edge is corrected.`);
4820
+ push("depends-on-unresolvable", `dispatch-gate depends-on: ${taskLabel} depends on "${dep.id}", which is UNRESOLVABLE — the resolver could not find a matching tranche/task/Issue for this edge (not a claim about merge status). Not dispatchable until the edge is corrected.`);
4505
4821
  continue;
4506
4822
  }
4507
4823
  if (!dep.merged && !isHandClosedByRecognizedPrincipal(dep, principalAllowlist)) {
4508
4824
  const issueStr = dep.issue !== null ? ` (#${dep.issue})` : "";
4509
- blockers.push(`dispatch-gate depends-on: ${taskLabel} depends on ${dep.id}${issueStr}, whose PR is not merged yet — not dispatchable, it serializes behind it.`);
4825
+ push("depends-on-not-merged", `dispatch-gate depends-on: ${taskLabel} depends on ${dep.id}${issueStr}, whose PR is not merged yet — not dispatchable, it serializes behind it.`);
4510
4826
  }
4511
4827
  }
4512
4828
  for (const c of input.conflictsWith) {
4513
4829
  if (c.openOrInFlight) {
4514
4830
  const issueStr = c.issue !== null ? ` (#${c.issue})` : "";
4515
- blockers.push(`dispatch-gate conflicts-with: ${taskLabel} conflicts with ${c.id}${issueStr}, whose PR is open or in-flight — not dispatchable until it merges.`);
4831
+ push("conflicts-with", `dispatch-gate conflicts-with: ${taskLabel} conflicts with ${c.id}${issueStr}, whose PR is open or in-flight — not dispatchable until it merges.`);
4516
4832
  }
4517
4833
  }
4518
4834
  for (const proj of input.priorTrancheArchival) {
4519
4835
  if (proj.priorTrancheSlug !== null && !proj.archived) {
4520
- blockers.push(`dispatch-gate prior-tranche-archival: project \`${proj.project}\`'s previous tranche \`${proj.priorTrancheSlug}\` is not archived — the Tranche Archivist must run before new work on this product.`);
4836
+ push("prior-tranche-archival", `dispatch-gate prior-tranche-archival: project \`${proj.project}\`'s previous tranche \`${proj.priorTrancheSlug}\` is not archived — the Tranche Archivist must run before new work on this product.`);
4521
4837
  }
4522
4838
  }
4523
- return { ready: blockers.length === 0, blockers };
4839
+ return { ready: blockerDetails.length === 0, blockers: blockerDetails.map((b) => b.message), blockerDetails };
4524
4840
  }
4525
4841
  // ../../packages/aeg-core/src/single-plan-pr.ts
4526
4842
  function trancheSlugFromTopologyPath(path) {
@@ -4622,6 +4938,91 @@ function checkBranchTopology(input) {
4622
4938
  reason: `Branch \`${branch}\` matches topology row \`${taskId}\` in ${topoPath}.`
4623
4939
  };
4624
4940
  }
4941
+ // ../../packages/aeg-core/src/task-tools.ts
4942
+ import { z } from "zod";
4943
+ var TASK_TOOL_ERROR_KINDS = [
4944
+ "validation",
4945
+ "authority",
4946
+ "precondition",
4947
+ "capability",
4948
+ "infrastructure",
4949
+ "cancellation",
4950
+ "timeout",
4951
+ "uncertain_effect"
4952
+ ];
4953
+ var TaskToolErrorSchema = z.object({
4954
+ kind: z.enum(TASK_TOOL_ERROR_KINDS),
4955
+ message: z.string().min(1),
4956
+ detail: z.string().optional()
4957
+ });
4958
+ var TaskToolRefSchema = z.union([
4959
+ z.object({ tranche: z.string().min(1), id: z.string().min(1) }),
4960
+ z.object({ issue: z.number().int().positive() })
4961
+ ]);
4962
+ var MAX_PAGE_LIMIT = 100;
4963
+ var PageRequestSchema = z.object({
4964
+ cursor: z.string().optional(),
4965
+ limit: z.number().int().positive().max(MAX_PAGE_LIMIT).optional()
4966
+ });
4967
+ var FreshnessSchema = z.enum(["fresh", "stale", "unknown"]);
4968
+ var ObservedSchema = z.object({
4969
+ observedAt: z.string(),
4970
+ freshness: FreshnessSchema
4971
+ });
4972
+ var TaskStatusInputSchema = z.object({
4973
+ task: TaskToolRefSchema.optional()
4974
+ }).merge(PageRequestSchema);
4975
+ var TaskStatusItemSchema = z.object({
4976
+ task: TaskToolRefSchema,
4977
+ issue: z.number().int().positive(),
4978
+ pr: z.number().int().positive().nullable(),
4979
+ state: z.string()
4980
+ }).merge(ObservedSchema);
4981
+ var TaskStatusResultSchema = z.object({
4982
+ items: z.array(TaskStatusItemSchema),
4983
+ nextCursor: z.string().nullable()
4984
+ });
4985
+ var RequestedAuthoritySchema = z.enum(["planner", "principal", "operator", "self"]);
4986
+ var EscalationInputsSchema = z.object({
4987
+ task: z.number().int().positive(),
4988
+ round: z.number().int().nonnegative(),
4989
+ head: z.string(),
4990
+ branch: z.string(),
4991
+ prNumber: z.number().int().positive()
4992
+ });
4993
+ var EscalationEvidenceSchema = z.object({
4994
+ round: z.number().int().nonnegative(),
4995
+ reviewer: z.string().nullable(),
4996
+ security: z.string().nullable()
4997
+ });
4998
+ var TaskEscalationReadInputSchema = z.object({
4999
+ task: TaskToolRefSchema
5000
+ }).merge(PageRequestSchema);
5001
+ var TaskEscalationPacketSchema = z.object({
5002
+ reason: z.string(),
5003
+ detail: z.string().nullable(),
5004
+ inputs: EscalationInputsSchema.nullable(),
5005
+ evidence: EscalationEvidenceSchema.nullable(),
5006
+ attemptedRecovery: z.string(),
5007
+ requestedAuthority: RequestedAuthoritySchema,
5008
+ permittedNextActions: z.array(z.string())
5009
+ }).merge(ObservedSchema);
5010
+ var TaskEscalationReadResultSchema = z.object({
5011
+ items: z.array(TaskEscalationPacketSchema),
5012
+ nextCursor: z.string().nullable()
5013
+ }).merge(ObservedSchema);
5014
+ var TaskStartInputSchema = z.object({
5015
+ tranche: z.string().min(1),
5016
+ id: z.string().min(1)
5017
+ });
5018
+ var TaskResumeInputSchema = z.object({
5019
+ task: TaskToolRefSchema
5020
+ });
5021
+ var TaskCancelInputSchema = z.object({
5022
+ task: TaskToolRefSchema,
5023
+ reason: z.string().min(1)
5024
+ });
5025
+ var NoResultSchema = z.never();
4625
5026
  // ../../packages/aeg-core/src/first-push-dispatch-gate.ts
4626
5027
  function parseTaskBranch(branch) {
4627
5028
  const m = /^task\/([^/]+)\/([^/]+)$/.exec(branch);
@@ -4705,7 +5106,6 @@ function decideIssueAssignment(input) {
4705
5106
  };
4706
5107
  }
4707
5108
  // ../../packages/aeg-core/src/test-plan-gate.ts
4708
- var TASK_BRANCH_PATTERN2 = /^task\/[^/]+\/[^/]+$/;
4709
5109
  function evaluateTestPlanGate(body, branch) {
4710
5110
  if (!body) {
4711
5111
  return {
@@ -4718,7 +5118,7 @@ function evaluateTestPlanGate(body, branch) {
4718
5118
  }
4719
5119
  const located = locateTestPlanSection(body);
4720
5120
  if (!located.found) {
4721
- if (TASK_BRANCH_PATTERN2.test(branch)) {
5121
+ if (parseTaskBranchIdentity(branch) !== null) {
4722
5122
  return {
4723
5123
  verdict: "fail",
4724
5124
  messages: [
@@ -4846,7 +5246,7 @@ function findWorkspaceEscapes(files, knownPaths, workspaceDirs = DEFAULT_WORKSPA
4846
5246
  return findings;
4847
5247
  }
4848
5248
  // ../../packages/aeg-core/src/log/schema.ts
4849
- import { z } from "zod";
5249
+ import { z as z2 } from "zod";
4850
5250
  var ROLE_VALUES = [
4851
5251
  "planner",
4852
5252
  "developer",
@@ -4856,136 +5256,172 @@ var ROLE_VALUES = [
4856
5256
  "archivist",
4857
5257
  "architect"
4858
5258
  ];
4859
- var RoleSchema = z.enum(ROLE_VALUES);
5259
+ var RoleSchema = z2.enum(ROLE_VALUES);
4860
5260
  var HOST_VALUES = ["hook", "ci", "cli", "loop"];
4861
- var HostSchema = z.enum(HOST_VALUES);
5261
+ var HostSchema = z2.enum(HOST_VALUES);
4862
5262
  var RUN_ID_PATTERN = /^[A-Za-z0-9_.-]{1,128}$/;
4863
- var HeaderMetaSchema = z.object({
4864
- schema: z.literal(1),
4865
- ts: z.string(),
4866
- run_id: z.string().regex(RUN_ID_PATTERN),
4867
- seq: z.number().int().nonnegative(),
4868
- repo: z.string().nullable(),
4869
- vinaya: z.string(),
4870
- doctrine: z.string(),
5263
+ var headerMetaCore = {
5264
+ ts: z2.string(),
5265
+ run_id: z2.string().regex(RUN_ID_PATTERN),
5266
+ seq: z2.number().int().nonnegative(),
5267
+ repo: z2.string().nullable(),
5268
+ vinaya: z2.string(),
5269
+ doctrine: z2.string(),
4871
5270
  host: HostSchema,
4872
- machine: z.string()
5271
+ machine: z2.string()
5272
+ };
5273
+ var HeaderMetaV1Schema = z2.object({
5274
+ schema: z2.literal(1),
5275
+ ...headerMetaCore
5276
+ }).strict();
5277
+ var LineageSchema = z2.object({
5278
+ run: z2.string().nullable(),
5279
+ attempt: z2.number().int().nullable(),
5280
+ parent: z2.string().nullable()
4873
5281
  }).strict();
4874
- var SubjectSchema = z.object({
4875
- issue: z.number().int().nullable(),
4876
- pr: z.number().int().optional(),
4877
- sha: z.string().optional(),
4878
- role: z.union([RoleSchema, z.literal("unattributed")]),
4879
- round: z.number().int().optional(),
4880
- objectives_version: z.string().optional()
5282
+ var InputVersionsSchema = z2.object({
5283
+ objectives_version: z2.string().nullable(),
5284
+ brief_hash: z2.string().nullable(),
5285
+ ruling_ordinal: z2.number().int().nullable(),
5286
+ policy_digest: z2.string().nullable()
4881
5287
  }).strict();
4882
- var HeaderSchema = z.object({
5288
+ var ProvenanceSchema = z2.enum(["parent_attributed", "env_correlated", "self_reported", "unavailable"]);
5289
+ var HeaderMetaV2Schema = z2.object({
5290
+ schema: z2.literal(2),
5291
+ ...headerMetaCore,
5292
+ event_id: z2.string().min(1),
5293
+ process_id: z2.string().min(1),
5294
+ actor_id: z2.string().nullable(),
5295
+ lineage: LineageSchema,
5296
+ input_versions: InputVersionsSchema,
5297
+ provenance: ProvenanceSchema
5298
+ }).strict();
5299
+ var HeaderMetaSchema = z2.discriminatedUnion("schema", [HeaderMetaV1Schema, HeaderMetaV2Schema]);
5300
+ var SubjectSchema = z2.object({
5301
+ issue: z2.number().int().nullable(),
5302
+ pr: z2.number().int().optional(),
5303
+ sha: z2.string().optional(),
5304
+ role: z2.union([RoleSchema, z2.literal("unattributed")]),
5305
+ round: z2.number().int().optional(),
5306
+ objectives_version: z2.string().optional()
5307
+ }).strict();
5308
+ var HeaderSchema = z2.object({
4883
5309
  meta: HeaderMetaSchema,
4884
5310
  subject: SubjectSchema
4885
5311
  }).strict();
4886
5312
  var envelopeTail = {
4887
- duration_ms: z.number().nonnegative().optional(),
4888
- payload: z.object({}).strict()
5313
+ duration_ms: z2.number().nonnegative().optional(),
5314
+ payload: z2.object({}).strict()
4889
5315
  };
4890
- var DispatchOutcomeSchema = z.discriminatedUnion("type", [
4891
- z.object({ type: z.literal("pr_opened"), pr: z.number().int(), head: z.string() }).strict(),
4892
- z.object({
4893
- type: z.literal("round_pushed"),
4894
- pr: z.number().int(),
4895
- head: z.string(),
4896
- comment_id: z.number().int()
5316
+ var ReviewFindingSchema = z2.object({
5317
+ id: z2.string(),
5318
+ severity: z2.string(),
5319
+ state: z2.string().optional(),
5320
+ severity_scale: z2.string().optional(),
5321
+ policy_treatment: z2.enum(["blocking", "non_blocking", "unavailable"]).optional(),
5322
+ confidence: z2.number().min(0).max(1).optional(),
5323
+ confidence_scale: z2.string().optional(),
5324
+ confidence_source: z2.string().optional()
5325
+ }).strict();
5326
+ var DispatchOutcomeSchema = z2.discriminatedUnion("type", [
5327
+ z2.object({ type: z2.literal("pr_opened"), pr: z2.number().int(), head: z2.string() }).strict(),
5328
+ z2.object({
5329
+ type: z2.literal("round_pushed"),
5330
+ pr: z2.number().int(),
5331
+ head: z2.string(),
5332
+ comment_id: z2.number().int()
4897
5333
  }).strict(),
4898
- z.object({
4899
- type: z.literal("verdict"),
4900
- verdict: z.enum(["APPROVE", "REQUEST CHANGES", "PASS", "FAIL"]),
4901
- head: z.string(),
4902
- comment_id: z.number().int(),
4903
- objectives: z.array(z.object({ id: z.string(), met: z.boolean() }).strict()),
4904
- findings: z.array(z.object({ id: z.string(), severity: z.string(), state: z.string().optional() }).strict())
5334
+ z2.object({
5335
+ type: z2.literal("verdict"),
5336
+ verdict: z2.enum(["APPROVE", "REQUEST CHANGES", "PASS", "FAIL"]),
5337
+ head: z2.string(),
5338
+ comment_id: z2.number().int(),
5339
+ objectives: z2.array(z2.object({ id: z2.string(), met: z2.boolean() }).strict()),
5340
+ findings: z2.array(ReviewFindingSchema)
4905
5341
  }).strict(),
4906
- z.object({
4907
- type: z.literal("escalation"),
4908
- class: z.enum(["authority", "strategy", "product"]),
4909
- comment_id: z.number().int()
5342
+ z2.object({
5343
+ type: z2.literal("escalation"),
5344
+ class: z2.enum(["authority", "strategy", "product"]),
5345
+ comment_id: z2.number().int()
4910
5346
  }).strict(),
4911
- z.object({ type: z.literal("brief"), comment_id: z.number().int(), hash: z.string() }).strict(),
4912
- z.object({ type: z.literal("plan"), issues: z.array(z.number().int()) }).strict(),
4913
- z.object({ type: z.literal("archive"), provenance_comment_id: z.number().int() }).strict()
5347
+ z2.object({ type: z2.literal("brief"), comment_id: z2.number().int(), hash: z2.string() }).strict(),
5348
+ z2.object({ type: z2.literal("plan"), issues: z2.array(z2.number().int()) }).strict(),
5349
+ z2.object({ type: z2.literal("archive"), provenance_comment_id: z2.number().int() }).strict()
4914
5350
  ]);
4915
5351
  var dispatchShared = {
4916
5352
  meta: HeaderMetaSchema,
4917
5353
  subject: SubjectSchema,
4918
- kind: z.literal("dispatch"),
5354
+ kind: z2.literal("dispatch"),
4919
5355
  ...envelopeTail,
4920
5356
  target_role: RoleSchema,
4921
- model: z.string(),
4922
- round: z.number().int().optional(),
4923
- effect_id: z.string()
5357
+ model: z2.string(),
5358
+ round: z2.number().int().optional(),
5359
+ effect_id: z2.string()
4924
5360
  };
4925
- var dispatchUsageField = z.object({ input: z.number().nonnegative(), output: z.number().nonnegative() }).strict().nullable();
4926
- var DispatchEventSchema = z.discriminatedUnion("event", [
4927
- z.object({ ...dispatchShared, event: z.literal("dispatched"), prompt_hash: z.string() }).strict(),
4928
- z.object({
5361
+ var dispatchUsageField = z2.object({ input: z2.number().nonnegative(), output: z2.number().nonnegative() }).strict().nullable();
5362
+ var DispatchEventSchema = z2.discriminatedUnion("event", [
5363
+ z2.object({ ...dispatchShared, event: z2.literal("dispatched"), prompt_hash: z2.string() }).strict(),
5364
+ z2.object({
4929
5365
  ...dispatchShared,
4930
- event: z.literal("outcome_received"),
5366
+ event: z2.literal("outcome_received"),
4931
5367
  outcome: DispatchOutcomeSchema,
4932
5368
  usage: dispatchUsageField
4933
5369
  }).strict(),
4934
- z.object({
5370
+ z2.object({
4935
5371
  ...dispatchShared,
4936
- event: z.literal("dispatch_failed"),
4937
- reason: z.enum(["timeout", "crash", "refused", "unattributed_write"]),
5372
+ event: z2.literal("dispatch_failed"),
5373
+ reason: z2.enum(["timeout", "crash", "refused", "unattributed_write"]),
4938
5374
  usage: dispatchUsageField
4939
5375
  }).strict()
4940
5376
  ]);
4941
5377
  var loopShared = {
4942
5378
  meta: HeaderMetaSchema,
4943
5379
  subject: SubjectSchema,
4944
- kind: z.literal("dev_review_loop"),
5380
+ kind: z2.literal("dev_review_loop"),
4945
5381
  ...envelopeTail,
4946
- loop_id: z.string()
5382
+ loop_id: z2.string()
4947
5383
  };
4948
- var DevReviewLoopEventSchema = z.discriminatedUnion("event", [
4949
- z.object({
5384
+ var DevReviewLoopEventSchema = z2.discriminatedUnion("event", [
5385
+ z2.object({
4950
5386
  ...loopShared,
4951
- event: z.literal("loop_started"),
4952
- task: z.number().int(),
4953
- policy: z.object({
4954
- max_rounds: z.number().int().nonnegative(),
4955
- reviewers: z.array(RoleSchema),
4956
- models: z.record(RoleSchema, z.string())
5387
+ event: z2.literal("loop_started"),
5388
+ task: z2.number().int(),
5389
+ policy: z2.object({
5390
+ max_rounds: z2.number().int().nonnegative(),
5391
+ reviewers: z2.array(RoleSchema),
5392
+ models: z2.record(RoleSchema, z2.string())
4957
5393
  }).strict()
4958
5394
  }).strict(),
4959
- z.object({ ...loopShared, event: z.literal("round_started"), round: z.number().int(), base_head: z.string() }).strict(),
4960
- z.object({
5395
+ z2.object({ ...loopShared, event: z2.literal("round_started"), round: z2.number().int(), base_head: z2.string() }).strict(),
5396
+ z2.object({
4961
5397
  ...loopShared,
4962
- event: z.literal("gate_result_read"),
4963
- round: z.number().int(),
4964
- head: z.string(),
4965
- green: z.boolean()
5398
+ event: z2.literal("gate_result_read"),
5399
+ round: z2.number().int(),
5400
+ head: z2.string(),
5401
+ green: z2.boolean()
4966
5402
  }).strict(),
4967
- z.object({
5403
+ z2.object({
4968
5404
  ...loopShared,
4969
- event: z.literal("verdicts_read"),
4970
- round: z.number().int(),
4971
- head: z.string(),
4972
- all_approve: z.boolean(),
4973
- blockers: z.number().int().nonnegative()
5405
+ event: z2.literal("verdicts_read"),
5406
+ round: z2.number().int(),
5407
+ head: z2.string(),
5408
+ all_approve: z2.boolean(),
5409
+ blockers: z2.number().int().nonnegative()
4974
5410
  }).strict(),
4975
- z.object({
5411
+ z2.object({
4976
5412
  ...loopShared,
4977
- event: z.literal("findings_compared"),
4978
- round: z.number().int(),
4979
- open: z.array(z.string()),
4980
- resolved: z.array(z.string()),
4981
- new: z.array(z.string()),
4982
- recurring: z.array(z.string())
5413
+ event: z2.literal("findings_compared"),
5414
+ round: z2.number().int(),
5415
+ open: z2.array(z2.string()),
5416
+ resolved: z2.array(z2.string()),
5417
+ new: z2.array(z2.string()),
5418
+ recurring: z2.array(z2.string())
4983
5419
  }).strict(),
4984
- z.object({
5420
+ z2.object({
4985
5421
  ...loopShared,
4986
- event: z.literal("stop_condition_met"),
4987
- round: z.number().int(),
4988
- condition: z.enum([
5422
+ event: z2.literal("stop_condition_met"),
5423
+ round: z2.number().int(),
5424
+ condition: z2.enum([
4989
5425
  "green",
4990
5426
  "max_rounds",
4991
5427
  "no_progress",
@@ -4995,42 +5431,49 @@ var DevReviewLoopEventSchema = z.discriminatedUnion("event", [
4995
5431
  "reappearance"
4996
5432
  ])
4997
5433
  }).strict(),
4998
- z.object({
5434
+ z2.object({
4999
5435
  ...loopShared,
5000
- event: z.literal("paused"),
5001
- round: z.number().int(),
5002
- reason: z.enum(["escalation", "principal_item", "refreeze_needed"])
5436
+ event: z2.literal("paused"),
5437
+ round: z2.number().int(),
5438
+ reason: z2.enum(["escalation", "principal_item", "refreeze_needed"])
5003
5439
  }).strict(),
5004
- z.object({
5440
+ z2.object({
5005
5441
  ...loopShared,
5006
- event: z.literal("resumed"),
5007
- round: z.number().int(),
5008
- by: z.literal("principal")
5442
+ event: z2.literal("resumed"),
5443
+ round: z2.number().int(),
5444
+ by: z2.literal("principal")
5009
5445
  }).strict(),
5010
- z.object({
5446
+ z2.object({
5011
5447
  ...loopShared,
5012
- event: z.literal("round_ended"),
5013
- round: z.number().int(),
5014
- base_head: z.string(),
5015
- head: z.string(),
5016
- files_changed: z.number().int().nonnegative(),
5017
- insertions: z.number().int().nonnegative(),
5018
- deletions: z.number().int().nonnegative(),
5019
- wall_ms: z.number().nonnegative(),
5020
- outcome: z.enum(["green", "changes_requested", "escalated"])
5448
+ event: z2.literal("unpushed_work_resume"),
5449
+ round: z2.number().int(),
5450
+ branch: z2.string(),
5451
+ detail: z2.string()
5021
5452
  }).strict(),
5022
- z.object({
5453
+ z2.object({
5023
5454
  ...loopShared,
5024
- event: z.literal("journal_finalized"),
5025
- rounds: z.number().int().nonnegative(),
5026
- total_wall_ms: z.number().nonnegative(),
5027
- time_to_green_ms: z.number().nonnegative().nullable(),
5028
- files_changed_total: z.number().int().nonnegative(),
5029
- final_head: z.string(),
5030
- result: z.enum(["merged_ready", "stopped"])
5455
+ event: z2.literal("round_ended"),
5456
+ round: z2.number().int(),
5457
+ base_head: z2.string(),
5458
+ head: z2.string(),
5459
+ files_changed: z2.number().int().nonnegative(),
5460
+ insertions: z2.number().int().nonnegative(),
5461
+ deletions: z2.number().int().nonnegative(),
5462
+ wall_ms: z2.number().nonnegative(),
5463
+ outcome: z2.enum(["green", "changes_requested", "escalated"])
5464
+ }).strict(),
5465
+ z2.object({
5466
+ ...loopShared,
5467
+ event: z2.literal("journal_finalized"),
5468
+ rounds: z2.number().int().nonnegative(),
5469
+ total_wall_ms: z2.number().nonnegative(),
5470
+ time_to_green_ms: z2.number().nonnegative().nullable(),
5471
+ files_changed_total: z2.number().int().nonnegative(),
5472
+ final_head: z2.string(),
5473
+ result: z2.enum(["merged_ready", "stopped"])
5031
5474
  }).strict()
5032
5475
  ]);
5033
- var ForgeOpSchema = z.enum([
5476
+ var ForgeOpSchema = z2.enum([
5034
5477
  "pr.create",
5035
5478
  "pr.comment",
5036
5479
  "pr.body.replace",
@@ -5044,24 +5487,210 @@ var ForgeOpSchema = z.enum([
5044
5487
  "label.add",
5045
5488
  "label.remove"
5046
5489
  ]);
5047
- var ForgeWriteTargetSchema = z.object({
5048
- issue: z.number().int().optional(),
5049
- pr: z.number().int().optional()
5490
+ var ForgeWriteTargetSchema = z2.object({
5491
+ issue: z2.number().int().optional(),
5492
+ pr: z2.number().int().optional()
5050
5493
  }).strict();
5051
5494
  var forgeWriteShared = {
5052
5495
  meta: HeaderMetaSchema,
5053
5496
  subject: SubjectSchema,
5054
- kind: z.literal("forge_write"),
5497
+ kind: z2.literal("forge_write"),
5055
5498
  ...envelopeTail,
5056
5499
  op: ForgeOpSchema,
5057
5500
  target: ForgeWriteTargetSchema
5058
5501
  };
5059
- var ForgeWriteEventSchema = z.discriminatedUnion("event", [
5060
- z.object({ ...forgeWriteShared, event: z.literal("validated") }).strict(),
5061
- z.object({ ...forgeWriteShared, event: z.literal("refused"), reason: z.string() }).strict(),
5062
- z.object({ ...forgeWriteShared, event: z.literal("written"), comment_ids: z.array(z.string()) }).strict()
5502
+ var ForgeWriteEventSchema = z2.discriminatedUnion("event", [
5503
+ z2.object({ ...forgeWriteShared, event: z2.literal("validated") }).strict(),
5504
+ z2.object({ ...forgeWriteShared, event: z2.literal("refused"), reason: z2.string() }).strict(),
5505
+ z2.object({ ...forgeWriteShared, event: z2.literal("written"), comment_ids: z2.array(z2.string()) }).strict()
5506
+ ]);
5507
+ var GateOutcomeSchema = z2.enum([
5508
+ "pass",
5509
+ "fail",
5510
+ "wait",
5511
+ "skip",
5512
+ "invalid_input",
5513
+ "unavailable_dependency",
5514
+ "timeout",
5515
+ "cancelled"
5516
+ ]);
5517
+ var gateShared = {
5518
+ meta: HeaderMetaSchema,
5519
+ subject: SubjectSchema,
5520
+ kind: z2.literal("gate"),
5521
+ ...envelopeTail,
5522
+ check: z2.string(),
5523
+ check_version: z2.string().nullable(),
5524
+ policy_version: z2.string().nullable(),
5525
+ input_fingerprint: z2.string().nullable()
5526
+ };
5527
+ var GateEventSchema = z2.discriminatedUnion("event", [
5528
+ z2.object({
5529
+ ...gateShared,
5530
+ event: z2.literal("checked"),
5531
+ outcome: GateOutcomeSchema,
5532
+ reason: z2.string().optional()
5533
+ }).strict()
5534
+ ]);
5535
+ var OperationResultSchema = z2.enum(["ok", "error", "refused", "timeout", "cancelled", "unavailable"]);
5536
+ var operationShared = {
5537
+ meta: HeaderMetaSchema,
5538
+ subject: SubjectSchema,
5539
+ kind: z2.literal("operation"),
5540
+ ...envelopeTail,
5541
+ operation: z2.string(),
5542
+ target: z2.string().nullable()
5543
+ };
5544
+ var OperationEventSchema = z2.discriminatedUnion("event", [
5545
+ z2.object({
5546
+ ...operationShared,
5547
+ event: z2.literal("completed"),
5548
+ result: OperationResultSchema,
5549
+ error_class: z2.string().nullable()
5550
+ }).strict()
5551
+ ]);
5552
+ var UsageUnitsSchema = z2.object({
5553
+ input: z2.number().nonnegative().nullable(),
5554
+ output: z2.number().nonnegative().nullable(),
5555
+ cache: z2.number().nonnegative().nullable()
5556
+ }).strict();
5557
+ var usageShared = {
5558
+ meta: HeaderMetaSchema,
5559
+ subject: SubjectSchema,
5560
+ kind: z2.literal("usage"),
5561
+ ...envelopeTail,
5562
+ model: z2.string().nullable(),
5563
+ source: z2.string(),
5564
+ semantics: z2.enum(["cumulative", "delta"])
5565
+ };
5566
+ var UsageEventSchema = z2.discriminatedUnion("event", [
5567
+ z2.object({
5568
+ ...usageShared,
5569
+ event: z2.literal("observed"),
5570
+ units: UsageUnitsSchema,
5571
+ unknown_reason: z2.string().nullable()
5572
+ }).strict()
5573
+ ]);
5574
+ var RoleAttemptOutcomeSchema = z2.enum([
5575
+ "completed",
5576
+ "incomplete",
5577
+ "infrastructure_failed",
5578
+ "cancelled",
5579
+ "timed_out",
5580
+ "capability_refused"
5581
+ ]);
5582
+ var roleAttemptShared = {
5583
+ meta: HeaderMetaSchema,
5584
+ subject: SubjectSchema,
5585
+ kind: z2.literal("role_attempt"),
5586
+ ...envelopeTail,
5587
+ actor: z2.string().nullable(),
5588
+ attempt: z2.number().int().nullable()
5589
+ };
5590
+ var RoleAttemptEventSchema = z2.discriminatedUnion("event", [
5591
+ z2.object({
5592
+ ...roleAttemptShared,
5593
+ event: z2.literal("attempted"),
5594
+ outcome: RoleAttemptOutcomeSchema,
5595
+ usage: dispatchUsageField
5596
+ }).strict()
5597
+ ]);
5598
+ var handoffShared = {
5599
+ meta: HeaderMetaSchema,
5600
+ subject: SubjectSchema,
5601
+ kind: z2.literal("handoff"),
5602
+ ...envelopeTail,
5603
+ class: z2.enum(["authority", "strategy", "product"]),
5604
+ reason: z2.string()
5605
+ };
5606
+ var HandoffEventSchema = z2.discriminatedUnion("event", [
5607
+ z2.object({
5608
+ ...handoffShared,
5609
+ event: z2.literal("raised"),
5610
+ requested_decision: z2.string().nullable()
5611
+ }).strict(),
5612
+ z2.object({
5613
+ ...handoffShared,
5614
+ event: z2.literal("resolved"),
5615
+ resolution: z2.string().nullable(),
5616
+ resolved_by: z2.string().nullable()
5617
+ }).strict()
5063
5618
  ]);
5064
- var LogEventSchema = z.union([DispatchEventSchema, DevReviewLoopEventSchema, ForgeWriteEventSchema]);
5619
+ var EffectTargetSchema = z2.object({
5620
+ kind: z2.string(),
5621
+ ref: z2.string()
5622
+ }).strict();
5623
+ var effectShared = {
5624
+ meta: HeaderMetaSchema,
5625
+ subject: SubjectSchema,
5626
+ kind: z2.literal("effect"),
5627
+ ...envelopeTail,
5628
+ effect_id: z2.string(),
5629
+ target: EffectTargetSchema
5630
+ };
5631
+ var EffectEventSchema = z2.discriminatedUnion("event", [
5632
+ z2.object({ ...effectShared, event: z2.literal("attempted") }).strict(),
5633
+ z2.object({ ...effectShared, event: z2.literal("observed"), outcome: z2.enum(["success", "failure", "uncertain"]) }).strict(),
5634
+ z2.object({ ...effectShared, event: z2.literal("verified"), outcome: z2.enum(["success", "failure", "uncertain"]) }).strict()
5635
+ ]);
5636
+ var LogEventSchema = z2.union([
5637
+ DispatchEventSchema,
5638
+ DevReviewLoopEventSchema,
5639
+ ForgeWriteEventSchema,
5640
+ GateEventSchema,
5641
+ OperationEventSchema,
5642
+ UsageEventSchema,
5643
+ RoleAttemptEventSchema,
5644
+ HandoffEventSchema,
5645
+ EffectEventSchema
5646
+ ]);
5647
+ // ../../packages/aeg-core/src/dev-review-loop/journal-reconstruction.ts
5648
+ var STOPPED_CONDITIONS = new Set(["confidence", "reappearance", "no_progress", "max_rounds"]);
5649
+ // ../../packages/aeg-core/src/control-store/records.ts
5650
+ import { z as z3 } from "zod";
5651
+ var isoTimestamp = z3.string().min(1);
5652
+ var taskId = z3.number().int().positive();
5653
+ var epochNumber = z3.number().int().nonnegative();
5654
+ var RunRecordSchema = z3.object({
5655
+ version: z3.literal(1),
5656
+ kind: z3.literal("run"),
5657
+ task: taskId,
5658
+ runId: z3.string().min(1),
5659
+ pid: z3.number().int().positive(),
5660
+ host: z3.string().min(1),
5661
+ startedAt: isoTimestamp
5662
+ }).strict();
5663
+ var InputRecordSchema = z3.object({
5664
+ version: z3.literal(1),
5665
+ kind: z3.literal("input"),
5666
+ task: taskId,
5667
+ runId: z3.string().min(1),
5668
+ source: z3.union([z3.literal("fresh"), z3.literal("resume")]),
5669
+ pr: z3.number().int().positive().nullable(),
5670
+ round: z3.number().int().nonnegative(),
5671
+ recordedAt: isoTimestamp
5672
+ }).strict();
5673
+ var OwnershipRecordSchema = z3.object({
5674
+ version: z3.literal(1),
5675
+ kind: z3.literal("ownership"),
5676
+ task: taskId,
5677
+ epoch: epochNumber,
5678
+ ownerId: z3.string().min(1),
5679
+ pid: z3.number().int().positive(),
5680
+ host: z3.string().min(1),
5681
+ acquiredAt: isoTimestamp
5682
+ }).strict();
5683
+ var TransitionRecordSchema = z3.object({
5684
+ version: z3.literal(1),
5685
+ kind: z3.literal("transition"),
5686
+ task: taskId,
5687
+ epoch: epochNumber,
5688
+ seq: z3.number().int().nonnegative(),
5689
+ from: z3.string().min(1),
5690
+ to: z3.string().min(1),
5691
+ detail: z3.string().optional(),
5692
+ at: isoTimestamp
5693
+ }).strict();
5065
5694
  // src/checks/contract.ts
5066
5695
  var CHECK_SCHEMA_VERSION = 1;
5067
5696
  function emitCheckError(error) {
@@ -5074,27 +5703,29 @@ import { execFileSync as execFileSync2 } from "node:child_process";
5074
5703
  import { existsSync, mkdirSync, readFileSync as readFileSync2, realpathSync, writeFileSync } from "node:fs";
5075
5704
  import { homedir } from "node:os";
5076
5705
  import { dirname, join, resolve } from "node:path";
5077
- import { z as z2 } from "zod";
5706
+ import { z as z4 } from "zod";
5078
5707
 
5079
5708
  // src/lib/agent-vendors.ts
5080
5709
  var AGENT_VENDORS = ["skills", "claude", "gemini"];
5081
5710
 
5082
5711
  // src/lib/config.ts
5083
- var EnvEntrySchema = z2.union([
5084
- z2.literal(true),
5085
- z2.object({ optional: z2.literal(true) }),
5086
- z2.object({ anyOf: z2.array(z2.string()).min(2) }),
5087
- z2.string()
5712
+ var EnvEntrySchema = z4.union([
5713
+ z4.literal(true),
5714
+ z4.object({ optional: z4.literal(true) }),
5715
+ z4.object({ anyOf: z4.array(z4.string()).min(2) }),
5716
+ z4.string()
5088
5717
  ]);
5089
- var CheckEntrySchema = z2.object({
5090
- run: z2.string(),
5091
- scope: z2.enum(["diff", "full"]),
5092
- include: z2.array(z2.string()).optional(),
5093
- args: z2.array(z2.string()).optional(),
5094
- timeoutMs: z2.number().optional(),
5095
- env: z2.record(z2.string(), EnvEntrySchema).optional(),
5096
- requiresOpenPr: z2.boolean().optional(),
5097
- ownWorkflow: z2.boolean().optional()
5718
+ var CheckEntrySchema = z4.object({
5719
+ run: z4.string(),
5720
+ scope: z4.enum(["diff", "full"]),
5721
+ include: z4.array(z4.string()).optional(),
5722
+ args: z4.array(z4.string()).optional(),
5723
+ timeoutMs: z4.number().optional(),
5724
+ env: z4.record(z4.string(), EnvEntrySchema).optional(),
5725
+ requiresOpenPr: z4.boolean().optional(),
5726
+ ownWorkflow: z4.boolean().optional(),
5727
+ principalOwed: z4.literal(true).optional(),
5728
+ validates: z4.enum(["body", "issue"]).optional()
5098
5729
  }).superRefine((entry2, ctx) => {
5099
5730
  if (!entry2.env)
5100
5731
  return;
@@ -5104,25 +5735,26 @@ var CheckEntrySchema = z2.object({
5104
5735
  const members = value.anyOf;
5105
5736
  if (new Set(members).size !== members.length) {
5106
5737
  ctx.addIssue({
5107
- code: z2.ZodIssueCode.custom,
5738
+ code: z4.ZodIssueCode.custom,
5108
5739
  message: `env.${key}.anyOf has duplicate members`,
5109
5740
  path: ["env", key, "anyOf"]
5110
5741
  });
5111
5742
  }
5112
5743
  if (!members.includes(key)) {
5113
5744
  ctx.addIssue({
5114
- code: z2.ZodIssueCode.custom,
5745
+ code: z4.ZodIssueCode.custom,
5115
5746
  message: `env.${key}.anyOf must include "${key}" itself as a member`,
5116
5747
  path: ["env", key, "anyOf"]
5117
5748
  });
5118
5749
  }
5119
5750
  }
5120
5751
  });
5121
- var RoleEntrySchema = z2.object({
5122
- contract: z2.string().refine((p) => p.includes("/"), {
5752
+ var RoleEntrySchema = z4.object({
5753
+ contract: z4.string().refine((p) => p.includes("/"), {
5123
5754
  message: 'must be a path (contain at least one "/"), not a bare filename'
5124
5755
  })
5125
5756
  });
5757
+ var MAX_REPORT_COMMAND_TIMEOUT_MS = 3600000;
5126
5758
  var BRIEF_BUILTINS = [
5127
5759
  "tier",
5128
5760
  "testPlan",
@@ -5142,17 +5774,17 @@ var BRIEF_BUILTINS = [
5142
5774
  "briefSections",
5143
5775
  "milestoneShape"
5144
5776
  ];
5145
- var BriefSectionSchema = z2.union([
5146
- z2.object({ builtin: z2.enum(BRIEF_BUILTINS) }),
5147
- z2.object({ heading: z2.string().min(1), name: z2.string().optional() }),
5148
- z2.object({ field: z2.string().min(1), name: z2.string().optional() }),
5149
- z2.object({ phrase: z2.string().min(1), name: z2.string().optional() })
5777
+ var BriefSectionSchema = z4.union([
5778
+ z4.object({ builtin: z4.enum(BRIEF_BUILTINS) }),
5779
+ z4.object({ heading: z4.string().min(1), name: z4.string().optional() }),
5780
+ z4.object({ field: z4.string().min(1), name: z4.string().optional() }),
5781
+ z4.object({ phrase: z4.string().min(1), name: z4.string().optional() })
5150
5782
  ]);
5151
- var BriefSchemaSchema = z2.object({
5152
- pr: z2.object({ sections: z2.array(BriefSectionSchema) }).optional(),
5153
- issue: z2.object({ sections: z2.array(BriefSectionSchema) }).optional(),
5154
- milestone: z2.object({ sections: z2.array(BriefSectionSchema) }).optional(),
5155
- ack: z2.array(z2.enum(BRIEF_BUILTINS)).optional()
5783
+ var BriefSchemaSchema = z4.object({
5784
+ pr: z4.object({ sections: z4.array(BriefSectionSchema) }).optional(),
5785
+ issue: z4.object({ sections: z4.array(BriefSectionSchema) }).optional(),
5786
+ milestone: z4.object({ sections: z4.array(BriefSectionSchema) }).optional(),
5787
+ ack: z4.array(z4.enum(BRIEF_BUILTINS)).optional()
5156
5788
  });
5157
5789
  function isSafeRepoRelPath(p) {
5158
5790
  if (p.length === 0)
@@ -5161,7 +5793,7 @@ function isSafeRepoRelPath(p) {
5161
5793
  return false;
5162
5794
  return p.split(/[\\/]/).every((seg) => seg !== ".." && seg !== "");
5163
5795
  }
5164
- var SafeRepoRelPath = z2.string().refine(isSafeRepoRelPath, {
5796
+ var SafeRepoRelPath = z4.string().refine(isSafeRepoRelPath, {
5165
5797
  message: "must be a repo-root-relative path with no `..` segment or absolute root"
5166
5798
  });
5167
5799
  var CANONICAL_HOOK_BLOCK_PREFIXES = [".git/", ".husky/", ".vinaya/hooks/", ".claude/hooks/"];
@@ -5171,22 +5803,22 @@ function isCanonicalHookBlockPath(p) {
5171
5803
  var ManagedHookBlockPath = SafeRepoRelPath.refine(isCanonicalHookBlockPath, {
5172
5804
  message: `must start with one of ${CANONICAL_HOOK_BLOCK_PREFIXES.join(", ")} (byte-exact, case-sensitive)`
5173
5805
  });
5174
- var ManagedBlockRecordSchema = z2.object({
5806
+ var ManagedBlockRecordSchema = z4.object({
5175
5807
  path: ManagedHookBlockPath,
5176
- marker: z2.string(),
5177
- comment: z2.enum(["hash", "html"])
5808
+ marker: z4.string(),
5809
+ comment: z4.enum(["hash", "html"])
5178
5810
  });
5179
- var ManagedManifestSchema = z2.object({
5180
- version: z2.number().int().positive(),
5181
- files: z2.array(SafeRepoRelPath),
5182
- blocks: z2.array(ManagedBlockRecordSchema),
5183
- labels: z2.array(z2.string()),
5184
- agents: z2.array(z2.enum(AGENT_VENDORS)).optional()
5811
+ var ManagedManifestSchema = z4.object({
5812
+ version: z4.number().int().positive(),
5813
+ files: z4.array(SafeRepoRelPath),
5814
+ blocks: z4.array(ManagedBlockRecordSchema),
5815
+ labels: z4.array(z4.string()),
5816
+ agents: z4.array(z4.enum(AGENT_VENDORS)).optional()
5185
5817
  });
5186
- var ProjectEntrySchema = z2.object({
5187
- name: z2.string().min(1),
5188
- description: z2.string().min(1).optional(),
5189
- path: z2.string().min(1).optional()
5818
+ var ProjectEntrySchema = z4.object({
5819
+ name: z4.string().min(1),
5820
+ description: z4.string().min(1).optional(),
5821
+ path: z4.string().min(1).optional()
5190
5822
  });
5191
5823
  function parseTokensCollectDeclaration(value) {
5192
5824
  const m = value.trim().match(/^(\S+)\s+(\S+)$/);
@@ -5197,34 +5829,55 @@ function parseTokensCollectDeclaration(value) {
5197
5829
  return null;
5198
5830
  return { interpreter, script };
5199
5831
  }
5200
- var VinayaConfigSchema = z2.object({
5201
- rings: z2.object({
5202
- ring1_forgeWriteInterception: z2.boolean(),
5203
- ring2_asyncAudits: z2.boolean()
5832
+ var VinayaConfigSchema = z4.object({
5833
+ rings: z4.object({
5834
+ ring1_forgeWriteInterception: z4.boolean(),
5835
+ ring2_asyncAudits: z4.boolean()
5204
5836
  }).optional(),
5205
- checks: z2.record(z2.string(), CheckEntrySchema).optional(),
5206
- roles: z2.record(z2.string(), RoleEntrySchema).optional(),
5837
+ checks: z4.record(z4.string(), CheckEntrySchema).optional(),
5838
+ roles: z4.record(z4.string(), RoleEntrySchema).optional(),
5207
5839
  briefSchema: BriefSchemaSchema.optional(),
5208
5840
  managed: ManagedManifestSchema.optional(),
5209
- principals: z2.array(z2.string()).min(1).optional(),
5210
- releaseActor: z2.string().min(1).optional(),
5211
- ci: z2.object({ setup: z2.string().min(1) }).optional(),
5212
- tokens: z2.object({
5213
- collect: z2.string().min(1).refine((v) => parseTokensCollectDeclaration(v) !== null, {
5841
+ principals: z4.array(z4.string()).min(1).optional(),
5842
+ releaseActor: z4.string().min(1).optional(),
5843
+ ci: z4.object({ setup: z4.string().min(1) }).optional(),
5844
+ tokens: z4.object({
5845
+ collect: z4.string().min(1).refine((v) => parseTokensCollectDeclaration(v) !== null, {
5214
5846
  message: 'tokens.collect must be exactly "<interpreter> <repo-relative-script-path>" — two whitespace-separated tokens, no flags, no shell syntax, no quoting'
5215
5847
  })
5216
5848
  }).optional(),
5217
- blastRadius: z2.object({ extraDomains: z2.array(z2.string()).optional() }).optional(),
5218
- proseGates: z2.object({
5219
- doctrineRoot: z2.string().min(1).optional(),
5220
- readerFacingPrefix: z2.string().min(1).optional(),
5221
- readerFacingSuffix: z2.string().min(1).optional(),
5222
- legacySlugDir: z2.string().min(1).optional()
5849
+ blastRadius: z4.object({ extraDomains: z4.array(z4.string()).optional() }).optional(),
5850
+ proseGates: z4.object({
5851
+ doctrineRoot: z4.string().min(1).optional(),
5852
+ readerFacingPrefix: z4.string().min(1).optional(),
5853
+ readerFacingSuffix: z4.string().min(1).optional(),
5854
+ legacySlugDir: z4.string().min(1).optional()
5223
5855
  }).optional(),
5224
- projects: z2.array(ProjectEntrySchema).optional(),
5225
- dispatch: z2.object({
5226
- timeoutMs: z2.number().int().positive().optional(),
5227
- agent: z2.enum(["claude", "codex", "gemini"]).optional()
5856
+ projects: z4.array(ProjectEntrySchema).optional(),
5857
+ dispatch: z4.object({
5858
+ timeoutMs: z4.number().int().positive().optional(),
5859
+ killGraceMs: z4.number().int().positive().optional(),
5860
+ agent: z4.enum(["claude", "codex", "gemini"]).optional()
5861
+ }).optional(),
5862
+ reviewPolicy: z4.object({
5863
+ codeReviewThreshold: z4.string().min(1).optional(),
5864
+ securityThreshold: z4.string().min(1).optional(),
5865
+ maxRounds: z4.number().optional()
5866
+ }).optional(),
5867
+ prePush: z4.object({
5868
+ alwaysRun: z4.array(z4.string()).optional()
5869
+ }).optional(),
5870
+ report: z4.object({
5871
+ commandTimeoutMs: z4.number().int().positive().optional()
5872
+ }).superRefine((report, ctx) => {
5873
+ const commandTimeoutMs = report.commandTimeoutMs;
5874
+ if (commandTimeoutMs !== undefined && commandTimeoutMs > MAX_REPORT_COMMAND_TIMEOUT_MS) {
5875
+ ctx.addIssue({
5876
+ code: z4.ZodIssueCode.custom,
5877
+ path: ["commandTimeoutMs"],
5878
+ message: "must be at most 3600000 (1 hour)"
5879
+ });
5880
+ }
5228
5881
  }).optional()
5229
5882
  });
5230
5883
  var GLOBAL_VINAYA_HOME = join(homedir(), ".vinaya");
@@ -5299,6 +5952,28 @@ function resolvePrincipalAllowlist(config) {
5299
5952
  function resolveReleaseActor(config) {
5300
5953
  return config?.releaseActor ?? DEFAULT_RELEASE_ACTOR;
5301
5954
  }
5955
+ function resolveReviewPolicy(config) {
5956
+ const raw = config?.reviewPolicy;
5957
+ if (!raw)
5958
+ return DEFAULT_REVIEW_POLICY;
5959
+ const codeReviewThreshold = raw.codeReviewThreshold ?? DEFAULT_REVIEW_POLICY.codeReviewThreshold;
5960
+ if (!isKnownSeverity(CODE_REVIEW_SEVERITY_ORDER, codeReviewThreshold)) {
5961
+ 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.`);
5962
+ }
5963
+ const securityThreshold = raw.securityThreshold ?? DEFAULT_REVIEW_POLICY.securityThreshold;
5964
+ if (!isKnownSeverity(SECURITY_SEVERITY_ORDER, securityThreshold)) {
5965
+ 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.`);
5966
+ }
5967
+ const maxRounds = raw.maxRounds ?? DEFAULT_REVIEW_POLICY.maxRounds;
5968
+ if (!Number.isInteger(maxRounds) || maxRounds < 1) {
5969
+ throw new Error(`vinaya.config.json: reviewPolicy.maxRounds "${maxRounds}" is not a positive integer — fix the config, this never falls back to a default.`);
5970
+ }
5971
+ return {
5972
+ codeReviewThreshold,
5973
+ securityThreshold,
5974
+ maxRounds
5975
+ };
5976
+ }
5302
5977
  function trustAnchorRepo() {
5303
5978
  const wellFormed = (slug) => /^[^/\s]+\/[^/\s]+$/.test(slug) ? slug : null;
5304
5979
  const fromRunner = process.env.GITHUB_REPOSITORY?.trim();
@@ -5426,6 +6101,44 @@ function fetchIssueBodyForObjectives(issueNumber) {
5426
6101
  stdio: ["ignore", "pipe", "pipe"]
5427
6102
  });
5428
6103
  }
6104
+ function fetchIssueCommentsForBrief(issueNumber) {
6105
+ const out = execFileSync4("gh", ["issue", "view", String(issueNumber), "--json", "comments"], {
6106
+ encoding: "utf8",
6107
+ stdio: ["ignore", "pipe", "pipe"]
6108
+ });
6109
+ const parsed = JSON.parse(out);
6110
+ return parsed.comments.map((c) => ({ body: c.body, author: c.author?.login ?? null }));
6111
+ }
6112
+ function resolveBriefHash(pr, principalAllowlist) {
6113
+ const { issue } = extractIssue(pr.body);
6114
+ if (issue === null)
6115
+ return null;
6116
+ let comments;
6117
+ try {
6118
+ comments = fetchIssueCommentsForBrief(issue);
6119
+ } catch (err) {
6120
+ if (isIssueNotFoundError(err)) {
6121
+ emitCheckError({
6122
+ schema: CHECK_SCHEMA_VERSION,
6123
+ check: CHECK_NAME,
6124
+ severity: "error",
6125
+ message: `review-gate severity:infra — Issue #${issue} does not resolve via \`gh issue view\` — cannot verify the brief-hash binding for a PR whose linked Issue no longer exists.`,
6126
+ agent_recovery_prompt: `Restore Issue #${issue}, fix \`Closes #N\` to name a real Issue, or have a principal apply the \`vinaya/waiver:review\` label, then re-run \`vinaya check review-gate\`.`
6127
+ });
6128
+ process.exit(1);
6129
+ }
6130
+ emitCheckError({
6131
+ schema: CHECK_SCHEMA_VERSION,
6132
+ check: CHECK_NAME,
6133
+ severity: "error",
6134
+ message: `review-gate severity:infra — could not fetch Issue #${issue}'s comments via \`gh issue view\` to resolve its frozen-brief hash: ${err.message}`,
6135
+ agent_recovery_prompt: "Confirm `gh auth status` passes and the Issue number is correct, then re-run `vinaya check review-gate`."
6136
+ });
6137
+ process.exit(1);
6138
+ }
6139
+ const frozen = resolveNewestFrozenBrief(comments, principalAllowlist);
6140
+ return frozen ? briefHash(frozen.content) : null;
6141
+ }
5429
6142
  function resolveObjectivesVersion(pr) {
5430
6143
  const { issue } = extractIssue(pr.body);
5431
6144
  const source = resolveObjectivesSource(pr.body, issue, OBJECTIVES_SINCE_ISSUE);
@@ -5481,6 +6194,15 @@ function resolveObjectivesVersion(pr) {
5481
6194
  }
5482
6195
  return objectivesVersion(own.objectives);
5483
6196
  }
6197
+ function uncheckedPrincipalReason(body, branch) {
6198
+ const result = evaluateTestPlanGate(body, branch);
6199
+ if (result.verdict !== "fail")
6200
+ return null;
6201
+ const uncheckedLines = result.messages.filter((m) => /^\s*[-*]\s+\[\s\]/.test(m)).map((m) => m.trim());
6202
+ if (uncheckedLines.length === 0)
6203
+ return null;
6204
+ return `unticked [principal] Test Plan item(s) — ${uncheckedLines.join("; ")}`;
6205
+ }
5484
6206
  function shaFromLsRemote(branch) {
5485
6207
  try {
5486
6208
  const out = execFileSync4("git", ["ls-remote", "origin", `refs/heads/${branch}`], {
@@ -5604,7 +6326,21 @@ function main() {
5604
6326
  });
5605
6327
  process.exit(1);
5606
6328
  }
5607
- const principalAllowlist = resolvePrincipalAllowlist(loadTrustAnchorConfig());
6329
+ const trustAnchorConfig = loadTrustAnchorConfig();
6330
+ const principalAllowlist = resolvePrincipalAllowlist(trustAnchorConfig);
6331
+ let reviewPolicy;
6332
+ try {
6333
+ reviewPolicy = resolveReviewPolicy(trustAnchorConfig);
6334
+ } catch (err) {
6335
+ emitCheckError({
6336
+ schema: CHECK_SCHEMA_VERSION,
6337
+ check: CHECK_NAME,
6338
+ severity: "error",
6339
+ message: `review-gate severity:infra — ${err instanceof Error ? err.message : String(err)}`,
6340
+ agent_recovery_prompt: "Fix `reviewPolicy` in the default branch's `vinaya.config.json` to a known severity on each role's own scale, then re-run `vinaya check review-gate`."
6341
+ });
6342
+ process.exit(1);
6343
+ }
5608
6344
  const waived = isWaiverLabelActorVerified({
5609
6345
  label: WAIVER_LABEL_REVIEW,
5610
6346
  labels,
@@ -5620,8 +6356,11 @@ function main() {
5620
6356
  headSha,
5621
6357
  patchIdOf: (sha) => patchIdAt(pr.baseRefName, sha),
5622
6358
  objectivesVersion: waived ? null : resolveObjectivesVersion(pr),
5623
- rulingOrdinal: newestPrincipalRulingOrdinal(pr.comments.map((c) => ({ body: c.body, author: c.author?.login ?? null })), principalAllowlist)
6359
+ rulingOrdinal: newestPrincipalRulingOrdinal(pr.comments.map((c) => ({ body: c.body, author: c.author?.login ?? null })), principalAllowlist),
6360
+ policy: reviewPolicy,
6361
+ briefHash: waived ? null : resolveBriefHash(pr, principalAllowlist)
5624
6362
  });
6363
+ let failed = false;
5625
6364
  if (result.verdict === "fail") {
5626
6365
  emitCheckError({
5627
6366
  schema: CHECK_SCHEMA_VERSION,
@@ -5630,8 +6369,24 @@ function main() {
5630
6369
  message: `review-gate (PR #${prNumber}): ${result.reason}`,
5631
6370
  agent_recovery_prompt: "Wait for a code-reviewer APPROVE and a security-review PASS on this PR (or ask a principal to apply the `vinaya/waiver:review` label), then re-run `vinaya check review-gate`."
5632
6371
  });
5633
- process.exit(1);
6372
+ failed = true;
5634
6373
  }
5635
- process.exit(0);
6374
+ const principalReason = uncheckedPrincipalReason(pr.body, pr.headRefName);
6375
+ if (principalReason !== null) {
6376
+ emitCheckError({
6377
+ schema: CHECK_SCHEMA_VERSION,
6378
+ check: CHECK_NAME,
6379
+ severity: "error",
6380
+ message: `review-gate (PR #${prNumber}): ${principalReason}`,
6381
+ agent_recovery_prompt: "Nothing for the Developer to fix here — wait for the Principal to verify in a real signed-in browser and tick each `[principal]` Test Plan box, then re-run `vinaya check review-gate`."
6382
+ });
6383
+ failed = true;
6384
+ }
6385
+ process.exit(failed ? 1 : 0);
6386
+ }
6387
+ if (__require.main == __require.module) {
6388
+ main();
5636
6389
  }
5637
- main();
6390
+ export {
6391
+ uncheckedPrincipalReason
6392
+ };