@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
@@ -1957,6 +1957,8 @@ function parseInlineFieldList(section) {
1957
1957
  var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
1958
1958
  var OBJECTIVES_VERSION_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Objectives version:\s*([0-9a-f]{64})(?![A-Za-z0-9])/im;
1959
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;
1960
1962
  function firstFiveLines(comment) {
1961
1963
  return comment.split(`
1962
1964
  `).slice(0, 5).join(`
@@ -1979,6 +1981,26 @@ function extractRulingOrdinal(comment) {
1979
1981
  const m = firstSevenLines(comment).match(RULING_ORDINAL_PATTERN);
1980
1982
  return m ? Number.parseInt(m[1], 10) : null;
1981
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
+ }
1982
2004
  function extractVerdict(comments, valuePattern, missingLabel) {
1983
2005
  const candidates = comments.filter((c) => valuePattern.test(c));
1984
2006
  if (candidates.length === 0) {
@@ -1987,6 +2009,9 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1987
2009
  headSha: null,
1988
2010
  objectivesVersion: null,
1989
2011
  rulingOrdinal: null,
2012
+ briefHash: null,
2013
+ policyDigest: null,
2014
+ findingSeverities: [],
1990
2015
  danglingNote: `no ${missingLabel} verdict comment found on this PR`
1991
2016
  };
1992
2017
  }
@@ -1998,6 +2023,9 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1998
2023
  headSha: null,
1999
2024
  objectivesVersion: null,
2000
2025
  rulingOrdinal: null,
2026
+ briefHash: null,
2027
+ policyDigest: null,
2028
+ findingSeverities: [],
2001
2029
  danglingNote: `the most recent ${missingLabel} verdict comment carries a VERDICT-shaped line outside the first-five-line read window`
2002
2030
  };
2003
2031
  }
@@ -2006,6 +2034,9 @@ function extractVerdict(comments, valuePattern, missingLabel) {
2006
2034
  headSha: extractHeadSha(latest),
2007
2035
  objectivesVersion: extractObjectivesVersion(latest),
2008
2036
  rulingOrdinal: extractRulingOrdinal(latest),
2037
+ briefHash: extractBriefHash(latest),
2038
+ policyDigest: extractPolicyDigest(latest),
2039
+ findingSeverities: extractFindingSeverities(latest),
2009
2040
  danglingNote: null
2010
2041
  };
2011
2042
  }
@@ -2341,6 +2372,53 @@ function evaluateC5(changed, docOwnersContent, prBody, fileExists, waiverActive,
2341
2372
  }
2342
2373
  return out;
2343
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
+ }
2344
2422
  // ../../packages/aeg-core/src/pr-tier.ts
2345
2423
  var TIER_FIELD = /(\*\*)?\s*Tier\s*(\*\*)?\s*:\s*(\*\*)?\s*([013])\b/i;
2346
2424
  function readTierFromPrBody(prBody) {
@@ -2711,7 +2789,7 @@ function checkForField(prBody) {
2711
2789
  ]
2712
2790
  };
2713
2791
  }
2714
- function checkClosesN(prBody) {
2792
+ function checkClosesNPresence(prBody) {
2715
2793
  const closesPattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s{0,8}:?\s{0,8}#\d+/i;
2716
2794
  if (closesPattern.test(stripCode(prBody))) {
2717
2795
  return { status: "pass", errors: [] };
@@ -2743,10 +2821,22 @@ var COMMIT_TYPES = [
2743
2821
  "Test"
2744
2822
  ];
2745
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
+ }
2746
2835
  var BRIEF_SHAPE_MARKERS = [checkSurfaceMap, checkDocUpdateList, checkStopConditions, checkAutonomyClause];
2747
2836
  var TASK_BRANCH_PATTERN = /^task\/[^/]+\/[^/]+$/;
2837
+ var TASK_ISSUE_BRANCH_PATTERN = /^task\/issue-\d+$/;
2748
2838
  function isTaskBranch(branch) {
2749
- return TASK_BRANCH_PATTERN.test(branch);
2839
+ return TASK_BRANCH_PATTERN.test(branch) || TASK_ISSUE_BRANCH_PATTERN.test(branch);
2750
2840
  }
2751
2841
  function isBriefShaped(prBody) {
2752
2842
  const stripped = stripCode(prBody);
@@ -2868,8 +2958,9 @@ function packagesNamedIn(text) {
2868
2958
  }
2869
2959
  function hasTestPathForConsumer(text, consumerDir) {
2870
2960
  const escaped = consumerDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2871
- const re = new RegExp(`${escaped}\\/[\\w./-]*\\.test\\.[A-Za-z0-9]+`);
2872
- 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);
2873
2964
  }
2874
2965
  function checkConsumerTests(prBody, consumersOf) {
2875
2966
  const section4 = extractNumberedSection(prBody, 4);
@@ -2973,7 +3064,7 @@ function checkBriefSections(prBody, readTier, options = {}) {
2973
3064
  checkConsumerTests(prBody, consumersOf),
2974
3065
  checkDefeatCases(prBody),
2975
3066
  ...issueObjectives !== undefined ? [checkObjectivesCopy(prBody, issueObjectives), checkObjectivesCoverage(prBody)] : [],
2976
- ...requireClosesN ? [checkClosesN(prBody)] : []
3067
+ ...requireClosesN ? [checkClosesNPresence(prBody)] : []
2977
3068
  ];
2978
3069
  return { errors: results.flatMap((r) => r.errors) };
2979
3070
  }
@@ -3263,6 +3354,28 @@ function topLevelSectionText(body, headingName) {
3263
3354
  const next = /^##[ \t]/m.exec(afterHeading);
3264
3355
  return next ? afterHeading.slice(0, next.index) : afterHeading;
3265
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
+ }
3266
3379
  function looksLikeFilePath(entry2) {
3267
3380
  const stripped = entry2.replace(/\/\*\*?$/, "");
3268
3381
  const lastSegment = stripped.split("/").pop() ?? stripped;
@@ -3303,6 +3416,18 @@ function globCoversPath(glob, path) {
3303
3416
  const p = path.replace(/\/+$/, "");
3304
3417
  return g === p || g.startsWith(`${p}/`) || p.startsWith(`${g}/`);
3305
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
+ }
3306
3431
  function checkSurfaceScope(changedFiles, outGlobs) {
3307
3432
  if (outGlobs.length === 0)
3308
3433
  return { ok: true };
@@ -3314,9 +3439,77 @@ function checkSurfaceScope(changedFiles, outGlobs) {
3314
3439
  }
3315
3440
  return violations.length > 0 ? { ok: false, violations } : { ok: true };
3316
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
+ }
3317
3493
  function isTaskIssueLabelSet(labels) {
3318
3494
  return hasLabel("tranche", labels);
3319
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
+ }
3320
3513
  var TYPE_LABEL_IDS = LABELS.filter((l) => l.category === "type").map((l) => l.id);
3321
3514
  function isControlCodePoint(codePoint) {
3322
3515
  return codePoint <= 31 || codePoint >= 127 && codePoint <= 159;
@@ -3381,6 +3574,19 @@ function checkProjectsRegistered(body, _labels, registeredNames) {
3381
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;
3382
3575
  var DOC_PATH_RE_GLOBAL = new RegExp(DOC_PATH_RE.source, "gi");
3383
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
+
3384
3590
  // ../../packages/aeg-core/src/coherence-checks.ts
3385
3591
  var COHERENCE_ENFORCED_FROM = "2026-07-01";
3386
3592
  function isGrandfathered(isoDate) {
@@ -3401,6 +3607,7 @@ function checkA1(entries, principalAllowlist = PRINCIPAL_ALLOWLIST) {
3401
3607
  if (handClosed)
3402
3608
  continue;
3403
3609
  failures.push({
3610
+ code: "closed-without-merge",
3404
3611
  issue: e.task.issue,
3405
3612
  tranche: e.trancheSlug,
3406
3613
  task: e.task.id,
@@ -3425,6 +3632,7 @@ function checkA3(entries) {
3425
3632
  continue;
3426
3633
  if (e.facts.prState === "merged" && e.facts.issueState !== "closed") {
3427
3634
  failures.push({
3635
+ code: "auto-close-misfire",
3428
3636
  issue: e.task.issue,
3429
3637
  tranche: e.trancheSlug,
3430
3638
  task: e.task.id,
@@ -3449,6 +3657,7 @@ function checkT1(entries) {
3449
3657
  continue;
3450
3658
  if (e.facts === undefined) {
3451
3659
  failures.push({
3660
+ code: "phantom-issue-ref",
3452
3661
  issue: e.task.issue,
3453
3662
  tranche: e.trancheSlug,
3454
3663
  task: e.task.id,
@@ -3467,6 +3676,7 @@ function checkT2(openIssuesBySlug, topologyIssuesBySlug, ciTrancheSlug) {
3467
3676
  for (const num of openNums) {
3468
3677
  if (!topologySet.has(num)) {
3469
3678
  failures.push({
3679
+ code: "orphan-task",
3470
3680
  issue: num,
3471
3681
  tranche: slug,
3472
3682
  reason: `Issue #${num} is open and labeled ${trancheLabel(slug)} but does not appear in the topology file`
@@ -3501,6 +3711,7 @@ function checkT3(entries, ciTrancheSlug, enrichedEntries, forgeUnavailableSlugs)
3501
3711
  continue;
3502
3712
  if (forgeUnavailableSlugs?.has(e.trancheSlug)) {
3503
3713
  failures.push({
3714
+ code: "tbd-in-active-tranche",
3504
3715
  issue: null,
3505
3716
  tranche: e.trancheSlug,
3506
3717
  task: e.task.id,
@@ -3510,6 +3721,7 @@ function checkT3(entries, ciTrancheSlug, enrichedEntries, forgeUnavailableSlugs)
3510
3721
  continue;
3511
3722
  }
3512
3723
  failures.push({
3724
+ code: "tbd-in-active-tranche",
3513
3725
  issue: null,
3514
3726
  tranche: e.trancheSlug,
3515
3727
  task: e.task.id,
@@ -3542,6 +3754,7 @@ function checkD1(entries, issueToEntry, taskToEntry) {
3542
3754
  const sameIssue = depEntry.task.issue !== null && e.task.issue !== null && depEntry.task.issue === e.task.issue;
3543
3755
  if (sameTask || sameIssue) {
3544
3756
  failures.push({
3757
+ code: "d1-self-dependency",
3545
3758
  issue: e.task.issue,
3546
3759
  tranche: e.trancheSlug,
3547
3760
  task: e.task.id,
@@ -3553,6 +3766,7 @@ function checkD1(entries, issueToEntry, taskToEntry) {
3553
3766
  const depClosed = depFacts?.issueState === "closed";
3554
3767
  if (!depClosed) {
3555
3768
  failures.push({
3769
+ code: "dispatched-on-unmet-deps",
3556
3770
  issue: e.task.issue,
3557
3771
  tranche: e.trancheSlug,
3558
3772
  task: e.task.id,
@@ -3578,11 +3792,13 @@ function checkR1(issuesBySlug, grandfatheredIssues, registeredNames = []) {
3578
3792
  const errors = [
3579
3793
  ...checkIssueRationale(issue.body).errors,
3580
3794
  ...checkProjectsRegistered(issue.body, issue.labels, registeredNames).errors,
3581
- ...checkIssueObjectives(issue.body, issue.number).errors
3795
+ ...checkIssueObjectives(issue.body, issue.number).errors,
3796
+ ...checkPartsCiteDefinedObjectives(issue.body).errors
3582
3797
  ];
3583
3798
  if (errors.length === 0)
3584
3799
  continue;
3585
3800
  failures.push({
3801
+ code: "missing-rationale-field",
3586
3802
  issue: issue.number,
3587
3803
  tranche: slug,
3588
3804
  reason: `Issue #${issue.number} fails the rationale gate: ${errors.join(" | ")}`,
@@ -3611,6 +3827,7 @@ function checkL1(files, entriesBySlug) {
3611
3827
  const allClosed = withFacts.every((e) => e.facts?.issueState === "closed");
3612
3828
  if (allClosed) {
3613
3829
  failures.push({
3830
+ code: "archive-recommended",
3614
3831
  tranche: f.slug,
3615
3832
  reason: "Active tranche has no open task-Issues — consider archiving to completed/"
3616
3833
  });
@@ -3642,27 +3859,38 @@ function extractClosesReferences(prBody) {
3642
3859
  }
3643
3860
  return referenced;
3644
3861
  }
3645
- function checkClosesN2(branch, prBody, trancheFiles, taskIssueRefs) {
3862
+ function checkClosesNTopology(branch, prBody, trancheFiles, taskIssueRefs) {
3646
3863
  const referenced = extractClosesReferences(prBody);
3647
3864
  if (taskIssueRefs) {
3648
3865
  for (const n of referenced) {
3649
- const ref = taskIssueRefs.get(n);
3650
- if (!ref)
3866
+ const ref2 = taskIssueRefs.get(n);
3867
+ if (!ref2)
3651
3868
  continue;
3652
- const expectedBranch = `task/${ref.trancheSlug}/${ref.taskId}`;
3869
+ const expectedBranch = `task/${ref2.trancheSlug}/${ref2.taskId}`;
3653
3870
  if (branch !== expectedBranch) {
3654
3871
  return {
3655
3872
  ok: false,
3656
- 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.`
3657
3874
  };
3658
3875
  }
3659
3876
  }
3660
3877
  }
3661
- const m = branch.match(/^task\/([^/]+)\/([^/]+)$/);
3662
- if (!m)
3878
+ const ref = parseTaskBranchIdentity(branch);
3879
+ if (!ref)
3663
3880
  return { ok: true };
3664
- const trancheSlug = m[1];
3665
- 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;
3666
3894
  const trancheFile = trancheFiles.find((f) => f.slug === trancheSlug);
3667
3895
  if (!trancheFile) {
3668
3896
  return {
@@ -3714,34 +3942,67 @@ function extractIssue(body) {
3714
3942
  const issue = headerNums.length > 0 ? headerNums[0] : bodyNums[0];
3715
3943
  return { issue, extraIssues: bodyNums.filter((n) => n !== issue), outsideHeader: headerNums.length === 0 };
3716
3944
  }
3717
- // ../../packages/aeg-core/src/review-gate.ts
3718
- function isBoundToHead(extraction, headSha) {
3719
- 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)
3720
3956
  return false;
3721
- return headSha.toLowerCase().startsWith(extraction.headSha.toLowerCase());
3957
+ return headSha.toLowerCase().startsWith(echoed.headSha.toLowerCase());
3722
3958
  }
3723
- function isBoundByPatchIdentity(extraction, headSha, patchIdOf) {
3724
- if (patchIdOf === undefined || !extraction.headSha)
3959
+ function isBoundByPatchIdentity(echoed, headSha, patchIdOf) {
3960
+ if (patchIdOf === undefined || !echoed.headSha)
3725
3961
  return false;
3726
- const judged = patchIdOf(extraction.headSha);
3962
+ const judged = patchIdOf(echoed.headSha);
3727
3963
  const current = patchIdOf(headSha);
3728
3964
  if (judged === null || current === null)
3729
3965
  return false;
3730
3966
  return judged === current;
3731
3967
  }
3732
- function isBoundToPatch(extraction, headSha, patchIdOf) {
3733
- return isBoundToHead(extraction, headSha) || isBoundByPatchIdentity(extraction, headSha, patchIdOf);
3968
+ function isBoundToPatch(echoed, headSha, patchIdOf) {
3969
+ return isBoundToHead(echoed, headSha) || isBoundByPatchIdentity(echoed, headSha, patchIdOf);
3734
3970
  }
3735
- function isBoundToObjectives(extraction, currentVersion) {
3971
+ function isBoundToObjectives(echoed, currentVersion) {
3736
3972
  if (currentVersion === null)
3737
3973
  return true;
3738
- return extraction.objectivesVersion === currentVersion;
3974
+ return echoed.objectivesVersion === currentVersion;
3739
3975
  }
3740
- function isBoundToRulings(extraction, currentOrdinal) {
3741
- if (extraction.rulingOrdinal === null)
3976
+ function isBoundToRulings(echoed, currentOrdinal) {
3977
+ if (echoed.rulingOrdinal === null)
3742
3978
  return currentOrdinal === 0;
3743
- return extraction.rulingOrdinal === currentOrdinal;
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
+ };
3744
4003
  }
4004
+
4005
+ // ../../packages/aeg-core/src/review-gate.ts
3745
4006
  function checkReviewGate(input) {
3746
4007
  const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
3747
4008
  const waived = isWaiverLabelActorVerified({
@@ -3764,15 +4025,55 @@ function checkReviewGate(input) {
3764
4025
  const verifiedBodies = verified.map((c) => c.body);
3765
4026
  const codeReview = extractCodeReviewVerdict(verifiedBodies);
3766
4027
  const security = extractSecurityReviewVerdict(verifiedBodies);
3767
- const codeReviewClean = codeReview.value === "APPROVE";
3768
- const securityClean = security.value === "PASS";
3769
- const codeReviewBound = isBoundToPatch(codeReview, input.headSha, input.patchIdOf);
3770
- const securityBound = isBoundToPatch(security, input.headSha, input.patchIdOf);
3771
- const codeReviewObjectivesBound = isBoundToObjectives(codeReview, input.objectivesVersion);
3772
- const securityObjectivesBound = isBoundToObjectives(security, input.objectivesVersion);
3773
- const codeReviewRulingsBound = isBoundToRulings(codeReview, input.rulingOrdinal);
3774
- const securityRulingsBound = isBoundToRulings(security, input.rulingOrdinal);
3775
- if (codeReviewClean && codeReviewBound && codeReviewObjectivesBound && codeReviewRulingsBound && securityClean && securityBound && securityObjectivesBound && securityRulingsBound && 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) {
3776
4077
  return {
3777
4078
  verdict: "pass",
3778
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.`,
@@ -3780,23 +4081,35 @@ function checkReviewGate(input) {
3780
4081
  };
3781
4082
  }
3782
4083
  const problems = [];
3783
- if (!codeReviewClean) {
4084
+ if (!codeReviewTextClean) {
3784
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`);
3785
4088
  } else if (!codeReviewBound) {
3786
4089
  problems.push(`the newest code-review verdict covers ${codeReview.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
3787
4090
  } else if (!codeReviewObjectivesBound) {
3788
4091
  problems.push(`the newest code-review verdict was cast against objectives version ${codeReview.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
3789
4092
  } else if (!codeReviewRulingsBound) {
3790
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}`);
3791
4098
  }
3792
- if (!securityClean) {
4099
+ if (!securityTextClean) {
3793
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`);
3794
4103
  } else if (!securityBound) {
3795
4104
  problems.push(`the newest security-review verdict covers ${security.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
3796
4105
  } else if (!securityObjectivesBound) {
3797
4106
  problems.push(`the newest security-review verdict was cast against objectives version ${security.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
3798
4107
  } else if (!securityRulingsBound) {
3799
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}`);
3800
4113
  }
3801
4114
  if (!mechanicalChecksClean) {
3802
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(", ")}`);
@@ -4488,42 +4801,45 @@ function checkDispatchReadiness(input) {
4488
4801
  const { trancheSlug, task } = input;
4489
4802
  const taskLabel = `task ${task.id} (tranche ${trancheSlug})`;
4490
4803
  const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
4491
- const blockers = [];
4804
+ const blockerDetails = [];
4805
+ const push = (blockerClass, message) => {
4806
+ blockerDetails.push({ class: blockerClass, message });
4807
+ };
4492
4808
  if (task.issue === null) {
4493
- 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.`);
4494
4810
  } else if (input.issue === null) {
4495
- 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).`);
4496
4812
  }
4497
4813
  if (input.issue !== null && !input.issueRationalePass) {
4498
- 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.`);
4499
4815
  }
4500
4816
  for (const dep of input.dependsOn) {
4501
4817
  if (isSelfDependency(dep, task, input.issue)) {
4502
4818
  const issueStr = input.issue !== null ? `#${input.issue.number}` : "?";
4503
- 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.`);
4504
4820
  continue;
4505
4821
  }
4506
4822
  if (dep.resolved === false) {
4507
- 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.`);
4508
4824
  continue;
4509
4825
  }
4510
4826
  if (!dep.merged && !isHandClosedByRecognizedPrincipal(dep, principalAllowlist)) {
4511
4827
  const issueStr = dep.issue !== null ? ` (#${dep.issue})` : "";
4512
- 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.`);
4513
4829
  }
4514
4830
  }
4515
4831
  for (const c of input.conflictsWith) {
4516
4832
  if (c.openOrInFlight) {
4517
4833
  const issueStr = c.issue !== null ? ` (#${c.issue})` : "";
4518
- 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.`);
4519
4835
  }
4520
4836
  }
4521
4837
  for (const proj of input.priorTrancheArchival) {
4522
4838
  if (proj.priorTrancheSlug !== null && !proj.archived) {
4523
- 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.`);
4524
4840
  }
4525
4841
  }
4526
- return { ready: blockers.length === 0, blockers };
4842
+ return { ready: blockerDetails.length === 0, blockers: blockerDetails.map((b) => b.message), blockerDetails };
4527
4843
  }
4528
4844
  // ../../packages/aeg-core/src/single-plan-pr.ts
4529
4845
  function trancheSlugFromTopologyPath(path) {
@@ -4625,6 +4941,91 @@ function checkBranchTopology(input) {
4625
4941
  reason: `Branch \`${branch}\` matches topology row \`${taskId}\` in ${topoPath}.`
4626
4942
  };
4627
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();
4628
5029
  // ../../packages/aeg-core/src/first-push-dispatch-gate.ts
4629
5030
  function parseTaskBranch(branch) {
4630
5031
  const m = /^task\/([^/]+)\/([^/]+)$/.exec(branch);
@@ -4708,7 +5109,6 @@ function decideIssueAssignment(input) {
4708
5109
  };
4709
5110
  }
4710
5111
  // ../../packages/aeg-core/src/test-plan-gate.ts
4711
- var TASK_BRANCH_PATTERN2 = /^task\/[^/]+\/[^/]+$/;
4712
5112
  function evaluateTestPlanGate(body, branch) {
4713
5113
  if (!body) {
4714
5114
  return {
@@ -4721,7 +5121,7 @@ function evaluateTestPlanGate(body, branch) {
4721
5121
  }
4722
5122
  const located = locateTestPlanSection(body);
4723
5123
  if (!located.found) {
4724
- if (TASK_BRANCH_PATTERN2.test(branch)) {
5124
+ if (parseTaskBranchIdentity(branch) !== null) {
4725
5125
  return {
4726
5126
  verdict: "fail",
4727
5127
  messages: [
@@ -4849,7 +5249,7 @@ function findWorkspaceEscapes(files, knownPaths, workspaceDirs = DEFAULT_WORKSPA
4849
5249
  return findings;
4850
5250
  }
4851
5251
  // ../../packages/aeg-core/src/log/schema.ts
4852
- import { z } from "zod";
5252
+ import { z as z2 } from "zod";
4853
5253
  var ROLE_VALUES = [
4854
5254
  "planner",
4855
5255
  "developer",
@@ -4859,136 +5259,172 @@ var ROLE_VALUES = [
4859
5259
  "archivist",
4860
5260
  "architect"
4861
5261
  ];
4862
- var RoleSchema = z.enum(ROLE_VALUES);
5262
+ var RoleSchema = z2.enum(ROLE_VALUES);
4863
5263
  var HOST_VALUES = ["hook", "ci", "cli", "loop"];
4864
- var HostSchema = z.enum(HOST_VALUES);
5264
+ var HostSchema = z2.enum(HOST_VALUES);
4865
5265
  var RUN_ID_PATTERN = /^[A-Za-z0-9_.-]{1,128}$/;
4866
- var HeaderMetaSchema = z.object({
4867
- schema: z.literal(1),
4868
- ts: z.string(),
4869
- run_id: z.string().regex(RUN_ID_PATTERN),
4870
- seq: z.number().int().nonnegative(),
4871
- repo: z.string().nullable(),
4872
- vinaya: z.string(),
4873
- 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(),
4874
5273
  host: HostSchema,
4875
- machine: z.string()
5274
+ machine: z2.string()
5275
+ };
5276
+ var HeaderMetaV1Schema = z2.object({
5277
+ schema: z2.literal(1),
5278
+ ...headerMetaCore
4876
5279
  }).strict();
4877
- var SubjectSchema = z.object({
4878
- issue: z.number().int().nullable(),
4879
- pr: z.number().int().optional(),
4880
- sha: z.string().optional(),
4881
- role: z.union([RoleSchema, z.literal("unattributed")]),
4882
- round: z.number().int().optional(),
4883
- objectives_version: z.string().optional()
5280
+ var LineageSchema = z2.object({
5281
+ run: z2.string().nullable(),
5282
+ attempt: z2.number().int().nullable(),
5283
+ parent: z2.string().nullable()
4884
5284
  }).strict();
4885
- var HeaderSchema = z.object({
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()
5290
+ }).strict();
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({
4886
5312
  meta: HeaderMetaSchema,
4887
5313
  subject: SubjectSchema
4888
5314
  }).strict();
4889
5315
  var envelopeTail = {
4890
- duration_ms: z.number().nonnegative().optional(),
4891
- payload: z.object({}).strict()
5316
+ duration_ms: z2.number().nonnegative().optional(),
5317
+ payload: z2.object({}).strict()
4892
5318
  };
4893
- var DispatchOutcomeSchema = z.discriminatedUnion("type", [
4894
- z.object({ type: z.literal("pr_opened"), pr: z.number().int(), head: z.string() }).strict(),
4895
- z.object({
4896
- type: z.literal("round_pushed"),
4897
- pr: z.number().int(),
4898
- head: z.string(),
4899
- 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()
4900
5336
  }).strict(),
4901
- z.object({
4902
- type: z.literal("verdict"),
4903
- verdict: z.enum(["APPROVE", "REQUEST CHANGES", "PASS", "FAIL"]),
4904
- head: z.string(),
4905
- comment_id: z.number().int(),
4906
- objectives: z.array(z.object({ id: z.string(), met: z.boolean() }).strict()),
4907
- 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)
4908
5344
  }).strict(),
4909
- z.object({
4910
- type: z.literal("escalation"),
4911
- class: z.enum(["authority", "strategy", "product"]),
4912
- 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()
4913
5349
  }).strict(),
4914
- z.object({ type: z.literal("brief"), comment_id: z.number().int(), hash: z.string() }).strict(),
4915
- z.object({ type: z.literal("plan"), issues: z.array(z.number().int()) }).strict(),
4916
- 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()
4917
5353
  ]);
4918
5354
  var dispatchShared = {
4919
5355
  meta: HeaderMetaSchema,
4920
5356
  subject: SubjectSchema,
4921
- kind: z.literal("dispatch"),
5357
+ kind: z2.literal("dispatch"),
4922
5358
  ...envelopeTail,
4923
5359
  target_role: RoleSchema,
4924
- model: z.string(),
4925
- round: z.number().int().optional(),
4926
- effect_id: z.string()
5360
+ model: z2.string(),
5361
+ round: z2.number().int().optional(),
5362
+ effect_id: z2.string()
4927
5363
  };
4928
- var dispatchUsageField = z.object({ input: z.number().nonnegative(), output: z.number().nonnegative() }).strict().nullable();
4929
- var DispatchEventSchema = z.discriminatedUnion("event", [
4930
- z.object({ ...dispatchShared, event: z.literal("dispatched"), prompt_hash: z.string() }).strict(),
4931
- 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({
4932
5368
  ...dispatchShared,
4933
- event: z.literal("outcome_received"),
5369
+ event: z2.literal("outcome_received"),
4934
5370
  outcome: DispatchOutcomeSchema,
4935
5371
  usage: dispatchUsageField
4936
5372
  }).strict(),
4937
- z.object({
5373
+ z2.object({
4938
5374
  ...dispatchShared,
4939
- event: z.literal("dispatch_failed"),
4940
- reason: z.enum(["timeout", "crash", "refused", "unattributed_write"]),
5375
+ event: z2.literal("dispatch_failed"),
5376
+ reason: z2.enum(["timeout", "crash", "refused", "unattributed_write"]),
4941
5377
  usage: dispatchUsageField
4942
5378
  }).strict()
4943
5379
  ]);
4944
5380
  var loopShared = {
4945
5381
  meta: HeaderMetaSchema,
4946
5382
  subject: SubjectSchema,
4947
- kind: z.literal("dev_review_loop"),
5383
+ kind: z2.literal("dev_review_loop"),
4948
5384
  ...envelopeTail,
4949
- loop_id: z.string()
5385
+ loop_id: z2.string()
4950
5386
  };
4951
- var DevReviewLoopEventSchema = z.discriminatedUnion("event", [
4952
- z.object({
5387
+ var DevReviewLoopEventSchema = z2.discriminatedUnion("event", [
5388
+ z2.object({
4953
5389
  ...loopShared,
4954
- event: z.literal("loop_started"),
4955
- task: z.number().int(),
4956
- policy: z.object({
4957
- max_rounds: z.number().int().nonnegative(),
4958
- reviewers: z.array(RoleSchema),
4959
- 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())
4960
5396
  }).strict()
4961
5397
  }).strict(),
4962
- z.object({ ...loopShared, event: z.literal("round_started"), round: z.number().int(), base_head: z.string() }).strict(),
4963
- z.object({
5398
+ z2.object({ ...loopShared, event: z2.literal("round_started"), round: z2.number().int(), base_head: z2.string() }).strict(),
5399
+ z2.object({
4964
5400
  ...loopShared,
4965
- event: z.literal("gate_result_read"),
4966
- round: z.number().int(),
4967
- head: z.string(),
4968
- green: z.boolean()
5401
+ event: z2.literal("gate_result_read"),
5402
+ round: z2.number().int(),
5403
+ head: z2.string(),
5404
+ green: z2.boolean()
4969
5405
  }).strict(),
4970
- z.object({
5406
+ z2.object({
4971
5407
  ...loopShared,
4972
- event: z.literal("verdicts_read"),
4973
- round: z.number().int(),
4974
- head: z.string(),
4975
- all_approve: z.boolean(),
4976
- 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()
4977
5413
  }).strict(),
4978
- z.object({
5414
+ z2.object({
4979
5415
  ...loopShared,
4980
- event: z.literal("findings_compared"),
4981
- round: z.number().int(),
4982
- open: z.array(z.string()),
4983
- resolved: z.array(z.string()),
4984
- new: z.array(z.string()),
4985
- 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())
4986
5422
  }).strict(),
4987
- z.object({
5423
+ z2.object({
4988
5424
  ...loopShared,
4989
- event: z.literal("stop_condition_met"),
4990
- round: z.number().int(),
4991
- condition: z.enum([
5425
+ event: z2.literal("stop_condition_met"),
5426
+ round: z2.number().int(),
5427
+ condition: z2.enum([
4992
5428
  "green",
4993
5429
  "max_rounds",
4994
5430
  "no_progress",
@@ -4998,42 +5434,49 @@ var DevReviewLoopEventSchema = z.discriminatedUnion("event", [
4998
5434
  "reappearance"
4999
5435
  ])
5000
5436
  }).strict(),
5001
- z.object({
5437
+ z2.object({
5002
5438
  ...loopShared,
5003
- event: z.literal("paused"),
5004
- round: z.number().int(),
5005
- reason: z.enum(["escalation", "principal_item", "refreeze_needed"])
5439
+ event: z2.literal("paused"),
5440
+ round: z2.number().int(),
5441
+ reason: z2.enum(["escalation", "principal_item", "refreeze_needed"])
5006
5442
  }).strict(),
5007
- z.object({
5443
+ z2.object({
5008
5444
  ...loopShared,
5009
- event: z.literal("resumed"),
5010
- round: z.number().int(),
5011
- by: z.literal("principal")
5445
+ event: z2.literal("resumed"),
5446
+ round: z2.number().int(),
5447
+ by: z2.literal("principal")
5012
5448
  }).strict(),
5013
- z.object({
5449
+ z2.object({
5014
5450
  ...loopShared,
5015
- event: z.literal("round_ended"),
5016
- round: z.number().int(),
5017
- base_head: z.string(),
5018
- head: z.string(),
5019
- files_changed: z.number().int().nonnegative(),
5020
- insertions: z.number().int().nonnegative(),
5021
- deletions: z.number().int().nonnegative(),
5022
- wall_ms: z.number().nonnegative(),
5023
- outcome: z.enum(["green", "changes_requested", "escalated"])
5451
+ event: z2.literal("unpushed_work_resume"),
5452
+ round: z2.number().int(),
5453
+ branch: z2.string(),
5454
+ detail: z2.string()
5024
5455
  }).strict(),
5025
- z.object({
5456
+ z2.object({
5026
5457
  ...loopShared,
5027
- event: z.literal("journal_finalized"),
5028
- rounds: z.number().int().nonnegative(),
5029
- total_wall_ms: z.number().nonnegative(),
5030
- time_to_green_ms: z.number().nonnegative().nullable(),
5031
- files_changed_total: z.number().int().nonnegative(),
5032
- final_head: z.string(),
5033
- result: z.enum(["merged_ready", "stopped"])
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"])
5467
+ }).strict(),
5468
+ z2.object({
5469
+ ...loopShared,
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"])
5034
5477
  }).strict()
5035
5478
  ]);
5036
- var ForgeOpSchema = z.enum([
5479
+ var ForgeOpSchema = z2.enum([
5037
5480
  "pr.create",
5038
5481
  "pr.comment",
5039
5482
  "pr.body.replace",
@@ -5047,24 +5490,210 @@ var ForgeOpSchema = z.enum([
5047
5490
  "label.add",
5048
5491
  "label.remove"
5049
5492
  ]);
5050
- var ForgeWriteTargetSchema = z.object({
5051
- issue: z.number().int().optional(),
5052
- pr: z.number().int().optional()
5493
+ var ForgeWriteTargetSchema = z2.object({
5494
+ issue: z2.number().int().optional(),
5495
+ pr: z2.number().int().optional()
5053
5496
  }).strict();
5054
5497
  var forgeWriteShared = {
5055
5498
  meta: HeaderMetaSchema,
5056
5499
  subject: SubjectSchema,
5057
- kind: z.literal("forge_write"),
5500
+ kind: z2.literal("forge_write"),
5058
5501
  ...envelopeTail,
5059
5502
  op: ForgeOpSchema,
5060
5503
  target: ForgeWriteTargetSchema
5061
5504
  };
5062
- var ForgeWriteEventSchema = z.discriminatedUnion("event", [
5063
- z.object({ ...forgeWriteShared, event: z.literal("validated") }).strict(),
5064
- z.object({ ...forgeWriteShared, event: z.literal("refused"), reason: z.string() }).strict(),
5065
- 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()
5638
+ ]);
5639
+ var LogEventSchema = z2.union([
5640
+ DispatchEventSchema,
5641
+ DevReviewLoopEventSchema,
5642
+ ForgeWriteEventSchema,
5643
+ GateEventSchema,
5644
+ OperationEventSchema,
5645
+ UsageEventSchema,
5646
+ RoleAttemptEventSchema,
5647
+ HandoffEventSchema,
5648
+ EffectEventSchema
5066
5649
  ]);
5067
- var LogEventSchema = z.union([DispatchEventSchema, DevReviewLoopEventSchema, ForgeWriteEventSchema]);
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();
5068
5697
  // ../../packages/sources/src/commands.ts
5069
5698
  var COMMANDS = [
5070
5699
  {
@@ -5223,6 +5852,17 @@ var COMMANDS = [
5223
5852
  ],
5224
5853
  status: "shipped"
5225
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."
5863
+ ],
5864
+ status: "shipped"
5865
+ },
5226
5866
  {
5227
5867
  name: "pr create",
5228
5868
  description: "Open a pull request after full brief-schema validation",
@@ -5732,10 +6372,10 @@ function createForgeSource(config) {
5732
6372
  };
5733
6373
  }
5734
6374
  // ../../packages/sources/src/select-source.ts
5735
- import { z as z2 } from "zod";
5736
- var StateSourceConfigSchema = z2.discriminatedUnion("kind", [
5737
- z2.object({ kind: z2.literal("forge"), owner: z2.string(), repo: z2.string() }),
5738
- 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() })
5739
6379
  ]);
5740
6380
  // src/checks/contract.ts
5741
6381
  var CHECK_SCHEMA_VERSION = 1;
@@ -5749,27 +6389,29 @@ import { execFileSync as execFileSync2 } from "node:child_process";
5749
6389
  import { existsSync, mkdirSync, readFileSync as readFileSync2, realpathSync, writeFileSync } from "node:fs";
5750
6390
  import { homedir } from "node:os";
5751
6391
  import { dirname, join, resolve } from "node:path";
5752
- import { z as z3 } from "zod";
6392
+ import { z as z5 } from "zod";
5753
6393
 
5754
6394
  // src/lib/agent-vendors.ts
5755
6395
  var AGENT_VENDORS = ["skills", "claude", "gemini"];
5756
6396
 
5757
6397
  // src/lib/config.ts
5758
- var EnvEntrySchema = z3.union([
5759
- z3.literal(true),
5760
- z3.object({ optional: z3.literal(true) }),
5761
- z3.object({ anyOf: z3.array(z3.string()).min(2) }),
5762
- 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()
5763
6403
  ]);
5764
- var CheckEntrySchema = z3.object({
5765
- run: z3.string(),
5766
- scope: z3.enum(["diff", "full"]),
5767
- include: z3.array(z3.string()).optional(),
5768
- args: z3.array(z3.string()).optional(),
5769
- timeoutMs: z3.number().optional(),
5770
- env: z3.record(z3.string(), EnvEntrySchema).optional(),
5771
- requiresOpenPr: z3.boolean().optional(),
5772
- 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()
5773
6415
  }).superRefine((entry2, ctx) => {
5774
6416
  if (!entry2.env)
5775
6417
  return;
@@ -5779,25 +6421,26 @@ var CheckEntrySchema = z3.object({
5779
6421
  const members = value.anyOf;
5780
6422
  if (new Set(members).size !== members.length) {
5781
6423
  ctx.addIssue({
5782
- code: z3.ZodIssueCode.custom,
6424
+ code: z5.ZodIssueCode.custom,
5783
6425
  message: `env.${key}.anyOf has duplicate members`,
5784
6426
  path: ["env", key, "anyOf"]
5785
6427
  });
5786
6428
  }
5787
6429
  if (!members.includes(key)) {
5788
6430
  ctx.addIssue({
5789
- code: z3.ZodIssueCode.custom,
6431
+ code: z5.ZodIssueCode.custom,
5790
6432
  message: `env.${key}.anyOf must include "${key}" itself as a member`,
5791
6433
  path: ["env", key, "anyOf"]
5792
6434
  });
5793
6435
  }
5794
6436
  }
5795
6437
  });
5796
- var RoleEntrySchema = z3.object({
5797
- contract: z3.string().refine((p) => p.includes("/"), {
6438
+ var RoleEntrySchema = z5.object({
6439
+ contract: z5.string().refine((p) => p.includes("/"), {
5798
6440
  message: 'must be a path (contain at least one "/"), not a bare filename'
5799
6441
  })
5800
6442
  });
6443
+ var MAX_REPORT_COMMAND_TIMEOUT_MS = 3600000;
5801
6444
  var BRIEF_BUILTINS = [
5802
6445
  "tier",
5803
6446
  "testPlan",
@@ -5817,17 +6460,17 @@ var BRIEF_BUILTINS = [
5817
6460
  "briefSections",
5818
6461
  "milestoneShape"
5819
6462
  ];
5820
- var BriefSectionSchema = z3.union([
5821
- z3.object({ builtin: z3.enum(BRIEF_BUILTINS) }),
5822
- z3.object({ heading: z3.string().min(1), name: z3.string().optional() }),
5823
- z3.object({ field: z3.string().min(1), name: z3.string().optional() }),
5824
- 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() })
5825
6468
  ]);
5826
- var BriefSchemaSchema = z3.object({
5827
- pr: z3.object({ sections: z3.array(BriefSectionSchema) }).optional(),
5828
- issue: z3.object({ sections: z3.array(BriefSectionSchema) }).optional(),
5829
- milestone: z3.object({ sections: z3.array(BriefSectionSchema) }).optional(),
5830
- 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()
5831
6474
  });
5832
6475
  function isSafeRepoRelPath(p) {
5833
6476
  if (p.length === 0)
@@ -5836,7 +6479,7 @@ function isSafeRepoRelPath(p) {
5836
6479
  return false;
5837
6480
  return p.split(/[\\/]/).every((seg) => seg !== ".." && seg !== "");
5838
6481
  }
5839
- var SafeRepoRelPath = z3.string().refine(isSafeRepoRelPath, {
6482
+ var SafeRepoRelPath = z5.string().refine(isSafeRepoRelPath, {
5840
6483
  message: "must be a repo-root-relative path with no `..` segment or absolute root"
5841
6484
  });
5842
6485
  var CANONICAL_HOOK_BLOCK_PREFIXES = [".git/", ".husky/", ".vinaya/hooks/", ".claude/hooks/"];
@@ -5846,22 +6489,22 @@ function isCanonicalHookBlockPath(p) {
5846
6489
  var ManagedHookBlockPath = SafeRepoRelPath.refine(isCanonicalHookBlockPath, {
5847
6490
  message: `must start with one of ${CANONICAL_HOOK_BLOCK_PREFIXES.join(", ")} (byte-exact, case-sensitive)`
5848
6491
  });
5849
- var ManagedBlockRecordSchema = z3.object({
6492
+ var ManagedBlockRecordSchema = z5.object({
5850
6493
  path: ManagedHookBlockPath,
5851
- marker: z3.string(),
5852
- comment: z3.enum(["hash", "html"])
6494
+ marker: z5.string(),
6495
+ comment: z5.enum(["hash", "html"])
5853
6496
  });
5854
- var ManagedManifestSchema = z3.object({
5855
- version: z3.number().int().positive(),
5856
- files: z3.array(SafeRepoRelPath),
5857
- blocks: z3.array(ManagedBlockRecordSchema),
5858
- labels: z3.array(z3.string()),
5859
- 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()
5860
6503
  });
5861
- var ProjectEntrySchema = z3.object({
5862
- name: z3.string().min(1),
5863
- description: z3.string().min(1).optional(),
5864
- 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()
5865
6508
  });
5866
6509
  function parseTokensCollectDeclaration(value) {
5867
6510
  const m = value.trim().match(/^(\S+)\s+(\S+)$/);
@@ -5872,34 +6515,55 @@ function parseTokensCollectDeclaration(value) {
5872
6515
  return null;
5873
6516
  return { interpreter, script };
5874
6517
  }
5875
- var VinayaConfigSchema = z3.object({
5876
- rings: z3.object({
5877
- ring1_forgeWriteInterception: z3.boolean(),
5878
- ring2_asyncAudits: z3.boolean()
6518
+ var VinayaConfigSchema = z5.object({
6519
+ rings: z5.object({
6520
+ ring1_forgeWriteInterception: z5.boolean(),
6521
+ ring2_asyncAudits: z5.boolean()
5879
6522
  }).optional(),
5880
- checks: z3.record(z3.string(), CheckEntrySchema).optional(),
5881
- roles: z3.record(z3.string(), RoleEntrySchema).optional(),
6523
+ checks: z5.record(z5.string(), CheckEntrySchema).optional(),
6524
+ roles: z5.record(z5.string(), RoleEntrySchema).optional(),
5882
6525
  briefSchema: BriefSchemaSchema.optional(),
5883
6526
  managed: ManagedManifestSchema.optional(),
5884
- principals: z3.array(z3.string()).min(1).optional(),
5885
- releaseActor: z3.string().min(1).optional(),
5886
- ci: z3.object({ setup: z3.string().min(1) }).optional(),
5887
- tokens: z3.object({
5888
- 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, {
5889
6532
  message: 'tokens.collect must be exactly "<interpreter> <repo-relative-script-path>" — two whitespace-separated tokens, no flags, no shell syntax, no quoting'
5890
6533
  })
5891
6534
  }).optional(),
5892
- blastRadius: z3.object({ extraDomains: z3.array(z3.string()).optional() }).optional(),
5893
- proseGates: z3.object({
5894
- doctrineRoot: z3.string().min(1).optional(),
5895
- readerFacingPrefix: z3.string().min(1).optional(),
5896
- readerFacingSuffix: z3.string().min(1).optional(),
5897
- 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()
5898
6547
  }).optional(),
5899
- projects: z3.array(ProjectEntrySchema).optional(),
5900
- dispatch: z3.object({
5901
- timeoutMs: z3.number().int().positive().optional(),
5902
- agent: z3.enum(["claude", "codex", "gemini"]).optional()
6548
+ reviewPolicy: z5.object({
6549
+ codeReviewThreshold: z5.string().min(1).optional(),
6550
+ securityThreshold: z5.string().min(1).optional(),
6551
+ maxRounds: z5.number().optional()
6552
+ }).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
+ }
5903
6567
  }).optional()
5904
6568
  });
5905
6569
  var GLOBAL_VINAYA_HOME = join(homedir(), ".vinaya");
@@ -5974,6 +6638,28 @@ function resolvePrincipalAllowlist(config) {
5974
6638
  function resolveReleaseActor(config) {
5975
6639
  return config?.releaseActor ?? DEFAULT_RELEASE_ACTOR;
5976
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
+ }
5977
6663
  function trustAnchorRepo() {
5978
6664
  const wellFormed = (slug) => /^[^/\s]+\/[^/\s]+$/.test(slug) ? slug : null;
5979
6665
  const fromRunner = process.env.GITHUB_REPOSITORY?.trim();
@@ -6119,38 +6805,71 @@ function taskToEntry(entries, slug) {
6119
6805
  m.set(`${slug}/${e.task.id}`, e);
6120
6806
  return m;
6121
6807
  }
6122
- function recoveryPromptFor(checkCode, detail = "") {
6123
- if (detail.includes("INTERNAL:")) {
6124
- 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.";
6125
- }
6126
- switch (checkCode) {
6127
- 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":
6128
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`.";
6129
- 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":
6130
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`.";
6131
- case "T1":
6818
+ case "phantom-issue-ref":
6132
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`.";
6133
- case "T2":
6820
+ case "orphan-task":
6134
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`.";
6135
- case "T3":
6822
+ case "tbd-in-active-tranche":
6136
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`.";
6137
- case "D1":
6824
+ case "dispatched-on-unmet-deps":
6138
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`.";
6139
- case "R1":
6826
+ case "missing-rationale-field":
6140
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`.";
6141
6846
  default:
6142
- 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);
6143
6848
  }
6144
6849
  }
6850
+ function assertNeverCoherenceCode(x) {
6851
+ throw new Error(`Unhandled CoherenceFailureCode: ${JSON.stringify(x)}`);
6852
+ }
6145
6853
  function emitFailure(result) {
6146
- const detail = result.failures.map((f) => f.reason).join(" | ") || result.note || "see check output";
6147
- emitCheckError({
6148
- schema: CHECK_SCHEMA_VERSION,
6149
- check: CHECK_NAME,
6150
- severity: "error",
6151
- message: `${result.check}: ${detail}`,
6152
- agent_recovery_prompt: recoveryPromptFor(result.check, detail)
6153
- });
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
+ }
6154
6873
  }
6155
6874
  async function main() {
6156
6875
  const slug = currentTrancheSlug();