@attalabs/vinaya 0.27.0 → 0.29.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 (143) hide show
  1. package/README.md +5 -4
  2. package/aeg-root/contracts/planner-developer.md +4 -1
  3. package/aeg-root/contracts/planner-operator.md +86 -0
  4. package/aeg-root/contracts/principal-operator.md +88 -0
  5. package/aeg-root/enforcement.md +4 -4
  6. package/aeg-root/roles/developer.md +25 -18
  7. package/aeg-root/roles/operator.md +133 -0
  8. package/aeg-root/roles/planner.md +6 -0
  9. package/aeg-root/roles/principal.md +4 -0
  10. package/aeg-root/roles/reviewer.md +3 -3
  11. package/aeg-root/roles/security.md +3 -3
  12. package/aeg-root/skills/aeg-context-packets/SKILL.md +123 -0
  13. package/aeg-root/skills/aeg-context-packets/examples/operator-start.md +30 -0
  14. package/aeg-root/templates/brief-template.md +6 -2
  15. package/dist/checks/bin/check-body-bare-digits.js +1275 -240
  16. package/dist/checks/bin/check-branch-topology.js +1207 -180
  17. package/dist/checks/bin/check-brief-shape.js +1279 -243
  18. package/dist/checks/bin/check-changeset-coverage.js +2176 -264
  19. package/dist/checks/bin/check-closes-n.js +1211 -184
  20. package/dist/checks/bin/check-coherence.js +1378 -265
  21. package/dist/checks/bin/check-dead-branch-push.js +1158 -176
  22. package/dist/checks/bin/check-dispatch-readiness.js +1440 -276
  23. package/dist/checks/bin/check-doc-coverage-push.js +2171 -259
  24. package/dist/checks/bin/check-doc-coverage.js +2173 -261
  25. package/dist/checks/bin/check-doctrine-no-procedures.js +1275 -240
  26. package/dist/checks/bin/check-doctrine-portability.js +2171 -259
  27. package/dist/checks/bin/check-evidence-fresh.js +2613 -347
  28. package/dist/checks/bin/check-exec-bits.js +2169 -257
  29. package/dist/checks/bin/check-first-push-dispatch.js +1326 -246
  30. package/dist/checks/bin/check-issue-assignment.js +1209 -182
  31. package/dist/checks/bin/check-issue-milestone-attach.js +6087 -0
  32. package/dist/checks/bin/check-issue-objectives-numbering.js +6089 -0
  33. package/dist/checks/bin/check-issue-parts-coverage.js +6089 -0
  34. package/dist/checks/bin/check-issue-surface-globs.js +7304 -0
  35. package/dist/checks/bin/check-issue-title-grammar.js +6089 -0
  36. package/dist/checks/bin/check-issue-tranche-label.js +6089 -0
  37. package/dist/checks/bin/check-main-branch-refusal.js +1158 -176
  38. package/dist/checks/bin/check-no-disk-state.js +1158 -176
  39. package/dist/checks/bin/check-pr-premise-reassert.js +1275 -240
  40. package/dist/checks/bin/check-pr-report-density.js +1158 -176
  41. package/dist/checks/bin/check-quoted-command.js +2150 -257
  42. package/dist/checks/bin/check-reader-resolvable-prose.js +2153 -260
  43. package/dist/checks/bin/check-registry-gates.js +1245 -176
  44. package/dist/checks/bin/check-retired-vocabulary.js +2148 -255
  45. package/dist/checks/bin/check-review-gate.js +1375 -245
  46. package/dist/checks/bin/check-single-plan-pr.js +1158 -176
  47. package/dist/checks/bin/check-surface-scope.js +1217 -182
  48. package/dist/checks/bin/check-test-plan.js +1187 -189
  49. package/dist/checks/bin/check-token-collection-wired.js +1158 -176
  50. package/dist/checks/bin/check-token-report.js +1171 -179
  51. package/dist/checks/bin/check-workspace-escape.js +2167 -255
  52. package/dist/index.js +17322 -8709
  53. package/dist/lib/pre-push-changed-files.js +57 -0
  54. package/dist/lib/pre-push-select-tests.js +1616 -0
  55. package/package.json +4 -2
  56. package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
  57. package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
  58. package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
  59. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error/page.js +2 -1
  60. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error/page.js.nft.json +1 -1
  61. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
  62. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
  63. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  64. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  65. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  66. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  67. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  68. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
  69. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js +8 -7
  70. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  71. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  72. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
  73. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js +8 -7
  74. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
  75. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
  76. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
  77. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js +9 -8
  78. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
  79. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
  80. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
  81. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js +9 -8
  82. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
  83. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
  84. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
  85. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js +8 -7
  86. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
  87. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
  88. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +16 -4
  89. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js +10 -8
  90. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
  91. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
  92. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
  93. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js +8 -7
  94. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
  95. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
  96. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
  97. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js +8 -7
  98. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
  99. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
  100. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
  101. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js +9 -8
  102. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
  103. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
  104. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/1q96_modules_@clerk_nextjs_dist_esm_app-router_client_keyless-creator-reader_0lom2js.js +1 -1
  105. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__08f5pba._.js +3 -0
  106. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__1o_0tjy._.js → [root-of-the-server]__0h9yhwx._.js} +2 -2
  107. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__0053k9k._.js → [root-of-the-server]__0ls23oh._.js} +3 -2
  108. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__0puovz5._.js → [root-of-the-server]__0nye8tb._.js} +3 -2
  109. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__18kqy4q._.js → [root-of-the-server]__0tfykr-._.js} +2 -2
  110. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__0o771t1._.js → [root-of-the-server]__0th3gyp._.js} +3 -2
  111. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1-zcufa._.js +51 -0
  112. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__0p8q38b._.js → [root-of-the-server]__11svemm._.js} +1 -1
  113. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__02c71kv._.js → [root-of-the-server]__140m1i_._.js} +2 -2
  114. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1bh2awk._.js +3 -0
  115. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__0112h-k._.js → [root-of-the-server]__1gjqmhq._.js} +3 -2
  116. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1qmxez3._.js +4 -0
  117. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1r47dr7._.js +28 -0
  118. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1wc4-ip._.js +1 -1
  119. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_01fv4fj._.js +1 -1
  120. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_0gvm3og._.js → _096f46i._.js} +3 -2
  121. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_0lwxg63._.js → _0af2one._.js} +3 -2
  122. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_1n0cnq-._.js → _0zf4d27._.js} +45 -2
  123. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_03x_w6q._.js → _1-wo68i._.js} +3 -2
  124. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_app_studio_projects_[name]_tranches_[slug]_05ufo8i._.js +1 -12
  125. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1wdusod._.js +3 -0
  126. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{node_modules_1vo08dj._.js → node_modules_@clerk_nextjs_dist_esm_app-router_server-actions_1x7vfhn.js} +3 -4
  127. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_next_1khj-jh._.js +3 -0
  128. package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
  129. package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
  130. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
  131. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +17 -5
  132. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3_9xytdmiv1xi.js → 25f3-jtiv8p5n.js} +4 -4
  133. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0bn8c8v5q429o.js → 27-6ym7lp2v8o.js} +1 -1
  134. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3-6a3tinbdb-7.js → 3daxns3o--9me.js} +1 -1
  135. package/studio-standalone/apps/vinaya-studio/web/tsconfig.json +12 -0
  136. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0z6dav-._.js +0 -51
  137. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__13rrmhr._.js +0 -3
  138. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__17h4sy8._.js +0 -45
  139. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1hs0dcu._.js +0 -3
  140. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_next_17sz44y._.js +0 -3
  141. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{DMofV2-9FrHE7100GfrpI → -UC6GS6zDHeiawEYgYbTN}/_buildManifest.js +0 -0
  142. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{DMofV2-9FrHE7100GfrpI → -UC6GS6zDHeiawEYgYbTN}/_clientMiddlewareManifest.js +0 -0
  143. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{DMofV2-9FrHE7100GfrpI → -UC6GS6zDHeiawEYgYbTN}/_ssgManifest.js +0 -0
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // src/checks/bin/check-evidence-fresh.ts
6
- import { execFileSync as execFileSync4 } from "node:child_process";
6
+ import { execFileSync as execFileSync7 } from "node:child_process";
7
7
  // ../../packages/aeg-core/src/gate-audience.ts
8
8
  var GATE_AUDIENCE = {
9
9
  "check-branch-topology": { shippedAs: "branch-topology", ring: 0 },
@@ -1644,6 +1644,52 @@ var systemEnv3 = {
1644
1644
  ...process.env,
1645
1645
  PATH: [process.env.PATH, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"].filter(Boolean).join(":")
1646
1646
  };
1647
+ var defaultExec = () => execFileAsync3("git", ["remote", "get-url", "origin"], { env: systemEnv3, timeout: 5000 });
1648
+ var cached;
1649
+ var inflight;
1650
+ async function resolveRepo(exec = defaultExec) {
1651
+ if (cached !== undefined)
1652
+ return cached;
1653
+ const fromEnv = process.env.AEG_REPO;
1654
+ if (fromEnv) {
1655
+ const parsed = parseOwnerRepo(fromEnv);
1656
+ if (parsed) {
1657
+ cached = parsed;
1658
+ return cached;
1659
+ }
1660
+ }
1661
+ if (inflight)
1662
+ return inflight;
1663
+ inflight = (async () => {
1664
+ try {
1665
+ const { stdout } = await exec();
1666
+ cached = parseGitRemoteUrl(stdout.trim());
1667
+ return cached;
1668
+ } catch (err) {
1669
+ const message = err instanceof Error ? err.message : String(err);
1670
+ console.warn(`[aeg-forge-state] resolveRepo: git remote lookup failed (will retry): ${message}`);
1671
+ return null;
1672
+ } finally {
1673
+ inflight = undefined;
1674
+ }
1675
+ })();
1676
+ return inflight;
1677
+ }
1678
+ function parseGitRemoteUrl(url) {
1679
+ const ssh = url.match(/^git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/);
1680
+ if (ssh?.[1] && ssh[2])
1681
+ return { owner: ssh[1], repo: ssh[2] };
1682
+ const https = url.match(/^https?:\/\/(?:[^@]+@)?github\.com\/([^/]+)\/(.+?)(?:\.git)?\/?$/);
1683
+ if (https?.[1] && https[2])
1684
+ return { owner: https[1], repo: https[2] };
1685
+ return null;
1686
+ }
1687
+ function parseOwnerRepo(value) {
1688
+ const match = value.match(/^([^/]+)\/(.+)$/);
1689
+ if (!match?.[1] || !match[2])
1690
+ return null;
1691
+ return { owner: match[1], repo: match[2] };
1692
+ }
1647
1693
  // ../../packages/aeg-core/src/anchored-region.ts
1648
1694
  function anchoredRegionBounds(body, field) {
1649
1695
  const masked = maskCode(body);
@@ -1954,6 +2000,9 @@ function parseInlineFieldList(section) {
1954
2000
  var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
1955
2001
  var OBJECTIVES_VERSION_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Objectives version:\s*([0-9a-f]{64})(?![A-Za-z0-9])/im;
1956
2002
  var RULING_ORDINAL_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Ruling ordinal:\s*(\d+)(?!\d)/im;
2003
+ var BRIEF_HASH_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Brief hash:\s*([0-9a-f]{64})(?![A-Za-z0-9])/im;
2004
+ var POLICY_DIGEST_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Policy digest:\s*([0-9a-f]{64})(?![A-Za-z0-9])/im;
2005
+ var BASE_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged base:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
1957
2006
  function firstFiveLines(comment) {
1958
2007
  return comment.split(`
1959
2008
  `).slice(0, 5).join(`
@@ -1976,6 +2025,35 @@ function extractRulingOrdinal(comment) {
1976
2025
  const m = firstSevenLines(comment).match(RULING_ORDINAL_PATTERN);
1977
2026
  return m ? Number.parseInt(m[1], 10) : null;
1978
2027
  }
2028
+ function firstElevenLines(comment) {
2029
+ return comment.split(`
2030
+ `).slice(0, 11).join(`
2031
+ `);
2032
+ }
2033
+ function extractBriefHash(comment) {
2034
+ const m = firstElevenLines(comment).match(BRIEF_HASH_PATTERN);
2035
+ return m ? m[1].toLowerCase() : null;
2036
+ }
2037
+ function extractPolicyDigest(comment) {
2038
+ const m = firstElevenLines(comment).match(POLICY_DIGEST_PATTERN);
2039
+ return m ? m[1].toLowerCase() : null;
2040
+ }
2041
+ function firstThirteenLines(comment) {
2042
+ return comment.split(`
2043
+ `).slice(0, 13).join(`
2044
+ `);
2045
+ }
2046
+ function extractBaseSha(comment) {
2047
+ const m = firstThirteenLines(comment).match(BASE_SHA_PATTERN);
2048
+ return m ? m[1].toLowerCase() : null;
2049
+ }
2050
+ var FINDING_SEVERITY_LINE = /^\d+\.\s+\[([A-Z][A-Z]*)\]\s+(.+?)\s+—/gm;
2051
+ function extractFindingSeverities(comment) {
2052
+ return [...comment.matchAll(FINDING_SEVERITY_LINE)].map((m) => ({
2053
+ severity: m[1],
2054
+ location: m[2]
2055
+ }));
2056
+ }
1979
2057
  function extractVerdict(comments, valuePattern, missingLabel) {
1980
2058
  const candidates = comments.filter((c) => valuePattern.test(c));
1981
2059
  if (candidates.length === 0) {
@@ -1984,6 +2062,10 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1984
2062
  headSha: null,
1985
2063
  objectivesVersion: null,
1986
2064
  rulingOrdinal: null,
2065
+ briefHash: null,
2066
+ policyDigest: null,
2067
+ baseSha: null,
2068
+ findingSeverities: [],
1987
2069
  danglingNote: `no ${missingLabel} verdict comment found on this PR`
1988
2070
  };
1989
2071
  }
@@ -1995,6 +2077,10 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1995
2077
  headSha: null,
1996
2078
  objectivesVersion: null,
1997
2079
  rulingOrdinal: null,
2080
+ briefHash: null,
2081
+ policyDigest: null,
2082
+ baseSha: null,
2083
+ findingSeverities: [],
1998
2084
  danglingNote: `the most recent ${missingLabel} verdict comment carries a VERDICT-shaped line outside the first-five-line read window`
1999
2085
  };
2000
2086
  }
@@ -2003,6 +2089,10 @@ function extractVerdict(comments, valuePattern, missingLabel) {
2003
2089
  headSha: extractHeadSha(latest),
2004
2090
  objectivesVersion: extractObjectivesVersion(latest),
2005
2091
  rulingOrdinal: extractRulingOrdinal(latest),
2092
+ briefHash: extractBriefHash(latest),
2093
+ policyDigest: extractPolicyDigest(latest),
2094
+ baseSha: extractBaseSha(latest),
2095
+ findingSeverities: extractFindingSeverities(latest),
2006
2096
  danglingNote: null
2007
2097
  };
2008
2098
  }
@@ -2338,6 +2428,53 @@ function evaluateC5(changed, docOwnersContent, prBody, fileExists, waiverActive,
2338
2428
  }
2339
2429
  return out;
2340
2430
  }
2431
+ // ../../packages/aeg-core/src/review-policy.ts
2432
+ var CODE_REVIEW_SEVERITY_ORDER = ["BLOCKER", "MAJOR", "MINOR"];
2433
+ var SECURITY_SEVERITY_ORDER = ["CRITICAL", "HIGH", "MEDIUM", "LOW"];
2434
+ var DEFAULT_MAX_ROUNDS = 3;
2435
+ var DEFAULT_REVIEW_POLICY = {
2436
+ codeReviewThreshold: "BLOCKER",
2437
+ securityThreshold: "HIGH",
2438
+ maxRounds: DEFAULT_MAX_ROUNDS
2439
+ };
2440
+ var FILE_SHAPED_LOCATION = /\.[a-zA-Z0-9]{1,10}(:\d+)?\s*$/;
2441
+ var PROSE_LOCATION_PATTERNS = [/\bpr\s*body\b/i, /\bcomment\b/i];
2442
+ var ROLE_FILE_LOCATION = /(^|\/)aeg-root\/roles\//i;
2443
+ function isProseLocation(location) {
2444
+ if (ROLE_FILE_LOCATION.test(location))
2445
+ return true;
2446
+ if (FILE_SHAPED_LOCATION.test(location))
2447
+ return false;
2448
+ return PROSE_LOCATION_PATTERNS.some((pattern) => pattern.test(location));
2449
+ }
2450
+ var PROSE_CAP_SEVERITY = "MINOR";
2451
+ function blockingSeverities(scale, threshold) {
2452
+ const idx = scale.indexOf(threshold);
2453
+ if (idx === -1) {
2454
+ throw new Error(`blockingSeverities: threshold "${threshold}" is not one of ${scale.join(" > ")}`);
2455
+ }
2456
+ return scale.slice(0, idx + 1);
2457
+ }
2458
+ function evaluateReviewFindings(findings, scale, threshold) {
2459
+ const blocking = new Set(blockingSeverities(scale, threshold));
2460
+ const blockingFindings = findings.filter((f) => {
2461
+ if (!scale.includes(f.severity)) {
2462
+ throw new Error(`evaluateReviewFindings: severity "${f.severity}" is not one of ${scale.join(" > ")}`);
2463
+ }
2464
+ const effectiveSeverity = f.location !== undefined && isProseLocation(f.location) ? PROSE_CAP_SEVERITY : f.severity;
2465
+ return blocking.has(effectiveSeverity);
2466
+ });
2467
+ return { outcome: blockingFindings.length > 0 ? "blocked" : "clean", blockingFindings };
2468
+ }
2469
+ function evaluateCodeReview(findings, policy) {
2470
+ return evaluateReviewFindings(findings, CODE_REVIEW_SEVERITY_ORDER, policy.codeReviewThreshold);
2471
+ }
2472
+ function evaluateSecurityReview(findings, policy) {
2473
+ return evaluateReviewFindings(findings, SECURITY_SEVERITY_ORDER, policy.securityThreshold);
2474
+ }
2475
+ function isKnownSeverity(scale, value) {
2476
+ return scale.includes(value);
2477
+ }
2341
2478
  // ../../packages/aeg-core/src/pr-tier.ts
2342
2479
  var TIER_FIELD = /(\*\*)?\s*Tier\s*(\*\*)?\s*:\s*(\*\*)?\s*([013])\b/i;
2343
2480
  function readTierFromPrBody(prBody) {
@@ -2708,7 +2845,7 @@ function checkForField(prBody) {
2708
2845
  ]
2709
2846
  };
2710
2847
  }
2711
- function checkClosesN(prBody) {
2848
+ function checkClosesNPresence(prBody) {
2712
2849
  const closesPattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s{0,8}:?\s{0,8}#\d+/i;
2713
2850
  if (closesPattern.test(stripCode(prBody))) {
2714
2851
  return { status: "pass", errors: [] };
@@ -2740,10 +2877,22 @@ var COMMIT_TYPES = [
2740
2877
  "Test"
2741
2878
  ];
2742
2879
  var COMMIT_TYPE_STYLE = new RegExp(`^(${COMMIT_TYPES.join("|")})(\\([a-z0-9-]+\\))?: \\S`);
2880
+ function checkForgeTitle(title) {
2881
+ const taskStyle = /^\[[a-z0-9._-]+\] \S+ — \S/;
2882
+ if (COMMIT_TYPE_STYLE.test(title) || taskStyle.test(title))
2883
+ return { status: "pass", errors: [] };
2884
+ return {
2885
+ status: "fail",
2886
+ errors: [
2887
+ `brief-validation title: "${title}" matches neither title grammar — expected \`Type: description\` / \`Type(scope): description\` (commitlint types + Plan) or \`[tranche] id — description\` (task form).`
2888
+ ]
2889
+ };
2890
+ }
2743
2891
  var BRIEF_SHAPE_MARKERS = [checkSurfaceMap, checkDocUpdateList, checkStopConditions, checkAutonomyClause];
2744
2892
  var TASK_BRANCH_PATTERN = /^task\/[^/]+\/[^/]+$/;
2893
+ var TASK_ISSUE_BRANCH_PATTERN = /^task\/issue-\d+$/;
2745
2894
  function isTaskBranch(branch) {
2746
- return TASK_BRANCH_PATTERN.test(branch);
2895
+ return TASK_BRANCH_PATTERN.test(branch) || TASK_ISSUE_BRANCH_PATTERN.test(branch);
2747
2896
  }
2748
2897
  function isBriefShaped(prBody) {
2749
2898
  const stripped = stripCode(prBody);
@@ -2865,8 +3014,9 @@ function packagesNamedIn(text) {
2865
3014
  }
2866
3015
  function hasTestPathForConsumer(text, consumerDir) {
2867
3016
  const escaped = consumerDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2868
- const re = new RegExp(`${escaped}\\/[\\w./-]*\\.test\\.[A-Za-z0-9]+`);
2869
- return re.test(text);
3017
+ const filePathRe = new RegExp(`${escaped}\\/[\\w./-]*\\.test\\.[A-Za-z0-9]+`);
3018
+ const testDirRe = new RegExp(`${escaped}\\/(?:[\\w-]+\\/)*(?:tests|specs)(?:\\/|\\b)`);
3019
+ return filePathRe.test(text) || testDirRe.test(text);
2870
3020
  }
2871
3021
  function checkConsumerTests(prBody, consumersOf) {
2872
3022
  const section4 = extractNumberedSection(prBody, 4);
@@ -2970,7 +3120,7 @@ function checkBriefSections(prBody, readTier, options = {}) {
2970
3120
  checkConsumerTests(prBody, consumersOf),
2971
3121
  checkDefeatCases(prBody),
2972
3122
  ...issueObjectives !== undefined ? [checkObjectivesCopy(prBody, issueObjectives), checkObjectivesCoverage(prBody)] : [],
2973
- ...requireClosesN ? [checkClosesN(prBody)] : []
3123
+ ...requireClosesN ? [checkClosesNPresence(prBody)] : []
2974
3124
  ];
2975
3125
  return { errors: results.flatMap((r) => r.errors) };
2976
3126
  }
@@ -3260,6 +3410,28 @@ function topLevelSectionText(body, headingName) {
3260
3410
  const next = /^##[ \t]/m.exec(afterHeading);
3261
3411
  return next ? afterHeading.slice(0, next.index) : afterHeading;
3262
3412
  }
3413
+ function partHasBacktickedPath(text) {
3414
+ let i = 0;
3415
+ while (i < text.length) {
3416
+ const start = text.indexOf("`", i);
3417
+ if (start === -1)
3418
+ return false;
3419
+ const end = text.indexOf("`", start + 1);
3420
+ if (end === -1)
3421
+ return false;
3422
+ if (text.slice(start + 1, end).includes("/"))
3423
+ return true;
3424
+ i = end + 1;
3425
+ }
3426
+ return false;
3427
+ }
3428
+ var PART_MIN_WORDS_OUTSIDE_BACKTICKS = 3;
3429
+ function stripPartBackticks(text) {
3430
+ return text.replace(/`[^`\n]*`/g, " ");
3431
+ }
3432
+ function partWordCount(text) {
3433
+ return text.split(/\s+/).filter((w) => /[a-z]/i.test(w)).length;
3434
+ }
3263
3435
  function looksLikeFilePath(entry2) {
3264
3436
  const stripped = entry2.replace(/\/\*\*?$/, "");
3265
3437
  const lastSegment = stripped.split("/").pop() ?? stripped;
@@ -3300,6 +3472,18 @@ function globCoversPath(glob, path) {
3300
3472
  const p = path.replace(/\/+$/, "");
3301
3473
  return g === p || g.startsWith(`${p}/`) || p.startsWith(`${g}/`);
3302
3474
  }
3475
+ function checkSurfaceGlobsResolve(body, resolvesToFile) {
3476
+ const surface = parseIssueSurface(body);
3477
+ if (!surface.ok)
3478
+ return { status: "pass", errors: [] };
3479
+ const errors = [];
3480
+ for (const glob of surface.value.in) {
3481
+ if (!resolvesToFile(glob)) {
3482
+ 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.`);
3483
+ }
3484
+ }
3485
+ return { status: errors.length > 0 ? "fail" : "pass", errors };
3486
+ }
3303
3487
  function checkSurfaceScope(changedFiles, outGlobs) {
3304
3488
  if (outGlobs.length === 0)
3305
3489
  return { ok: true };
@@ -3311,9 +3495,77 @@ function checkSurfaceScope(changedFiles, outGlobs) {
3311
3495
  }
3312
3496
  return violations.length > 0 ? { ok: false, violations } : { ok: true };
3313
3497
  }
3498
+ var ISSUE_PART_LINE_RE = /^Part\s+(\d+)\s*\(([^)]*)\)\s*[-—–]\s*(.*)$/i;
3499
+ function parseIssueParts(body) {
3500
+ const section = topLevelSectionText(body, "Parts");
3501
+ if (section === null)
3502
+ return { ok: false, errors: ["no `## Parts` heading found in the body."] };
3503
+ const lines = section.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
3504
+ const parts = [];
3505
+ const errors = [];
3506
+ for (const line of lines) {
3507
+ const m = ISSUE_PART_LINE_RE.exec(line);
3508
+ if (!m) {
3509
+ errors.push(`"${line}" is not a well-formed Parts line — expected \`Part <n> (<refs>) — <outcome>\`.`);
3510
+ continue;
3511
+ }
3512
+ const n = Number.parseInt(m[1], 10);
3513
+ const refs = m[2];
3514
+ const text = m[3].trim();
3515
+ if (text.length === 0) {
3516
+ errors.push(`Part ${n} has no outcome text after the dash — every Part states one observable outcome.`);
3517
+ continue;
3518
+ }
3519
+ if (partHasBacktickedPath(text) && partWordCount(stripPartBackticks(text)) < PART_MIN_WORDS_OUTSIDE_BACKTICKS) {
3520
+ errors.push(`Part ${n} is little more than a file path — a Part names an outcome and symbols, never a bare path.`);
3521
+ continue;
3522
+ }
3523
+ const objectiveIds = [...refs.matchAll(/O(\d+)/g)].map((r) => Number.parseInt(r[1], 10));
3524
+ parts.push({ n, objectiveIds, text });
3525
+ }
3526
+ if (parts.length === 0) {
3527
+ errors.push("the `## Parts` section has no well-formed `Part <n> (<refs>) — <outcome>` lines.");
3528
+ }
3529
+ if (errors.length > 0)
3530
+ return { ok: false, errors };
3531
+ return { ok: true, value: parts };
3532
+ }
3533
+ function checkPartsCiteDefinedObjectives(body) {
3534
+ const parts = parseIssueParts(body);
3535
+ const objectives = objectivesOf(body);
3536
+ if (!parts.ok || !objectives.ok)
3537
+ return { status: "pass", errors: [] };
3538
+ const definedIds = new Set(objectives.objectives.map((o) => Number.parseInt(o.id.slice(1), 10)));
3539
+ const errors = [];
3540
+ for (const part of parts.value) {
3541
+ for (const objectiveId of part.objectiveIds) {
3542
+ if (!definedIds.has(objectiveId)) {
3543
+ errors.push(`issue-validation Parts: Part ${part.n} cites O${objectiveId}, which the Issue's own \`## Objectives\` section does not define.`);
3544
+ }
3545
+ }
3546
+ }
3547
+ return { status: errors.length > 0 ? "fail" : "pass", errors };
3548
+ }
3314
3549
  function isTaskIssueLabelSet(labels) {
3315
3550
  return hasLabel("tranche", labels);
3316
3551
  }
