@attalabs/vinaya 0.22.0 → 0.23.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 (188) hide show
  1. package/README.md +13 -3
  2. package/aeg-root/contracts/archivist-tranche-archivist.md +2 -2
  3. package/aeg-root/contracts/brief-developer.md +1 -0
  4. package/aeg-root/contracts/developer-reviewer.md +3 -3
  5. package/aeg-root/contracts/reviewer-archivist.md +3 -3
  6. package/aeg-root/enforcement.md +26 -20
  7. package/aeg-root/process.md +8 -4
  8. package/aeg-root/roles/archivist.md +2 -2
  9. package/aeg-root/roles/brief-author.md +9 -3
  10. package/aeg-root/roles/developer.md +63 -31
  11. package/aeg-root/roles/planner.md +1 -1
  12. package/aeg-root/roles/principal.md +14 -0
  13. package/aeg-root/roles/reviewer.md +32 -15
  14. package/aeg-root/roles/security.md +15 -5
  15. package/aeg-root/skills/aeg/SKILL.md +9 -5
  16. package/aeg-root/skills/aeg-roles/SKILL.md +2 -2
  17. package/aeg-root/skills/brief-authoring/SKILL.md +39 -27
  18. package/aeg-root/state-machine.md +9 -11
  19. package/aeg-root/templates/brief-template.md +6 -6
  20. package/aeg-root/templates/pr-report-template.md +10 -7
  21. package/aeg-root/tranche-model.md +6 -2
  22. package/dist/checks/bin/check-body-bare-digits.js +552 -139
  23. package/dist/checks/bin/check-branch-topology.js +603 -144
  24. package/dist/checks/bin/check-brief-shape.js +593 -142
  25. package/dist/checks/bin/check-changeset-coverage.js +935 -161
  26. package/dist/checks/bin/check-closes-n.js +603 -144
  27. package/dist/checks/bin/check-coherence.js +614 -147
  28. package/dist/checks/bin/check-dead-branch-push.js +552 -139
  29. package/dist/checks/bin/check-dispatch-readiness.js +612 -145
  30. package/dist/checks/bin/check-doc-coverage-push.js +689 -218
  31. package/dist/checks/bin/check-doc-coverage.js +689 -218
  32. package/dist/checks/bin/check-doctrine-no-procedures.js +4794 -0
  33. package/dist/checks/bin/check-doctrine-portability.js +684 -146
  34. package/dist/checks/bin/check-evidence-fresh.js +994 -174
  35. package/dist/checks/bin/check-exec-bits.js +4931 -0
  36. package/dist/checks/bin/check-first-push-dispatch.js +603 -144
  37. package/dist/checks/bin/check-issue-assignment.js +603 -144
  38. package/dist/checks/bin/check-main-branch-refusal.js +552 -139
  39. package/dist/checks/bin/check-no-disk-state.js +552 -139
  40. package/dist/checks/bin/check-pr-report-density.js +4461 -0
  41. package/dist/checks/bin/check-quoted-command.js +621 -155
  42. package/dist/checks/bin/check-reader-resolvable-prose.js +617 -154
  43. package/dist/checks/bin/check-registry-gates.js +595 -144
  44. package/dist/checks/bin/check-retired-vocabulary.js +617 -154
  45. package/dist/checks/bin/check-review-gate.js +670 -141
  46. package/dist/checks/bin/check-single-plan-pr.js +552 -139
  47. package/dist/checks/bin/check-test-plan.js +553 -140
  48. package/dist/checks/bin/check-token-collection-wired.js +552 -139
  49. package/dist/checks/bin/check-token-report.js +552 -139
  50. package/dist/checks/bin/check-workspace-escape.js +985 -145
  51. package/dist/index.js +8500 -5518
  52. package/package.json +1 -1
  53. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/actions.ts +173 -0
  54. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/anchored-region.ts +113 -0
  55. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/archive-task.ts +206 -0
  56. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/baseline-capture.ts +65 -0
  57. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/blast-radius-domains.ts +192 -0
  58. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/branch-topology-gate.ts +85 -0
  59. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/brief-validation.ts +511 -0
  60. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/claude-code-transcript.ts +460 -0
  61. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/coherence-checks.ts +787 -0
  62. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dead-branch-push-audit.ts +55 -0
  63. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dead-branch-push-guard.ts +77 -0
  64. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/derive-section7.ts +66 -0
  65. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/derive-tranche.ts +94 -0
  66. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/diagram-model.ts +372 -0
  67. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/direct-main-push.ts +39 -0
  68. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dispatch-gate.ts +224 -0
  69. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doc-owners.ts +314 -0
  70. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/build-doc-nav.ts +38 -0
  71. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/docs-coherence.ts +136 -0
  72. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/index.ts +29 -0
  73. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/legacy-anchors.ts +48 -0
  74. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/nav-helpers.ts +17 -0
  75. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/node-route.ts +134 -0
  76. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/parse-doc.ts +43 -0
  77. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/published-prose.ts +306 -0
  78. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/surfaced-manifest.ts +89 -0
  79. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/types.ts +33 -0
  80. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-portability.ts +170 -0
  81. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-source.ts +23 -0
  82. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/ensure-label.ts +28 -0
  83. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/file-classify.ts +51 -0
  84. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/first-push-dispatch-gate.ts +80 -0
  85. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/gate-audience.ts +168 -0
  86. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/index.ts +301 -0
  87. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/issue-assignment.ts +102 -0
  88. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/issue-validation.ts +746 -0
  89. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/leftover-detection.ts +68 -0
  90. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/local-anchor-coverage.ts +245 -0
  91. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/main-branch-refusal.ts +64 -0
  92. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/manifest-validity.ts +81 -0
  93. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/markdown-table.ts +76 -0
  94. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/metering-io-guard.ts +119 -0
  95. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/milestone-validation.ts +240 -0
  96. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/no-disk-state.ts +50 -0
  97. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-ledger.ts +77 -0
  98. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-registry.ts +105 -0
  99. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-token-report.ts +165 -0
  100. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-tranche.ts +183 -0
  101. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/pr-tier.ts +81 -0
  102. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/premise-check.ts +125 -0
  103. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/quoted-command.ts +275 -0
  104. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/reader-resolvable-prose.ts +320 -0
  105. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-checks.ts +255 -0
  106. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-parse.ts +164 -0
  107. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-scaffold.ts +216 -0
  108. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/report-tokens.ts +261 -0
  109. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/retired-vocabulary.ts +163 -0
  110. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/review-gate.ts +269 -0
  111. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/single-plan-pr.ts +73 -0
  112. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/state-machine-model.ts +269 -0
  113. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/state-source.ts +14 -0
  114. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/status-block.ts +8 -0
  115. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/sum-ledger.ts +26 -0
  116. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/symbol-collisions.ts +78 -0
  117. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-gate.ts +104 -0
  118. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-section.ts +65 -0
  119. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/types.ts +174 -0
  120. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/verdict-extraction.ts +140 -0
  121. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/vocabulary-citation.ts +114 -0
  122. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/waiver-label.ts +52 -0
  123. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/workspace-escape.ts +172 -0
  124. package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
  125. package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
  126. package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
  127. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
  128. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
  129. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  130. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  131. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  132. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  133. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  134. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
  135. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  136. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  137. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
  138. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
  139. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
  140. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
  141. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
  142. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
  143. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
  144. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
  145. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
  146. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
  147. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
  148. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
  149. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
  150. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
  151. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
  152. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
  153. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
  154. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
  155. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
  156. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
  157. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
  158. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
  159. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
  160. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
  161. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
  162. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/[root-of-the-server]__02a-3g8._.js +1 -1
  163. 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
  164. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0053k9k._.js +1 -1
  165. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0112h-k._.js +1 -1
  166. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0o771t1._.js +1 -1
  167. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0puovz5._.js +1 -1
  168. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1hs0dcu._.js +1 -1
  169. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__069-_41._.js → [root-of-the-server]__1mcitst._.js} +1 -1
  170. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1wc4-ip._.js +1 -1
  171. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_03x_w6q._.js +1 -1
  172. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0gvm3og._.js +1 -1
  173. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0lwxg63._.js +1 -1
  174. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +1 -1
  175. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1n0cnq-._.js +1 -1
  176. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
  177. package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
  178. package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
  179. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
  180. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
  181. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{37hnluhzj9bei.js → 0jer_drkl1e4r.js} +4 -4
  182. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{267o-tsrnuwnv.js → 20x-t0a0v129z.js} +1 -1
  183. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1hufpuody6vjv.js → 2ou_81ahqw31e.js} +1 -1
  184. package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
  185. /package/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/{aeg-core → vinaya/studio-standalone/_node_modules/@attalabs/aeg-core}/bin/verify-coherence.ts +0 -0
  186. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → 5xy_GA7G5KRuE3HDumIiw}/_buildManifest.js +0 -0
  187. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → 5xy_GA7G5KRuE3HDumIiw}/_clientMiddlewareManifest.js +0 -0
  188. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → 5xy_GA7G5KRuE3HDumIiw}/_ssgManifest.js +0 -0