3552
+ function checkTrancheLabelPresence(_body, _labels) {
3553
+ return { status: "pass", errors: [] };
3554
+ }
3555
+ function checkMilestoneAttach(labels, currentMilestoneTitle, resolvedMilestoneTitle) {
3556
+ if (!isTaskIssueLabelSet(labels))
3557
+ return { status: "pass", errors: [] };
3558
+ if (resolvedMilestoneTitle === null)
3559
+ return { status: "pass", errors: [] };
3560
+ if (currentMilestoneTitle === resolvedMilestoneTitle)
3561
+ return { status: "pass", errors: [] };
3562
+ return {
3563
+ status: "fail",
3564
+ errors: [
3565
+ `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}".`
3566
+ ]
3567
+ };
3568
+ }
3317
3569
  var TYPE_LABEL_IDS = LABELS.filter((l) => l.category === "type").map((l) => l.id);
3318
3570
  function isControlCodePoint(codePoint) {
3319
3571
  return codePoint <= 31 || codePoint >= 127 && codePoint <= 159;
@@ -3378,6 +3630,19 @@ function checkProjectsRegistered(body, _labels, registeredNames) {
3378
3630
  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
3631
  var DOC_PATH_RE_GLOBAL = new RegExp(DOC_PATH_RE.source, "gi");
3380
3632
 
3633
+ // ../../packages/aeg-core/src/task-branch-identity.ts
3634
+ var ISSUE_BRANCH_PATTERN = /^task\/issue-(\d+)$/;
3635
+ var TRANCHE_BRANCH_PATTERN = /^task\/([^/]+)\/([^/]+)$/;
3636
+ function parseTaskBranchIdentity(branch) {
3637
+ const issueMatch = ISSUE_BRANCH_PATTERN.exec(branch);
3638
+ if (issueMatch)
3639
+ return { kind: "issue", issueNumber: Number(issueMatch[1]) };
3640
+ const trancheMatch = TRANCHE_BRANCH_PATTERN.exec(branch);
3641
+ if (trancheMatch)
3642
+ return { kind: "tranche", tranche: trancheMatch[1], taskId: trancheMatch[2] };
3643
+ return null;
3644
+ }
3645
+
3381
3646
  // ../../packages/aeg-core/src/coherence-checks.ts
3382
3647
  var COHERENCE_ENFORCED_FROM = "2026-07-01";
3383
3648
  function isGrandfathered(isoDate) {
@@ -3398,6 +3663,7 @@ function checkA1(entries, principalAllowlist = PRINCIPAL_ALLOWLIST) {
3398
3663
  if (handClosed)
3399
3664
  continue;
3400
3665
  failures.push({
3666
+ code: "closed-without-merge",
3401
3667
  issue: e.task.issue,
3402
3668
  tranche: e.trancheSlug,
3403
3669
  task: e.task.id,
@@ -3422,6 +3688,7 @@ function checkA3(entries) {
3422
3688
  continue;
3423
3689
  if (e.facts.prState === "merged" && e.facts.issueState !== "closed") {
3424
3690
  failures.push({
3691
+ code: "auto-close-misfire",
3425
3692
  issue: e.task.issue,
3426
3693
  tranche: e.trancheSlug,
3427
3694
  task: e.task.id,
@@ -3446,6 +3713,7 @@ function checkT1(entries) {
3446
3713
  continue;
3447
3714
  if (e.facts === undefined) {
3448
3715
  failures.push({
3716
+ code: "phantom-issue-ref",
3449
3717
  issue: e.task.issue,
3450
3718
  tranche: e.trancheSlug,
3451
3719
  task: e.task.id,
@@ -3464,6 +3732,7 @@ function checkT2(openIssuesBySlug, topologyIssuesBySlug, ciTrancheSlug) {
3464
3732
  for (const num of openNums) {
3465
3733
  if (!topologySet.has(num)) {
3466
3734
  failures.push({
3735
+ code: "orphan-task",
3467
3736
  issue: num,
3468
3737
  tranche: slug,
3469
3738
  reason: `Issue #${num} is open and labeled ${trancheLabel(slug)} but does not appear in the topology file`
@@ -3498,6 +3767,7 @@ function checkT3(entries, ciTrancheSlug, enrichedEntries, forgeUnavailableSlugs)
3498
3767
  continue;
3499
3768
  if (forgeUnavailableSlugs?.has(e.trancheSlug)) {
3500
3769
  failures.push({
3770
+ code: "tbd-in-active-tranche",
3501
3771
  issue: null,
3502
3772
  tranche: e.trancheSlug,
3503
3773
  task: e.task.id,
@@ -3507,6 +3777,7 @@ function checkT3(entries, ciTrancheSlug, enrichedEntries, forgeUnavailableSlugs)
3507
3777
  continue;
3508
3778
  }
3509
3779
  failures.push({
3780
+ code: "tbd-in-active-tranche",
3510
3781
  issue: null,
3511
3782
  tranche: e.trancheSlug,
3512
3783
  task: e.task.id,
@@ -3539,6 +3810,7 @@ function checkD1(entries, issueToEntry, taskToEntry) {
3539
3810
  const sameIssue = depEntry.task.issue !== null && e.task.issue !== null && depEntry.task.issue === e.task.issue;
3540
3811
  if (sameTask || sameIssue) {
3541
3812
  failures.push({
3813
+ code: "d1-self-dependency",
3542
3814
  issue: e.task.issue,
3543
3815
  tranche: e.trancheSlug,
3544
3816
  task: e.task.id,
@@ -3550,6 +3822,7 @@ function checkD1(entries, issueToEntry, taskToEntry) {
3550
3822
  const depClosed = depFacts?.issueState === "closed";
3551
3823
  if (!depClosed) {
3552
3824
  failures.push({
3825
+ code: "dispatched-on-unmet-deps",
3553
3826
  issue: e.task.issue,
3554
3827
  tranche: e.trancheSlug,
3555
3828
  task: e.task.id,
@@ -3575,11 +3848,13 @@ function checkR1(issuesBySlug, grandfatheredIssues, registeredNames = []) {
3575
3848
  const errors = [
3576
3849
  ...checkIssueRationale(issue.body).errors,
3577
3850
  ...checkProjectsRegistered(issue.body, issue.labels, registeredNames).errors,
3578
- ...checkIssueObjectives(issue.body, issue.number).errors
3851
+ ...checkIssueObjectives(issue.body, issue.number).errors,
3852
+ ...checkPartsCiteDefinedObjectives(issue.body).errors
3579
3853
  ];
3580
3854
  if (errors.length === 0)
3581
3855
  continue;
3582
3856
  failures.push({
3857
+ code: "missing-rationale-field",
3583
3858
  issue: issue.number,
3584
3859
  tranche: slug,
3585
3860
  reason: `Issue #${issue.number} fails the rationale gate: ${errors.join(" | ")}`,
@@ -3608,6 +3883,7 @@ function checkL1(files, entriesBySlug) {
3608
3883
  const allClosed = withFacts.every((e) => e.facts?.issueState === "closed");
3609
3884
  if (allClosed) {
3610
3885
  failures.push({
3886
+ code: "archive-recommended",
3611
3887
  tranche: f.slug,
3612
3888
  reason: "Active tranche has no open task-Issues — consider archiving to completed/"
3613
3889
  });
@@ -3639,27 +3915,38 @@ function extractClosesReferences(prBody) {
3639
3915
  }
3640
3916
  return referenced;
3641
3917
  }
3642
- function checkClosesN2(branch, prBody, trancheFiles, taskIssueRefs) {
3918
+ function checkClosesNTopology(branch, prBody, trancheFiles, taskIssueRefs) {
3643
3919
  const referenced = extractClosesReferences(prBody);
3644
3920
  if (taskIssueRefs) {
3645
3921
  for (const n of referenced) {
3646
- const ref = taskIssueRefs.get(n);
3647
- if (!ref)
3922
+ const ref2 = taskIssueRefs.get(n);
3923
+ if (!ref2)
3648
3924
  continue;
3649
- const expectedBranch = `task/${ref.trancheSlug}/${ref.taskId}`;
3925
+ const expectedBranch = `task/${ref2.trancheSlug}/${ref2.taskId}`;
3650
3926
  if (branch !== expectedBranch) {
3651
3927
  return {
3652
3928
  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.`
3929
+ 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
3930
  };
3655
3931
  }
3656
3932
  }
3657
3933
  }
3658
- const m = branch.match(/^task\/([^/]+)\/([^/]+)$/);
3659
- if (!m)
3934
+ const ref = parseTaskBranchIdentity(branch);
3935
+ if (!ref)
3660
3936
  return { ok: true };
3661
- const trancheSlug = m[1];
3662
- const taskId = m[2];
3937
+ if (ref.kind === "issue") {
3938
+ const expectedIssue2 = ref.issueNumber;
3939
+ if (!referenced.has(expectedIssue2)) {
3940
+ return {
3941
+ ok: false,
3942
+ expectedIssue: expectedIssue2,
3943
+ message: `closes-n: PR body does not contain \`Closes #${expectedIssue2}\` (required for branch "${branch}"). Add it to the PR body Summary section.`
3944
+ };
3945
+ }
3946
+ return { ok: true, expectedIssue: expectedIssue2 };
3947
+ }
3948
+ const trancheSlug = ref.tranche;
3949
+ const taskId = ref.taskId;
3663
3950
  const trancheFile = trancheFiles.find((f) => f.slug === trancheSlug);
3664
3951
  if (!trancheFile) {
3665
3952
  return {
@@ -3711,34 +3998,79 @@ function extractIssue(body) {
3711
3998
  const issue = headerNums.length > 0 ? headerNums[0] : bodyNums[0];
3712
3999
  return { issue, extraIssues: bodyNums.filter((n) => n !== issue), outsideHeader: headerNums.length === 0 };
3713
4000
  }
3714
- // ../../packages/aeg-core/src/review-gate.ts
3715
- function isBoundToHead(extraction, headSha) {
3716
- if (!extraction.headSha)
4001
+ // ../../packages/aeg-core/src/review-input-manifest.ts
4002
+ import { createHash as createHash4 } from "node:crypto";
4003
+ function briefHash(brief) {
4004
+ return createHash4("sha256").update(`${brief}
4005
+ `).digest("hex");
4006
+ }
4007
+ function policyDigest(policy) {
4008
+ return createHash4("sha256").update(JSON.stringify({
4009
+ codeReviewThreshold: policy.codeReviewThreshold,
4010
+ securityThreshold: policy.securityThreshold,
4011
+ maxRounds: policy.maxRounds
4012
+ })).digest("hex");
4013
+ }
4014
+ function isBoundToHead(echoed, headSha) {
4015
+ if (!echoed.headSha)
3717
4016
  return false;
3718
- return headSha.toLowerCase().startsWith(extraction.headSha.toLowerCase());
4017
+ return headSha.toLowerCase().startsWith(echoed.headSha.toLowerCase());
3719
4018
  }
3720
- function isBoundByPatchIdentity(extraction, headSha, patchIdOf) {
3721
- if (patchIdOf === undefined || !extraction.headSha)
4019
+ function isBoundByPatchIdentity(echoed, headSha, patchIdOf) {
4020
+ if (patchIdOf === undefined || !echoed.headSha)
3722
4021
  return false;
3723
- const judged = patchIdOf(extraction.headSha);
4022
+ const judged = patchIdOf(echoed.headSha);
3724
4023
  const current = patchIdOf(headSha);
3725
4024
  if (judged === null || current === null)
3726
4025
  return false;
3727
4026
  return judged === current;
3728
4027
  }
3729
- function isBoundToPatch(extraction, headSha, patchIdOf) {
3730
- return isBoundToHead(extraction, headSha) || isBoundByPatchIdentity(extraction, headSha, patchIdOf);
4028
+ function isBoundToBase(echoed, currentBase) {
4029
+ if (currentBase === null)
4030
+ return true;
4031
+ if (!echoed.baseSha)
4032
+ return false;
4033
+ return currentBase.toLowerCase().startsWith(echoed.baseSha.toLowerCase());
3731
4034
  }
3732
- function isBoundToObjectives(extraction, currentVersion) {
4035
+ function isBoundToObjectives(echoed, currentVersion) {
3733
4036
  if (currentVersion === null)
3734
4037
  return true;
3735
- return extraction.objectivesVersion === currentVersion;
4038
+ return echoed.objectivesVersion === currentVersion;
3736
4039
  }
3737
- function isBoundToRulings(extraction, currentOrdinal) {
3738
- if (extraction.rulingOrdinal === null)
4040
+ function isBoundToRulings(echoed, currentOrdinal) {
4041
+ if (echoed.rulingOrdinal === null)
3739
4042
  return currentOrdinal === 0;
3740
- return extraction.rulingOrdinal === currentOrdinal;
4043
+ return echoed.rulingOrdinal === currentOrdinal;
4044
+ }
4045
+ function isBoundToBriefHash(echoed, currentHash) {
4046
+ if (currentHash === null)
4047
+ return true;
4048
+ return echoed.briefHash === currentHash;
4049
+ }
4050
+ function isBoundToPolicy(echoed, currentDigest) {
4051
+ return echoed.policyDigest === currentDigest;
4052
+ }
4053
+ function compareManifest(echoed, current, patchIdOf) {
4054
+ const exactHead = isBoundToHead(echoed, current.headSha);
4055
+ const patchHead = exactHead ? false : isBoundByPatchIdentity(echoed, current.headSha, patchIdOf);
4056
+ const head = exactHead || patchHead;
4057
+ const base = patchHead ? true : isBoundToBase(echoed, current.baseSha);
4058
+ const briefHashBound = isBoundToBriefHash(echoed, current.briefHash);
4059
+ const objectivesVersion2 = isBoundToObjectives(echoed, current.objectivesVersion);
4060
+ const rulingOrdinal = isBoundToRulings(echoed, current.rulingOrdinal);
4061
+ const policyDigestBound = isBoundToPolicy(echoed, current.policyDigest);
4062
+ return {
4063
+ bound: head && base && briefHashBound && objectivesVersion2 && rulingOrdinal && policyDigestBound,
4064
+ head,
4065
+ base,
4066
+ briefHash: briefHashBound,
4067
+ objectivesVersion: objectivesVersion2,
4068
+ rulingOrdinal,
4069
+ policyDigest: policyDigestBound
4070
+ };
3741
4071
  }
4072
+
4073
+ // ../../packages/aeg-core/src/review-gate.ts
3742
4074
  function checkReviewGate(input) {
3743
4075
  const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
3744
4076
  const waived = isWaiverLabelActorVerified({
@@ -3761,15 +4093,58 @@ function checkReviewGate(input) {
3761
4093
  const verifiedBodies = verified.map((c) => c.body);
3762
4094
  const codeReview = extractCodeReviewVerdict(verifiedBodies);
3763
4095
  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) {
4096
+ const policy = input.policy ?? DEFAULT_REVIEW_POLICY;
4097
+ let codeReviewPolicyEvaluation;
4098
+ let securityPolicyEvaluation;
4099
+ try {
4100
+ codeReviewPolicyEvaluation = evaluateCodeReview(codeReview.findingSeverities, policy);
4101
+ securityPolicyEvaluation = evaluateSecurityReview(security.findingSeverities, policy);
4102
+ } catch (err) {
4103
+ return {
4104
+ verdict: "fail",
4105
+ reason: `a verdict comment carries a finding severity this repository's policy does not recognize: ${err instanceof Error ? err.message : String(err)}`,
4106
+ waived: false
4107
+ };
4108
+ }
4109
+ const codeReviewTextClean = codeReview.value === "APPROVE";
4110
+ const securityTextClean = security.value === "PASS";
4111
+ const codeReviewPolicyClean = codeReviewPolicyEvaluation.outcome === "clean";
4112
+ const securityPolicyClean = securityPolicyEvaluation.outcome === "clean";
4113
+ const codeReviewClean = codeReviewTextClean && codeReviewPolicyClean;
4114
+ const securityClean = securityTextClean && securityPolicyClean;
4115
+ const currentManifest = {
4116
+ headSha: input.headSha,
4117
+ baseSha: input.baseSha ?? null,
4118
+ briefHash: input.briefHash ?? null,
4119
+ objectivesVersion: input.objectivesVersion,
4120
+ rulingOrdinal: input.rulingOrdinal,
4121
+ policyDigest: policyDigest(policy)
4122
+ };
4123
+ const codeReviewEchoed = {
4124
+ headSha: codeReview.headSha,
4125
+ baseSha: codeReview.baseSha,
4126
+ briefHash: codeReview.briefHash,
4127
+ objectivesVersion: codeReview.objectivesVersion,
4128
+ rulingOrdinal: codeReview.rulingOrdinal,
4129
+ policyDigest: codeReview.policyDigest
4130
+ };
4131
+ const securityEchoed = {
4132
+ headSha: security.headSha,
4133
+ baseSha: security.baseSha,
4134
+ briefHash: security.briefHash,
4135
+ objectivesVersion: security.objectivesVersion,
4136
+ rulingOrdinal: security.rulingOrdinal,
4137
+ policyDigest: security.policyDigest
4138
+ };
4139
+ const codeReviewBinding = compareManifest(codeReviewEchoed, currentManifest, input.patchIdOf);
4140
+ const securityBinding = compareManifest(securityEchoed, currentManifest, input.patchIdOf);
4141
+ const codeReviewBound = codeReviewBinding.head;
4142
+ const securityBound = securityBinding.head;
4143
+ const codeReviewObjectivesBound = codeReviewBinding.objectivesVersion;
4144
+ const securityObjectivesBound = securityBinding.objectivesVersion;
4145
+ const codeReviewRulingsBound = codeReviewBinding.rulingOrdinal;
4146
+ const securityRulingsBound = securityBinding.rulingOrdinal;
4147
+ if (codeReviewClean && codeReviewBinding.bound && securityClean && securityBinding.bound && mechanicalChecksClean) {
3773
4148
  return {
3774
4149
  verdict: "pass",
3775
4150
  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 +4152,39 @@ function checkReviewGate(input) {
3777
4152
  };
3778
4153
  }
3779
4154
  const problems = [];
3780
- if (!codeReviewClean) {
4155
+ if (!codeReviewTextClean) {
3781
4156
  problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
4157
+ } else if (!codeReviewPolicyClean) {
4158
+ 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
4159
  } else if (!codeReviewBound) {
3783
4160
  problems.push(`the newest code-review verdict covers ${codeReview.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
4161
+ } else if (!codeReviewBinding.base) {
4162
+ problems.push(`the newest code-review verdict was cast against base ${codeReview.baseSha ?? "no recorded base"}, the PR's base is now ${currentManifest.baseSha ?? "unresolved"} (a base-only change under an unchanged candidate — same patch text on a new base is not automatically equivalent)`);
3784
4163
  } else if (!codeReviewObjectivesBound) {
3785
4164
  problems.push(`the newest code-review verdict was cast against objectives version ${codeReview.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
3786
4165
  } else if (!codeReviewRulingsBound) {
3787
4166
  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`);
4167
+ } else if (!codeReviewBinding.briefHash) {
4168
+ 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"}`);
4169
+ } else if (!codeReviewBinding.policyDigest) {
4170
+ 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
4171
  }
3789
- if (!securityClean) {
4172
+ if (!securityTextClean) {
3790
4173
  problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
4174
+ } else if (!securityPolicyClean) {
4175
+ 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
4176
  } else if (!securityBound) {
3792
4177
  problems.push(`the newest security-review verdict covers ${security.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
4178
+ } else if (!securityBinding.base) {
4179
+ problems.push(`the newest security-review verdict was cast against base ${security.baseSha ?? "no recorded base"}, the PR's base is now ${currentManifest.baseSha ?? "unresolved"} (a base-only change under an unchanged candidate — same patch text on a new base is not automatically equivalent)`);
3793
4180
  } else if (!securityObjectivesBound) {
3794
4181
  problems.push(`the newest security-review verdict was cast against objectives version ${security.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
3795
4182
  } else if (!securityRulingsBound) {
3796
4183
  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`);
4184
+ } else if (!securityBinding.briefHash) {
4185
+ 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"}`);
4186
+ } else if (!securityBinding.policyDigest) {
4187
+ 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
4188
  }
3798
4189
  if (!mechanicalChecksClean) {
3799
4190
  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 +4876,45 @@ function checkDispatchReadiness(input) {
4485
4876
  const { trancheSlug, task } = input;
4486
4877
  const taskLabel = `task ${task.id} (tranche ${trancheSlug})`;
4487
4878
  const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
4488
- const blockers = [];
4879
+ const blockerDetails = [];
4880
+ const push = (blockerClass, message) => {
4881
+ blockerDetails.push({ class: blockerClass, message });
4882
+ };
4489
4883
  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.`);
4884
+ 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
4885
  } 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).`);
4886
+ 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
4887
  }
4494
4888
  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.`);
4889
+ 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
4890
  }
4497
4891
  for (const dep of input.dependsOn) {
4498
4892
  if (isSelfDependency(dep, task, input.issue)) {
4499
4893
  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.`);
4894
+ 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
4895
  continue;
4502
4896
  }
4503
4897
  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.`);
4898
+ 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
4899
  continue;
4506
4900
  }
4507
4901
  if (!dep.merged && !isHandClosedByRecognizedPrincipal(dep, principalAllowlist)) {
4508
4902
  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.`);
4903
+ 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
4904
  }
4511
4905
  }
4512
4906
  for (const c of input.conflictsWith) {
4513
4907
  if (c.openOrInFlight) {
4514
4908
  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.`);
4909
+ 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
4910
  }
4517
4911
  }
4518
4912
  for (const proj of input.priorTrancheArchival) {
4519
4913
  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.`);
4914
+ 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
4915
  }
4522
4916
  }
4523
- return { ready: blockers.length === 0, blockers };
4917
+ return { ready: blockerDetails.length === 0, blockers: blockerDetails.map((b) => b.message), blockerDetails };
4524
4918
  }
4525
4919
  // ../../packages/aeg-core/src/single-plan-pr.ts
4526
4920
  function trancheSlugFromTopologyPath(path) {
@@ -4622,6 +5016,137 @@ function checkBranchTopology(input) {
4622
5016
  reason: `Branch \`${branch}\` matches topology row \`${taskId}\` in ${topoPath}.`
4623
5017
  };
4624
5018
  }
5019
+ // ../../packages/aeg-core/src/task-tools.ts
5020
+ import { z } from "zod";
5021
+ var TASK_TOOL_ERROR_KINDS = [
5022
+ "validation",
5023
+ "authority",
5024
+ "precondition",
5025
+ "capability",
5026
+ "infrastructure",
5027
+ "cancellation",
5028
+ "timeout",
5029
+ "uncertain_effect"
5030
+ ];
5031
+ var TaskToolErrorSchema = z.object({
5032
+ kind: z.enum(TASK_TOOL_ERROR_KINDS),
5033
+ message: z.string().min(1),
5034
+ detail: z.string().optional()
5035
+ });
5036
+ var TaskToolRefSchema = z.union([
5037
+ z.object({ tranche: z.string().min(1), id: z.string().min(1) }).strict(),
5038
+ z.object({ issue: z.number().int().positive() }).strict()
5039
+ ]);
5040
+ var MAX_PAGE_LIMIT = 100;
5041
+ var PageRequestSchema = z.object({
5042
+ cursor: z.string().optional(),
5043
+ limit: z.number().int().positive().max(MAX_PAGE_LIMIT).optional()
5044
+ });
5045
+ var FreshnessSchema = z.enum(["fresh", "stale", "unknown"]);
5046
+ var ObservedSchema = z.object({
5047
+ observedAt: z.string(),
5048
+ freshness: FreshnessSchema
5049
+ });
5050
+ var TaskStatusInputSchema = z.object({
5051
+ task: TaskToolRefSchema.optional()
5052
+ }).merge(PageRequestSchema).strict();
5053
+ var TaskStatusItemSchema = z.object({
5054
+ task: TaskToolRefSchema,
5055
+ issue: z.number().int().positive(),
5056
+ pr: z.number().int().positive().nullable(),
5057
+ state: z.string()
5058
+ }).merge(ObservedSchema);
5059
+ var TaskStatusResultSchema = z.object({
5060
+ items: z.array(TaskStatusItemSchema),
5061
+ nextCursor: z.string().nullable()
5062
+ });
5063
+ var RequestedAuthoritySchema = z.enum(["planner", "principal", "operator", "self"]);
5064
+ var EscalationInputsSchema = z.object({
5065
+ task: z.number().int().positive(),
5066
+ round: z.number().int().nonnegative(),
5067
+ head: z.string(),
5068
+ branch: z.string(),
5069
+ prNumber: z.number().int().positive()
5070
+ });
5071
+ var EscalationEvidenceSchema = z.object({
5072
+ round: z.number().int().nonnegative(),
5073
+ reviewer: z.string().nullable(),
5074
+ security: z.string().nullable()
5075
+ });
5076
+ var EscalationRunIdentitySchema = z.object({
5077
+ runId: z.string(),
5078
+ pid: z.number().int().positive(),
5079
+ host: z.string()
5080
+ });
5081
+ var EscalationInputVersionsSchema = z.object({
5082
+ briefHash: z.string().nullable(),
5083
+ objectivesVersion: z.string().nullable(),
5084
+ rulingOrdinal: z.number().int().nonnegative(),
5085
+ policyDigest: z.string()
5086
+ });
5087
+ var TaskEscalationReadInputSchema = z.object({
5088
+ task: TaskToolRefSchema
5089
+ }).merge(PageRequestSchema).strict();
5090
+ var TaskEscalationPacketSchema = z.object({
5091
+ reason: z.string(),
5092
+ detail: z.string().nullable(),
5093
+ inputs: EscalationInputsSchema.nullable(),
5094
+ evidence: EscalationEvidenceSchema.nullable(),
5095
+ attemptedRecovery: z.string(),
5096
+ requestedAuthority: RequestedAuthoritySchema,
5097
+ permittedNextActions: z.array(z.string()),
5098
+ runIdentity: EscalationRunIdentitySchema.nullable(),
5099
+ inputVersions: EscalationInputVersionsSchema.nullable()
5100
+ }).merge(ObservedSchema);
5101
+ var TaskEscalationReadResultSchema = z.object({
5102
+ items: z.array(TaskEscalationPacketSchema),
5103
+ nextCursor: z.string().nullable()
5104
+ }).merge(ObservedSchema);
5105
+ var TaskStartInputSchema = z.object({
5106
+ tranche: z.string().min(1),
5107
+ id: z.string().min(1)
5108
+ }).strict();
5109
+ var TaskStartResultSchema = z.object({
5110
+ requestId: z.string().min(1),
5111
+ run: z.object({ tranche: z.string().min(1), id: z.string().min(1) }),
5112
+ started: z.boolean(),
5113
+ startedAt: z.string(),
5114
+ mode: z.literal("attended")
5115
+ });
5116
+ var TaskResumeInputSchema = z.object({
5117
+ task: TaskToolRefSchema
5118
+ }).strict();
5119
+ var TaskCancelInputSchema = z.object({
5120
+ task: TaskToolRefSchema,
5121
+ reason: z.string().min(1)
5122
+ }).strict();
5123
+ var TaskResumeResultSchema = z.object({
5124
+ task: z.number().int().positive(),
5125
+ pr: z.number().int().positive(),
5126
+ escalationId: z.string().min(1),
5127
+ outcome: z.enum(["started", "already_resumed"]),
5128
+ authenticatedBy: z.string().min(1),
5129
+ authenticatedFrom: z.string().min(1)
5130
+ });
5131
+ var TaskCancelOutcomeSchema = z.enum(["confirmed", "pending", "uncertain"]);
5132
+ var TaskCancelResultSchema = z.object({
5133
+ task: z.number().int().positive(),
5134
+ pr: z.number().int().positive(),
5135
+ escalationId: z.string().min(1),
5136
+ outcome: TaskCancelOutcomeSchema,
5137
+ authenticatedBy: z.string().min(1),
5138
+ authenticatedFrom: z.string().min(1),
5139
+ fencedEffectKeys: z.array(z.string())
5140
+ });
5141
+ var TASK_TOOL_NAMES = [
5142
+ "task_start",
5143
+ "task_status",
5144
+ "task_escalation_read",
5145
+ "task_resume",
5146
+ "task_cancel"
5147
+ ];
5148
+ var OPERATOR_STATUS_FOLLOW = "task_status_follow";
5149
+ var OPERATOR_TOOL_GRANT = [...TASK_TOOL_NAMES, OPERATOR_STATUS_FOLLOW];
4625
5150
  // ../../packages/aeg-core/src/first-push-dispatch-gate.ts
4626
5151
  function parseTaskBranch(branch) {
4627
5152
  const m = /^task\/([^/]+)\/([^/]+)$/.exec(branch);
@@ -4705,7 +5230,6 @@ function decideIssueAssignment(input) {
4705
5230
  };
4706
5231
  }
4707
5232
  // ../../packages/aeg-core/src/test-plan-gate.ts
4708
- var TASK_BRANCH_PATTERN2 = /^task\/[^/]+\/[^/]+$/;
4709
5233
  function evaluateTestPlanGate(body, branch) {
4710
5234
  if (!body) {
4711
5235
  return {
@@ -4718,7 +5242,7 @@ function evaluateTestPlanGate(body, branch) {
4718
5242
  }
4719
5243
  const located = locateTestPlanSection(body);
4720
5244
  if (!located.found) {
4721
- if (TASK_BRANCH_PATTERN2.test(branch)) {
5245
+ if (parseTaskBranchIdentity(branch) !== null) {
4722
5246
  return {
4723
5247
  verdict: "fail",
4724
5248
  messages: [
@@ -4845,8 +5369,11 @@ function findWorkspaceEscapes(files, knownPaths, workspaceDirs = DEFAULT_WORKSPA
4845
5369
  }
4846
5370
  return findings;
4847
5371
  }
5372
+ // ../../packages/aeg-core/src/log/envelope.ts
5373
+ import { createHash as createHash5 } from "node:crypto";
5374
+
4848
5375
  // ../../packages/aeg-core/src/log/schema.ts
4849
- import { z } from "zod";
5376
+ import { z as z2 } from "zod";
4850
5377
  var ROLE_VALUES = [
4851
5378
  "planner",
4852
5379
  "developer",
@@ -4856,136 +5383,174 @@ var ROLE_VALUES = [
4856
5383
  "archivist",
4857
5384
  "architect"
4858
5385
  ];
4859
- var RoleSchema = z.enum(ROLE_VALUES);
5386
+ var RoleSchema = z2.enum(ROLE_VALUES);
4860
5387
  var HOST_VALUES = ["hook", "ci", "cli", "loop"];
4861
- var HostSchema = z.enum(HOST_VALUES);
5388
+ var HostSchema = z2.enum(HOST_VALUES);
4862
5389
  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(),
5390
+ var headerMetaCore = {
5391
+ ts: z2.string(),
5392
+ run_id: z2.string().regex(RUN_ID_PATTERN),
5393
+ seq: z2.number().int().nonnegative(),
5394
+ repo: z2.string().nullable(),
5395
+ vinaya: z2.string(),
5396
+ doctrine: z2.string(),
4871
5397
  host: HostSchema,
4872
- machine: z.string()
5398
+ machine: z2.string()
5399
+ };
5400
+ var HeaderMetaV1Schema = z2.object({
5401
+ schema: z2.literal(1),
5402
+ ...headerMetaCore
4873
5403
  }).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()
5404
+ var LineageSchema = z2.object({
5405
+ run: z2.string().nullable(),
5406
+ attempt: z2.number().int().nullable(),
5407
+ parent: z2.string().nullable()
4881
5408
  }).strict();
4882
- var HeaderSchema = z.object({
5409
+ var InputVersionsSchema = z2.object({
5410
+ objectives_version: z2.string().nullable(),
5411
+ brief_hash: z2.string().nullable(),
5412
+ ruling_ordinal: z2.number().int().nullable(),
5413
+ policy_digest: z2.string().nullable()
5414
+ }).strict();
5415
+ var ProvenanceSchema = z2.enum(["parent_attributed", "env_correlated", "self_reported", "unavailable"]);
5416
+ var HeaderMetaV2Schema = z2.object({
5417
+ schema: z2.literal(2),
5418
+ ...headerMetaCore,
5419
+ event_id: z2.string().min(1),
5420
+ process_id: z2.string().min(1),
5421
+ actor_id: z2.string().nullable(),
5422
+ lineage: LineageSchema,
5423
+ input_versions: InputVersionsSchema,
5424
+ provenance: ProvenanceSchema
5425
+ }).strict();
5426
+ var HeaderMetaSchema = z2.discriminatedUnion("schema", [HeaderMetaV1Schema, HeaderMetaV2Schema]);
5427
+ var SubjectSchema = z2.object({
5428
+ issue: z2.number().int().nullable(),
5429
+ pr: z2.number().int().optional(),
5430
+ sha: z2.string().optional(),
5431
+ role: z2.union([RoleSchema, z2.literal("unattributed")]),
5432
+ round: z2.number().int().optional(),
5433
+ objectives_version: z2.string().optional()
5434
+ }).strict();
5435
+ var HeaderSchema = z2.object({
4883
5436
  meta: HeaderMetaSchema,
4884
5437
  subject: SubjectSchema
4885
5438
  }).strict();
4886
5439
  var envelopeTail = {
4887
- duration_ms: z.number().nonnegative().optional(),
4888
- payload: z.object({}).strict()
5440
+ duration_ms: z2.number().nonnegative().optional(),
5441
+ payload: z2.object({}).strict()
4889
5442
  };
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()
5443
+ var ReviewFindingSchema = z2.object({
5444
+ id: z2.string(),
5445
+ severity: z2.string(),
5446
+ state: z2.string().optional(),
5447
+ severity_scale: z2.string().optional(),
5448
+ policy_treatment: z2.enum(["blocking", "non_blocking", "unavailable"]).optional(),
5449
+ confidence: z2.number().min(0).max(1).optional(),
5450
+ confidence_scale: z2.string().optional(),
5451
+ confidence_source: z2.string().optional()
5452
+ }).strict();
5453
+ var DispatchOutcomeSchema = z2.discriminatedUnion("type", [
5454
+ z2.object({ type: z2.literal("pr_opened"), pr: z2.number().int(), head: z2.string() }).strict(),
5455
+ z2.object({
5456
+ type: z2.literal("round_pushed"),
5457
+ pr: z2.number().int(),
5458
+ head: z2.string(),
5459
+ comment_id: z2.number().int()
4897
5460
  }).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())
5461
+ z2.object({
5462
+ type: z2.literal("verdict"),
5463
+ verdict: z2.enum(["APPROVE", "REQUEST CHANGES", "PASS", "FAIL"]),
5464
+ head: z2.string(),
5465
+ comment_id: z2.number().int(),
5466
+ objectives: z2.array(z2.object({ id: z2.string(), met: z2.boolean() }).strict()),
5467
+ findings: z2.array(ReviewFindingSchema)
4905
5468
  }).strict(),
4906
- z.object({
4907
- type: z.literal("escalation"),
4908
- class: z.enum(["authority", "strategy", "product"]),
4909
- comment_id: z.number().int()
5469
+ z2.object({
5470
+ type: z2.literal("escalation"),
5471
+ class: z2.enum(["authority", "strategy", "product"]),
5472
+ comment_id: z2.number().int()
4910
5473
  }).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()
5474
+ z2.object({ type: z2.literal("brief"), comment_id: z2.number().int(), hash: z2.string() }).strict(),
5475
+ z2.object({ type: z2.literal("plan"), issues: z2.array(z2.number().int()) }).strict(),
5476
+ z2.object({ type: z2.literal("archive"), provenance_comment_id: z2.number().int() }).strict(),
5477
+ z2.object({ type: z2.literal("completed") }).strict()
4914
5478
  ]);
4915
5479
  var dispatchShared = {
4916
5480
  meta: HeaderMetaSchema,
4917
5481
  subject: SubjectSchema,
4918
- kind: z.literal("dispatch"),
5482
+ kind: z2.literal("dispatch"),
4919
5483
  ...envelopeTail,
4920
5484
  target_role: RoleSchema,
4921
- model: z.string(),
4922
- round: z.number().int().optional(),
4923
- effect_id: z.string()
5485
+ model: z2.string(),
5486
+ round: z2.number().int().optional(),
5487
+ effect_id: z2.string()
4924
5488
  };
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({
5489
+ var dispatchUsageField = z2.object({ input: z2.number().nonnegative(), output: z2.number().nonnegative() }).strict().nullable();
5490
+ var DispatchEventSchema = z2.discriminatedUnion("event", [
5491
+ z2.object({ ...dispatchShared, event: z2.literal("dispatched"), prompt_hash: z2.string() }).strict(),
5492
+ z2.object({
4929
5493
  ...dispatchShared,
4930
- event: z.literal("outcome_received"),
5494
+ event: z2.literal("outcome_received"),
4931
5495
  outcome: DispatchOutcomeSchema,
4932
5496
  usage: dispatchUsageField
4933
5497
  }).strict(),
4934
- z.object({
5498
+ z2.object({
4935
5499
  ...dispatchShared,
4936
- event: z.literal("dispatch_failed"),
4937
- reason: z.enum(["timeout", "crash", "refused", "unattributed_write"]),
5500
+ event: z2.literal("dispatch_failed"),
5501
+ reason: z2.enum(["timeout", "crash", "refused", "unattributed_write"]),
4938
5502
  usage: dispatchUsageField
4939
5503
  }).strict()
4940
5504
  ]);
4941
5505
  var loopShared = {
4942
5506
  meta: HeaderMetaSchema,
4943
5507
  subject: SubjectSchema,
4944
- kind: z.literal("dev_review_loop"),
5508
+ kind: z2.literal("dev_review_loop"),
4945
5509
  ...envelopeTail,
4946
- loop_id: z.string()
5510
+ loop_id: z2.string()
4947
5511
  };
4948
- var DevReviewLoopEventSchema = z.discriminatedUnion("event", [
4949
- z.object({
5512
+ var DevReviewLoopEventSchema = z2.discriminatedUnion("event", [
5513
+ z2.object({
4950
5514
  ...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())
5515
+ event: z2.literal("loop_started"),
5516
+ task: z2.number().int(),
5517
+ policy: z2.object({
5518
+ max_rounds: z2.number().int().nonnegative(),
5519
+ reviewers: z2.array(RoleSchema),
5520
+ models: z2.record(RoleSchema, z2.string())
4957
5521
  }).strict()
4958
5522
  }).strict(),
4959
- z.object({ ...loopShared, event: z.literal("round_started"), round: z.number().int(), base_head: z.string() }).strict(),
4960
- z.object({
5523
+ z2.object({ ...loopShared, event: z2.literal("round_started"), round: z2.number().int(), base_head: z2.string() }).strict(),
5524
+ z2.object({
4961
5525
  ...loopShared,
4962
- event: z.literal("gate_result_read"),
4963
- round: z.number().int(),
4964
- head: z.string(),
4965
- green: z.boolean()
5526
+ event: z2.literal("gate_result_read"),
5527
+ round: z2.number().int(),
5528
+ head: z2.string(),
5529
+ green: z2.boolean()
4966
5530
  }).strict(),
4967
- z.object({
5531
+ z2.object({
4968
5532
  ...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()
5533
+ event: z2.literal("verdicts_read"),
5534
+ round: z2.number().int(),
5535
+ head: z2.string(),
5536
+ all_approve: z2.boolean(),
5537
+ blockers: z2.number().int().nonnegative(),
5538
+ findings: z2.array(ReviewFindingSchema).optional()
4974
5539
  }).strict(),
4975
- z.object({
5540
+ z2.object({
4976
5541
  ...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())
5542
+ event: z2.literal("findings_compared"),
5543
+ round: z2.number().int(),
5544
+ open: z2.array(z2.string()),
5545
+ resolved: z2.array(z2.string()),
5546
+ new: z2.array(z2.string()),
5547
+ recurring: z2.array(z2.string())
4983
5548
  }).strict(),
4984
- z.object({
5549
+ z2.object({
4985
5550
  ...loopShared,
4986
- event: z.literal("stop_condition_met"),
4987
- round: z.number().int(),
4988
- condition: z.enum([
5551
+ event: z2.literal("stop_condition_met"),
5552
+ round: z2.number().int(),
5553
+ condition: z2.enum([
4989
5554
  "green",
4990
5555
  "max_rounds",
4991
5556
  "no_progress",
@@ -4995,42 +5560,49 @@ var DevReviewLoopEventSchema = z.discriminatedUnion("event", [
4995
5560
  "reappearance"
4996
5561
  ])
4997
5562
  }).strict(),
4998
- z.object({
5563
+ z2.object({
4999
5564
  ...loopShared,
5000
- event: z.literal("paused"),
5001
- round: z.number().int(),
5002
- reason: z.enum(["escalation", "principal_item", "refreeze_needed"])
5565
+ event: z2.literal("paused"),
5566
+ round: z2.number().int(),
5567
+ reason: z2.enum(["escalation", "principal_item", "refreeze_needed"])
5003
5568
  }).strict(),
5004
- z.object({
5569
+ z2.object({
5005
5570
  ...loopShared,
5006
- event: z.literal("resumed"),
5007
- round: z.number().int(),
5008
- by: z.literal("principal")
5571
+ event: z2.literal("resumed"),
5572
+ round: z2.number().int(),
5573
+ by: z2.literal("principal")
5009
5574
  }).strict(),
5010
- z.object({
5575
+ z2.object({
5011
5576
  ...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"])
5577
+ event: z2.literal("unpushed_work_resume"),
5578
+ round: z2.number().int(),
5579
+ branch: z2.string(),
5580
+ detail: z2.string()
5021
5581
  }).strict(),
5022
- z.object({
5582
+ z2.object({
5023
5583
  ...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"])
5584
+ event: z2.literal("round_ended"),
5585
+ round: z2.number().int(),
5586
+ base_head: z2.string(),
5587
+ head: z2.string(),
5588
+ files_changed: z2.number().int().nonnegative(),
5589
+ insertions: z2.number().int().nonnegative(),
5590
+ deletions: z2.number().int().nonnegative(),
5591
+ wall_ms: z2.number().nonnegative(),
5592
+ outcome: z2.enum(["green", "changes_requested", "escalated"])
5593
+ }).strict(),
5594
+ z2.object({
5595
+ ...loopShared,
5596
+ event: z2.literal("journal_finalized"),
5597
+ rounds: z2.number().int().nonnegative(),
5598
+ total_wall_ms: z2.number().nonnegative(),
5599
+ time_to_green_ms: z2.number().nonnegative().nullable(),
5600
+ files_changed_total: z2.number().int().nonnegative(),
5601
+ final_head: z2.string(),
5602
+ result: z2.enum(["merged_ready", "stopped"])
5031
5603
  }).strict()
5032
5604
  ]);
5033
- var ForgeOpSchema = z.enum([
5605
+ var ForgeOpSchema = z2.enum([
5034
5606
  "pr.create",
5035
5607
  "pr.comment",
5036
5608
  "pr.body.replace",
@@ -5044,24 +5616,434 @@ var ForgeOpSchema = z.enum([
5044
5616
  "label.add",
5045
5617
  "label.remove"
5046
5618
  ]);
5047
- var ForgeWriteTargetSchema = z.object({
5048
- issue: z.number().int().optional(),
5049
- pr: z.number().int().optional()
5619
+ var ForgeWriteTargetSchema = z2.object({
5620
+ issue: z2.number().int().optional(),
5621
+ pr: z2.number().int().optional()
5050
5622
  }).strict();
5051
5623
  var forgeWriteShared = {
5052
5624
  meta: HeaderMetaSchema,
5053
5625
  subject: SubjectSchema,
5054
- kind: z.literal("forge_write"),
5626
+ kind: z2.literal("forge_write"),
5055
5627
  ...envelopeTail,
5056
5628
  op: ForgeOpSchema,
5057
5629
  target: ForgeWriteTargetSchema
5058
5630
  };
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()
5631
+ var ForgeWriteEventSchema = z2.discriminatedUnion("event", [
5632
+ z2.object({ ...forgeWriteShared, event: z2.literal("validated") }).strict(),
5633
+ z2.object({ ...forgeWriteShared, event: z2.literal("refused"), reason: z2.string() }).strict(),
5634
+ z2.object({ ...forgeWriteShared, event: z2.literal("written"), comment_ids: z2.array(z2.string()) }).strict()
5635
+ ]);
5636
+ var GateOutcomeSchema = z2.enum([
5637
+ "pass",
5638
+ "fail",
5639
+ "wait",
5640
+ "skip",
5641
+ "invalid_input",
5642
+ "unavailable_dependency",
5643
+ "timeout",
5644
+ "cancelled"
5645
+ ]);
5646
+ var gateShared = {
5647
+ meta: HeaderMetaSchema,
5648
+ subject: SubjectSchema,
5649
+ kind: z2.literal("gate"),
5650
+ ...envelopeTail,
5651
+ check: z2.string(),
5652
+ check_version: z2.string().nullable(),
5653
+ policy_version: z2.string().nullable(),
5654
+ input_fingerprint: z2.string().nullable()
5655
+ };
5656
+ var GateEventSchema = z2.discriminatedUnion("event", [
5657
+ z2.object({
5658
+ ...gateShared,
5659
+ event: z2.literal("checked"),
5660
+ outcome: GateOutcomeSchema,
5661
+ reason: z2.string().optional()
5662
+ }).strict()
5663
+ ]);
5664
+ var OperationResultSchema = z2.enum(["ok", "error", "refused", "timeout", "cancelled", "unavailable"]);
5665
+ var operationShared = {
5666
+ meta: HeaderMetaSchema,
5667
+ subject: SubjectSchema,
5668
+ kind: z2.literal("operation"),
5669
+ ...envelopeTail,
5670
+ operation: z2.string(),
5671
+ target: z2.string().nullable()
5672
+ };
5673
+ var OperationEventSchema = z2.discriminatedUnion("event", [
5674
+ z2.object({
5675
+ ...operationShared,
5676
+ event: z2.literal("completed"),
5677
+ result: OperationResultSchema,
5678
+ error_class: z2.string().nullable()
5679
+ }).strict()
5680
+ ]);
5681
+ var UsageUnitsSchema = z2.object({
5682
+ input: z2.number().nonnegative().nullable(),
5683
+ output: z2.number().nonnegative().nullable(),
5684
+ cache: z2.number().nonnegative().nullable()
5685
+ }).strict();
5686
+ var usageShared = {
5687
+ meta: HeaderMetaSchema,
5688
+ subject: SubjectSchema,
5689
+ kind: z2.literal("usage"),
5690
+ ...envelopeTail,
5691
+ model: z2.string().nullable(),
5692
+ source: z2.string(),
5693
+ semantics: z2.enum(["cumulative", "delta"])
5694
+ };
5695
+ var UsageEventSchema = z2.discriminatedUnion("event", [
5696
+ z2.object({
5697
+ ...usageShared,
5698
+ event: z2.literal("observed"),
5699
+ units: UsageUnitsSchema,
5700
+ unknown_reason: z2.string().nullable()
5701
+ }).strict()
5702
+ ]);
5703
+ var RoleAttemptOutcomeSchema = z2.enum([
5704
+ "completed",
5705
+ "incomplete",
5706
+ "infrastructure_failed",
5707
+ "cancelled",
5708
+ "timed_out",
5709
+ "capability_refused"
5710
+ ]);
5711
+ var roleAttemptShared = {
5712
+ meta: HeaderMetaSchema,
5713
+ subject: SubjectSchema,
5714
+ kind: z2.literal("role_attempt"),
5715
+ ...envelopeTail,
5716
+ actor: z2.string().nullable(),
5717
+ attempt: z2.number().int().nullable(),
5718
+ effect_id: z2.string(),
5719
+ model: z2.string().nullable()
5720
+ };
5721
+ var RoleAttemptEventSchema = z2.discriminatedUnion("event", [
5722
+ z2.object({
5723
+ ...roleAttemptShared,
5724
+ event: z2.literal("attempted"),
5725
+ outcome: RoleAttemptOutcomeSchema,
5726
+ usage: dispatchUsageField
5727
+ }).strict()
5728
+ ]);
5729
+ var handoffShared = {
5730
+ meta: HeaderMetaSchema,
5731
+ subject: SubjectSchema,
5732
+ kind: z2.literal("handoff"),
5733
+ ...envelopeTail,
5734
+ class: z2.enum(["authority", "strategy", "product"]),
5735
+ reason: z2.string()
5736
+ };
5737
+ var HandoffEventSchema = z2.discriminatedUnion("event", [
5738
+ z2.object({
5739
+ ...handoffShared,
5740
+ event: z2.literal("raised"),
5741
+ requested_decision: z2.string().nullable()
5742
+ }).strict(),
5743
+ z2.object({
5744
+ ...handoffShared,
5745
+ event: z2.literal("resolved"),
5746
+ resolution: z2.string().nullable(),
5747
+ resolved_by: z2.string().nullable()
5748
+ }).strict()
5749
+ ]);
5750
+ var EffectTargetSchema = z2.object({
5751
+ kind: z2.string(),
5752
+ ref: z2.string()
5753
+ }).strict();
5754
+ var effectShared = {
5755
+ meta: HeaderMetaSchema,
5756
+ subject: SubjectSchema,
5757
+ kind: z2.literal("effect"),
5758
+ ...envelopeTail,
5759
+ effect_id: z2.string(),
5760
+ target: EffectTargetSchema
5761
+ };
5762
+ var EffectEventSchema = z2.discriminatedUnion("event", [
5763
+ z2.object({ ...effectShared, event: z2.literal("attempted") }).strict(),
5764
+ z2.object({ ...effectShared, event: z2.literal("observed"), outcome: z2.enum(["success", "failure", "uncertain"]) }).strict(),
5765
+ z2.object({ ...effectShared, event: z2.literal("verified"), outcome: z2.enum(["success", "failure", "uncertain"]) }).strict()
5766
+ ]);
5767
+ var LogEventSchema = z2.union([
5768
+ DispatchEventSchema,
5769
+ DevReviewLoopEventSchema,
5770
+ ForgeWriteEventSchema,
5771
+ GateEventSchema,
5772
+ OperationEventSchema,
5773
+ UsageEventSchema,
5774
+ RoleAttemptEventSchema,
5775
+ HandoffEventSchema,
5776
+ EffectEventSchema
5063
5777
  ]);
5064
- var LogEventSchema = z.union([DispatchEventSchema, DevReviewLoopEventSchema, ForgeWriteEventSchema]);
5778
+
5779
+ // ../../packages/aeg-core/src/log/envelope.ts
5780
+ function isRole(value) {
5781
+ return value !== undefined && ROLE_VALUES.includes(value);
5782
+ }
5783
+ function actorIdFromEnv(role) {
5784
+ return role === undefined || role === "" ? null : role;
5785
+ }
5786
+ function lineageFieldFromEnv(value) {
5787
+ return value === undefined || value === "" ? null : value;
5788
+ }
5789
+ function attemptFromEnv(value) {
5790
+ if (value === undefined || value === "")
5791
+ return null;
5792
+ const n = Number(value);
5793
+ return Number.isInteger(n) ? n : null;
5794
+ }
5795
+ function provenanceFor(input) {
5796
+ if (input.provenance)
5797
+ return input.provenance;
5798
+ if (input.env.role || input.env.task)
5799
+ return "env_correlated";
5800
+ return "unavailable";
5801
+ }
5802
+ function issueFromTask(task) {
5803
+ if (task === undefined || task === "")
5804
+ return null;
5805
+ const n = Number(task);
5806
+ return Number.isInteger(n) ? n : null;
5807
+ }
5808
+ function roundFromEnv(round) {
5809
+ if (round === undefined || round === "")
5810
+ return;
5811
+ const n = Number(round);
5812
+ return Number.isInteger(n) ? n : undefined;
5813
+ }
5814
+ function buildHeader(input) {
5815
+ const role = isRole(input.env.role) ? input.env.role : "unattributed";
5816
+ const round = roundFromEnv(input.env.round);
5817
+ const subject = {
5818
+ issue: issueFromTask(input.env.task),
5819
+ role,
5820
+ ...round !== undefined ? { round } : {},
5821
+ ...input.subject ?? {}
5822
+ };
5823
+ return {
5824
+ meta: {
5825
+ schema: 2,
5826
+ ts: input.now.toISOString(),
5827
+ run_id: input.runId,
5828
+ seq: input.seq,
5829
+ repo: input.repo,
5830
+ vinaya: input.vinaya,
5831
+ doctrine: input.doctrine,
5832
+ host: input.host,
5833
+ machine: createHash5("sha256").update(input.hostname).digest("hex"),
5834
+ event_id: input.eventId,
5835
+ process_id: input.processId,
5836
+ actor_id: actorIdFromEnv(input.env.role),
5837
+ lineage: {
5838
+ run: lineageFieldFromEnv(input.env.run),
5839
+ attempt: attemptFromEnv(input.env.attempt),
5840
+ parent: lineageFieldFromEnv(input.env.parent)
5841
+ },
5842
+ input_versions: {
5843
+ objectives_version: input.inputVersions?.objectivesVersion ?? null,
5844
+ brief_hash: input.inputVersions?.briefHash ?? null,
5845
+ ruling_ordinal: input.inputVersions?.rulingOrdinal ?? null,
5846
+ policy_digest: input.inputVersions?.policyDigest ?? null
5847
+ },
5848
+ provenance: provenanceFor(input)
5849
+ },
5850
+ subject
5851
+ };
5852
+ }
5853
+ // ../../packages/aeg-core/src/log/redact.ts
5854
+ var GITHUB_TOKEN_PATTERN = /\b(?:gho_|ghp_|ghs_|ghu_|ghr_|github_pat_)[A-Za-z0-9_]+\b/g;
5855
+ var CLASSIC_GITHUB_PAT_PATTERN = /\b((?:token|GITHUB_TOKEN|PAT|api[_-]?key|api[_-]?token)\s*[:=]\s*)[a-f0-9]{40}\b/gi;
5856
+ var BEARER_TOKEN_PATTERN = /\bBearer\s+\S+/g;
5857
+ var AWS_ACCESS_KEY_PATTERN = /\bAKIA[0-9A-Z]{16}\b/g;
5858
+ var SLACK_TOKEN_PATTERN = /\bxox[baprs]-[0-9A-Za-z-]+\b/g;
5859
+ var STRIPE_KEY_PATTERN = /\b(?:sk|pk|rk)_(?:live|test)_[0-9A-Za-z]{10,}\b/g;
5860
+ var ANTHROPIC_KEY_PATTERN = /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g;
5861
+ var OPENAI_KEY_PATTERN = /\bsk-(?!ant-)[A-Za-z0-9]{20,}\b/g;
5862
+ var NPM_TOKEN_PATTERN = /\bnpm_[A-Za-z0-9]{36}\b/g;
5863
+ var JWT_PATTERN = /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g;
5864
+ var PEM_BLOCK_PATTERN = /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g;
5865
+ var BASIC_AUTH_URL_PATTERN = /(https?:\/\/)[^/\s:@]+:[^/\s@]+@/g;
5866
+ var GENERIC_SECRET_ASSIGNMENT_PATTERN = /\b((?:secret|password|passwd|api[_-]?key|access[_-]?key|client[_-]?secret|private[_-]?key)\s*[:=]\s*)[A-Za-z0-9+/_=-]{16,}\b/gi;
5867
+ function redactString(value, home) {
5868
+ let out = value.replace(GITHUB_TOKEN_PATTERN, "<redacted>").replace(CLASSIC_GITHUB_PAT_PATTERN, "$1<redacted>").replace(BEARER_TOKEN_PATTERN, "<redacted>").replace(AWS_ACCESS_KEY_PATTERN, "<redacted>").replace(SLACK_TOKEN_PATTERN, "<redacted>").replace(STRIPE_KEY_PATTERN, "<redacted>").replace(ANTHROPIC_KEY_PATTERN, "<redacted>").replace(OPENAI_KEY_PATTERN, "<redacted>").replace(NPM_TOKEN_PATTERN, "<redacted>").replace(JWT_PATTERN, "<redacted>").replace(PEM_BLOCK_PATTERN, "<redacted>").replace(BASIC_AUTH_URL_PATTERN, "$1<redacted>@").replace(GENERIC_SECRET_ASSIGNMENT_PATTERN, "$1<redacted>");
5869
+ if (home.length > 0 && out.startsWith(home)) {
5870
+ const rest = out.slice(home.length);
5871
+ if (rest === "" || rest.startsWith("/"))
5872
+ out = `~${rest}`;
5873
+ }
5874
+ return out;
5875
+ }
5876
+ function redact(value, home) {
5877
+ if (typeof value === "string")
5878
+ return redactString(value, home);
5879
+ if (Array.isArray(value))
5880
+ return value.map((v) => redact(v, home));
5881
+ if (value !== null && typeof value === "object") {
5882
+ const out = {};
5883
+ for (const [k, v] of Object.entries(value)) {
5884
+ out[k] = redact(v, home);
5885
+ }
5886
+ return out;
5887
+ }
5888
+ return value;
5889
+ }
5890
+ // ../../packages/aeg-core/src/log/store.ts
5891
+ function metaOf(obj) {
5892
+ if (obj === null || typeof obj !== "object")
5893
+ return null;
5894
+ const meta = obj.meta;
5895
+ if (meta === null || typeof meta !== "object")
5896
+ return null;
5897
+ return meta;
5898
+ }
5899
+ function recordIdentity(obj) {
5900
+ const meta = metaOf(obj);
5901
+ if (meta === null)
5902
+ return null;
5903
+ const eventId = meta.event_id;
5904
+ if (typeof eventId === "string" && eventId.length > 0)
5905
+ return eventId;
5906
+ const runId = meta.run_id;
5907
+ const seq = meta.seq;
5908
+ if (typeof runId === "string" && runId.length > 0 && typeof seq === "number" && Number.isInteger(seq)) {
5909
+ return `${runId}:${seq}`;
5910
+ }
5911
+ return null;
5912
+ }
5913
+
5914
+ // ../../packages/aeg-core/src/log/artifact.ts
5915
+ var TASK_LOG_ARTIFACT_MAX_BYTES = 8 * 1024 * 1024;
5916
+ // ../../packages/aeg-core/src/dev-review-loop/journal-reconstruction.ts
5917
+ var STOPPED_CONDITIONS = new Set(["confidence", "reappearance", "no_progress", "max_rounds"]);
5918
+ // ../../packages/aeg-core/src/control-store/records.ts
5919
+ import { z as z3 } from "zod";
5920
+ var isoTimestamp = z3.string().min(1);
5921
+ var taskId = z3.number().int().positive();
5922
+ var epochNumber = z3.number().int().nonnegative();
5923
+ var RunRecordSchema = z3.object({
5924
+ version: z3.literal(1),
5925
+ kind: z3.literal("run"),
5926
+ task: taskId,
5927
+ runId: z3.string().min(1),
5928
+ pid: z3.number().int().positive(),
5929
+ host: z3.string().min(1),
5930
+ startedAt: isoTimestamp,
5931
+ childStartedAt: z3.string().min(1).nullable().optional(),
5932
+ childCommand: z3.string().min(1).nullable().optional()
5933
+ }).strict();
5934
+ var InputRecordSchema = z3.object({
5935
+ version: z3.literal(1),
5936
+ kind: z3.literal("input"),
5937
+ task: taskId,
5938
+ runId: z3.string().min(1),
5939
+ source: z3.union([z3.literal("fresh"), z3.literal("resume")]),
5940
+ pr: z3.number().int().positive().nullable(),
5941
+ round: z3.number().int().nonnegative(),
5942
+ recordedAt: isoTimestamp
5943
+ }).strict();
5944
+ var OwnershipRecordSchema = z3.object({
5945
+ version: z3.literal(1),
5946
+ kind: z3.literal("ownership"),
5947
+ task: taskId,
5948
+ epoch: epochNumber,
5949
+ ownerId: z3.string().min(1),
5950
+ pid: z3.number().int().positive(),
5951
+ host: z3.string().min(1),
5952
+ acquiredAt: isoTimestamp
5953
+ }).strict();
5954
+ var TransitionRecordSchema = z3.object({
5955
+ version: z3.literal(1),
5956
+ kind: z3.literal("transition"),
5957
+ task: taskId,
5958
+ epoch: epochNumber,
5959
+ seq: z3.number().int().nonnegative(),
5960
+ from: z3.string().min(1),
5961
+ to: z3.string().min(1),
5962
+ detail: z3.string().optional(),
5963
+ at: isoTimestamp
5964
+ }).strict();
5965
+ var ManifestRecordSchema = z3.object({
5966
+ version: z3.literal(1),
5967
+ kind: z3.literal("manifest"),
5968
+ task: taskId,
5969
+ round: epochNumber,
5970
+ repository: z3.string().min(1),
5971
+ pr: z3.number().int().positive(),
5972
+ branch: z3.string().min(1),
5973
+ baseSha: z3.string().min(1).nullable(),
5974
+ headSha: z3.string().min(1),
5975
+ briefHash: z3.string().min(1).nullable(),
5976
+ objectivesVersion: z3.string().min(1).nullable(),
5977
+ rulingOrdinal: epochNumber,
5978
+ policyDigest: z3.string().min(1),
5979
+ recordedAt: isoTimestamp
5980
+ }).strict();
5981
+ var roundHeadIdentity = z3.object({ round: z3.number().int().positive(), head: z3.string().min(1) }).strict();
5982
+ var LoopBudgetsSchema = z3.object({
5983
+ mechanicalRetries: z3.number().int().nonnegative(),
5984
+ reviewRounds: z3.number().int().nonnegative(),
5985
+ infrastructureRetries: z3.number().int().nonnegative()
5986
+ }).strict();
5987
+ var LoopStateRecordSchema = z3.object({
5988
+ version: z3.literal(1),
5989
+ kind: z3.literal("loop_state"),
5990
+ task: taskId,
5991
+ round: z3.number().int().positive(),
5992
+ phase: z3.string().min(1),
5993
+ pauseReason: z3.string().min(1).nullable(),
5994
+ budgets: LoopBudgetsSchema,
5995
+ heldResult: roundHeadIdentity.nullable(),
5996
+ deliveredFindings: roundHeadIdentity.nullable(),
5997
+ recordedAt: isoTimestamp
5998
+ }).strict();
5999
+ var EffectRecordSchema = z3.object({
6000
+ version: z3.literal(1),
6001
+ kind: z3.literal("effect"),
6002
+ task: taskId,
6003
+ key: z3.string().min(1),
6004
+ operation: z3.string().min(1),
6005
+ target: z3.string().min(1),
6006
+ inputVersion: z3.number().int().nonnegative(),
6007
+ payloadDigest: z3.string().min(1),
6008
+ status: z3.union([z3.literal("started"), z3.literal("verified"), z3.literal("uncertain")]),
6009
+ url: z3.string().optional(),
6010
+ recordedAt: isoTimestamp
6011
+ }).strict();
6012
+ var EscalationRecordSchema = z3.object({
6013
+ version: z3.literal(1),
6014
+ kind: z3.literal("escalation"),
6015
+ task: taskId,
6016
+ escalationId: z3.string().min(1),
6017
+ round: epochNumber,
6018
+ head: z3.string().min(1),
6019
+ branch: z3.string().min(1),
6020
+ pr: z3.number().int().positive().nullable(),
6021
+ runId: z3.string().min(1),
6022
+ pid: z3.number().int().positive(),
6023
+ host: z3.string().min(1),
6024
+ agent: z3.string().min(1).optional(),
6025
+ reason: z3.string().min(1),
6026
+ detail: z3.string().optional(),
6027
+ evidence: z3.string().optional(),
6028
+ attemptedRecovery: z3.string().min(1),
6029
+ requestedDecision: z3.string().min(1),
6030
+ recipient: z3.string().min(1),
6031
+ briefHash: z3.string().min(1).nullable(),
6032
+ objectivesVersion: z3.string().min(1).nullable(),
6033
+ rulingOrdinal: epochNumber,
6034
+ policyDigest: z3.string().min(1),
6035
+ recordedAt: isoTimestamp
6036
+ }).strict();
6037
+ var ResolutionRecordSchema = z3.object({
6038
+ version: z3.literal(1),
6039
+ kind: z3.literal("resolution"),
6040
+ task: taskId,
6041
+ escalationId: z3.string().min(1),
6042
+ decision: z3.union([z3.literal("resume"), z3.literal("cancel")]),
6043
+ authenticatedBy: z3.string().min(1),
6044
+ authenticatedFrom: z3.string().min(1),
6045
+ consumedAt: isoTimestamp
6046
+ }).strict();
5065
6047
  // src/checks/registry.ts
5066
6048
  import { existsSync as existsSync2 } from "node:fs";
5067
6049
  import { join as join2 } from "node:path";
@@ -5095,6 +6077,7 @@ var REGISTRY = [
5095
6077
  [
5096
6078
  {
5097
6079
  name: "brief-shape",
6080
+ validates: "body",
5098
6081
  run: bin("check-brief-shape"),
5099
6082
  scope: "diff",
5100
6083
  timeoutMs: 15000,
@@ -5111,6 +6094,7 @@ var REGISTRY = [
5111
6094
  [
5112
6095
  {
5113
6096
  name: "pr-report-density",
6097
+ validates: "body",
5114
6098
  run: bin("check-pr-report-density"),
5115
6099
  scope: "diff",
5116
6100
  timeoutMs: 15000,
@@ -5123,6 +6107,7 @@ var REGISTRY = [
5123
6107
  [
5124
6108
  {
5125
6109
  name: "doc-coverage",
6110
+ validates: "body",
5126
6111
  run: bin("check-doc-coverage"),
5127
6112
  scope: "diff",
5128
6113
  timeoutMs: 15000,
@@ -5156,6 +6141,7 @@ var REGISTRY = [
5156
6141
  [
5157
6142
  {
5158
6143
  name: "pr-premise-reassert",
6144
+ validates: "body",
5159
6145
  run: bin("check-pr-premise-reassert"),
5160
6146
  scope: "diff",
5161
6147
  timeoutMs: 15000,
@@ -5184,6 +6170,7 @@ var REGISTRY = [
5184
6170
  [
5185
6171
  {
5186
6172
  name: "closes-n",
6173
+ validates: "body",
5187
6174
  run: bin("check-closes-n"),
5188
6175
  scope: "diff",
5189
6176
  timeoutMs: 15000,
@@ -5232,10 +6219,12 @@ var REGISTRY = [
5232
6219
  [
5233
6220
  {
5234
6221
  name: "test-plan",
6222
+ validates: "body",
5235
6223
  run: bin("check-test-plan"),
5236
6224
  scope: "diff",
5237
6225
  timeoutMs: 15000,
5238
6226
  requiresOpenPr: true,
6227
+ principalOwed: true,
5239
6228
  env: {
5240
6229
  PR_BODY: { optional: true },
5241
6230
  BRANCH: { optional: true }
@@ -5246,6 +6235,7 @@ var REGISTRY = [
5246
6235
  [
5247
6236
  {
5248
6237
  name: "body-bare-digits",
6238
+ validates: "body",
5249
6239
  run: bin("check-body-bare-digits"),
5250
6240
  scope: "diff",
5251
6241
  timeoutMs: 15000,
@@ -5264,6 +6254,7 @@ var REGISTRY = [
5264
6254
  [
5265
6255
  {
5266
6256
  name: "token-report",
6257
+ validates: "body",
5267
6258
  run: bin("check-token-report"),
5268
6259
  scope: "diff",
5269
6260
  timeoutMs: 15000,
@@ -5364,6 +6355,7 @@ var REGISTRY = [
5364
6355
  [
5365
6356
  {
5366
6357
  name: "doc-coverage-push",
6358
+ validates: "body",
5367
6359
  run: bin("check-doc-coverage-push"),
5368
6360
  scope: "diff",
5369
6361
  timeoutMs: 15000,
@@ -5399,6 +6391,7 @@ var REGISTRY = [
5399
6391
  [
5400
6392
  {
5401
6393
  name: "evidence-fresh",
6394
+ validates: "body",
5402
6395
  run: bin("check-evidence-fresh"),
5403
6396
  scope: "diff",
5404
6397
  timeoutMs: 15000,
@@ -5527,124 +6520,134 @@ var REGISTRY = [
5527
6520
  }
5528
6521
  },
5529
6522
  0
5530
- ]
5531
- ];
5532
- function coreCheckRegistry() {
5533
- return REGISTRY.map(([spec]) => spec);
5534
- }
5535
- var CORE_CHECK_RING = Object.fromEntries(REGISTRY.map(([spec, ring]) => [spec.name, ring]));
5536
-
5537
- // src/checks/contract.ts
5538
- var CHECK_SCHEMA_VERSION = 1;
5539
- function emitCheckError(error) {
5540
- process.stderr.write(`${JSON.stringify(error)}
5541
- `);
5542
- }
5543
-
5544
- // src/checks/runner.ts
5545
- var activeKillers = new Set;
5546
-
5547
- // src/lib/numstat.ts
5548
- function summariseNumstat(numstat) {
5549
- const rows = numstat.split(`
5550
- `).filter((l) => l.trim() !== "");
5551
- let added = 0;
5552
- let deleted = 0;
5553
- let binary = 0;
5554
- for (const row of rows) {
5555
- const [a, d] = row.split("\t");
5556
- if (a === "-" || d === "-") {
5557
- binary++;
5558
- continue;
5559
- }
5560
- added += Number(a) || 0;
5561
- deleted += Number(d) || 0;
5562
- }
5563
- const files = `${rows.length} file${rows.length === 1 ? "" : "s"} changed`;
5564
- const bin2 = binary > 0 ? `, ${binary} binary` : "";
5565
- return `${files}, ${added} insertion${added === 1 ? "" : "s"}(+), ${deleted} deletion${deleted === 1 ? "" : "s"}(-)${bin2}`;
5566
- }
5567
- var EVIDENCE_SUMMARY_PREFIX = "Summary: ";
5568
-
5569
- // src/checks/scan-context.ts
5570
- var ZERO_WIDTH = /[\u200B-\u200D\u2060\uFEFF]/g;
5571
- var HTML_ENTITIES = {
5572
- "&quot;": '"',
5573
- "&apos;": "'",
5574
- "&lt;": "<",
5575
- "&gt;": ">",
5576
- "&amp;": "&"
5577
- };
5578
- function decodeNamedEntities(body) {
5579
- return body.replace(/&(?:quot|apos|lt|gt|amp);/g, (m) => HTML_ENTITIES[m] ?? m);
5580
- }
5581
- function normalizeBody(rawBody) {
5582
- return decodeNamedEntities(rawBody.replace(ZERO_WIDTH, ""));
5583
- }
5584
- function buildAnchorLookupMask(body) {
5585
- return maskDetailsBlocks(maskCode(body));
6523
+ ],
6524
+ [
6525
+ {
6526
+ name: "issue-title-grammar",
6527
+ run: bin("check-issue-title-grammar"),
6528
+ validates: "issue",
6529
+ scope: "full",
6530
+ ownWorkflow: true,
6531
+ timeoutMs: 15000,
6532
+ env: { ISSUE_TITLE: { optional: true } }
6533
+ },
6534
+ 1
6535
+ ],
6536
+ [
6537
+ {
6538
+ name: "issue-objectives-numbering",
6539
+ run: bin("check-issue-objectives-numbering"),
6540
+ validates: "issue",
6541
+ scope: "full",
6542
+ ownWorkflow: true,
6543
+ timeoutMs: 15000,
6544
+ env: { ISSUE_BODY: { optional: true }, ISSUE_NUMBER: { optional: true } }
6545
+ },
6546
+ 1
6547
+ ],
6548
+ [
6549
+ {
6550
+ name: "issue-parts-coverage",
6551
+ run: bin("check-issue-parts-coverage"),
6552
+ validates: "issue",
6553
+ scope: "full",
6554
+ ownWorkflow: true,
6555
+ timeoutMs: 15000,
6556
+ env: { ISSUE_BODY: { optional: true } }
6557
+ },
6558
+ 1
6559
+ ],
6560
+ [
6561
+ {
6562
+ name: "issue-surface-globs",
6563
+ run: bin("check-issue-surface-globs"),
6564
+ validates: "issue",
6565
+ scope: "full",
6566
+ ownWorkflow: true,
6567
+ timeoutMs: 15000,
6568
+ env: { ISSUE_BODY: { optional: true } }
6569
+ },
6570
+ 1
6571
+ ],
6572
+ [
6573
+ {
6574
+ name: "issue-tranche-label",
6575
+ run: bin("check-issue-tranche-label"),
6576
+ validates: "issue",
6577
+ scope: "full",
6578
+ ownWorkflow: true,
6579
+ timeoutMs: 15000,
6580
+ env: { ISSUE_BODY: { optional: true }, ISSUE_LABELS: { optional: true } }
6581
+ },
6582
+ 1
6583
+ ],
6584
+ [
6585
+ {
6586
+ name: "issue-milestone-attach",
6587
+ run: bin("check-issue-milestone-attach"),
6588
+ validates: "issue",
6589
+ scope: "full",
6590
+ ownWorkflow: true,
6591
+ timeoutMs: 15000,
6592
+ env: {
6593
+ ISSUE_LABELS: { optional: true },
6594
+ CURRENT_MILESTONE_TITLE: { optional: true },
6595
+ RESOLVED_MILESTONE_TITLE: { optional: true }
6596
+ }
6597
+ },
6598
+ 1
6599
+ ]
6600
+ ];
6601
+ function coreCheckRegistry() {
6602
+ return REGISTRY.map(([spec]) => spec);
5586
6603
  }
6604
+ var CORE_CHECK_RING = Object.fromEntries(REGISTRY.map(([spec, ring]) => [spec.name, ring]));
5587
6605
 
5588
- class ScanContext {
5589
- raw;
5590
- normalised;
5591
- masked;
5592
- constructor(raw, normalised, masked) {
5593
- this.raw = raw;
5594
- this.normalised = normalised;
5595
- this.masked = masked;
5596
- }
5597
- static from(rawBody) {
5598
- const normalised = normalizeBody(rawBody);
5599
- return new ScanContext(rawBody, normalised, buildAnchorLookupMask(normalised));
5600
- }
5601
- rawResolutionAgrees(field) {
5602
- const rawHasPair = anchoredRegionBounds(this.raw, field) !== null;
5603
- return rawHasPair === (anchoredRegionBounds(this.masked, field) !== null);
5604
- }
5605
- }
5606
- function resolveAnchoredRegion(ctx, field) {
5607
- const bounds = anchoredRegionBounds(ctx.masked, field);
5608
- if (bounds === null) {
5609
- return anchoredRegionBounds(ctx.normalised, field) !== null ? "hidden" : null;
5610
- }
5611
- return {
5612
- region: ctx.normalised.slice(bounds.innerStart, bounds.innerEnd),
5613
- maskedRegion: ctx.masked.slice(bounds.innerStart, bounds.innerEnd)
5614
- };
5615
- }
5616
- function summaryLineIndex(maskedRegion) {
5617
- const index = maskedRegion.split(`
5618
- `).findIndex((line) => line.startsWith(EVIDENCE_SUMMARY_PREFIX));
5619
- return index === -1 ? null : index;
5620
- }
6606
+ // src/lib/log-sink.ts
6607
+ import { execFileSync as execFileSync3 } from "node:child_process";
6608
+ import { randomUUID } from "node:crypto";
6609
+ import {
6610
+ closeSync as closeSync2,
6611
+ constants as fsConstants2,
6612
+ existsSync as existsSync4,
6613
+ fstatSync as fstatSync2,
6614
+ mkdirSync as mkdirSync2,
6615
+ openSync as openSync2,
6616
+ readFileSync as readFileSync3,
6617
+ renameSync,
6618
+ writeSync
6619
+ } from "node:fs";
6620
+ import { hostname as osHostname, homedir as homedir2 } from "node:os";
6621
+ import { dirname as dirname3, join as join4 } from "node:path";
5621
6622
 
5622
6623
  // src/lib/config.ts
5623
6624
  import { execFileSync as execFileSync2 } from "node:child_process";
5624
6625
  import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, realpathSync, writeFileSync } from "node:fs";
5625
6626
  import { homedir } from "node:os";
5626
6627
  import { dirname as dirname2, join as join3, resolve } from "node:path";
5627
- import { z as z2 } from "zod";
6628
+ import { z as z4 } from "zod";
5628
6629
 
5629
6630
  // src/lib/agent-vendors.ts
5630
6631
  var AGENT_VENDORS = ["skills", "claude", "gemini"];
5631
6632
 
5632
6633
  // src/lib/config.ts
5633
- var EnvEntrySchema = z2.union([
5634
- z2.literal(true),
5635
- z2.object({ optional: z2.literal(true) }),
5636
- z2.object({ anyOf: z2.array(z2.string()).min(2) }),
5637
- z2.string()
6634
+ var EnvEntrySchema = z4.union([
6635
+ z4.literal(true),
6636
+ z4.object({ optional: z4.literal(true) }),
6637
+ z4.object({ anyOf: z4.array(z4.string()).min(2) }),
6638
+ z4.string()
5638
6639
  ]);
5639
- var CheckEntrySchema = z2.object({
5640
- run: z2.string(),
5641
- scope: z2.enum(["diff", "full"]),
5642
- include: z2.array(z2.string()).optional(),
5643
- args: z2.array(z2.string()).optional(),
5644
- timeoutMs: z2.number().optional(),
5645
- env: z2.record(z2.string(), EnvEntrySchema).optional(),
5646
- requiresOpenPr: z2.boolean().optional(),
5647
- ownWorkflow: z2.boolean().optional()
6640
+ var CheckEntrySchema = z4.object({
6641
+ run: z4.string(),
6642
+ scope: z4.enum(["diff", "full"]),
6643
+ include: z4.array(z4.string()).optional(),
6644
+ args: z4.array(z4.string()).optional(),
6645
+ timeoutMs: z4.number().optional(),
6646
+ env: z4.record(z4.string(), EnvEntrySchema).optional(),
6647
+ requiresOpenPr: z4.boolean().optional(),
6648
+ ownWorkflow: z4.boolean().optional(),
6649
+ principalOwed: z4.literal(true).optional(),
6650
+ validates: z4.enum(["body", "issue"]).optional()
5648
6651
  }).superRefine((entry2, ctx) => {
5649
6652
  if (!entry2.env)
5650
6653
  return;
@@ -5654,25 +6657,26 @@ var CheckEntrySchema = z2.object({
5654
6657
  const members = value.anyOf;
5655
6658
  if (new Set(members).size !== members.length) {
5656
6659
  ctx.addIssue({
5657
- code: z2.ZodIssueCode.custom,
6660
+ code: z4.ZodIssueCode.custom,
5658
6661
  message: `env.${key}.anyOf has duplicate members`,
5659
6662
  path: ["env", key, "anyOf"]
5660
6663
  });
5661
6664
  }
5662
6665
  if (!members.includes(key)) {
5663
6666
  ctx.addIssue({
5664
- code: z2.ZodIssueCode.custom,
6667
+ code: z4.ZodIssueCode.custom,
5665
6668
  message: `env.${key}.anyOf must include "${key}" itself as a member`,
5666
6669
  path: ["env", key, "anyOf"]
5667
6670
  });
5668
6671
  }
5669
6672
  }
5670
6673
  });
5671
- var RoleEntrySchema = z2.object({
5672
- contract: z2.string().refine((p) => p.includes("/"), {
6674
+ var RoleEntrySchema = z4.object({
6675
+ contract: z4.string().refine((p) => p.includes("/"), {
5673
6676
  message: 'must be a path (contain at least one "/"), not a bare filename'
5674
6677
  })
5675
6678
  });
6679
+ var MAX_REPORT_COMMAND_TIMEOUT_MS = 3600000;
5676
6680
  var BRIEF_BUILTINS = [
5677
6681
  "tier",
5678
6682
  "testPlan",
@@ -5692,17 +6696,17 @@ var BRIEF_BUILTINS = [
5692
6696
  "briefSections",
5693
6697
  "milestoneShape"
5694
6698
  ];
5695
- var BriefSectionSchema = z2.union([
5696
- z2.object({ builtin: z2.enum(BRIEF_BUILTINS) }),
5697
- z2.object({ heading: z2.string().min(1), name: z2.string().optional() }),
5698
- z2.object({ field: z2.string().min(1), name: z2.string().optional() }),
5699
- z2.object({ phrase: z2.string().min(1), name: z2.string().optional() })
6699
+ var BriefSectionSchema = z4.union([
6700
+ z4.object({ builtin: z4.enum(BRIEF_BUILTINS) }),
6701
+ z4.object({ heading: z4.string().min(1), name: z4.string().optional() }),
6702
+ z4.object({ field: z4.string().min(1), name: z4.string().optional() }),
6703
+ z4.object({ phrase: z4.string().min(1), name: z4.string().optional() })
5700
6704
  ]);
5701
- var BriefSchemaSchema = z2.object({
5702
- pr: z2.object({ sections: z2.array(BriefSectionSchema) }).optional(),
5703
- issue: z2.object({ sections: z2.array(BriefSectionSchema) }).optional(),
5704
- milestone: z2.object({ sections: z2.array(BriefSectionSchema) }).optional(),
5705
- ack: z2.array(z2.enum(BRIEF_BUILTINS)).optional()
6705
+ var BriefSchemaSchema = z4.object({
6706
+ pr: z4.object({ sections: z4.array(BriefSectionSchema) }).optional(),
6707
+ issue: z4.object({ sections: z4.array(BriefSectionSchema) }).optional(),
6708
+ milestone: z4.object({ sections: z4.array(BriefSectionSchema) }).optional(),
6709
+ ack: z4.array(z4.enum(BRIEF_BUILTINS)).optional()
5706
6710
  });
5707
6711
  function isSafeRepoRelPath(p) {
5708
6712
  if (p.length === 0)
@@ -5711,7 +6715,7 @@ function isSafeRepoRelPath(p) {
5711
6715
  return false;
5712
6716
  return p.split(/[\\/]/).every((seg) => seg !== ".." && seg !== "");
5713
6717
  }
5714
- var SafeRepoRelPath = z2.string().refine(isSafeRepoRelPath, {
6718
+ var SafeRepoRelPath = z4.string().refine(isSafeRepoRelPath, {
5715
6719
  message: "must be a repo-root-relative path with no `..` segment or absolute root"
5716
6720
  });
5717
6721
  var CANONICAL_HOOK_BLOCK_PREFIXES = [".git/", ".husky/", ".vinaya/hooks/", ".claude/hooks/"];
@@ -5721,22 +6725,22 @@ function isCanonicalHookBlockPath(p) {
5721
6725
  var ManagedHookBlockPath = SafeRepoRelPath.refine(isCanonicalHookBlockPath, {
5722
6726
  message: `must start with one of ${CANONICAL_HOOK_BLOCK_PREFIXES.join(", ")} (byte-exact, case-sensitive)`
5723
6727
  });
5724
- var ManagedBlockRecordSchema = z2.object({
6728
+ var ManagedBlockRecordSchema = z4.object({
5725
6729
  path: ManagedHookBlockPath,
5726
- marker: z2.string(),
5727
- comment: z2.enum(["hash", "html"])
6730
+ marker: z4.string(),
6731
+ comment: z4.enum(["hash", "html"])
5728
6732
  });
5729
- var ManagedManifestSchema = z2.object({
5730
- version: z2.number().int().positive(),
5731
- files: z2.array(SafeRepoRelPath),
5732
- blocks: z2.array(ManagedBlockRecordSchema),
5733
- labels: z2.array(z2.string()),
5734
- agents: z2.array(z2.enum(AGENT_VENDORS)).optional()
6733
+ var ManagedManifestSchema = z4.object({
6734
+ version: z4.number().int().positive(),
6735
+ files: z4.array(SafeRepoRelPath),
6736
+ blocks: z4.array(ManagedBlockRecordSchema),
6737
+ labels: z4.array(z4.string()),
6738
+ agents: z4.array(z4.enum(AGENT_VENDORS)).optional()
5735
6739
  });
5736
- var ProjectEntrySchema = z2.object({
5737
- name: z2.string().min(1),
5738
- description: z2.string().min(1).optional(),
5739
- path: z2.string().min(1).optional()
6740
+ var ProjectEntrySchema = z4.object({
6741
+ name: z4.string().min(1),
6742
+ description: z4.string().min(1).optional(),
6743
+ path: z4.string().min(1).optional()
5740
6744
  });
5741
6745
  function parseTokensCollectDeclaration(value) {
5742
6746
  const m = value.trim().match(/^(\S+)\s+(\S+)$/);
@@ -5747,34 +6751,62 @@ function parseTokensCollectDeclaration(value) {
5747
6751
  return null;
5748
6752
  return { interpreter, script };
5749
6753
  }
5750
- var VinayaConfigSchema = z2.object({
5751
- rings: z2.object({
5752
- ring1_forgeWriteInterception: z2.boolean(),
5753
- ring2_asyncAudits: z2.boolean()
6754
+ var VinayaConfigSchema = z4.object({
6755
+ rings: z4.object({
6756
+ ring1_forgeWriteInterception: z4.boolean(),
6757
+ ring2_asyncAudits: z4.boolean()
5754
6758
  }).optional(),
5755
- checks: z2.record(z2.string(), CheckEntrySchema).optional(),
5756
- roles: z2.record(z2.string(), RoleEntrySchema).optional(),
6759
+ checks: z4.record(z4.string(), CheckEntrySchema).optional(),
6760
+ roles: z4.record(z4.string(), RoleEntrySchema).optional(),
5757
6761
  briefSchema: BriefSchemaSchema.optional(),
5758
6762
  managed: ManagedManifestSchema.optional(),
5759
- principals: z2.array(z2.string()).min(1).optional(),
5760
- releaseActor: z2.string().min(1).optional(),
5761
- ci: z2.object({ setup: z2.string().min(1) }).optional(),
5762
- tokens: z2.object({
5763
- collect: z2.string().min(1).refine((v) => parseTokensCollectDeclaration(v) !== null, {
6763
+ principals: z4.array(z4.string()).min(1).optional(),
6764
+ releaseActor: z4.string().min(1).optional(),
6765
+ ci: z4.object({ setup: z4.string().min(1) }).optional(),
6766
+ tokens: z4.object({
6767
+ collect: z4.string().min(1).refine((v) => parseTokensCollectDeclaration(v) !== null, {
5764
6768
  message: 'tokens.collect must be exactly "<interpreter> <repo-relative-script-path>" — two whitespace-separated tokens, no flags, no shell syntax, no quoting'
5765
6769
  })
5766
6770
  }).optional(),
5767
- blastRadius: z2.object({ extraDomains: z2.array(z2.string()).optional() }).optional(),
5768
- proseGates: z2.object({
5769
- doctrineRoot: z2.string().min(1).optional(),
5770
- readerFacingPrefix: z2.string().min(1).optional(),
5771
- readerFacingSuffix: z2.string().min(1).optional(),
5772
- legacySlugDir: z2.string().min(1).optional()
6771
+ blastRadius: z4.object({ extraDomains: z4.array(z4.string()).optional() }).optional(),
6772
+ proseGates: z4.object({
6773
+ doctrineRoot: z4.string().min(1).optional(),
6774
+ readerFacingPrefix: z4.string().min(1).optional(),
6775
+ readerFacingSuffix: z4.string().min(1).optional(),
6776
+ legacySlugDir: z4.string().min(1).optional()
6777
+ }).optional(),
6778
+ projects: z4.array(ProjectEntrySchema).optional(),
6779
+ dispatch: z4.object({
6780
+ timeoutMs: z4.number().int().positive().optional(),
6781
+ killGraceMs: z4.number().int().positive().optional(),
6782
+ agent: z4.enum(["claude", "codex", "gemini"]).optional()
6783
+ }).optional(),
6784
+ reviewPolicy: z4.object({
6785
+ codeReviewThreshold: z4.string().min(1).optional(),
6786
+ securityThreshold: z4.string().min(1).optional(),
6787
+ maxRounds: z4.number().optional()
6788
+ }).optional(),
6789
+ prePush: z4.object({
6790
+ alwaysRun: z4.array(z4.string()).optional()
6791
+ }).optional(),
6792
+ report: z4.object({
6793
+ commandTimeoutMs: z4.number().int().positive().optional()
6794
+ }).superRefine((report, ctx) => {
6795
+ const commandTimeoutMs = report.commandTimeoutMs;
6796
+ if (commandTimeoutMs !== undefined && commandTimeoutMs > MAX_REPORT_COMMAND_TIMEOUT_MS) {
6797
+ ctx.addIssue({
6798
+ code: z4.ZodIssueCode.custom,
6799
+ path: ["commandTimeoutMs"],
6800
+ message: "must be at most 3600000 (1 hour)"
6801
+ });
6802
+ }
5773
6803
  }).optional(),
5774
- projects: z2.array(ProjectEntrySchema).optional(),
5775
- dispatch: z2.object({
5776
- timeoutMs: z2.number().int().positive().optional(),
5777
- agent: z2.enum(["claude", "codex", "gemini"]).optional()
6804
+ logPublish: z4.object({
6805
+ issue: z4.number().int().positive().optional(),
6806
+ pr: z4.number().int().positive().optional(),
6807
+ maxChunksPerFlush: z4.number().int().positive().optional()
6808
+ }).refine((v) => !(v.issue !== undefined && v.pr !== undefined), {
6809
+ message: "logPublish: set at most one of issue/pr, not both"
5778
6810
  }).optional()
5779
6811
  });
5780
6812
  var GLOBAL_VINAYA_HOME = join3(homedir(), ".vinaya");
@@ -5849,6 +6881,28 @@ function resolvePrincipalAllowlist(config) {
5849
6881
  function resolveReleaseActor(config) {
5850
6882
  return config?.releaseActor ?? DEFAULT_RELEASE_ACTOR;
5851
6883
  }
6884
+ function resolveReviewPolicy(config) {
6885
+ const raw = config?.reviewPolicy;
6886
+ if (!raw)
6887
+ return DEFAULT_REVIEW_POLICY;
6888
+ const codeReviewThreshold = raw.codeReviewThreshold ?? DEFAULT_REVIEW_POLICY.codeReviewThreshold;
6889
+ if (!isKnownSeverity(CODE_REVIEW_SEVERITY_ORDER, codeReviewThreshold)) {
6890
+ 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.`);
6891
+ }
6892
+ const securityThreshold = raw.securityThreshold ?? DEFAULT_REVIEW_POLICY.securityThreshold;
6893
+ if (!isKnownSeverity(SECURITY_SEVERITY_ORDER, securityThreshold)) {
6894
+ 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.`);
6895
+ }
6896
+ const maxRounds = raw.maxRounds ?? DEFAULT_REVIEW_POLICY.maxRounds;
6897
+ if (!Number.isInteger(maxRounds) || maxRounds < 1) {
6898
+ throw new Error(`vinaya.config.json: reviewPolicy.maxRounds "${maxRounds}" is not a positive integer — fix the config, this never falls back to a default.`);
6899
+ }
6900
+ return {
6901
+ codeReviewThreshold,
6902
+ securityThreshold,
6903
+ maxRounds
6904
+ };
6905
+ }
5852
6906
  function trustAnchorRepo() {
5853
6907
  const wellFormed = (slug) => /^[^/\s]+\/[^/\s]+$/.test(slug) ? slug : null;
5854
6908
  const fromRunner = process.env.GITHUB_REPOSITORY?.trim();
@@ -5928,6 +6982,1188 @@ function loadConfig() {
5928
6982
  }
5929
6983
  var TOKENS_COLLECT_TRUST_PATH = join3(GLOBAL_VINAYA_HOME, "tokens-collect-trust.json");
5930
6984
 
6985
+ // src/lib/log-sink.ts
6986
+ var OUTBOX_MAX_BYTES = 8 * 1024 * 1024;
6987
+ function isEnoent(err) {
6988
+ return typeof err === "object" && err !== null && err.code === "ENOENT";
6989
+ }
6990
+ function resolveDoctrine(cwd, vinayaVersion) {
6991
+ let toplevel;
6992
+ try {
6993
+ toplevel = execFileSync3("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
6994
+ encoding: "utf8",
6995
+ stdio: ["ignore", "pipe", "pipe"]
6996
+ }).trim();
6997
+ } catch (err) {
6998
+ return isEnoent(err) ? "unknown" : vinayaVersion;
6999
+ }
7000
+ if (!existsSync4(join4(toplevel, "aeg-root", "roles")))
7001
+ return vinayaVersion;
7002
+ try {
7003
+ const sha = execFileSync3("git", ["-C", toplevel, "rev-parse", "HEAD:aeg-root"], {
7004
+ encoding: "utf8",
7005
+ stdio: ["ignore", "pipe", "pipe"]
7006
+ }).trim();
7007
+ return `aeg-root@${sha}`;
7008
+ } catch {
7009
+ return vinayaVersion;
7010
+ }
7011
+ }
7012
+ function readVinayaVersion() {
7013
+ try {
7014
+ const pkg = JSON.parse(readFileSync3(join4(packageRoot(import.meta.url), "package.json"), "utf8"));
7015
+ return pkg.version ?? "unknown";
7016
+ } catch {
7017
+ return "unknown";
7018
+ }
7019
+ }
7020
+ var SAFE_PATH_SEGMENT = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
7021
+ function isSafeRepoSegment(segment) {
7022
+ return SAFE_PATH_SEGMENT.test(segment) && !segment.includes("..");
7023
+ }
7024
+ function outboxPathFor(deps, repo, issue) {
7025
+ const dirName = repo ? `${repo.owner}-${repo.repo}` : "unresolved";
7026
+ const fileName = `${issue ?? "none"}.ndjson`;
7027
+ return join4(deps.outboxRoot(), dirName, fileName);
7028
+ }
7029
+ function hostFromEnv(env) {
7030
+ if (env.GITHUB_ACTIONS)
7031
+ return "ci";
7032
+ if (env.VINAYA_HOST === "hook")
7033
+ return "hook";
7034
+ if (env.VINAYA_HOST === "loop")
7035
+ return "loop";
7036
+ return "cli";
7037
+ }
7038
+ function defaultDeps() {
7039
+ return {
7040
+ outboxRoot: () => join4(GLOBAL_VINAYA_HOME, "outbox"),
7041
+ home: () => homedir2(),
7042
+ hostname: () => osHostname(),
7043
+ cwd: () => process.cwd(),
7044
+ now: () => new Date,
7045
+ env: () => process.env,
7046
+ resolveRepo: () => resolveRepo(),
7047
+ vinayaVersion: () => readVinayaVersion(),
7048
+ stderr: (message) => {
7049
+ process.stderr.write(message);
7050
+ },
7051
+ inputVersions: () => {
7052
+ return;
7053
+ }
7054
+ };
7055
+ }
7056
+ var APPEND_OPEN_FLAGS = fsConstants2.O_WRONLY | fsConstants2.O_APPEND | fsConstants2.O_CREAT | fsConstants2.O_NOFOLLOW | fsConstants2.O_NONBLOCK;
7057
+ function guardedAppendOpen(path) {
7058
+ try {
7059
+ return openSync2(path, APPEND_OPEN_FLAGS, 384);
7060
+ } catch {
7061
+ return;
7062
+ }
7063
+ }
7064
+ function reportRotationOverflow(backupPath, warn) {
7065
+ let raw;
7066
+ try {
7067
+ raw = readFileSync3(backupPath, "utf8");
7068
+ } catch {
7069
+ return;
7070
+ }
7071
+ const lines = raw.split(`
7072
+ `).filter((l) => l.length > 0);
7073
+ if (lines.length === 0)
7074
+ return;
7075
+ const droppedIdentities = lines.map((line, i) => {
7076
+ try {
7077
+ return recordIdentity(JSON.parse(line)) ?? `unreadable:${i}`;
7078
+ } catch {
7079
+ return `unreadable:${i}`;
7080
+ }
7081
+ });
7082
+ const diagnostic = { reason: "capacity", dropped: droppedIdentities.length, droppedIdentities };
7083
+ const shown = diagnostic.droppedIdentities.slice(0, 20);
7084
+ const more = diagnostic.dropped > shown.length ? `, +${diagnostic.dropped - shown.length} more` : "";
7085
+ warn(`vinaya: log outbox rotation is overwriting ${backupPath} — ${diagnostic.dropped} record(s) permanently lost: ${shown.join(", ")}${more}
7086
+ `);
7087
+ }
7088
+ function appendLine(path, line, warn) {
7089
+ try {
7090
+ mkdirSync2(dirname3(path), { recursive: true, mode: 448 });
7091
+ let fd = guardedAppendOpen(path);
7092
+ if (fd === undefined) {
7093
+ warn(`vinaya: log outbox target could not be opened (symlink, FIFO, or unwritable) — refusing: ${path}
7094
+ `);
7095
+ return;
7096
+ }
7097
+ let closed = false;
7098
+ try {
7099
+ const stat = fstatSync2(fd);
7100
+ if (!stat.isFile()) {
7101
+ warn(`vinaya: log outbox target is not a regular file — refusing to write: ${path}
7102
+ `);
7103
+ return;
7104
+ }
7105
+ if (stat.size > OUTBOX_MAX_BYTES) {
7106
+ closeSync2(fd);
7107
+ closed = true;
7108
+ const backupPath = path.replace(/\.ndjson$/, ".1.ndjson");
7109
+ reportRotationOverflow(backupPath, warn);
7110
+ renameSync(path, backupPath);
7111
+ const rotated = guardedAppendOpen(path);
7112
+ if (rotated === undefined) {
7113
+ warn(`vinaya: log outbox target could not be reopened after rotation — refusing: ${path}
7114
+ `);
7115
+ return;
7116
+ }
7117
+ fd = rotated;
7118
+ closed = false;
7119
+ }
7120
+ writeSync(fd, line);
7121
+ } finally {
7122
+ if (!closed)
7123
+ closeSync2(fd);
7124
+ }
7125
+ } catch (err) {
7126
+ warn(`vinaya: log outbox write failed — ${err instanceof Error ? err.message : String(err)}
7127
+ `);
7128
+ }
7129
+ }
7130
+ function createLogSink(overrides = {}) {
7131
+ const deps = { ...defaultDeps(), ...overrides };
7132
+ const runId = deps.env().VINAYA_RUN_ID || randomUUID();
7133
+ const processId = randomUUID();
7134
+ let seq = 0;
7135
+ let warned = false;
7136
+ const warnOnce = (message) => {
7137
+ if (warned)
7138
+ return;
7139
+ warned = true;
7140
+ try {
7141
+ deps.stderr(message);
7142
+ } catch {}
7143
+ };
7144
+ let doctrineCache;
7145
+ const doctrine = () => {
7146
+ if (doctrineCache === undefined)
7147
+ doctrineCache = resolveDoctrine(deps.cwd(), deps.vinayaVersion());
7148
+ return doctrineCache;
7149
+ };
7150
+ let resolveRepoCache;
7151
+ const resolveRepoOnce = () => {
7152
+ if (resolveRepoCache === undefined)
7153
+ resolveRepoCache = deps.resolveRepo();
7154
+ return resolveRepoCache;
7155
+ };
7156
+ function log(e) {
7157
+ try {
7158
+ const env = deps.env();
7159
+ const host = hostFromEnv(env);
7160
+ const now = deps.now();
7161
+ const mySeq = seq++;
7162
+ resolveRepoOnce().then((resolved) => {
7163
+ const repo = resolved && isSafeRepoSegment(resolved.owner) && isSafeRepoSegment(resolved.repo) ? resolved : null;
7164
+ const header = buildHeader({
7165
+ now,
7166
+ runId,
7167
+ seq: mySeq,
7168
+ repo: repo ? `${repo.owner}/${repo.repo}` : null,
7169
+ vinaya: deps.vinayaVersion(),
7170
+ doctrine: doctrine(),
7171
+ host,
7172
+ hostname: deps.hostname(),
7173
+ env: {
7174
+ role: env.VINAYA_ROLE,
7175
+ task: env.VINAYA_TASK,
7176
+ round: env.VINAYA_ROUND,
7177
+ run: env.VINAYA_RUN,
7178
+ attempt: env.VINAYA_ATTEMPT,
7179
+ parent: env.VINAYA_PARENT_EVENT
7180
+ },
7181
+ eventId: randomUUID(),
7182
+ processId,
7183
+ inputVersions: deps.inputVersions()
7184
+ });
7185
+ const full = { ...e, ...header };
7186
+ const parsed = LogEventSchema.safeParse(full);
7187
+ if (!parsed.success) {
7188
+ warnOnce(`vinaya: log() refused an invalid payload — ${parsed.error.issues[0]?.message ?? "schema violation"}
7189
+ `);
7190
+ return;
7191
+ }
7192
+ const line = `${JSON.stringify(redact(parsed.data, deps.home()))}
7193
+ `;
7194
+ appendLine(outboxPathFor(deps, repo, header.subject.issue), line, warnOnce);
7195
+ }).catch((err) => {
7196
+ warnOnce(`vinaya: log() failed — ${err instanceof Error ? err.message : String(err)}
7197
+ `);
7198
+ });
7199
+ } catch (err) {
7200
+ warnOnce(`vinaya: log() failed — ${err instanceof Error ? err.message : String(err)}
7201
+ `);
7202
+ }
7203
+ }
7204
+ const warmup = () => {
7205
+ doctrine();
7206
+ const originalWarn = console.warn;
7207
+ console.warn = () => {};
7208
+ resolveRepoOnce().finally(() => {
7209
+ console.warn = originalWarn;
7210
+ });
7211
+ };
7212
+ return { log, runId, warmup };
7213
+ }
7214
+ var defaultSink = createLogSink();
7215
+
7216
+ // src/checks/contract.ts
7217
+ var CHECK_SCHEMA_VERSION = 1;
7218
+ function emitCheckError(error) {
7219
+ process.stderr.write(`${JSON.stringify(error)}
7220
+ `);
7221
+ }
7222
+
7223
+ // src/checks/runner.ts
7224
+ var activeChecks = new Map;
7225
+
7226
+ // src/lib/numstat.ts
7227
+ function summariseNumstat(numstat) {
7228
+ const rows = numstat.split(`
7229
+ `).filter((l) => l.trim() !== "");
7230
+ let added = 0;
7231
+ let deleted = 0;
7232
+ let binary = 0;
7233
+ for (const row of rows) {
7234
+ const [a, d] = row.split("\t");
7235
+ if (a === "-" || d === "-") {
7236
+ binary++;
7237
+ continue;
7238
+ }
7239
+ added += Number(a) || 0;
7240
+ deleted += Number(d) || 0;
7241
+ }
7242
+ const files = `${rows.length} file${rows.length === 1 ? "" : "s"} changed`;
7243
+ const bin2 = binary > 0 ? `, ${binary} binary` : "";
7244
+ return `${files}, ${added} insertion${added === 1 ? "" : "s"}(+), ${deleted} deletion${deleted === 1 ? "" : "s"}(-)${bin2}`;
7245
+ }
7246
+ var EVIDENCE_SUMMARY_PREFIX = "Summary: ";
7247
+
7248
+ // src/checks/scan-context.ts
7249
+ var ZERO_WIDTH = /[\u200B-\u200D\u2060\uFEFF]/g;
7250
+ var HTML_ENTITIES = {
7251
+ "&quot;": '"',
7252
+ "&apos;": "'",
7253
+ "&lt;": "<",
7254
+ "&gt;": ">",
7255
+ "&amp;": "&"
7256
+ };
7257
+ function decodeNamedEntities(body) {
7258
+ return body.replace(/&(?:quot|apos|lt|gt|amp);/g, (m) => HTML_ENTITIES[m] ?? m);
7259
+ }
7260
+ function normalizeBody(rawBody) {
7261
+ return decodeNamedEntities(rawBody.replace(ZERO_WIDTH, ""));
7262
+ }
7263
+ function buildAnchorLookupMask(body) {
7264
+ return maskDetailsBlocks(maskCode(body));
7265
+ }
7266
+
7267
+ class ScanContext {
7268
+ raw;
7269
+ normalised;
7270
+ masked;
7271
+ constructor(raw, normalised, masked) {
7272
+ this.raw = raw;
7273
+ this.normalised = normalised;
7274
+ this.masked = masked;
7275
+ }
7276
+ static from(rawBody) {
7277
+ const normalised = normalizeBody(rawBody);
7278
+ return new ScanContext(rawBody, normalised, buildAnchorLookupMask(normalised));
7279
+ }
7280
+ rawResolutionAgrees(field) {
7281
+ const rawHasPair = anchoredRegionBounds(this.raw, field) !== null;
7282
+ return rawHasPair === (anchoredRegionBounds(this.masked, field) !== null);
7283
+ }
7284
+ }
7285
+ function resolveAnchoredRegion(ctx, field) {
7286
+ const bounds = anchoredRegionBounds(ctx.masked, field);
7287
+ if (bounds === null) {
7288
+ return anchoredRegionBounds(ctx.normalised, field) !== null ? "hidden" : null;
7289
+ }
7290
+ return {
7291
+ region: ctx.normalised.slice(bounds.innerStart, bounds.innerEnd),
7292
+ maskedRegion: ctx.masked.slice(bounds.innerStart, bounds.innerEnd)
7293
+ };
7294
+ }
7295
+ function summaryLineIndex(maskedRegion) {
7296
+ const index = maskedRegion.split(`
7297
+ `).findIndex((line) => line.startsWith(EVIDENCE_SUMMARY_PREFIX));
7298
+ return index === -1 ? null : index;
7299
+ }
7300
+
7301
+ // src/lib/loop-log.ts
7302
+ var LOOP_LOG_MAX_BYTES = 8 * 1024 * 1024;
7303
+
7304
+ // src/lib/dispatch.ts
7305
+ var MAX_TEE_BYTES = 8 * 1024 * 1024;
7306
+ var CLAUDE_MODEL_ALIASES = new Set(["fable", "opus", "sonnet", "haiku"]);
7307
+
7308
+ // src/lib/brief-assembly.ts
7309
+ import { execFileSync as execFileSync5 } from "node:child_process";
7310
+
7311
+ // ../../packages/sources/src/commands.ts
7312
+ var COMMANDS = [
7313
+ {
7314
+ name: "help",
7315
+ description: "Show this help text",
7316
+ status: "shipped"
7317
+ },
7318
+ {
7319
+ name: "version",
7320
+ description: "Print the CLI version",
7321
+ flags: [{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }],
7322
+ status: "shipped"
7323
+ },
7324
+ {
7325
+ name: "init",
7326
+ description: "Install Vinaya's git hooks, CI workflow, and starter config (diff-and-confirm, non-destructive)",
7327
+ flags: [
7328
+ { flag: "--dry-run", description: "Print the full diff without installing anything" },
7329
+ { flag: "--yes", description: "Skip the confirmation prompt" }
7330
+ ],
7331
+ details: [
7332
+ "It detects your repo, prints the complete diff of every intended change, and waits for your confirmation before installing anything. `--dry-run` prints that same diff and installs nothing. Nothing ever runs automatically on package install.",
7333
+ "It installs one CI workflow that runs `vinaya check --all --diff-only`, alongside your existing workflows — refusing to overwrite rather than touching foreign content already at that path. Git hook stubs invoke the `vinaya` binary directly; if a hook already exists, it appends a delimited managed block, shown verbatim in the diff first, rather than overwriting it.",
7334
+ "`vinaya.config.json` is seeded with a starter ruleset extracted from Vinaya's own battle-tested gates, not invented defaults. Issue and PR templates carrying the brief schema are added alongside your own; tier and `needs:*-input` labels are created only if they don't already exist — your existing labels are never modified.",
7335
+ "Two empty folders — `vinaya/checks/` and `vinaya/roles/` — are scaffolded alongside the config, each held open by a placeholder file (git does not track empty directories). `vinaya new noop-check` and `vinaya new role` write their real output into these folders later; both placeholders are recorded in the ownership manifest, so `eject` removes them exactly like everything else `init` created.",
7336
+ "The generated workflows reach the `vinaya` binary through `npx @attalabs/vinaya`, with no build step — unless your repo vendors the CLI itself (a workspace member declaring the name `@attalabs/vinaya`), in which case `npx` would resolve to that unbuilt local member instead of the registry. `init` detects that at generation time and writes workflows that build and run your own copy by path instead, so your CI exercises the code in the pull request. `upgrade` and `doctor` make the same determination, so regenerating is stable.",
7337
+ "The recommended branch-protection command is printed for you to run yourself — it is never applied, and your PATH is never touched. `eject` removes exactly the managed block it owns, or a whole file only if `init` created it."
7338
+ ],
7339
+ status: "shipped"
7340
+ },
7341
+ {
7342
+ name: "init product",
7343
+ description: "Register a project in .vinaya/projects.md in an already-initialized repo",
7344
+ flags: [
7345
+ {
7346
+ flag: "--path <path>",
7347
+ description: "The project's home folder — declared, not derived. Defaults to the repo root."
7348
+ }
7349
+ ],
7350
+ details: [
7351
+ "Writes (or appends to) `.vinaya/projects.md` — the registry Vinaya Studio's tranche board resolves a project's board link against. Idempotent: re-running with the same name updates nothing.",
7352
+ "Reaches no forge and needs no GitHub remote or credentials: the registry row is a pure local file write, and it is deliberately NOT recorded in the ownership manifest, so `eject` does not reverse adopter-declared data.",
7353
+ "Creates no label. Project is a field, not a label: a task Issue declares its project in the body's `**Project:**` field, and a `project:*` label is ignored wherever one still exists."
7354
+ ],
7355
+ status: "shipped"
7356
+ },
7357
+ {
7358
+ name: "check",
7359
+ description: "Run one check, or every registered check",
7360
+ flags: [
7361
+ { flag: "--all", description: "Run every registered check instead of one named check" },
7362
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" },
7363
+ { flag: "--diff-only", description: "Scope diff-declared checks to changed files" },
7364
+ {
7365
+ flag: "--local",
7366
+ description: "Skip every requiresOpenPr check (closes-n, test-plan) — set by the generated hooks, never by CI"
7367
+ },
7368
+ { flag: "--parallel[=n]", description: "Concurrency cap (default: cpu-derived)" },
7369
+ {
7370
+ flag: "--plan",
7371
+ description: "Print the resolved check registry and the resolved `roles` registry (default/overridden/additive) without running anything"
7372
+ }
7373
+ ],
7374
+ details: [
7375
+ "Each spawned check's child process sees only a fixed baseline (`PATH`, `LANG`, `HOME`, `HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY`, `TMPDIR`) plus whatever its `CheckSpec['env']` declaration explicitly forwards — never the full parent environment. A required (`true`) or unsatisfied `anyOf` declaration missing from the caller's environment synthesizes a `CheckError` before the check ever spawns. Declare `env` (a core check's own registration, or `vinaya.config.json`'s `checks.<name>.env` for a custom one) for any check that reads `process.env`/`Bun.env`/`Deno.env` directly — `vinaya doctor` carries the permanent diagnostic for one that doesn't.",
7376
+ "The resolved registry IS what runs. A `checks` key that exactly matches a core check id REPLACES that core check (the core one does not run); any other key must be namespaced `<yourname>/<id>`. Anything the resolver cannot classify — a malformed entry, a bare un-namespaced key matching no core id, a duplicate id — makes `vinaya check` refuse the ENTIRE run: exit 1, nothing executes, never a partial ruleset and never a core-only fallback. `vinaya doctor` carries the permanent diagnostic for each rejected entry, so a refused config is still diagnosable.",
7377
+ "`--plan` composes with `--json`. It requires zero env vars and never prints an env value — only how each one resolves (passthrough, optional, literal, or anyOf). A `FAIL_CLOSED` entry always renders inline rather than being dropped, and exits non-zero. `--plan` and execution read the same resolution, so what the plan prints is what runs.",
7378
+ "`--local` exists because a `requiresOpenPr` check (the core `closes-n`/`test-plan`, or a custom check declaring the same field) can only evaluate for real once a pull request exists — the generated `pre-commit`/`pre-push` hooks pass it so the first commit on a fresh task branch is never asked to satisfy a PR-body field before a PR can possibly exist. CI's `vinaya-checks.yml` omits it, so these checks always run for real once a PR is open.",
7379
+ "`--plan`'s `roles` half resolves `vinaya.config.json`'s `roles` block against bundled doctrine — same override/additive shape as `checks`, with its own RENDERS AS column (the registry key and the role's own `role_id` differ for an additive entry) and a GATING column (`core` vs. `inert` — an additive role carries no core `ACTIONS` wiring). `roles.available` is `false` only when no bundled doctrine can be found next to this CLI install."
7380
+ ],
7381
+ status: "shipped"
7382
+ },
7383
+ {
7384
+ name: "commit-msg",
7385
+ description: "The generated `commit-msg` hook's invocation target — validates a commit message's first line",
7386
+ details: [
7387
+ "Not meant to be run by hand day-to-day: the managed `commit-msg` hook calls `vinaya commit-msg <message-file> [source]` with the two arguments git itself passes a commit-msg hook (githooks(5)) — the message file path and, when known, the commit's source keyword.",
7388
+ "Validates the message file's first line against the same `Type(scope): Description` vocabulary `check`'s `forge-title`-shaped gates enforce on PR/Issue titles (`@attalabs/aeg-core`'s `COMMIT_TYPES` — the commitlint type set plus `Plan`). Exits 0 with no output on a conforming message; a `source` of `merge` is skipped outright, since a merge commit's message is written by git, not the person committing."
7389
+ ],
7390
+ status: "shipped"
7391
+ },
7392
+ {
7393
+ name: "new check",
7394
+ description: "Scaffold a custom check into ./scripts/vinaya-checks/",
7395
+ details: [
7396
+ "Takes the REGISTRATION KEY, not a bare name: `vinaya new check <yourname>/<id>` writes `./scripts/vinaya-checks/<id>.ts` and prints the namespaced `checks` entry to paste. It refuses a bare, un-namespaced name — `vinaya check` refuses its entire run over a key it cannot resolve, so scaffolding one would brick every check invocation in the repo — and refuses a core check id, since registering one REPLACES that core gate and a scaffolded stub is never what an adopter means by that."
7397
+ ],
7398
+ status: "shipped"
7399
+ },
7400
+ {
7401
+ name: "new noop-check",
7402
+ description: "Scaffold an explicit no-op into vinaya/checks/ that silences a core check",
7403
+ details: [
7404
+ "Takes a CORE check id — the opposite of what `new check` accepts, which refuses one. `vinaya new noop-check <core-check-id>` writes `vinaya/checks/<id>.ts`, an explicit, contract-satisfying no-op (always exits `0`, emits no findings, carries a comment marking the silencing as intentional) and prints the `checks` entry that REPLACES the named core check with it. This is the only sanctioned way to silence a core check."
7405
+ ],
7406
+ status: "shipped"
7407
+ },
7408
+ {
7409
+ name: "new role",
7410
+ description: "Scaffold an additive role contract into vinaya/roles/",
7411
+ details: [
7412
+ 'Takes the REGISTRATION KEY: `vinaya new role <yourname>/<id>` writes `vinaya/roles/<id>.md` — a structurally-valid role contract stub (the six frontmatter keys plus `title`/`order`, and a non-empty "## The short version" section) whose own `role_id` is set to `<id>` — and prints the `roles` entry to paste. It refuses a bare, un-namespaced key: that shape resolves as an OVERRIDE of a core role, a complete replacement of that role\'s contract and a real governance decision this scaffolder does not make for you.'
7413
+ ],
7414
+ status: "shipped"
7415
+ },
7416
+ {
7417
+ name: "brief render",
7418
+ description: "Emit the twelve-section brief skeleton from the forge and the tree, every derivable section filled",
7419
+ flags: [
7420
+ { flag: "--surfaces <glob1,glob2,...>", description: "Intended surface globs, expanded against tracked files" },
7421
+ { flag: "--out <path>", description: "Write the rendered brief to a file instead of stdout" }
7422
+ ],
7423
+ details: [
7424
+ "Reads the task Issue (`vinaya/tranche:<tranche>`-labeled, id `<n>`) and the tree, and fills every section a program can derive: the header `Project:`/`Tier:`/`Closes #N`, the Step 0 worktree line, the dispatch-gate status as the pre-flight line, §4's file list (with consumer packages and a `sha256` premise pin per file, and Out of surface from the Issue's own `## Surface` `out:` list), §6 from the Issue's `## Parts`, §7 from the `.vinaya/doc-owners` derivation, §9 from the Issue's `## Test plan`, §10 from the Issue's `## Stop conditions` plus the rationale's Stop-and-escalate field, and every remaining section from the Issue's eight-field Planner rationale. Refuses — naming the missing section — when it cannot be derived: no Issue, the dispatch gate not clear, a `--surfaces` glob matching no tracked file, or the Issue missing/malformed `## Surface`/`## Parts`/`## Test plan`/`## Stop conditions` — never a bracketed placeholder.",
7425
+ "Never writes under `aeg-root/` or to the Issue — a brief is pasted to the Developer, never committed. Reads `aeg-root/templates/brief-template.md` at run time for its one fixed sentence and the standing autonomy clause; every other byte is generated from the forge/tree facts."
7426
+ ],
7427
+ status: "shipped"
7428
+ },
7429
+ {
7430
+ name: "task dispatch",
7431
+ description: "Deprecated — render, pin, and post the brief on the Issue as the frozen original; start the developer",
7432
+ flags: [
7433
+ { flag: "--agent <claude|codex|gemini>", description: "Start the developer through dispatchRole once posted" }
7434
+ ],
7435
+ details: [
7436
+ "Renders the brief from the Issue and the tree (the same assembly `brief render` uses) and posts it once as an Issue comment whose first line is `<!-- aeg:brief:v1 -->` and whose second line is `Brief hash: <sha256>` — the hash covers only the body below those two lines, so any reader recomputes it. Refuses outright, naming the existing comment's URL, when a `v1` comment already exists on the Issue — the brief is frozen by design, never overwritten or silently reissued.",
7437
+ "With `--agent`, starts the Developer through `dispatchRole` when `apps/cli/src/lib/dispatch.ts` exports it; otherwise prints the rendered brief and the manual dispatch instruction and exits `0` — a soft dependency, never a hard block.",
7438
+ "Principal-only, with or without `--agent`: refuses before any render, forge read, or post when the authenticated `gh` identity is not on the Principal allowlist (or cannot be resolved at all) — dispatching is the `todo → in-flight` transition, not a general-purpose comment poster.",
7439
+ "Deprecated in favor of `task brief` (preparation only) and `task run` (the full unattended loop) — kept for a documented compatibility window while callers migrate."
7440
+ ],
7441
+ status: "shipped"
7442
+ },
7443
+ {
7444
+ name: "task brief",
7445
+ description: "Render and freeze the brief as the Issue's original comment — preparation only, starts nobody",
7446
+ details: [
7447
+ "The preparation half of `task dispatch`, extracted so it is callable on its own: resolves the task's Issue, renders the brief, refuses on any gap, refuses when a frozen brief already exists, and posts it once as the same `aeg:brief:v1` Issue comment `task dispatch` posts. Starts no agent under any circumstances — there is no `--agent` flag here at all.",
7448
+ "Successor to `task dispatch` for the preparation step; the full unattended run (preparation, then the developer, then the review loop) is `task run`."
7449
+ ],
7450
+ status: "shipped"
7451
+ },
7452
+ {
7453
+ name: "task run",
7454
+ description: "One command from a planned Issue to a reviewed pull request — exactly one developer started",
7455
+ flags: [
7456
+ {
7457
+ flag: "--agent <claude|codex|gemini>",
7458
+ description: "Vendor for the developer and both reviewers this run dispatches"
7459
+ }
7460
+ ],
7461
+ details: [
7462
+ "Composes `task brief`'s own preparation (`prepareTask`) with `dev-review-loop` (`devReviewLoop`) — nothing else. Preparation starts no agent; the loop's own round 1 reads the frozen brief off the Issue and is the only place a developer is ever dispatched from a fresh task, so exactly one developer is started by construction.",
7463
+ "A brief already frozen on the Issue is reused, never re-posted — the second `task dispatch`/`task brief` call this composes around does not fail the whole run, it just skips straight to running the loop. A task whose Issue refuses preparation (a missing brief section, an unmet dispatch gate) is refused before any agent starts, with nothing posted.",
7464
+ "Refuses when the frozen brief's developer branch already has an open pull request — the old `task dispatch` followed by `task run` cannot start two developers this way.",
7465
+ "Exit and printed summary distinguish a published, reviewed pull request (exit `0`, the PR URL) from a pause (exit `1`, with the exact `vinaya dev-review-loop --resume <pr>` command to continue), a usage/argv error (exit `2`), and any other failure (exit `3`) — never sharing `1` with a pause, so an unattended host tells them apart from the exit code alone. A run that pauses is resumed with the loop's own existing `--resume <pr>` flag, never a flag on this command."
7466
+ ],
7467
+ status: "shipped"
7468
+ },
7469
+ {
7470
+ name: "task status",
7471
+ description: "Every open task with a frozen brief, its pull request, and whether its loop is running, paused, or published",
7472
+ flags: [{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }],
7473
+ details: [
7474
+ "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`.",
7475
+ "`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.",
7476
+ "`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."
7477
+ ],
7478
+ status: "shipped"
7479
+ },
7480
+ {
7481
+ name: "task-tools serve",
7482
+ description: "Run the task-operator MCP tool server over stdio — the command both runtime adapters register",
7483
+ details: [
7484
+ "Exposes the task-operator tool catalog (`packages/aeg-core/src/task-tools.ts`) to an agent runtime as one MCP server speaking newline-delimited JSON-RPC 2.0 (`initialize`/`tools/list`/`tools/call`). Claude registers it through the generated `.mcp.json`; Codex through its documented `~/.codex/config.toml` `[mcp_servers]` table — both point at this same command.",
7485
+ "stdout carries JSON-RPC and nothing else: any stray write from the CLI (a forge-read warning) is redirected to stderr, so the protocol stream is never corrupted.",
7486
+ "`task_status`/`task_escalation_read` read; `task_start` starts a run in attended mode only, requiring an authenticated caller from the invocation context (`VINAYA_MCP_CALLER`) and refusing without one — MCP is a transport, not authorization. `task_resume`/`task_cancel` require the same authenticated caller, plus a current Principal ruling read fresh from the run's own PR — never a caller-supplied approval; `task_resume` triggers the existing `dev-review-loop --resume` continuation exactly once per escalation, and `task_cancel` reports a truthful `confirmed`/`pending`/`uncertain` outcome, idempotently, across repeated calls. No unattended start until the worker-isolation boundary and a capability flag land."
7487
+ ],
7488
+ status: "shipped"
7489
+ },
7490
+ {
7491
+ name: "pr create",
7492
+ description: "Open a pull request after full brief-schema validation",
7493
+ flags: [
7494
+ { flag: "--title", description: "PR title (validated against the forge-title grammar)" },
7495
+ { flag: "--body-file", description: "Path to the PR body (stream-safe; the same bytes are validated and sent)" },
7496
+ { flag: "--label", description: "Label(s) to apply (repeatable, comma-separated)" },
7497
+ { flag: "--validate-only", description: "Run every gate and report PASS without opening the PR" },
7498
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
7499
+ ],
7500
+ details: [
7501
+ "Runs the config-defined brief-schema gate (`briefSchema.pr` in `vinaya.config.json`) LOCALLY before any `gh` write — prevention, not detection. On any failure it refuses with the versioned CheckError contract (one JSON line per finding on stderr, exit 1) whose `agent_recovery_prompt` names the exact corrective command.",
7502
+ "Which sections bind depends on the branch, matching the `brief-shape` check that runs the same grammar in CI. On a `task/<tranche>/<n>` branch every configured section binds, `closesN` included. On any other branch a body that is not brief-shaped is exempt entirely — an ordinary one-line dependency bump is not made to grow a brief — while a brief-shaped body is still graded on every other section, since a standalone fix brief is a brief; only `closesN` is dropped, because such a branch has no task Issue to close. The title grammar sits outside all of this and binds on every branch.",
7503
+ "A branch counts as resolvable only when HEAD is a symbolic ref that also resolves to a commit. Neither git query answers that alone: `rev-parse --abbrev-ref HEAD` reports the literal string `HEAD` on a detached HEAD, and `symbolic-ref` reports the not-yet-created branch name when HEAD is unborn. Reading either as an ordinary branch would take the relaxed path, so both states — plus running outside a repo — resolve to nothing and the gate is fail-closed: every configured section is enforced."
7504
+ ],
7505
+ status: "shipped"
7506
+ },
7507
+ {
7508
+ name: "pr edit",
7509
+ description: "Edit an existing pull request (<n>) after full brief-schema validation",
7510
+ flags: [
7511
+ { flag: "--title", description: "New PR title (validated against the forge-title grammar)" },
7512
+ { flag: "--body-file", description: "Path to the new PR body (stream-safe; same bytes validated and sent)" },
7513
+ { flag: "--validate-only", description: "Run every gate and report PASS without editing the PR" },
7514
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
7515
+ ],
7516
+ details: [
7517
+ "The target PR's real head branch and changed files are fetched from the forge to build the validation context — a failed fetch is a hard refusal, never a fall-back to the local checkout.",
7518
+ "That fetched head branch is what selects the section set, by the same branch grammar `pr create` uses — a property of the target PR, never of whatever the local checkout happens to have checked out."
7519
+ ],
7520
+ status: "shipped"
7521
+ },
7522
+ {
7523
+ name: "pr report",
7524
+ description: "Emit the AEG:EVIDENCE block — a PR body's factual claims, from commands, never typed",
7525
+ flags: [
7526
+ {
7527
+ flag: "--write",
7528
+ description: "Path to the PR body file; replaces the content between the AEG:EVIDENCE anchors in place"
7529
+ },
7530
+ {
7531
+ flag: "--push",
7532
+ description: "PR number; splices the same content into that PR's LIVE body (fetched from the forge) and pushes it via `gh pr edit`, self-verifying nothing outside the AEG:EVIDENCE/AEG:TOKENS regions changed. Mutually exclusive with --write."
7533
+ }
7534
+ ],
7535
+ details: [
7536
+ "Two groups: Group A (recomputable) is the head sha and a width-invariant `git diff --numstat` against the merge-base with `BASE_SHA` (else `origin/main`, then `main`) — `check-evidence-fresh` recomputes and byte-compares this exactly. Group B (attested) is the result of `vinaya check --all --diff-only`, this CLI's own portable gate suite — `check-evidence-fresh` can only check it for staleness (the block's recorded head still matches the PR's real head), never re-run it.",
7537
+ "With no `--write`/`--push`, prints the block to stdout instead of writing a file. Exits non-zero whenever the gate run failed, whether or not `--write`/`--push` was given — the block records a failing result rather than hiding one, and a non-zero exit stops a scripted `--write && open-pr` (or `--push`) from carrying a failing suite onto the forge.",
7538
+ "`--push` refuses before writing anything when the live body carries no real `AEG:EVIDENCE` pair (it never appends one, unlike `--write`), when the anchor resolves differently before and after normalisation, or when `<pr>` isn't a bare number. When the live body has no real `AEG:TOKENS` pair, the token splice is skipped (same reasoning, softer outcome) rather than creating one. After pushing, it re-reads the live body and restores the pre-edit body — exiting non-zero — if anything outside the two anchored regions changed."
7539
+ ],
7540
+ status: "shipped"
7541
+ },
7542
+ {
7543
+ name: "pr rule",
7544
+ description: "Post a Principal ruling on a PR, marked and versioned — never mistaken for a review verdict",
7545
+ flags: [
7546
+ { flag: "--file", description: "Path to the ruling file to post as a PR comment" },
7547
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
7548
+ ],
7549
+ details: [
7550
+ "Refuses before posting when the file's first line reads as an escalation (`ESCALATE:`), or when the file carries verdict grammar anywhere `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` would treat as a candidate — a `VERDICT:` line past the extractor's own first-five-line window still counts, since the whole-body candidate test is what disqualifies the file, not merely a clean read.",
7551
+ "Marker numbers (`<!-- aeg:principal:ruling:<pr>-<k> -->`) are counted on the forge at post time from `gh pr view --json comments`, never derived from a local file — two rulings racing to the same number are a known, undocumented-lock case."
7552
+ ],
7553
+ status: "shipped"
7554
+ },
7555
+ {
7556
+ name: "pr verify-evidence",
7557
+ description: "Prove a pull request's AEG:EVIDENCE region was machine-generated — regenerate it and compare",
7558
+ flags: [],
7559
+ details: [
7560
+ "Deliberately a command, not a check: `evidence-fresh` runs inside `vinaya check --all`, and `pr report` runs `vinaya check --all --diff-only`, so a check that regenerated the block would run the suite containing itself. That recursion is why `evidence-fresh` verifies Group A only and leaves Group B attested.",
7561
+ "Must be run from the repository ROOT of a CLEAN checkout at the pull request's head, and refuses otherwise. The regeneration inherits the working directory and several gates resolve their scan root from it, so a subdirectory silently narrows what the fresh run inspects. On cleanliness: Group B is regenerated by diff-scoped checks, so uncommitted or untracked files change which files are scanned and can produce a MATCH a clean checkout would not. Ignored paths are deliberately NOT inspected — `git diff` never reports one, so they cannot change the scope, and including them would refuse in every real checkout since `node_modules` is ignored and always present. The head is read from the forge and both a mismatch AND an unreadable head refuse — the verdict is bound to a commit, never merely attested. `BASE_SHA` is refused as well, since it steers the regeneration's merge-base.",
7562
+ "Exits 0 on MATCH; 1 on DIFFERS, HIDDEN, or no block; 2 on a refusal (dirty worktree, wrong head, usage error). HIDDEN means the anchor pair sits inside a collapsed `<details>` block, where `body-bare-digits` blanks every digit and nothing can verify what it claims.",
7563
+ "Comparison is a multiset of repo-relative, control-stripped lines: absolute paths (local AND foreign, so a block generated in CI compares against one generated on a laptop) and line order are ignored — a reorder is not a fabrication. Only the AEG:EVIDENCE region is read, since AEG:TOKENS appends by design. A moved merge-base is reported ALONGSIDE the differing lines, never instead of them."
7564
+ ],
7565
+ status: "shipped"
7566
+ },
7567
+ {
7568
+ name: "issue create",
7569
+ description: "Open an issue after full brief-schema validation",
7570
+ flags: [
7571
+ { flag: "--title", description: "Issue title (validated on task Issues)" },
7572
+ { flag: "--body-file", description: "Path to the Issue body (stream-safe; same bytes validated and sent)" },
7573
+ { flag: "--label", description: "Label(s) to apply; a `vinaya/tranche:*` label marks a task Issue" },
7574
+ { flag: "--validate-only", description: "Run every gate and report PASS without opening the Issue" },
7575
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
7576
+ ],
7577
+ details: [
7578
+ "A task Issue (any `vinaya/tranche:*` label) must carry the full Planner rationale (`briefSchema.issue`); non-task Issues pass through unvalidated."
7579
+ ],
7580
+ status: "shipped"
7581
+ },
7582
+ {
7583
+ name: "issue edit",
7584
+ description: "Edit an existing issue (<n>) after full brief-schema validation",
7585
+ flags: [
7586
+ { flag: "--title", description: "New Issue title (validated on task Issues)" },
7587
+ { flag: "--body-file", description: "Path to the new Issue body (stream-safe; same bytes validated and sent)" },
7588
+ { flag: "--validate-only", description: "Run every gate and report PASS without editing the Issue" },
7589
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
7590
+ ],
7591
+ details: [
7592
+ "The target Issue's actual labels are fetched from the forge and unioned with argv to decide task-Issue applicability — a failed fetch is a hard refusal."
7593
+ ],
7594
+ status: "shipped"
7595
+ },
7596
+ {
7597
+ name: "issue objectives edit",
7598
+ description: "Rewrite a task Issue's `## Objectives` section by command — versioned, findable on the forge",
7599
+ flags: [
7600
+ { flag: "--add", description: "Append a new objective as `O<max+1>` with the given sentence" },
7601
+ { flag: "--drop", description: "Remove an objective by id (`O<k>`) — never renumbers the survivors" },
7602
+ {
7603
+ flag: "--replace",
7604
+ description: 'Replace an objective\'s sentence in place, keeping its id (`O<k>` "<sentence>")'
7605
+ },
7606
+ { flag: "--reason", description: "Required, non-empty: why this change is being made" },
7607
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
7608
+ ],
7609
+ details: [
7610
+ "Exactly one of `--add`/`--drop`/`--replace` is required. Every edit goes through the same validated `issue edit` write path (`writeValidatedIssueEdit`) as `vinaya issue edit` itself — no second, unvalidated write path.",
7611
+ "A `--drop` that leaves the surviving objectives non-contiguous from `O1` is refused with `objectivesOf`'s own parser message: dropping never renumbers survivors, and task 1's contiguous-from-O1 grammar is the one parser everything else reads, so a live contradiction between the two stops here for a Principal ruling rather than silently renumbering.",
7612
+ "Splices the rendered section back in place (`## Objectives` heading through the next `##` heading, or end of body) — every other byte of the Issue body is untouched. Posts one comment marked `<!-- aeg:objectives:v<k> -->` carrying the previous list, the new list, the reason, and the new `objectivesVersion`; `k` is counted on the forge at post time, never derived from a local file."
7613
+ ],
7614
+ status: "shipped"
7615
+ },
7616
+ {
7617
+ name: "log flush",
7618
+ description: "Post a target Issue or PR's outbox as one or more marked comments, then truncate what the forge confirmed",
7619
+ flags: [
7620
+ { flag: "--issue", description: "Flush this Issue's outbox (`~/.vinaya/outbox/<owner>-<repo>/<n>.ndjson`)" },
7621
+ {
7622
+ flag: "--pr",
7623
+ description: "Flush the Issue named by this PR's `Closes #N` line, posting the comments on the PR"
7624
+ },
7625
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
7626
+ ],
7627
+ details: [
7628
+ "Exactly one of `--issue`/`--pr` is required. `--pr` resolves the Issue from the PR body's `Closes #N` line — the same anchor every gate reads — and refuses with a check error naming the missing line when the body carries none.",
7629
+ "Splits the outbox first at `run_id` boundaries (a maximal run of consecutive lines sharing one `run_id`, never a global group-by, so an interleaved outbox never produces a range spanning a gap), then at `FORGE_COMMENT_MAX_CHARS` (65536) within each run. Each comment opens with `<!-- aeg:log:<run_id>:<seq_from>-<seq_to> -->` on its own line, then one fenced `ndjson` block, one outbox line per line, verbatim. A single line too large to fit alone is refused by name, never split across two comments.",
7630
+ "Logs its own `forge_write` line — `validated` before posting, `written` (with every returned comment id) after the last post succeeds, or `refused` (with gh's error) on any failure — through the same `log()` every other family uses, into the same outbox, before truncating. The outbox is truncated only to the lines confirmed posted; a failed chunk's lines, and anything appended to the outbox during the flush (including the flush's own `validated`/`written`/`refused` line), always survive to ride the next flush."
7631
+ ],
7632
+ status: "shipped"
7633
+ },
7634
+ {
7635
+ name: "log export-artifact",
7636
+ description: "Copy this run's own local outbox into a file, for the generated task-path workflow's own upload step",
7637
+ flags: [],
7638
+ details: [
7639
+ "Concatenates every `*.ndjson` file under `~/.vinaya/outbox/` into `<dest>`, verbatim — no filtering, no second redaction pass (lines are already redacted at `log()`'s own write boundary). Writes nothing when the outbox is empty, and exits 0 either way: no gate events this run is a legitimate outcome, never a failure.",
7640
+ "Runs inside the generated `vinaya-checks.yml` job, which holds no forge-write credential — this command never calls the forge. Both this step and the `actions/upload-artifact` step after it run `if: always()`, so a cancelled or failed job still exports whatever partial outbox it has."
7641
+ ],
7642
+ status: "shipped"
7643
+ },
7644
+ {
7645
+ name: "log collect-artifact",
7646
+ description: "Validate a downloaded task-log artifact and publish the valid records through the existing flush path",
7647
+ flags: [
7648
+ { flag: "--pr", description: "The pull request this artifact belongs to" },
7649
+ { flag: "--repo", description: "Expected `owner/repo` — a provenance cross-check against each record" }
7650
+ ],
7651
+ details: [
7652
+ "Runs inside the generated `vinaya-task-log-collector.yml` workflow — a `workflow_run`-triggered job on the default branch, holding the write credential the task-path job that produced the artifact never gets. `<path>` is already downloaded by that workflow, scoped to a specific, API-verified run id — a provenance guarantee this command never re-derives from the artifact bytes.",
7653
+ "Never executes, imports, or evaluates the artifact's content: every line is read as text and validated (schema, an 8 MiB whole-artifact size cap, redaction, and a repo cross-check) before anything is trusted. A rejected line becomes a named gap — printed, never a silent drop — and the artifact is never partially trusted past that check into executing anything.",
7654
+ "The valid, re-redacted survivors are appended into the same local outbox `vinaya log flush` would read for that PR's Issue, then published through the existing `flushOutbox` unmodified — reusing its chunking, its `<!-- aeg:log:… -->` marker, and its idempotent-retry read, rather than a second publisher. A retried collector run (a fresh runner, the same forge state) therefore dedupes for free: the marker already on the forge is acknowledged, never re-posted."
7655
+ ],
7656
+ status: "shipped"
7657
+ },
7658
+ {
7659
+ name: "milestone create",
7660
+ description: "Create a GitHub Milestone from a validated body",
7661
+ flags: [
7662
+ { flag: "--title", description: "Milestone title — free text, never parsed for a version" },
7663
+ {
7664
+ flag: "--body-file",
7665
+ description: "Path to the Milestone description (stream-safe; same bytes validated and sent)"
7666
+ },
7667
+ { flag: "--validate-only", description: "Run every gate and report PASS without creating the Milestone" },
7668
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
7669
+ ],
7670
+ details: [
7671
+ "Refuses before any `gh` call when the goal is absent, an optional `Release:` field is present but not a version, or an optional `### Tranche intents` section (`- <slug>: <intent text>`) doesn't parse. `Release:` is the sole authority for the milestone's version — the title is never parsed for one."
7672
+ ],
7673
+ status: "shipped"
7674
+ },
7675
+ {
7676
+ name: "milestone adopt",
7677
+ description: "Move one or more existing tranches into a target Milestone",
7678
+ flags: [
7679
+ { flag: "--target", description: "The Milestone every named slug is adopted into" },
7680
+ { flag: "--slug", description: "A tranche slug to adopt — repeatable for a multi-slug move" },
7681
+ { flag: "--validate-only", description: "Run every gate and report PASS without writing anything" },
7682
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
7683
+ ],
7684
+ details: [
7685
+ "Reattaches every Issue carrying each named `vinaya/tranche:<slug>` label to `--target`, then closes (never deletes) each slug's old tranche-Milestone. Every fact for every named slug is gathered and checked in ONE call before any write, so a single unsafe slug — an unknown slug, a slug whose label carries no Issues, a target that does not exist or is closed, or a slug already adopted into a different Milestone — refuses the whole invocation, not just its own slug."
7686
+ ],
7687
+ status: "shipped"
7688
+ },
7689
+ {
7690
+ name: "milestone edit",
7691
+ description: "Edit an existing Milestone (<n>) after full brief-schema validation",
7692
+ flags: [
7693
+ {
7694
+ flag: "--body-file",
7695
+ description: "Path to the new Milestone description (stream-safe; same bytes validated and sent)"
7696
+ },
7697
+ { flag: "--validate-only", description: "Run every gate and report PASS without editing the Milestone" },
7698
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
7699
+ ],
7700
+ details: [
7701
+ "Same `checkMilestoneShape` refusal `create` runs, before any `gh` call — the gated replacement for a raw `gh api PATCH` against a Milestone. Only the description changes; the title is untouched."
7702
+ ],
7703
+ status: "shipped"
7704
+ },
7705
+ {
7706
+ name: "milestone close",
7707
+ description: "Close a tranche's Milestone after verifying every labeled Issue is actually attached",
7708
+ flags: [
7709
+ { flag: "--slug", description: "The tranche whose Milestone is being closed" },
7710
+ { flag: "--validate-only", description: "Run every gate and report PASS without closing the Milestone" },
7711
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
7712
+ ],
7713
+ details: [
7714
+ "Resolves the target Milestone the same legacy-or-intent-declared way Issue create auto-attach does, then refuses to close on any mismatch between the label's Issues and the Milestone's natively attached Issues — naming each unattached or foreign Issue and its repair path (`gh issue edit <n> --milestone <title>`, or `vinaya milestone adopt`) — before the PATCH ever reaches the forge."
7715
+ ],
7716
+ status: "shipped"
7717
+ },
7718
+ {
7719
+ name: "milestone status",
7720
+ description: "Print each of a Milestone's declared tranche intents with its derived lifecycle and issue counts",
7721
+ flags: [{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }],
7722
+ details: [
7723
+ "Read-only — writes nothing. For each `- <slug>: …` line in the Milestone's `### Tranche intents` section, prints the tranche's lifecycle (`planned`/`active`/`complete`) and its labeled Issues' counts (merged, open, not planned), all derived from the forge. Refuses if `<n>` isn't a real Milestone in this repo, or if the forge is unreachable."
7724
+ ],
7725
+ status: "shipped"
7726
+ },
7727
+ {
7728
+ name: "review status",
7729
+ description: "Print the review loop's own state for a PR, and its branch's distance from the base",
7730
+ details: [
7731
+ "Two lines at most. The first is `CONTINUE`, `PAUSE: <reason>[ <id>]` for `reappearance`, `zero-deaths` or `max-rounds` — or, for `stale`, the actionable fact itself: `push after verdict — re-review required`. The second reads `behind main by <n> — merge first` when the branch is behind its base, or `behind main: unknown — fetch origin/<base> first` when git cannot measure the distance; it is absent only when the branch is measurably not behind.",
7732
+ "Rounds are derived from the PR's own verdict comments — one round per `Judged head:` value, read through the same extractors the merge gate blocks on, and only from comments an allowlisted principal authored. A Developer round comment is recognised by its `<!-- aeg:developer:round-<n> -->` marker, at that fixed position, never by scanning its prose.",
7733
+ "Exit `0` only when the state is `CONTINUE` and the branch is not behind; `1` otherwise, so a script can gate on the exit code without parsing the text."
7734
+ ],
7735
+ status: "shipped"
7736
+ },
7737
+ {
7738
+ name: "review post",
7739
+ description: "Render, post, and self-verify a code-reviewer or security-review verdict comment on a PR",
7740
+ flags: [
7741
+ { flag: "--role", description: "`code-reviewer` or `security` — selects which role template is rendered" },
7742
+ {
7743
+ flag: "--pr",
7744
+ description: "Target PR number — its real head is resolved via `gh pr view`, never caller-supplied"
7745
+ },
7746
+ {
7747
+ flag: "--verdict",
7748
+ description: "Optional — derived from the findings file (code-reviewer: `APPROVE`/`REQUEST_CHANGES`; security: `PASS`/`FAIL`). Refused before posting if it disagrees with the derivation."
7749
+ },
7750
+ {
7751
+ flag: "--escalate",
7752
+ description: "`authority` | `strategy` | `product` — posts an `ESCALATE:` comment instead of a verdict. Refused together with `--verdict` or a blocking finding."
7753
+ },
7754
+ { flag: "--summary", description: "Required with `--escalate` — the escalation body text" },
7755
+ {
7756
+ flag: "--findings-file",
7757
+ description: "One finding per line: `SEVERITY|file:line|description` (`|`-delimited). A re-review names a prior id in the description as `F<n> <class> <state>:`. Omit for zero findings."
7758
+ },
7759
+ {
7760
+ flag: "--objectives-file",
7761
+ description: "One line per objective: `O<n>|MET|<evidence>` or `O<n>|NOT MET|<evidence>` (`|`-delimited; evidence is the rest of the line). Required whenever the closed Issue (or the PR body's own `## Objectives`) has an objectives list to judge; its ids must cover that list exactly, and a re-review must restate every prior objective. Never together with `--escalate`."
7762
+ },
7763
+ { flag: "--brief-conformance", description: "code-reviewer only: the BRIEF CONFORMANCE line" },
7764
+ { flag: "--spec-conformance", description: "code-reviewer only: the SPEC CONFORMANCE line" },
7765
+ { flag: "--scope", description: "code-reviewer only: the SCOPE line" },
7766
+ {
7767
+ flag: "--scope-evidence-file",
7768
+ description: "code-reviewer only: pasted diff-stat output, rendered as a fence directly below the verdict block"
7769
+ },
7770
+ { flag: "--tests", description: "code-reviewer only: the TESTS line" },
7771
+ { flag: "--docs", description: "code-reviewer only: the DOCS line" },
7772
+ { flag: "--config-scan", description: "security only: the CONFIG SCAN line" },
7773
+ { flag: "--secrets", description: "security only: the SECRETS line" },
7774
+ {
7775
+ flag: "--secrets-evidence-file",
7776
+ description: 'security only: required whenever `--secrets` claims "none found" — the pasted scanner output backing that claim'
7777
+ },
7778
+ { flag: "--task-id", description: "the closing `Tokens:` line's task id" },
7779
+ { flag: "--model", description: "the closing `Tokens:` line's model name" },
7780
+ { flag: "--tokens-in", description: "a non-negative integer, or `-` if unknown" },
7781
+ { flag: "--tokens-out", description: "a non-negative integer, or `-` if unknown" },
7782
+ { flag: "--cost", description: "free text, or `-` if unknown" },
7783
+ {
7784
+ flag: "--print-only",
7785
+ description: "Render and self-check the comment, print it, and exit — never calls `gh pr comment`"
7786
+ },
7787
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
7788
+ ],
7789
+ details: [
7790
+ "`--print-only` runs the exact same render-then-self-check path as a real post, then returns before `gh pr comment` — closes atta-labs/vinaya#184, where the old command silently posted a verdict anyway after a reviewer guessed this flag existed.",
7791
+ "Every structural line (`VERDICT:`, `Judged head:`) is rendered from this command's own validated enum/sha inputs — never from a caller-supplied string — so a Reviewer's free-typed prose can no longer produce a shape the merge gate's line-anchored regex fails to see.",
7792
+ "The verdict is derived, not typed: a BLOCKER (or CRITICAL/HIGH) finding forces REQUEST_CHANGES/FAIL and its absence forces APPROVE/PASS, before posting anything — an explicit `--verdict` that disagrees is refused naming the derived value. When a same-role verdict comment already exists on the PR, a new findings file must carry every prior id with a state and no non-blocking finding outside the diff since that comment's judged head, or the post is refused.",
7793
+ "Renders an `OBJECTIVES:` block (one `O<n>: MET | NOT MET — <evidence>` line per objective) after `SPEC CONFORMANCE:`/before `CONFIG SCAN:`, and an `Objectives version:` line at line 5 — resolved from the closed Issue's `## Objectives` list, or the PR body's own section when it closes none. A clean verdict (`APPROVE`/`PASS`) is refused alongside any `NOT MET`; an Issue below the objectives cutover renders neither line at all, matching the merge gate's own skip.",
7794
+ "Before the post ever reaches the forge, runs the exact `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` functions `checkReviewGate` calls over its own rendered text and refuses (exit 2) unless exactly the intended verdict extracts and the other role extracts none — an escalation requires both to extract none. Both extractors read only a comment's first five lines; a code-review or security render's caller-supplied fields never open one of those lines, but an escalation's `--summary` can (pre-cutover, it becomes line 5 unprefixed) — this pre-post re-parse, not the render's construction, is what catches that case before it ever reaches the forge.",
7795
+ "After posting, re-fetches the PR's comments and runs them through the same extractors `checkReviewGate` calls — the same functions, not a second implementation — and exits non-zero naming precisely what failed to re-parse if the post does not come back clean, bound to the resolved head and objectives version, and free of the other role's verdict. There is no `--skip-verify` escape."
7796
+ ],
7797
+ status: "shipped"
7798
+ },
7799
+ {
7800
+ name: "doctor",
7801
+ description: "Diagnose hook, workflow, and config health — report only, never mutates",
7802
+ flags: [{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }],
7803
+ details: [
7804
+ 'Carries the same env-declaration diagnostic `vinaya check` warns with — permanently, at `info` severity, not just ahead of the spawn-default flip — plus a `warn`-severity lint over suspicious `env` literal forms (a stray `"true"`/`"false"` string, or a high-entropy literal that reads like a leaked secret committed to config).',
7805
+ "Also carries the two permanent `checks`-classification diagnostics: a config key that REPLACES a core check (`warn`), and a bare un-namespaced key that is REJECTED (`error`, naming the rename requirement). These are the reason a config `vinaya check` now refuses outright is still diagnosable — the refusal runs nothing, so `doctor` is the surface that explains why."
7806
+ ],
7807
+ status: "shipped"
7808
+ },
7809
+ {
7810
+ name: "tokens",
7811
+ description: "Print a role's `Tokens: …` report line — the portable front door over the collection adapter",
7812
+ flags: [
7813
+ { flag: "--phase", description: 'e.g. `"<task-id>: develop"` — required' },
7814
+ { flag: "--role", description: "e.g. `Developer` — required" },
7815
+ { flag: "--model", description: "Overrides the model id the adapter derived, if given" },
7816
+ {
7817
+ flag: "--transcript",
7818
+ description: "Read this transcript directly. Supported primary route — use it whenever you know which transcript " + "is yours, and always in a repo with no track-transcript.sh hook. Omitted resolves via the Stop-hook " + "pointer file, if this repo installs that hook."
7819
+ },
7820
+ {
7821
+ flag: "--in / --out",
7822
+ description: "Manual entry: the exact token figures, for a host whose usage arrives by some other means than a " + "Claude Code transcript. Both required together; skips transcript resolution entirely."
7823
+ }
7824
+ ],
7825
+ details: [
7826
+ "Resolves the Claude Code collection adapter (`resolveMeteringCapability`, `summarizeTranscript`, `formatTokensLine`) from the INSTALLED `@attalabs/aeg-core` package, never by a repo-relative path — the fix for `packages/aeg-core/bin/report-tokens.ts` not existing in an adopter repo with no local `packages/`.",
7827
+ "Refuses — never emits a `0/0/—` line — when no transcript resolves, a resolved transcript can't be read, or it summarizes to zero usage records (empty, unparseable, or not yet flushed to disk). Capability is probed by actually attempting resolution, never declared from the host being Claude Code."
7828
+ ],
7829
+ status: "shipped"
7830
+ },
7831
+ {
7832
+ name: "doctrine",
7833
+ description: "Print the absolute path of the bundled doctrine's front door (aeg-root/skills/aeg/SKILL.md) on this machine",
7834
+ flags: [{ flag: "--json", description: "Enveloped JSON output (schema: 1) — `{ root, entry }`" }],
7835
+ details: [
7836
+ "The committed root `VINAYA.md` pointer names the `@attalabs/vinaya` package, never a filesystem path — where the package sits is a property of each machine, not of the repo, and the pointer is committed for every clone. This command is the read-time resolution step the pointer hands the reader: it resolves the installed package's own bundled `aeg-root/` wherever the CLI physically sits and prints the front door's absolute path, so `cat \"$(vinaya doctrine)\"` opens the doctrine on any machine at any version.",
7837
+ "In a repo that vendors the CLI, the bundled copy is a gitignored pack-time artifact, so the command falls back to the monorepo root's own `aeg-root/` — the same directory `bundle-doctrine` copies from. Exits 1 with a corrective message when neither location holds a doctrine."
7838
+ ],
7839
+ status: "shipped"
7840
+ },
7841
+ {
7842
+ name: "upgrade",
7843
+ description: "Regenerate hooks, workflow, and config to the current contract version (diff-and-confirm)",
7844
+ flags: [
7845
+ { flag: "--dry-run", description: "Print the full diff without regenerating anything" },
7846
+ { flag: "--yes", description: "Skip the confirmation prompt" }
7847
+ ],
7848
+ details: [
7849
+ "When `claude` is a selected agent vendor, also retrofits the Claude Code `Stop` hook (`.claude/hooks/track-transcript.sh`, `.claude/settings.json`) the same way `init` installs it on a fresh repo — for a repo that ran `init` before this hook existed, closing the gap that left token rows reading `—/—/—` with no operator ever told to re-run `init`. Never overwrites a foreign `.claude/settings.json` (refuse-if-foreign, same as `init`); appends to a foreign Stop-hook script rather than replacing it (managed-block, same discipline as the git hooks)."
7850
+ ],
7851
+ status: "shipped"
7852
+ },
7853
+ {
7854
+ name: "archive",
7855
+ description: "Run the post-merge Archivist directly: provenance + Issue close-out for a merged task PR",
7856
+ flags: [{ flag: "--merge-sha", description: "The merge commit to resolve (defaults to the current HEAD)" }],
7857
+ details: [
7858
+ "Resolves the merge commit's associated PR via `gh`, and — if it's a task PR without a provenance comment yet — posts the provenance block and closes the linked Issue. Idempotent: re-running against an already-archived PR is a no-op.",
7859
+ "The same logic the generated `vinaya-archivist.yml` workflow's `post-merge` job runs on every push to `main` — callable directly for a one-off run or local verification.",
7860
+ "All progress output writes through `process.stdout.write`/`process.stderr.write` (never bare `console.*`), so piping this command into a log file or a CI step captures every line in order."
7861
+ ],
7862
+ status: "shipped"
7863
+ },
7864
+ {
7865
+ name: "archive tranche",
7866
+ description: "Close a tranche — the tranche-level bookend to `init product`, closing the Milestone via the CLI",
7867
+ flags: [{ flag: "--yes", description: "Skip the confirmation prompt" }],
7868
+ details: [
7869
+ "Refuses if any task Issue attached to the named tranche is still open, naming each one — closing a tranche with unresolved work is never silently allowed.",
7870
+ "Once every task Issue is closed, prompts for confirmation (unless `--yes`) and closes the GitHub Milestone."
7871
+ ],
7872
+ status: "shipped"
7873
+ },
7874
+ {
7875
+ name: "audit",
7876
+ description: "Run the ring-2 dead-branch-push and direct-main-push detection checks directly",
7877
+ flags: [
7878
+ { flag: "--only", description: "Scope to one check: 'dead-branches' or 'direct-push'" },
7879
+ { flag: "--sha", description: "The commit to check for direct-main-push (defaults to the current HEAD)" },
7880
+ { flag: "--json", description: "Enveloped JSON output" }
7881
+ ],
7882
+ details: [
7883
+ "Dead-branch-push is never-red — a notification channel that flags (label + PR comment) any `task/*` branch whose tip commit lands after its own PR already resolved. Direct-main-push is a real pass/fail — it polls the merge-association API for up to ~100s before deciding, then opens an incident Issue and exits 1 if a commit on `main` genuinely has no associated merged PR.",
7884
+ "The same logic the generated `vinaya-archivist.yml` workflow's `daily-drift` and `direct-main-push-detection` jobs run on schedule / on every push to `main` — callable directly for a one-off run or local verification.",
7885
+ "All progress output writes through `process.stdout.write`/`process.stderr.write` (never bare `console.*`), so piping this command into a log file or a CI step captures every line in order."
7886
+ ],
7887
+ status: "shipped"
7888
+ },
7889
+ {
7890
+ name: "eject",
7891
+ description: "Remove every Vinaya-installed artifact, restoring the repo to stock",
7892
+ flags: [
7893
+ { flag: "--dry-run", description: "Print the full removal diff without removing anything" },
7894
+ { flag: "--yes", description: "Skip the confirmation prompt" }
7895
+ ],
7896
+ status: "shipped"
7897
+ },
7898
+ {
7899
+ name: "demo break",
7900
+ description: "Run a guided refusal-then-fix demo on an isolated, discardable branch",
7901
+ flags: [{ flag: "--keep", description: "Skip cleanup and leave the demo branch checked out to inspect" }],
7902
+ details: [
7903
+ "Creates a collision-safe `vinaya/demo-break-<id>` branch off the current one, stages a deliberately incomplete draft brief, and attempts a real `git commit` — the repo's actually-installed pre-commit hook refuses it with its real output, not a scripted string. Applies the minimal fix, commits again, then switches back and deletes the demo branch.",
7904
+ "Safe to run twice: refuses on a dirty working tree, refuses from a detached HEAD, and recovers automatically from a prior crashed run before starting a fresh one — never leaves the original branch touched or a stray demo branch behind."
7905
+ ],
7906
+ status: "shipped"
7907
+ },
7908
+ {
7909
+ name: "waiver",
7910
+ description: "Apply the actor-verified 'vinaya/waiver:docs' or 'vinaya/waiver:review' label after prompting for a reason",
7911
+ flags: [
7912
+ { flag: "--reason", description: "The waiver rationale, posted as a PR comment (prompted for if omitted)" },
7913
+ { flag: "--print-only", description: "Print the exact `gh` commands instead of running them" }
7914
+ ],
7915
+ details: [
7916
+ "Applies the label via `gh pr edit --add-label`, under the invoking human's own authenticated `gh` identity — this command never fabricates an actor. A waiver is never an agent-emittable string: not a PR body field, not a commit trailer, not a comment — the label plus its own labeling-timeline actor is the only mechanism ring 1 honors.",
7917
+ "`--print-only` prints the exact `gh pr edit`/`gh pr comment` commands and runs nothing — for a human who wants to run them itself, or a CI/non-interactive context where an agent session should never be the one applying its own waiver."
7918
+ ],
7919
+ status: "shipped"
7920
+ },
7921
+ {
7922
+ name: "studio",
7923
+ description: "Launch Vinaya Studio — runs the Studio dev app when its source (apps/vinaya-studio/web) is in a checkout above the current directory; a published install launches its bundled standalone server instead",
7924
+ flags: [
7925
+ {
7926
+ flag: "--port <n>",
7927
+ description: "Bind this exact port. Without the flag the default is unchanged — 3008, falling back to 3108 when it is taken. With it there is no fallback: a taken port is refused, so you always know which server answered. Accepts `--port 3208` and `--port=3208`. Applies to a published install; in a workspace checkout Studio's own dev script owns the port and the flag is refused."
7928
+ }
7929
+ ],
7930
+ details: [
7931
+ "Resolution happens in this order: a workspace checkout carrying `apps/vinaya-studio/web` (Studio's source, which lives in the attalabs monorepo — not this repository) runs the dev app directly; a published install's `studio-standalone/` bundle (fetched from attalabs' release artifact at publish time, see `scripts/bundle-studio.ts`) runs that bundled server; anything else — a publish that shipped without the bundle — gets an explicit refusal and exit 1 rather than a silent no-op.",
7932
+ "Every published `@attalabs/vinaya` build ships the `studio-standalone/` bundle: `prepack` fetches attalabs’ latest CI-built standalone Studio artifact and assembles it into the tarball before publish."
7933
+ ],
7934
+ status: "shipped"
7935
+ },
7936
+ {
7937
+ name: "quickstart",
7938
+ description: "Guided wizard: init, doc-owners, project, commit, demo break, doctor, push — one command",
7939
+ details: [
7940
+ "Calls `init`'s own diff-and-confirm flow unchanged (pausing on Enter before the diff prints, so its own step header isn't scrolled off by a long diff), then Y/n-prompts through the workarounds a guest used to run by hand: binding `.vinaya/doc-owners` pairs (bad input offers a retry instead of silently skipping, and a pointer that doesn't exist on disk is refused outright — both loop across as many pairs as the guest wants, not just one), registering tracked projects (`init product`, same retry/loop shape), committing the install, running `demo break` as proof the gates actually work (default yes — the one step this wizard makes hardest to skip), running `doctor`, and pushing. Each declined prompt skips only that step; the install commit itself is never prompt-gated — it just no-ops when there is genuinely nothing to commit.",
7941
+ "Never reimplements or edits `init`/`init product`/`demo break`/`doctor` — it only calls their existing, unmodified entry points in sequence."
7942
+ ],
7943
+ status: "shipped"
7944
+ },
7945
+ {
7946
+ name: "release",
7947
+ description: "Run this repo's own publish sequence in one command, refusing to start unless every precondition holds",
7948
+ flags: [
7949
+ {
7950
+ flag: "--dry-run",
7951
+ description: "Run the preconditions only and print the plan; publishes nothing"
7952
+ },
7953
+ {
7954
+ flag: "--allow-any-commit",
7955
+ description: "Skip the check that HEAD's commit is a Version Packages commit"
7956
+ }
7957
+ ],
7958
+ details: [
7959
+ "Refuses unless HEAD is the default branch, the working tree is clean, HEAD equals `origin/<default>` (after `git fetch origin`), HEAD's commit subject starts with `Chore(release): Version packages` (unless `--allow-any-commit`), and `npm whoami` exits `0` — each its own refusal naming the fix.",
7960
+ "Then streams `bun install --frozen-lockfile`, `bun run build`, `bun run changeset:publish`, and `git push origin --tags` — a real push, so the repo's own generated pre-push hook sees it exactly as any other push would. After it, prints `npm view <pkg> version` for every tag now on HEAD, noting registry lag on `@attalabs/vinaya` (observed ~20 minutes) when it still shows the previous version.",
7961
+ 'This is the one procedure named in `apps/cli/specs/self-hosting.md`, "How the published version is produced" — publishing itself stays manual and human-triggered; this command only removes the hand-typed four-step recipe.'
7962
+ ],
7963
+ status: "shipped"
7964
+ },
7965
+ {
7966
+ name: "dispatch",
7967
+ description: "Start a role's headless agent session (claude/codex/gemini) with attribution set on its environment, recording the outcome through the Vinaya Log",
7968
+ flags: [
7969
+ {
7970
+ flag: "--agent",
7971
+ description: "Vendor to start: `claude`, `codex`, or `gemini` (falls back to `dispatch.agent` in config)"
7972
+ },
7973
+ { flag: "--prompt-file", description: "Path to the prompt text sent to the agent (never on argv)" },
7974
+ { flag: "--task", description: "This dispatch's task Issue number — mutually exclusive with `--pr`" },
7975
+ {
7976
+ flag: "--pr",
7977
+ description: "Flush the outbox to this PR's Issue after the dispatch — mutually exclusive with `--task`"
7978
+ },
7979
+ { flag: "--round", description: "Round number, for a dispatch inside a review loop" },
7980
+ {
7981
+ flag: "--resume <id>",
7982
+ description: "Resume the vendor's own session/thread from a prior dispatch's returned `resumeId`, instead of starting a fresh one"
7983
+ },
7984
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
7985
+ ],
7986
+ details: [
7987
+ "Sets `VINAYA_RUN_ID`/`VINAYA_ROLE`/`VINAYA_TASK`/`VINAYA_ROUND` on the child only — never on this process's own environment — and refuses by name, before any spawn attempt, when the named vendor binary is absent from `PATH` or present but not executable.",
7988
+ "Records `dispatched` (with the prompt's sha256), `outcome_received` (duration, the vendor's own usage when its stdout prints a recognizable shape), or `dispatch_failed` (`timeout`, `crash`, or `refused`) through the Vinaya Log's one `dispatch` family writer, `dispatchRole`. A wall-time ceiling (`dispatch.timeoutMs` in config, default one hour) sends `SIGTERM` then, after a grace window, `SIGKILL`.",
7989
+ "When `--task` or `--pr` is given, flushes that outbox via `vinaya log flush` immediately after the child settles — `--task` and `--pr` are mutually exclusive here, matching `log flush`'s own single-target rule. Without either, the dispatch still runs and logs; nothing is flushed, and the lines ride to the next flush.",
7990
+ "A successful dispatch's `DispatchHandle` carries `resumeId` — the vendor's own session/thread identifier (claude/gemini: `session_id`; codex: `thread_id`), parsed from its stdout, `null` on any failure. Passing that value as `--resume <id>` on a later call swaps in that vendor's own resume invocation (`claude -p -r <id> ...`; `codex exec resume <id> ...`; `gemini ... --resume <id> ...`) in place of its first-dispatch args."
7991
+ ],
7992
+ status: "shipped"
7993
+ },
7994
+ {
7995
+ name: "dev-review-loop",
7996
+ description: "Dispatch the developer, run review rounds against the forge, resume the same developer session every round, and hold every verdict until the policy says publish",
7997
+ flags: [
7998
+ { flag: "--task <n>", description: "This task's Issue number — its frozen `aeg:brief:v1` comment is the brief" },
7999
+ {
8000
+ flag: "--agent <claude|codex|gemini>",
8001
+ description: "Vendor for every dispatch this loop makes (falls back to `dispatch.agent` in config)"
8002
+ },
8003
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
8004
+ ],
8005
+ details: [
8006
+ "Dispatches the developer through `dispatchRole` with the brief read from the Issue, waits for the PR it opens, then runs `assessRound` (`@attalabs/aeg-core`) — the entire policy — against observations this command reads from the forge: the head via `git ls-remote` only (never `gh pr view headRefOid`, which can lag a push), CI conclusion via the check-runs API (never run locally), and ruling comments matching `<!-- aeg:principal:ruling:<pr>-<k> -->`.",
8007
+ "Nothing is posted to the PR before the policy decides `publish`: each round's reviewer and security verdicts are dispatched fresh (never a resumed session) through `dispatchRole`, rendered through `review post`'s own render functions, and written to a local file under the outbox — never `gh pr comment`/`gh pr review`. Posting the held verdicts is a separate, later task.",
8008
+ "The developer's session is resumed every round via `dispatchRole`'s `resumeId` — never a fresh session — for every vendor; a round whose resume fails for a vendor that resumed successfully the round before stops the loop rather than silently falling back to a fresh developer session.",
8009
+ "Every dispatch and round transition is a log line through the Vinaya Log, flushed to the forge at each round boundary."
8010
+ ],
8011
+ status: "shipped"
8012
+ }
8013
+ ];
8014
+ // ../../packages/sources/src/forge-adapter.ts
8015
+ function createForgeSource(config) {
8016
+ return {
8017
+ async getTranche(slug) {
8018
+ return deriveTrancheFromForge(config.owner, config.repo, slug);
8019
+ }
8020
+ };
8021
+ }
8022
+ // ../../packages/sources/src/select-source.ts
8023
+ import { z as z5 } from "zod";
8024
+ var StateSourceConfigSchema = z5.discriminatedUnion("kind", [
8025
+ z5.object({ kind: z5.literal("forge"), owner: z5.string(), repo: z5.string() }),
8026
+ z5.object({ kind: z5.literal("file"), root: z5.string().optional() })
8027
+ ]);
8028
+ // src/checks/edge-resolve.ts
8029
+ import { execFileSync as execFileSync4 } from "node:child_process";
8030
+ var DIRECT_ISSUE_REF = /#(\d+)/;
8031
+ var issueCache = new Map;
8032
+ function fetchIssueState(num, repo) {
8033
+ const key = `${repo.owner}/${repo.repo}#${num}`;
8034
+ if (issueCache.has(key))
8035
+ return issueCache.get(key) ?? null;
8036
+ let out = null;
8037
+ try {
8038
+ const raw = execFileSync4("gh", [
8039
+ "issue",
8040
+ "view",
8041
+ String(num),
8042
+ "-R",
8043
+ `${repo.owner}/${repo.repo}`,
8044
+ "--json",
8045
+ "state,stateReason,closedByPullRequestsReferences"
8046
+ ], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
8047
+ out = JSON.parse(raw);
8048
+ } catch {
8049
+ out = null;
8050
+ }
8051
+ issueCache.set(key, out);
8052
+ return out;
8053
+ }
8054
+ var defaultResolveSiblingTranche = async (slug, repo) => {
8055
+ let tranche;
8056
+ try {
8057
+ tranche = await deriveTrancheFromForge(repo.owner, repo.repo, slug);
8058
+ } catch {
8059
+ return null;
8060
+ }
8061
+ const taskRefs = tranche.tasks.map((t) => ({ id: t.id, issue: t.issue }));
8062
+ const snapshot = await fetchForgeFacts({ owner: repo.owner, repo: repo.repo, tranche: slug, tasks: taskRefs });
8063
+ return {
8064
+ tasks: new Map(tranche.tasks.map((t) => [t.id, { id: t.id, issue: t.issue }])),
8065
+ facts: snapshot.facts
8066
+ };
8067
+ };
8068
+ var siblingTrancheCache = new Map;
8069
+ function edgeFromIssueJson(num, json) {
8070
+ if (json === null) {
8071
+ return {
8072
+ issue: num,
8073
+ merged: false,
8074
+ open: false,
8075
+ issueState: null,
8076
+ stateReason: null,
8077
+ closedByActor: null,
8078
+ resolved: true
8079
+ };
8080
+ }
8081
+ const state = json.state === "CLOSED" ? "closed" : json.state === "OPEN" ? "open" : null;
8082
+ const reason = json.stateReason === "COMPLETED" ? "completed" : json.stateReason === "NOT_PLANNED" ? "not_planned" : null;
8083
+ const closedByMergedPr = (json.closedByPullRequestsReferences ?? []).length > 0;
8084
+ return {
8085
+ issue: num,
8086
+ merged: state === "closed" && closedByMergedPr,
8087
+ open: false,
8088
+ issueState: state,
8089
+ stateReason: reason,
8090
+ closedByActor: null,
8091
+ resolved: true
8092
+ };
8093
+ }
8094
+ async function resolveEdge(id, taskById, factsByTaskId, repo, resolveSibling = defaultResolveSiblingTranche) {
8095
+ const target = taskById.get(id);
8096
+ if (target) {
8097
+ const facts = target.issue !== null ? factsByTaskId.get(target.id) : undefined;
8098
+ return {
8099
+ issue: target.issue,
8100
+ merged: facts?.prState === "merged",
8101
+ open: facts?.prState === "open",
8102
+ issueState: facts?.issueState ?? null,
8103
+ stateReason: facts?.stateReason ?? null,
8104
+ closedByActor: facts?.closedByActor ?? null,
8105
+ resolved: true
8106
+ };
8107
+ }
8108
+ const direct = id.match(DIRECT_ISSUE_REF);
8109
+ if (direct) {
8110
+ const num = Number(direct[1]);
8111
+ return edgeFromIssueJson(num, fetchIssueState(num, repo));
8112
+ }
8113
+ const qualified = splitSlugQualifiedEdge(id);
8114
+ if (qualified) {
8115
+ const cacheKey = `${repo.owner}/${repo.repo}:${qualified.slug}`;
8116
+ let sibling = siblingTrancheCache.get(cacheKey);
8117
+ if (sibling === undefined) {
8118
+ sibling = await resolveSibling(qualified.slug, repo);
8119
+ siblingTrancheCache.set(cacheKey, sibling);
8120
+ }
8121
+ const siblingTask = sibling?.tasks.get(qualified.bareId);
8122
+ if (sibling && siblingTask) {
8123
+ const facts = siblingTask.issue !== null ? sibling.facts.get(siblingTask.id) : undefined;
8124
+ return {
8125
+ issue: siblingTask.issue,
8126
+ merged: facts?.prState === "merged",
8127
+ open: facts?.prState === "open",
8128
+ issueState: facts?.issueState ?? null,
8129
+ stateReason: facts?.stateReason ?? null,
8130
+ closedByActor: facts?.closedByActor ?? null,
8131
+ resolved: true
8132
+ };
8133
+ }
8134
+ }
8135
+ return {
8136
+ issue: null,
8137
+ merged: false,
8138
+ open: false,
8139
+ issueState: null,
8140
+ stateReason: null,
8141
+ closedByActor: null,
8142
+ resolved: false
8143
+ };
8144
+ }
8145
+
8146
+ // src/lib/brief-assembly.ts
8147
+ function git(args) {
8148
+ try {
8149
+ return execFileSync5("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
8150
+ } catch {
8151
+ return "";
8152
+ }
8153
+ }
8154
+ function expandGlob(glob) {
8155
+ const out = git(["ls-files", "--", glob]);
8156
+ return out.split(`
8157
+ `).map((s) => s.trim()).filter(Boolean);
8158
+ }
8159
+ var DRAFT_ISSUE_SENTINEL = Number.MAX_SAFE_INTEGER;
8160
+
8161
+ // src/lib/forge-write.ts
8162
+ var INFORMATIONAL_DISPATCH_BLOCKER_CLASSES = new Set([
8163
+ "depends-on-not-merged",
8164
+ "conflicts-with"
8165
+ ]);
8166
+
5931
8167
  // src/commands/tokens.ts
5932
8168
  var USAGE = [
5933
8169
  'Usage: vinaya tokens --phase "<task-id>: develop" --role Developer',
@@ -5948,7 +8184,7 @@ var USAGE = [
5948
8184
  ].join(`
5949
8185
  `);
5950
8186
 
5951
- // src/commands/pr-report.ts
8187
+ // src/lib/pr-report-engine.ts
5952
8188
  var BODY_READING_CHECK_NAMES = new Set(coreCheckRegistry().filter((spec) => {
5953
8189
  const decl = spec.env?.PR_BODY;
5954
8190
  return decl === true || typeof decl === "object" && decl !== null && "optional" in decl;
@@ -5986,23 +8222,24 @@ var TOKEN_ROW_REMEDY = [
5986
8222
  "and a line inside the block that does not start with `|` truncates the table for every row after it."
5987
8223
  ].join(`
5988
8224
  `);
5989
- var USAGE2 = "Usage: vinaya pr report [--write <body-file> | --push <pr>] [--phase <phase>] [--role <role>] " + "[--model <id>] [--transcript <path>]";
8225
+ // src/commands/pr-report.ts
8226
+ var USAGE2 = "Usage: vinaya pr report [--write <body-file> | --push <pr> [--body-file <path>]] [--phase <phase>] " + "[--role <role>] [--model <id>] [--transcript <path>]";
5990
8227
 
5991
8228
  // src/lib/patch-id.ts
5992
- import { execFileSync as execFileSync3 } from "node:child_process";
8229
+ import { execFileSync as execFileSync6 } from "node:child_process";
5993
8230
  function patchIdAt(base, sha) {
5994
8231
  try {
5995
- execFileSync3("git", ["fetch", "--quiet", "origin", sha], { stdio: ["ignore", "ignore", "ignore"] });
8232
+ execFileSync6("git", ["fetch", "--quiet", "origin", sha], { stdio: ["ignore", "ignore", "ignore"] });
5996
8233
  } catch {}
5997
8234
  try {
5998
- const diff = execFileSync3("git", ["diff", `origin/${base}...${sha}`], {
8235
+ const diff = execFileSync6("git", ["diff", `origin/${base}...${sha}`], {
5999
8236
  encoding: "utf8",
6000
8237
  maxBuffer: 64 * 1024 * 1024,
6001
8238
  stdio: ["ignore", "pipe", "pipe"]
6002
8239
  });
6003
8240
  if (diff === "")
6004
8241
  return null;
6005
- const out = execFileSync3("git", ["patch-id", "--stable"], {
8242
+ const out = execFileSync6("git", ["patch-id", "--stable"], {
6006
8243
  input: diff,
6007
8244
  encoding: "utf8",
6008
8245
  stdio: ["pipe", "pipe", "pipe"]
@@ -6017,8 +8254,21 @@ function patchIdAt(base, sha) {
6017
8254
  // src/checks/evidence-fresh-logic.ts
6018
8255
  var HEAD_LINE = /^Head:\s*([0-9a-f]{7,40})\s*$/m;
6019
8256
  var FENCE = /```[^\n]*\n?([\s\S]*?)```/g;
6020
- function compareEvidenceBlock(resolved, resolvedHead, actualNumstat, expectedGroupCCommandLines, patchIdOf) {
8257
+ var EVIDENCE_PLACEHOLDER_TEXT = "[run `vinaya pr report --write` to populate — do not type this block by hand]";
8258
+ function compareEvidenceBlock(resolved, resolvedHead, actualNumstat, expectedGroupCCommandLines, patchIdOf, hasPriorDeveloperRound) {
6021
8259
  const region = resolved.region;
8260
+ const isPlaceholder = region.trim() === EVIDENCE_PLACEHOLDER_TEXT;
8261
+ if (isPlaceholder) {
8262
+ if (hasPriorDeveloperRound !== true) {
8263
+ return { status: "pass" };
8264
+ }
8265
+ return {
8266
+ status: "fail",
8267
+ errors: [
8268
+ "evidence-fresh: the AEG:EVIDENCE block reads as the untouched placeholder, but this PR has already had at least one developer round — a real report should exist for this head. Re-run `vinaya pr report --write` to regenerate it; re-pasting the placeholder over a real block never passes."
8269
+ ]
8270
+ };
8271
+ }
6022
8272
  const headMatch = region.match(HEAD_LINE);
6023
8273
  const fences = [...region.matchAll(FENCE)].map((m) => (m[1] ?? "").trim());
6024
8274
  if (!headMatch || fences.length < 1) {
@@ -6098,47 +8348,61 @@ function compareEvidenceBlock(resolved, resolvedHead, actualNumstat, expectedGro
6098
8348
  }
6099
8349
 
6100
8350
  // src/checks/bin/check-evidence-fresh.ts
8351
+ var DEVELOPER_ROUND_MARKER = /<!--\s*aeg:developer:round-\d+\s*-->/i;
8352
+ function fetchHasPriorDeveloperRound(prNumber) {
8353
+ try {
8354
+ const out = execFileSync7("gh", ["pr", "view", String(prNumber), "--json", "comments"], {
8355
+ encoding: "utf8",
8356
+ stdio: ["ignore", "pipe", "pipe"]
8357
+ });
8358
+ const parsed = JSON.parse(out);
8359
+ const allowlist = resolvePrincipalAllowlist(loadTrustAnchorConfig());
8360
+ return (parsed.comments ?? []).some((c) => DEVELOPER_ROUND_MARKER.test(c.body ?? "") && isPrincipal(c.author?.login ?? null, allowlist));
8361
+ } catch {
8362
+ return true;
8363
+ }
8364
+ }
6101
8365
  var CHECK_NAME = "evidence-fresh";
6102
- function git(args) {
8366
+ function git2(args) {
6103
8367
  try {
6104
- return execFileSync4("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
8368
+ return execFileSync7("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
6105
8369
  } catch {
6106
8370
  return "";
6107
8371
  }
6108
8372
  }
6109
8373
 
6110
- class UnresolvableMergeBaseError extends Error {
8374
+ class UnresolvableMergeBaseError2 extends Error {
6111
8375
  constructor(triedRefs) {
6112
8376
  super(`could not resolve a merge-base against any of: ${triedRefs.join(", ")}.`);
6113
8377
  }
6114
8378
  }
6115
8379
 
6116
- class GitCommandError extends Error {
8380
+ class GitCommandError2 extends Error {
6117
8381
  constructor(args, cause) {
6118
8382
  super(`\`git ${args.join(" ")}\` failed: ${cause}`);
6119
8383
  }
6120
8384
  }
6121
8385
  function gitStrict(args) {
6122
8386
  try {
6123
- return execFileSync4("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
8387
+ return execFileSync7("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
6124
8388
  } catch (err) {
6125
8389
  const stderr = err.stderr;
6126
- throw new GitCommandError(args, String(stderr ?? err.message).trim() || "non-zero exit");
8390
+ throw new GitCommandError2(args, String(stderr ?? err.message).trim() || "non-zero exit");
6127
8391
  }
6128
8392
  }
6129
8393
  function resolveMergeBase(head) {
6130
8394
  const primary = process.env.BASE_SHA || "origin/main";
6131
8395
  const tried = primary === "main" ? [primary] : [primary, "main"];
6132
8396
  for (const ref of tried) {
6133
- const base = git(["merge-base", ref, head]);
8397
+ const base = git2(["merge-base", ref, head]);
6134
8398
  if (base)
6135
8399
  return base;
6136
8400
  }
6137
- throw new UnresolvableMergeBaseError(tried);
8401
+ throw new UnresolvableMergeBaseError2(tried);
6138
8402
  }
6139
8403
  function fetchPrRefs(prNumber) {
6140
8404
  try {
6141
- const out = execFileSync4("gh", ["pr", "view", String(prNumber), "--json", "headRefOid,baseRefName"], {
8405
+ const out = execFileSync7("gh", ["pr", "view", String(prNumber), "--json", "headRefOid,baseRefName"], {
6142
8406
  encoding: "utf8",
6143
8407
  stdio: ["ignore", "pipe", "pipe"]
6144
8408
  });
@@ -6202,7 +8466,9 @@ function main() {
6202
8466
  }
6203
8467
  const expectedGroupCCommandLines = extractAgentCommandLines(body).map(agentCommandText);
6204
8468
  const patchIdOf = (sha) => patchIdAt(prRefs.base, sha);
6205
- const result = compareEvidenceBlock(resolved, resolvedHead, actualNumstat, expectedGroupCCommandLines, patchIdOf);
8469
+ const isPlaceholder = resolved.region.trim() === EVIDENCE_PLACEHOLDER_TEXT;
8470
+ const hasPriorDeveloperRound = isPlaceholder ? fetchHasPriorDeveloperRound(Number(prNumberStr)) : undefined;
8471
+ const result = compareEvidenceBlock(resolved, resolvedHead, actualNumstat, expectedGroupCCommandLines, patchIdOf, hasPriorDeveloperRound);
6206
8472
  if (result.status === "fail") {
6207
8473
  for (const message of result.errors) {
6208
8474
  emitCheckError({