@@ -0,0 +1,511 @@
1
+ /**
2
+ * Brief→Developer brief-validation grammar (aeg-governance-hardening
3
+ * task 2). Pure — no `fs`, no `fetch`, no `process.env`. The CLI shim
4
+ * (`bin/verify-brief.ts`) reads `PR_BODY`, then
5
+ * calls `checkBriefSections`.
6
+ *
7
+ * Scope is presence-only (per the Planner's trap): this gate cannot judge
8
+ * whether a Test Plan item is truly scriptable, whether `unit-tests-only` is
9
+ * justified by the surface map, or whether a doc-update entry is factually
10
+ * correct — those remain Reviewer + Verification judgment. It only confirms each
11
+ * required brief section exists in the PR body, in the shape the
12
+ * `brief-authoring` skill and `brief-developer` contract define.
13
+ */
14
+
15
+ import { type AnchorField, anchoredRegion, stripCode } from './anchored-region'
16
+ import { parsePremiseBlock } from './premise-check'
17
+ import { locateTestPlanSection } from './test-plan-section'
18
+
19
+ export type BriefSectionResult = { status: 'pass' | 'fail'; errors: string[] }
20
+
21
+ /**
22
+ * The PR body's header block: everything before the first h2+ heading. The
23
+ * canonical PR-body form (`roles/developer.md`) puts the metadata fields
24
+ * (Tier, For, Project, Closes) at the top, before `## Summary`.
25
+ * Anchoring field extraction here — shared with `archive-task.ts` — is what
26
+ * stops prose in later sections that merely *mentions* a field name (e.g. a
27
+ * "Decisions made" paragraph discussing the `Ticket:` field) from being
28
+ * parsed as the field itself. Regression from #311's first live archivist
29
+ * run, where exactly that happened.
30
+ */
31
+ export function headerRegion(prBody: string): string {
32
+ const m = prBody.match(/^##\s/m)
33
+ return m?.index !== undefined ? prBody.slice(0, m.index) : prBody
34
+ }
35
+
36
+ /**
37
+ * Tolerant header-field reader: accepts `Field: value` and `**Field:** value`,
38
+ * line-anchored, searched only in the header region. Stops at line end or a
39
+ * `·` metadata separator.
40
+ *
41
+ * When `anchor` is given and the body carries that anchor pair
42
+ * (`anchored-region.ts`, task 30), the pair replaces the header region as the
43
+ * one place the field is read from — a field-shaped line anywhere else in the
44
+ * body is ignored. Bodies without the pair parse exactly as before.
45
+ */
46
+ function headerField(prBody: string, labelPattern: string, anchor?: AnchorField): string | null {
47
+ const anchored = anchor !== undefined ? anchoredRegion(prBody, anchor) : null
48
+ const region = anchored ?? headerRegion(prBody)
49
+ const re = new RegExp(`^(?:\\*\\*)?\\s*${labelPattern}\\s*(?:\\*\\*)?\\s*:\\s*(?:\\*\\*)?\\s*([^\\n·]+)`, 'im')
50
+ const m = region.match(re)
51
+ if (!m) return null
52
+ const value = (m[1] as string).trim()
53
+ return value.length > 0 ? value : null
54
+ }
55
+
56
+ /** Strips markdown emphasis markers and collapses whitespace, for tolerant phrase matching. */
57
+ function normalize(text: string): string {
58
+ return text.replace(/[*_]/g, '').replace(/\s+/g, ' ')
59
+ }
60
+
61
+ function headingCheck(prBody: string, keywordPattern: string, sectionName: string): BriefSectionResult {
62
+ const re = new RegExp(`^#{1,4}\\s*(?:\\*\\*)?(?:\\d+[a-z]?\\.\\s*)?[^\\n]*${keywordPattern}`, 'im')
63
+ if (re.test(prBody)) return { status: 'pass', errors: [] }
64
+ return {
65
+ status: 'fail',
66
+ errors: [`brief-validation ${sectionName}: no "${sectionName}" section found in the PR body.`]
67
+ }
68
+ }
69
+
70
+ /** Tier field — reuses the canonical `@attalabs/aeg-core` parser (do not re-implement). */
71
+ export function checkTierField(prBody: string, readTier: (body: string) => 0 | 1 | 3 | null): BriefSectionResult {
72
+ if (readTier(prBody) !== null) return { status: 'pass', errors: [] }
73
+ return {
74
+ status: 'fail',
75
+ errors: [
76
+ 'brief-validation tier: no `Tier:` field found in the PR body (expected `Tier: 0|1|3` or `**Tier:** 0|1|3`).'
77
+ ]
78
+ }
79
+ }
80
+
81
+ /**
82
+ * The `unit-tests-only` sentinel, tolerating the markdown emphasis every other
83
+ * brief field carries (`**Tier:**`, `**Project:**`, …). All three of
84
+ * `Test Plan: unit-tests-only`, `**Test Plan:** unit-tests-only` and
85
+ * `**Test Plan**: unit-tests-only` match.
86
+ *
87
+ * The bold forms used to fail: the pattern was `/Test Plan\s*:\s*unit-tests-only/i`,
88
+ * and `\s*` cannot cross the `**` sitting between the colon and the value — so
89
+ * `brief-authoring/SKILL.md` §9's own canonical example (`**Test Plan:** unit-tests-only`)
90
+ * was rejected by the gate that documents it, and any brief copying the skill
91
+ * verbatim bounced. Two checks read this sentinel and both were wrong in
92
+ * different directions: `checkTestPlan` false-FAILED a well-formed brief, while
93
+ * `checkTestPlanExclusivity` silently false-PASSED a self-contradictory one (a
94
+ * bolded `unit-tests-only` **plus** tagged checkboxes never tripped it, because
95
+ * its own guard clause never matched either) — the exact combination that guard
96
+ * was built for in #340. Fixing the doc instead of the pattern was rejected:
97
+ * bold is the house convention for brief fields, so an unbolded Test Plan is the
98
+ * odd one out and drifts back the moment someone tidies it.
99
+ */
100
+ const TEST_PLAN_UNIT_TESTS_ONLY_RE = /(?:\*\*)?Test Plan(?:\*\*)?\s*:\s*(?:\*\*)?\s*unit-tests-only/i
101
+
102
+ /**
103
+ * The Test Plan section text these three checks scan — never the raw body.
104
+ * Delegates to `locateTestPlanSection` (the same locator `test-plan.ts`'s
105
+ * merge-gate check uses) so an `AEG:TEST-PLAN` anchor pair is authoritative
106
+ * here exactly as it already is there. Without this, a body that anchors its
107
+ * real Test Plan AND carries a pasted verbatim reference copy of the brief
108
+ * (the template's own instruction) has its checkbox lines double-counted:
109
+ * `checkTestPlanExclusivity`/`checkPrincipalPlaceholder` scanned the whole
110
+ * body and fired on checkboxes living only in the quoted reference copy,
111
+ * disagreeing with `test-plan.ts` about which bytes are the real field
112
+ * (found live: a well-formed anchored brief failed `brief-shape` while
113
+ * `test-plan` passed the same body). Falls back to the full body when no
114
+ * section is locatable, matching every other anchor consumer's "anchors are
115
+ * additive, never required" contract.
116
+ */
117
+ function testPlanRegion(prBody: string): string {
118
+ const located = locateTestPlanSection(prBody)
119
+ return located.found ? located.section : prBody
120
+ }
121
+
122
+ /**
123
+ * Test Plan — pass iff the body contains the `Test Plan: unit-tests-only`
124
+ * sentinel (bolded or not — see `TEST_PLAN_UNIT_TESTS_ONLY_RE`), OR at least one
125
+ * `**[agent]**`/`**[principal]**`-tagged checklist line. Presence-only: does not
126
+ * judge whether tags are correctly assigned or whether `unit-tests-only` is
127
+ * justified by the surface map.
128
+ */
129
+ export function checkTestPlan(prBody: string): BriefSectionResult {
130
+ const region = testPlanRegion(prBody)
131
+ if (TEST_PLAN_UNIT_TESTS_ONLY_RE.test(region)) return { status: 'pass', errors: [] }
132
+ if (/\*\*\[(?:agent|principal)\]\*\*/.test(region)) return { status: 'pass', errors: [] }
133
+ return {
134
+ status: 'fail',
135
+ errors: [
136
+ 'brief-validation Test Plan: no Test Plan section found — expected `Test Plan: unit-tests-only`, or at least one `**[agent]**`/`**[principal]**`-tagged checklist item.'
137
+ ]
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Test Plan shape guard (aeg-governance-hardening task 20 follow-up, #340) —
143
+ * `Test Plan: unit-tests-only` and a tagged checkbox item are mutually
144
+ * exclusive per brief-authoring §9 ("The two fields are coupled; Brief
145
+ * Validation cross-checks them"): `unit-tests-only` declares there is no
146
+ * checklist because there is nothing runtime to check off, so a body
147
+ * carrying both is self-contradictory. Regression source: PR #363's own
148
+ * original brief declared `unit-tests-only` while its Test Plan also listed
149
+ * `- [x]`/`- [ ]` `[agent]`/`[principal]` items — a combination this gate
150
+ * previously let through.
151
+ */
152
+ export function checkTestPlanExclusivity(prBody: string): BriefSectionResult {
153
+ const region = testPlanRegion(prBody)
154
+ if (!TEST_PLAN_UNIT_TESTS_ONLY_RE.test(region)) return { status: 'pass', errors: [] }
155
+ if (/^-\s*\[[ xX]\]\s*\*{2}\[(?:agent|principal)\]\*{2}/im.test(region)) {
156
+ return {
157
+ status: 'fail',
158
+ errors: [
159
+ 'brief-validation Test Plan shape: `Test Plan: unit-tests-only` and a `- [ ]`/`- [x]` tagged checkbox item are mutually exclusive (brief-authoring §9) — declare one form, not both.'
160
+ ]
161
+ }
162
+ }
163
+ return { status: 'pass', errors: [] }
164
+ }
165
+
166
+ /**
167
+ * Principal-placeholder guard (aeg-governance-hardening task 20 follow-up,
168
+ * #340) — a `**[principal]**` checkbox item whose content is a
169
+ * none-placeholder ("None — …") is untickable by construction: nobody can
170
+ * check a box asserting there is nothing to verify, so it blocks the merge
171
+ * gate forever. If a brief genuinely has no principal-runnable surface, the
172
+ * item must be omitted entirely, not declared and left permanently unticked.
173
+ */
174
+ export function checkPrincipalPlaceholder(prBody: string): BriefSectionResult {
175
+ const region = testPlanRegion(prBody)
176
+ const lineRe = /^-\s*\[[ xX]\]\s*\*{2}\[principal\]\*{2}(.*)$/gim
177
+ for (const m of region.matchAll(lineRe)) {
178
+ const content = m[1] ?? ''
179
+ if (/^\s*None\b/i.test(content)) {
180
+ return {
181
+ status: 'fail',
182
+ errors: [
183
+ 'brief-validation Test Plan shape: a `**[principal]**` checkbox item is a "None" placeholder — if there is no principal-runnable surface, omit the item entirely; an untickable placeholder box blocks the merge gate forever.'
184
+ ]
185
+ }
186
+ }
187
+ }
188
+ return { status: 'pass', errors: [] }
189
+ }
190
+
191
+ /**
192
+ * Premise coverage (this task, aeg-governance-hardening 11, #324) — pass iff
193
+ * either (a) at least one `Premise:` assertion's path matches a file in
194
+ * `surfaceFiles` (the §4 surface map's file list — in practice, the PR's
195
+ * actual changed-file list, the same diff-derived-truth philosophy
196
+ * `deriveTierFromDiff` already uses), or (b) `surfaceFiles` is empty (a
197
+ * Tier 0 brief with zero runtime/code surface has nothing to pin — mirrors
198
+ * `checkTestPlan`'s `unit-tests-only` exemption). Presence-only: does not
199
+ * judge whether the pinned premise is the *right* one to have pinned.
200
+ */
201
+ export function checkPremiseCoverage(prBody: string, surfaceFiles: string[]): BriefSectionResult {
202
+ if (surfaceFiles.length === 0) return { status: 'pass', errors: [] }
203
+ const assertions = parsePremiseBlock(prBody)
204
+ if (assertions.some((a) => surfaceFiles.includes(a.path))) return { status: 'pass', errors: [] }
205
+ return {
206
+ status: 'fail',
207
+ errors: [
208
+ 'brief-validation Premise: no `Premise:` assertion found whose path matches a file in the surface map — a brief with a real code surface must pin at least one premise (aeg-governance-hardening task 11).'
209
+ ]
210
+ }
211
+ }
212
+
213
+ export function checkSurfaceMap(prBody: string): BriefSectionResult {
214
+ return headingCheck(prBody, '(?:technical\\s+)?surface map', 'Technical surface map')
215
+ }
216
+
217
+ export function checkDocUpdateList(prBody: string): BriefSectionResult {
218
+ return headingCheck(prBody, '(?:documentation|doc)[- ]update(?:\\s+list)?', 'Documentation-update list')
219
+ }
220
+
221
+ export function checkWorktreeStep0(prBody: string): BriefSectionResult {
222
+ if (/git worktree add/.test(prBody)) return { status: 'pass', errors: [] }
223
+ return {
224
+ status: 'fail',
225
+ errors: ['brief-validation worktree Step 0: no `git worktree add` command found in the PR body.']
226
+ }
227
+ }
228
+
229
+ export function checkStopConditions(prBody: string): BriefSectionResult {
230
+ return headingCheck(prBody, 'stop conditions', 'Stop conditions')
231
+ }
232
+
233
+ /**
234
+ * Autonomy clause — matches the standing clause's core phrase, case-insensitively,
235
+ * tolerant of whitespace and emphasis markup (`**Autonomy:**` vs `Autonomy:`).
236
+ * Per the Planner's trap, this is deliberately verbatim-ish rather than a loose
237
+ * "mentions autonomy somewhere" match — the phrase is the actual invariant.
238
+ */
239
+ export function checkAutonomyClause(prBody: string): BriefSectionResult {
240
+ const normalized = normalize(prBody).toLowerCase()
241
+ if (/do not stop to ask clarifying questions/.test(normalized)) return { status: 'pass', errors: [] }
242
+ return {
243
+ status: 'fail',
244
+ errors: [
245
+ 'brief-validation autonomy clause: the standing autonomy clause ("Do not stop to ask clarifying questions...") was not found in the PR body.'
246
+ ]
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Project field — required in a multi-project repo (`brief-authoring` skill,
252
+ * `planner-brief` contract: the field routes the Developer to the right specs
253
+ * and tells the Reviewer whose behavior to verify). Must appear in the header
254
+ * block, where the Archivist's provenance assembly also reads it — gate and
255
+ * archivist share `headerRegion`, so a body that passes this gate can never
256
+ * produce a DANGLING Project field in provenance. Added after #311 merged
257
+ * without it: the Developer satisfied exactly the sections this gate checked
258
+ * and dropped everything it didn't — whatever the gate doesn't enforce, agents
259
+ * will eventually omit (contract-gate parity is the fix, not discipline).
260
+ */
261
+ export function checkProjectField(prBody: string): BriefSectionResult {
262
+ if (headerField(prBody, 'Project(?:\\(s\\))?', 'PROJECT') !== null) return { status: 'pass', errors: [] }
263
+ return {
264
+ status: 'fail',
265
+ errors: [
266
+ 'brief-validation Project: no `Project:` field found in the PR body header block (before the first `##` heading). Required per the brief-developer contract — expected `Project: <name>[, <name>]` or `**Project:** …`.'
267
+ ]
268
+ }
269
+ }
270
+
271
+ /**
272
+ * For/model field — the brief's mandatory `For:` header (`brief-authoring`
273
+ * skill: "the `For:` + `Reason:` lines are mandatory"). AEG forbids
274
+ * commit-trailer attribution, so this line is the provenance block's only
275
+ * source for Model/agent — same headerRegion parity rationale as
276
+ * `checkProjectField`.
277
+ */
278
+ export function checkForField(prBody: string): BriefSectionResult {
279
+ if (headerField(prBody, 'For') !== null) return { status: 'pass', errors: [] }
280
+ return {
281
+ status: 'fail',
282
+ errors: [
283
+ 'brief-validation For: no `For:` field found in the PR body header block (before the first `##` heading). Required per the brief-authoring skill — expected `For: <model + environment>` or `**For:** …`.'
284
+ ]
285
+ }
286
+ }
287
+
288
+ /**
289
+ * `Closes #N` gate — the match runs on **code-stripped** body text (fenced
290
+ * blocks + inline spans removed via `stripCode`), so it agrees byte-for-byte
291
+ * with GitHub's own auto-close parser, which also ignores `Closes #N` inside
292
+ * code. Without the strip, a body whose only closing reference is backticked
293
+ * (`` `Closes #600` ``) passed this gate green yet merged **without** closing
294
+ * its Issue — stranding #600 (PR #608) and #601 (PR #611) and reddening every
295
+ * open PR via the A3 `auto-close-misfire` oracle. "verify-docs green" must
296
+ * imply "GitHub will auto-close"; stripping code here is what makes it so.
297
+ *
298
+ * The separator groups are **bounded** (`\s{0,8}`, not `\s*`). Two adjacent
299
+ * unbounded `\s*` around an optional `:` backtrack quadratically on a body of
300
+ * the shape `closes` + long whitespace + no `#` — ~2.65 s at GitHub's
301
+ * 65,536-char body cap, and this function runs the pattern twice on the fail
302
+ * path (PR #617 security pass). A constant bound makes the work per start
303
+ * position constant, so the scan is linear in body length. Eight is far past
304
+ * any real separator; a body needing more is malformed by the brief's own
305
+ * convention (a bare ref inside the `AEG:CLOSES` anchor) and fails with an
306
+ * actionable message rather than silently stranding its Issue.
307
+ */
308
+ export function checkClosesN(prBody: string): BriefSectionResult {
309
+ const closesPattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s{0,8}:?\s{0,8}#\d+/i
310
+ if (closesPattern.test(stripCode(prBody))) {
311
+ return { status: 'pass', errors: [] }
312
+ }
313
+ // Distinguish "only inside code" (actionable — move it out) from "absent entirely".
314
+ if (closesPattern.test(prBody)) {
315
+ return {
316
+ status: 'fail',
317
+ errors: [
318
+ "brief-validation Closes #N: `Closes #N` found only inside a code span — GitHub won't auto-close it. Put a bare `Closes #N` on its own line inside the `AEG:CLOSES` anchor."
319
+ ]
320
+ }
321
+ }
322
+ return {
323
+ status: 'fail',
324
+ errors: ['brief-validation Closes #N: no `Closes #<N>` (or Fixes/Resolves) reference found in the PR body.']
325
+ }
326
+ }
327
+
328
+ /**
329
+ * The commit-type vocabulary this repo enforces — the commitlint type set
330
+ * plus `Plan` (plan PRs; deliberately excluded from the separate
331
+ * `vinaya/type:*` label axis, since a commit type and a task type are
332
+ * different things — see `packages/aeg-forge-state/src/labels.ts`). The one
333
+ * list: `COMMIT_TYPE_STYLE` below is derived from it, and the `commit-msg`
334
+ * hook (`apps/cli/src/commands/commit-msg.ts`) reads `COMMIT_TYPES` for its
335
+ * own error text — neither carries a second, independently-typed copy.
336
+ */
337
+ export const COMMIT_TYPES = [
338
+ 'Build',
339
+ 'Chore',
340
+ 'Docs',
341
+ 'Feat',
342
+ 'Fix',
343
+ 'Perf',
344
+ 'Plan',
345
+ 'Refactor',
346
+ 'Revert',
347
+ 'Style',
348
+ 'Test'
349
+ ] as const
350
+
351
+ export const COMMIT_TYPE_STYLE = new RegExp(`^(${COMMIT_TYPES.join('|')})(\\([a-z0-9-]+\\))?: \\S`)
352
+
353
+ /**
354
+ * Forge-title grammar — the two title forms this repo actually uses:
355
+ * 1. Commit-style: `Type: description` or `Type(scope): description`, with
356
+ * the commitlint type set plus `Plan` (plan PRs).
357
+ * 2. Task-style: `[tranche] id — description` (task Issues and task PRs).
358
+ * Anything else is refused: titles ride into merge commits and the forge's
359
+ * derived views, so they carry the same grammar obligation as commit messages
360
+ * (husky/commitlint parity, applied at the wrapper).
361
+ */
362
+ export function checkForgeTitle(title: string): BriefSectionResult {
363
+ const taskStyle = /^\[[a-z0-9._-]+\] \S+ — \S/
364
+ if (COMMIT_TYPE_STYLE.test(title) || taskStyle.test(title)) return { status: 'pass', errors: [] }
365
+ return {
366
+ status: 'fail',
367
+ errors: [
368
+ `brief-validation title: "${title}" matches neither title grammar — expected \`Type: description\` / \`Type(scope): description\` (commitlint types + Plan) or \`[tranche] id — description\` (task form).`
369
+ ]
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Plan-PR Closes guard — a `plan/*` PR body must never carry a
375
+ * `Closes #N` reference. Three confirmed live incidents (#294→#293,
376
+ * #298→#297, #288→#287) show a plan PR's `Closes #N` prematurely closing
377
+ * the task Issue when the *plan* merged — before the task itself ever ran.
378
+ * `roles/planner.md`'s Plan-PR close-out section already forbids this in
379
+ * prose ("a plan PR creates Issues; it does not resolve one"); this is the
380
+ * mechanical gate. Separate from `checkBriefSections` (it needs the branch,
381
+ * not just the body, and it's a guard against a forbidden shape, not a
382
+ * presence check) — the shim runs it before the non-task-branch bypass, since
383
+ * a `plan/*` branch would otherwise never reach a brief-shape check at all.
384
+ * Plan branches without `Closes` continue to bypass, as today.
385
+ */
386
+ export function checkPlanPrNoCloses(branch: string, prBody: string): BriefSectionResult {
387
+ if (!branch.startsWith('plan/')) return { status: 'pass', errors: [] }
388
+ if (!/\bcloses\s+#\d+/i.test(prBody)) return { status: 'pass', errors: [] }
389
+ return {
390
+ status: 'fail',
391
+ errors: [
392
+ 'brief-validation plan-PR guard: a `plan/*` PR must not carry `Closes #N` — a plan PR creates Issues, it does not resolve one (roles/planner.md). Remove the `Closes #N` reference; the task Issue closes from the task PR that does the work.'
393
+ ]
394
+ }
395
+ }
396
+
397
+ /**
398
+ * The four markers that make a body *a brief* rather than an ordinary PR
399
+ * description. Each is an existing section detector, reused unchanged — so a
400
+ * body that trips this detector is, by construction, a body the section checks
401
+ * were written to grade.
402
+ *
403
+ * Test Plan is deliberately **not** a marker even though it is a required brief
404
+ * section: the canonical PR-body template gives every AEG PR a Test plan with
405
+ * `**[agent]**` tags, so it is the one section an ordinary non-brief PR
406
+ * plausibly carries. The four kept here are brief grammar and nothing else — a
407
+ * dependency bump has none of them, and needing *two* means one stray phrase
408
+ * can never force a brief onto a PR that was never meant to carry one.
409
+ */
410
+ const BRIEF_SHAPE_MARKERS = [checkSurfaceMap, checkDocUpdateList, checkStopConditions, checkAutonomyClause] as const
411
+
412
+ /**
413
+ * A task branch, per the topology naming convention (`task/<tranche>/<n>`).
414
+ * The one shared copy — `bin/verify-brief.ts`, `test-plan-gate.ts`, and
415
+ * `archive-task.ts` each still define this pattern locally (out of this
416
+ * export's blast radius; not deduped onto it here), but a new consumer
417
+ * (`check-brief-shape.ts`, #870) reuses this one rather than adding a
418
+ * fourth copy.
419
+ */
420
+ const TASK_BRANCH_PATTERN = /^task\/[^/]+\/[^/]+$/
421
+
422
+ export function isTaskBranch(branch: string): boolean {
423
+ return TASK_BRANCH_PATTERN.test(branch)
424
+ }
425
+
426
+ /**
427
+ * Is this PR body a brief? — the predicate that replaces the branch name as
428
+ * `verify-brief`'s trigger for running `checkBriefSections`.
429
+ *
430
+ * The old rule was "validate iff the branch is `task/<tranche>/<n>`", which meant a
431
+ * standalone `fix/*` brief bypassed **every** section check. Confirmed live: a
432
+ * fix brief on `fix/studio-tranche-href` shipped with no §7
433
+ * documentation-update list, and `checkDocUpdateList` — the checker that exists
434
+ * for exactly that — never ran, because the branch wasn't a task branch. The
435
+ * exemption still has to exist (an ordinary one-line dependency-bump PR has no
436
+ * brief and must not be forced to grow one), so the rule becomes: *if a body is
437
+ * a brief, it must be a complete brief, whatever the branch is called.*
438
+ *
439
+ * Detection runs on `stripCode(prBody)` (the single shared stripper from
440
+ * `anchored-region`, per #617's "one stripper, never a duplicated regex"). A PR
441
+ * that *quotes* a brief inside a fence — "here's a sample brief: ``` …Technical
442
+ * surface map… ```" — is discussing a brief, not carrying one, and must stay
443
+ * exempt; matching on raw text would force-validate it. Note this also rules out
444
+ * the worktree `git worktree add … -b` line as a marker: Step 0 lives inside a
445
+ * fence in every real brief, so it never survives the strip.
446
+ *
447
+ * Threshold is ≥2 of four rather than any-one so that a brief missing one
448
+ * section is still detected as a brief — the failure mode this gate exists to
449
+ * catch. The `fix/studio-tranche-href` body trips three with §7 absent.
450
+ */
451
+ export function isBriefShaped(prBody: string): boolean {
452
+ const stripped = stripCode(prBody)
453
+ return BRIEF_SHAPE_MARKERS.filter((check) => check(stripped).status === 'pass').length >= 2
454
+ }
455
+
456
+ /**
457
+ * The branch a brief declares for itself, read from its Step 0 command. At
458
+ * authoring time there is no `BRANCH` env var and no branch yet — but every
459
+ * brief carries `git worktree add <path> -b <branch> origin/main`, so the brief
460
+ * states what it is going to be, and `verify-brief --body-file` can grade it as
461
+ * that. Matched on the RAW body, not `stripCode`'s output: Step 0 lives inside a
462
+ * fence in every real brief, so the stripped text never contains it.
463
+ */
464
+ export function inferBranchFromBody(prBody: string): string {
465
+ const m = prBody.match(/git worktree add\s+\S+\s+-b\s+(\S+)/)
466
+ return m?.[1] ?? ''
467
+ }
468
+
469
+ /** Composition knobs for `checkBriefSections` — see each field. */
470
+ export type BriefSectionsOptions = {
471
+ /**
472
+ * Whether a `Closes #N` reference is required. Defaults to `true` (every
473
+ * existing caller keeps today's behavior).
474
+ *
475
+ * A task PR must close its Issue, so `verify-brief` leaves this on for
476
+ * `task/<tranche>/<n>`. A brief-shaped body on a non-task branch must not: a
477
+ * standalone fix brief has no task Issue to close, and a `plan/*` PR is
478
+ * *forbidden* to carry `Closes #N` by `checkPlanPrNoCloses` — so
479
+ * requiring it there would make the two gates jointly unsatisfiable. Issue
480
+ * linkage is a task-branch obligation; brief completeness is not.
481
+ */
482
+ requireClosesN?: boolean
483
+ }
484
+
485
+ /**
486
+ * Aggregates every section detector into one combined result — one error
487
+ * line per failing section, mirroring `doc-owners.ts`'s `parseDocOwners`
488
+ * error-message style.
489
+ */
490
+ export function checkBriefSections(
491
+ prBody: string,
492
+ readTier: (body: string) => 0 | 1 | 3 | null,
493
+ options: BriefSectionsOptions = {}
494
+ ): { errors: string[] } {
495
+ const { requireClosesN = true } = options
496
+ const results = [
497
+ checkTierField(prBody, readTier),
498
+ checkTestPlan(prBody),
499
+ checkTestPlanExclusivity(prBody),
500
+ checkPrincipalPlaceholder(prBody),
501
+ checkSurfaceMap(prBody),
502
+ checkDocUpdateList(prBody),
503
+ checkWorktreeStep0(prBody),
504
+ checkStopConditions(prBody),
505
+ checkAutonomyClause(prBody),
506
+ checkProjectField(prBody),
507
+ checkForField(prBody),
508
+ ...(requireClosesN ? [checkClosesN(prBody)] : [])
509
+ ]
510
+ return { errors: results.flatMap((r) => r.errors) }
511
+ }