@attalabs/vinaya 0.22.0 → 0.24.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 (194) hide show
  1. package/README.md +14 -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 +559 -140
  23. package/dist/checks/bin/check-branch-topology.js +630 -145
  24. package/dist/checks/bin/check-brief-shape.js +600 -143
  25. package/dist/checks/bin/check-changeset-coverage.js +942 -162
  26. package/dist/checks/bin/check-closes-n.js +630 -145
  27. package/dist/checks/bin/check-coherence.js +641 -148
  28. package/dist/checks/bin/check-dead-branch-push.js +559 -140
  29. package/dist/checks/bin/check-dispatch-readiness.js +639 -146
  30. package/dist/checks/bin/check-doc-coverage-push.js +696 -219
  31. package/dist/checks/bin/check-doc-coverage.js +696 -219
  32. package/dist/checks/bin/check-doctrine-no-procedures.js +4800 -0
  33. package/dist/checks/bin/check-doctrine-portability.js +691 -147
  34. package/dist/checks/bin/check-evidence-fresh.js +1048 -222
  35. package/dist/checks/bin/check-exec-bits.js +4937 -0
  36. package/dist/checks/bin/check-first-push-dispatch.js +630 -145
  37. package/dist/checks/bin/check-issue-assignment.js +630 -145
  38. package/dist/checks/bin/check-main-branch-refusal.js +565 -141
  39. package/dist/checks/bin/check-no-disk-state.js +559 -140
  40. package/dist/checks/bin/check-pr-report-density.js +4467 -0
  41. package/dist/checks/bin/check-quoted-command.js +656 -164
  42. package/dist/checks/bin/check-reader-resolvable-prose.js +652 -163
  43. package/dist/checks/bin/check-registry-gates.js +602 -145
  44. package/dist/checks/bin/check-retired-vocabulary.js +652 -163
  45. package/dist/checks/bin/check-review-gate.js +677 -142
  46. package/dist/checks/bin/check-single-plan-pr.js +559 -140
  47. package/dist/checks/bin/check-test-plan.js +560 -141
  48. package/dist/checks/bin/check-token-collection-wired.js +559 -140
  49. package/dist/checks/bin/check-token-report.js +559 -140
  50. package/dist/checks/bin/check-workspace-escape.js +992 -146
  51. package/dist/index.js +8884 -5627
  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 +118 -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 +198 -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-render.ts +462 -0
  60. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/brief-validation.ts +912 -0
  61. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/claude-code-transcript.ts +460 -0
  62. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/coherence-checks.ts +806 -0
  63. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/consumer-enumeration.ts +75 -0
  64. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dead-branch-push-audit.ts +55 -0
  65. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dead-branch-push-guard.ts +77 -0
  66. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/derive-section7.ts +66 -0
  67. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/derive-tranche.ts +94 -0
  68. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/diagram-model.ts +372 -0
  69. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/direct-main-push.ts +39 -0
  70. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dispatch-gate.ts +268 -0
  71. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doc-owners.ts +314 -0
  72. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/build-doc-nav.ts +38 -0
  73. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/docs-coherence.ts +136 -0
  74. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/index.ts +29 -0
  75. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/legacy-anchors.ts +48 -0
  76. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/nav-helpers.ts +17 -0
  77. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/node-route.ts +134 -0
  78. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/parse-doc.ts +43 -0
  79. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/published-prose.ts +306 -0
  80. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/surfaced-manifest.ts +89 -0
  81. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/types.ts +33 -0
  82. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-no-procedures.ts +94 -0
  83. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-portability.ts +293 -0
  84. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-source.ts +23 -0
  85. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/ensure-label.ts +28 -0
  86. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/file-classify.ts +51 -0
  87. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/first-push-dispatch-gate.ts +80 -0
  88. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/gate-audience.ts +171 -0
  89. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/index.ts +323 -0
  90. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/issue-assignment.ts +102 -0
  91. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/issue-validation.ts +746 -0
  92. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/leftover-detection.ts +68 -0
  93. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/local-anchor-coverage.ts +245 -0
  94. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/main-branch-refusal.ts +64 -0
  95. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/manifest-validity.ts +81 -0
  96. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/markdown-table.ts +76 -0
  97. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/metering-io-guard.ts +119 -0
  98. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/milestone-validation.ts +240 -0
  99. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/no-disk-state.ts +50 -0
  100. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-ledger.ts +77 -0
  101. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-registry.ts +105 -0
  102. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-token-report.ts +177 -0
  103. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-tranche.ts +183 -0
  104. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/pr-report-density.ts +116 -0
  105. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/pr-tier.ts +81 -0
  106. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/premise-check.ts +160 -0
  107. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/quoted-command.ts +275 -0
  108. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/reader-resolvable-prose.ts +320 -0
  109. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-checks.ts +255 -0
  110. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-parse.ts +164 -0
  111. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-scaffold.ts +216 -0
  112. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/report-tokens.ts +261 -0
  113. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/retired-vocabulary.ts +163 -0
  114. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/review-gate.ts +378 -0
  115. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/review-status.ts +200 -0
  116. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/single-plan-pr.ts +73 -0
  117. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/state-machine-model.ts +269 -0
  118. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/state-source.ts +14 -0
  119. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/status-block.ts +8 -0
  120. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/sum-ledger.ts +26 -0
  121. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/symbol-collisions.ts +78 -0
  122. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-gate.ts +117 -0
  123. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-section.ts +65 -0
  124. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/types.ts +174 -0
  125. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/verdict-extraction.ts +199 -0
  126. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/vocabulary-citation.ts +114 -0
  127. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/waiver-label.ts +52 -0
  128. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/workspace-escape.ts +172 -0
  129. package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
  130. package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
  131. package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
  132. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
  133. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
  134. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  135. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  136. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  137. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  138. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  139. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
  140. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  141. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  142. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
  143. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
  144. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
  145. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
  146. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
  147. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
  148. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
  149. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
  150. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
  151. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
  152. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
  153. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
  154. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
  155. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
  156. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
  157. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
  158. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
  159. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
  160. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
  161. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
  162. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
  163. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
  164. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
  165. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
  166. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
  167. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/[root-of-the-server]__02a-3g8._.js +1 -1
  168. 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
  169. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0053k9k._.js +1 -1
  170. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0112h-k._.js +1 -1
  171. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0o771t1._.js +1 -1
  172. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0puovz5._.js +1 -1
  173. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1hs0dcu._.js +1 -1
  174. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__069-_41._.js → [root-of-the-server]__1pndh3_._.js} +1 -1
  175. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1wc4-ip._.js +1 -1
  176. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_03x_w6q._.js +1 -1
  177. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0gvm3og._.js +1 -1
  178. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0lwxg63._.js +1 -1
  179. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +2 -2
  180. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1n0cnq-._.js +1 -1
  181. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_app_studio_projects_[name]_tranches_[slug]_05ufo8i._.js +3 -3
  182. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
  183. package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
  184. package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
  185. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
  186. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
  187. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{37hnluhzj9bei.js → 0zebsmmk1bvnb.js} +1 -1
  188. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{267o-tsrnuwnv.js → 1uogpj5w2n5ju.js} +1 -1
  189. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1hufpuody6vjv.js → 3m1kgax7j2vgs.js} +1 -1
  190. package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
  191. /package/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/{aeg-core → vinaya/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/aeg-core}/bin/verify-coherence.ts +0 -0
  192. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → QY2GhiI47-765D9DHGy1I}/_buildManifest.js +0 -0
  193. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → QY2GhiI47-765D9DHGy1I}/_clientMiddlewareManifest.js +0 -0
  194. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → QY2GhiI47-765D9DHGy1I}/_ssgManifest.js +0 -0
@@ -0,0 +1,462 @@
1
+ /**
2
+ * Task-brief renderer (review-convergence-v1 task 12, #387). Pure — no `fs`,
3
+ * no `gh`/`git`, no `process.env`. Reads `aeg-root/templates/brief-template.md`
4
+ * (passed in as `template`, read at run time by the CLI shim
5
+ * `apps/cli/src/commands/brief.ts`) and every derivable fact (`BriefFacts`,
6
+ * assembled by that same shim from the forge and the tree) and emits the
7
+ * twelve-section brief skeleton with every mechanically-derivable section
8
+ * filled. A section this module cannot derive from a stated fact is never
9
+ * defaulted — `renderBrief` refuses, naming the missing fact, per Issue #387's
10
+ * rule: "no section is written by hand; the rationale is the one hand-written
11
+ * artefact and the Issue creation gate is its review."
12
+ *
13
+ * Every judgment section (Technical dependencies' free-form detail beyond the
14
+ * Dependency-rationale field, the Pre-flight task-specific checks, the
15
+ * Verification command list beyond the fixed full run) is out of this task's
16
+ * scope — Issue #387: "NOT this task: rendering any judgment section." Those
17
+ * sections are rendered with the mechanical content this module CAN derive;
18
+ * anything genuinely judgment-only is left for the Brief Author to add by
19
+ * hand after render.
20
+ */
21
+
22
+ import { deriveSection7 } from './derive-section7'
23
+ import { isDocFile } from './file-classify'
24
+ import { deriveTierFromDiff } from './pr-tier'
25
+
26
+ export type RationaleFieldKey =
27
+ | 'boundary'
28
+ | 'dependencyRationale'
29
+ | 'trapsToAvoid'
30
+ | 'suggestedAgentClass'
31
+ | 'stopAndEscalate'
32
+
33
+ /**
34
+ * Tolerant label patterns for the five rationale fields this renderer
35
+ * consumes — the same patterns `issue-validation.ts`'s `checkIssueRationale`
36
+ * uses for presence, so a field this renderer can read is exactly a field
37
+ * that gate already required present. Not imported directly:
38
+ * `issue-validation.ts` keeps its own copy private (`RATIONALE_FIELDS`), and
39
+ * duplicating the five patterns here is cheaper than exporting a new surface
40
+ * from a file outside this task's boundary.
41
+ */
42
+ const RATIONALE_FIELD_PATTERNS: Record<RationaleFieldKey, string> = {
43
+ boundary: 'Boundary',
44
+ dependencyRationale: 'Dependency rationale|Depends[- ]on',
45
+ trapsToAvoid: 'Traps',
46
+ suggestedAgentClass: '(?:Suggested\\s+)?agent-class',
47
+ stopAndEscalate: 'Stop-and-escalate'
48
+ }
49
+
50
+ const RATIONALE_FIELD_NAMES: Record<RationaleFieldKey, string> = {
51
+ boundary: 'Boundary',
52
+ dependencyRationale: 'Dependency rationale',
53
+ trapsToAvoid: 'Traps to avoid',
54
+ suggestedAgentClass: 'Suggested agent-class',
55
+ stopAndEscalate: 'Stop-and-escalate'
56
+ }
57
+
58
+ /**
59
+ * Slices one rationale field's prose out of an Issue body: from its label to
60
+ * the next bold/heading field, or the end. Adapted from `issue-validation.ts`'s
61
+ * private `rationaleFieldText` (see that function's doc comment for why the
62
+ * terminator is `(?![\s\S])`, not `$`, and why the label pattern must stay
63
+ * grouped) — duplicated rather than imported because that helper is not
64
+ * exported and `issue-validation.ts` is out of this task's surface.
65
+ *
66
+ * Anchors the label's own `**`/heading marker to the START of a line
67
+ * (`^\s*`), unlike the copied original — a presence-only check can tolerate
68
+ * matching a field name mentioned in passing mid-paragraph (the field is
69
+ * still present somewhere), but an EXTRACTION cannot: found live, self-
70
+ * rendering this very task's own Issue (#387), whose "Boundary" field
71
+ * prose contains the inline aside "`For:`/`Reason:` from
72
+ * **Suggested agent-class**" — the unanchored original matched that inline
73
+ * mention first (it is merely preceded by literal `**` characters
74
+ * somewhere in the text, never required to start a line) and returned the
75
+ * Boundary field's own tail as the "Suggested agent-class" field's content.
76
+ */
77
+ function sliceRationaleField(text: string, labelPattern: string): string {
78
+ const re = new RegExp(
79
+ `^\\s*(?:\\*\\*|#{1,4}\\s+)\\s*(?:${labelPattern})[^\\n]*\\n?([\\s\\S]*?)(?=\\n\\s*(?:\\*\\*[A-Z]|#{1,4}\\s)|(?![\\s\\S]))`,
80
+ 'im'
81
+ )
82
+ const m = re.exec(text)
83
+ return m ? m[0].trim() : ''
84
+ }
85
+
86
+ /**
87
+ * Reads the five rationale fields this renderer needs out of a task Issue's
88
+ * body, tolerant of both live serializations `checkIssueRationale` accepts
89
+ * (`**Field** — …` bold-inline, `### Field` heading). A field absent from the
90
+ * body is simply absent from the returned record — never an empty string —
91
+ * so `renderBrief`'s missing-fact check can tell "absent" from "present but
92
+ * empty".
93
+ */
94
+ export function parseRationaleFields(body: string): Partial<Record<RationaleFieldKey, string>> {
95
+ const out: Partial<Record<RationaleFieldKey, string>> = {}
96
+ for (const key of Object.keys(RATIONALE_FIELD_PATTERNS) as RationaleFieldKey[]) {
97
+ const text = sliceRationaleField(body, RATIONALE_FIELD_PATTERNS[key])
98
+ if (text) out[key] = text
99
+ }
100
+ return out
101
+ }
102
+
103
+ /** One §4 surface-map file: its path, a whole-file `sha256` premise pin, and the workspace package that owns it (`null` for a file outside every workspace member, e.g. a doctrine file under `aeg-root/`). */
104
+ export type SurfaceFileFact = {
105
+ path: string
106
+ /** `null` for a file that does not exist yet on disk (a "Create" entry) — a not-yet-created file has no current sha256 to pin. */
107
+ sha256: string | null
108
+ /** The owning workspace package's own `name` field (e.g. `@attalabs/aeg-core`) — never a directory path; this is what feeds `bunx turbo test --filter=<packageName>` directly. */
109
+ packageName: string | null
110
+ }
111
+
112
+ export type BriefFacts = {
113
+ trancheSlug: string
114
+ taskId: string
115
+ title: string
116
+ issue: number
117
+ projects: string[]
118
+ dependsOn: string[]
119
+ conflictsWith: string[]
120
+ rationale: Partial<Record<RationaleFieldKey, string>>
121
+ dispatchReady: boolean
122
+ dispatchBlockers: string[]
123
+ surfaceFiles: SurfaceFileFact[]
124
+ /** Workspace package directories (e.g. `apps/cli`) that depend on `@attalabs/<pkg>` — same shape `checkConsumerTests` consumes, injected rather than derived here (this module reads no `fs`). */
125
+ consumersOf: (pkg: string) => string[]
126
+ /** `.vinaya/doc-owners` file content, or `null` when the file is absent — passed through to `deriveSection7` unchanged. */
127
+ docOwnersContent: string | null
128
+ }
129
+
130
+ export type RenderResult = { ok: true; brief: string } | { ok: false; missing: string[] }
131
+
132
+ function bulletList(items: string[]): string {
133
+ return items.map((i) => `- ${i}`).join('\n')
134
+ }
135
+
136
+ function isTestFile(path: string): boolean {
137
+ return /\.test\.[jt]sx?$/i.test(path)
138
+ }
139
+
140
+ const PRINCIPAL_OBSERVATION_RE = /\bprincipal\b[^.\n]*\b(?:browser|signed-in|visual|session)\b/i
141
+
142
+ function renderHeader(facts: BriefFacts, template: string): string {
143
+ const introMatch = template.match(/^You are the AEG Developer\.[^\n]*$/m)
144
+ const intro = introMatch
145
+ ? (introMatch[0] as string)
146
+ .replace(/\s*\[[^\]]*\]/g, '')
147
+ .replace(/\bBoth mandatory\.$/, 'Mandatory.')
148
+ .trim()
149
+ : 'You are the AEG Developer. Read `aeg-root/roles/developer.md` first. Mandatory.'
150
+
151
+ const reason = facts.rationale.suggestedAgentClass ?? ''
152
+ // `deriveTierFromDiff` never returns `3` — Tier 3 stays a judgment call the
153
+ // Brief Author raises by hand; the mechanical floor this renderer can prove
154
+ // is 0 or 1, exactly the two values that check itself is capable of ruling
155
+ // out for a doc/spec-touching surface.
156
+ const tier = deriveTierFromDiff(facts.surfaceFiles.map((f) => f.path))
157
+ const lines = [
158
+ '**For:** [model] (coding-agent CLI on a dev machine, dispatched locally, unattended)',
159
+ `**Reason:** ${reason}`,
160
+ '**Owner:** the Principal',
161
+ `**Goal:** ${facts.title}`,
162
+ `**Project:** ${facts.projects.join(', ')}`,
163
+ `**Tier:** ${tier}`,
164
+ '',
165
+ `Closes #${facts.issue}`,
166
+ '',
167
+ intro
168
+ ]
169
+ return lines.join('\n')
170
+ }
171
+
172
+ function renderSection2(facts: BriefFacts): string {
173
+ const depends = facts.dependsOn.length > 0 ? facts.dependsOn.join(', ') : '—'
174
+ const conflicts = facts.conflictsWith.length > 0 ? facts.conflictsWith.join(', ') : '—'
175
+ const lines = [
176
+ '## 2. Context — read before doing anything',
177
+ '',
178
+ `- **Tranche:** \`${facts.trancheSlug}\`, task ${facts.taskId}, Issue #${facts.issue}. Branch \`task/${facts.trancheSlug}/${facts.taskId}\`. \`Depends-on: ${depends}\`, \`Conflicts-with: ${conflicts}\`. Confirm \`READY TO DISPATCH\` at your own Step 0.`,
179
+ `- **Read Issue #${facts.issue} in full** for the complete rationale — do not re-derive it.`,
180
+ `- ${facts.rationale.boundary}`,
181
+ `- ${facts.rationale.trapsToAvoid}`
182
+ ]
183
+ return lines.join('\n')
184
+ }
185
+
186
+ function renderSection3(facts: BriefFacts): string {
187
+ return ['## 3. Technical dependencies', '', `${facts.rationale.dependencyRationale}`].join('\n')
188
+ }
189
+
190
+ function renderSection4(facts: BriefFacts): string {
191
+ const created = facts.surfaceFiles.filter((f) => f.sha256 === null).map((f) => f.path)
192
+ const modified = facts.surfaceFiles.filter((f) => f.sha256 !== null).map((f) => f.path)
193
+
194
+ const byPackage = new Map<string, SurfaceFileFact[]>()
195
+ for (const f of facts.surfaceFiles) {
196
+ if (!f.packageName) continue
197
+ const list = byPackage.get(f.packageName) ?? []
198
+ list.push(f)
199
+ byPackage.set(f.packageName, list)
200
+ }
201
+
202
+ // checkConsumerTests (brief-validation.ts) requires, for every workspace
203
+ // package a touched `packages/<pkg>` consumer depends on, either a named
204
+ // test path under that consumer OR the `consumer-tests: none — <reason>`
205
+ // sentinel — ONE sentinel occurrence anywhere in §4 satisfies the whole
206
+ // section, so a single combined line covers every uncovered consumer.
207
+ const uncovered: string[] = []
208
+ for (const pkg of byPackage.keys()) {
209
+ const shortName = pkg.replace(/^@[^/]+\//, '')
210
+ for (const consumer of facts.consumersOf(shortName)) {
211
+ const covered = facts.surfaceFiles.some((f) => f.path.startsWith(`${consumer}/`) && isTestFile(f.path))
212
+ if (!covered) uncovered.push(`${pkg} (${consumer})`)
213
+ }
214
+ }
215
+ const consumerLines =
216
+ uncovered.length > 0
217
+ ? [
218
+ `- consumer-tests: none — no consumer test path named yet for ${uncovered.join(', ')}; name one before dispatch, or confirm no consumer-facing behavior changed.`
219
+ ]
220
+ : []
221
+
222
+ const lines = [
223
+ '## 4. Technical surface map',
224
+ '',
225
+ '**Create:**',
226
+ created.length > 0 ? bulletList(created) : '- (none — every surface file already exists)',
227
+ '',
228
+ '**Modify:**',
229
+ modified.length > 0 ? bulletList(modified) : '- (none named)',
230
+ ...(consumerLines.length > 0 ? ['', ...consumerLines] : []),
231
+ '',
232
+ '**Out of surface:** [named explicitly by the Brief Author — not mechanically derivable]',
233
+ '',
234
+ '#### Premise pins',
235
+ '',
236
+ '**Premise:**',
237
+ bulletList(
238
+ facts.surfaceFiles
239
+ .filter((f): f is SurfaceFileFact & { sha256: string } => f.sha256 !== null)
240
+ .map((f) => `${f.path} sha256: ${f.sha256}`)
241
+ )
242
+ ]
243
+ return lines.join('\n')
244
+ }
245
+
246
+ function renderSection5(facts: BriefFacts): string {
247
+ const lines = [
248
+ '## 5. Pre-flight checks',
249
+ '',
250
+ '**Step 0 (mandatory, verbatim):**',
251
+ '',
252
+ '```',
253
+ `git worktree add .worktrees/task/${facts.trancheSlug}/${facts.taskId} -b task/${facts.trancheSlug}/${facts.taskId} origin/main && cd .worktrees/task/${facts.trancheSlug}/${facts.taskId} && bun install --frozen-lockfile --silent`,
254
+ '```',
255
+ '',
256
+ '1. Clean status; parent `origin/main`; branch suffix literal-matches the task id.',
257
+ '2. `bun packages/aeg-core/bin/verify-dispatch.ts <tranche> <n>` → `READY TO DISPATCH` (re-derived at render time: it was).',
258
+ '',
259
+ 'On any failure: STOP and report.'
260
+ ]
261
+ return lines.join('\n')
262
+ }
263
+
264
+ function renderSection6(facts: BriefFacts): string {
265
+ const byPackage = new Map<string, SurfaceFileFact[]>()
266
+ const rootFiles: SurfaceFileFact[] = []
267
+ for (const f of facts.surfaceFiles) {
268
+ if (!f.packageName) {
269
+ rootFiles.push(f)
270
+ continue
271
+ }
272
+ const list = byPackage.get(f.packageName) ?? []
273
+ list.push(f)
274
+ byPackage.set(f.packageName, list)
275
+ }
276
+
277
+ const parts: string[] = []
278
+ let n = 1
279
+ for (const [pkg, files] of byPackage) {
280
+ parts.push(
281
+ [
282
+ `${n}. **Part ${n}:** ${facts.rationale.boundary}`,
283
+ '',
284
+ ' Files:',
285
+ ...files.map((f) => ` - ${f.path}`),
286
+ '',
287
+ ` Run \`bunx turbo test --affected\` before committing this Part; touches ${pkg}.`
288
+ ].join('\n')
289
+ )
290
+ n++
291
+ }
292
+ if (rootFiles.length > 0) {
293
+ parts.push(
294
+ [`${n}. **Part ${n}:** doctrine/root files.`, '', ' Files:', ...rootFiles.map((f) => ` - ${f.path}`)].join(
295
+ '\n'
296
+ )
297
+ )
298
+ }
299
+
300
+ return ['## 6. Numbered parts — commit after EACH part; push once, before opening the PR', '', ...parts].join('\n')
301
+ }
302
+
303
+ function renderSection7(section7Pointers: string[]): string {
304
+ const lines = ['## 7. Documentation-update list', '']
305
+ if (section7Pointers.length > 0) {
306
+ lines.push(bulletList(section7Pointers))
307
+ } else {
308
+ lines.push(
309
+ "`.vinaya/doc-owners` derivation matched zero bindings against this task's surface — no mechanically-derived doc updates. Confirm no additional doc artifact applies before treating this list as final."
310
+ )
311
+ }
312
+ return lines.join('\n')
313
+ }
314
+
315
+ function renderSection8(): string {
316
+ return [
317
+ '## 8. Verification before claiming done',
318
+ '',
319
+ // Never `rm -rf apps/cli/dist &&`-prefixed (Principal ruling, PR
320
+ // `open-1`): that removal is a Developer pre-flight step in doctrine,
321
+ // never a command a check or `pr report` executes — `evidence-fresh`
322
+ // re-running it under the twenty-six sibling checks CI had just built
323
+ // deleted their own `dist` out from under them.
324
+ '- `bunx turbo test --affected --force` (the command and its output are attested in the Evidence block).',
325
+ "- The full `bun run test` suite is CI's to run, on the one push — never run it locally.",
326
+ '- Every blast-radius consumer named in §4, re-verified by name.',
327
+ '- `roles/developer.md`\'s tier checklist genuinely satisfied, and `PR_BODY="$(cat <body-file>)" bun packages/aeg-core/bin/verify-docs.ts --pr` green.'
328
+ ].join('\n')
329
+ }
330
+
331
+ function renderSection9(facts: BriefFacts): string {
332
+ const runtimeFiles = facts.surfaceFiles.filter((f) => !isDocFile(f.path))
333
+ if (runtimeFiles.length === 0) {
334
+ // Deliberately unbolded: `locateTestPlanSection`'s heading-form slicer
335
+ // treats a bold `**Field:**`-shaped line as the START of the NEXT
336
+ // section (`NEXT_SECTION_RE`) — a bolded `**Test Plan:** unit-tests-only`
337
+ // line right under the `## 9. Test Plan` heading would cut the section
338
+ // boundary before its own content, leaving `checkTestPlan` nothing to
339
+ // find. Plain `Test Plan: unit-tests-only` still satisfies
340
+ // `TEST_PLAN_UNIT_TESTS_ONLY_RE` (both forms are equivalent to it).
341
+ return ['## 9. Test Plan', '', 'Test Plan: unit-tests-only'].join('\n')
342
+ }
343
+
344
+ const testFiles = runtimeFiles.filter((f) => isTestFile(f.path))
345
+ const commandLines =
346
+ testFiles.length > 0
347
+ ? testFiles.map((f) => `bun test ${f.path} → 0 fail`)
348
+ : ['bunx turbo test --affected --force → summary line ends "0 fail"']
349
+
350
+ const observationText = [
351
+ facts.rationale.boundary ?? '',
352
+ facts.rationale.trapsToAvoid ?? '',
353
+ facts.rationale.stopAndEscalate ?? ''
354
+ ].join('\n')
355
+ const principalLine = PRINCIPAL_OBSERVATION_RE.test(observationText)
356
+ ? [
357
+ '',
358
+ "- [ ] **[principal]** Verify the change described in this task's rationale in a real signed-in session/browser."
359
+ ]
360
+ : []
361
+
362
+ return ['## 9. Test Plan', '', '```', ...commandLines, '```', ...principalLine].join('\n')
363
+ }
364
+
365
+ function renderSection10(facts: BriefFacts): string {
366
+ return ['## 10. Stop conditions', '', `${facts.rationale.stopAndEscalate}`].join('\n')
367
+ }
368
+
369
+ function renderSection11(template: string, facts: BriefFacts): string {
370
+ const trapsText = facts.rationale.trapsToAvoid ?? ''
371
+ const doNotLines = trapsText
372
+ .split('\n')
373
+ .map((l) => l.trim())
374
+ .filter((l) => /^\(?\d+\)?[.)]?\s/.test(l) || l.startsWith('-'))
375
+ .map((l) => l.replace(/^\(?\d+\)?[.)]?\s*/, '').replace(/^-\s*/, ''))
376
+ .filter((l) => l.length > 0)
377
+ .map((l) => `- Do NOT ${l.charAt(0).toLowerCase()}${l.slice(1)}`)
378
+
379
+ const autonomyMatch = template.match(/> \*\*Autonomy:\*\*[\s\S]*?(?=\n\n## \d+\. Deliverable|\n\n## Deliverable|$)/)
380
+ const autonomy = autonomyMatch ? (autonomyMatch[0] as string).trim() : ''
381
+
382
+ return ['## 11. Constraints', '', ...doNotLines, '', autonomy].join('\n')
383
+ }
384
+
385
+ function renderSection12(facts: BriefFacts): string {
386
+ return [
387
+ '## 12. Deliverable',
388
+ '',
389
+ `- PR title (exact): \`[${facts.trancheSlug}] ${facts.taskId} — ${facts.title}\``,
390
+ '- Open the PR only via `bun apps/cli/src/index.ts pr create --body-file <path> --title "<title above>"`.',
391
+ "- PR body = the Developer's PR report (start from `aeg-root/templates/pr-report-template.md`), with this entire brief pasted as the reference copy inside a collapsed `<details>` block, and `Closes #" +
392
+ facts.issue +
393
+ '` at the top of the header block.',
394
+ '- Pre-open gate: tier checklist satisfied, and `PR_BODY="$(cat <body-file>)" bun packages/aeg-core/bin/verify-docs.ts --pr` green.',
395
+ '- Include `git diff main --stat` and a token report (if unavailable, state so).',
396
+ '- Then STOP. Review and Verification are separate invocations.'
397
+ ].join('\n')
398
+ }
399
+
400
+ /**
401
+ * Emits the twelve-section brief skeleton from `facts`, reading `template`
402
+ * (the raw content of `aeg-root/templates/brief-template.md`) only for the
403
+ * one fixed sentence (`renderHeader`'s intro line) and the standing autonomy
404
+ * clause (`renderSection11`) — every other byte is generated from `facts`,
405
+ * never copied from a hardcoded template string in this module.
406
+ *
407
+ * Refuses — `{ ok: false, missing }` — the moment a fact this renderer needs
408
+ * is absent: never a default, per Issue #387's rule that a brief states no
409
+ * fact it did not actually derive.
410
+ */
411
+ export function renderBrief(facts: BriefFacts, template: string): RenderResult {
412
+ const missing: string[] = []
413
+
414
+ if (facts.projects.length === 0) missing.push('Project (task has no Project(s) declared)')
415
+ if (!facts.dispatchReady) missing.push(...facts.dispatchBlockers)
416
+
417
+ for (const key of Object.keys(RATIONALE_FIELD_PATTERNS) as RationaleFieldKey[]) {
418
+ if (!facts.rationale[key]) missing.push(RATIONALE_FIELD_NAMES[key])
419
+ }
420
+
421
+ if (missing.length > 0) return { ok: false, missing }
422
+
423
+ const section7Pointers = facts.docOwnersContent
424
+ ? deriveSection7Pointers(
425
+ facts.surfaceFiles.map((f) => f.path),
426
+ facts.docOwnersContent
427
+ )
428
+ : []
429
+
430
+ const brief = [
431
+ renderHeader(facts, template),
432
+ '',
433
+ renderSection2(facts),
434
+ '',
435
+ renderSection3(facts),
436
+ '',
437
+ renderSection4(facts),
438
+ '',
439
+ renderSection5(facts),
440
+ '',
441
+ renderSection6(facts),
442
+ '',
443
+ renderSection7(section7Pointers),
444
+ '',
445
+ renderSection8(),
446
+ '',
447
+ renderSection9(facts),
448
+ '',
449
+ renderSection10(facts),
450
+ '',
451
+ renderSection11(template, facts),
452
+ '',
453
+ renderSection12(facts)
454
+ ].join('\n')
455
+
456
+ return { ok: true, brief }
457
+ }
458
+
459
+ /** `deriveSection7`'s `pointers` alone — the only part of its result `renderSection7` needs. */
460
+ function deriveSection7Pointers(intendedSurfaces: string[], docOwnersContent: string): string[] {
461
+ return deriveSection7(intendedSurfaces, docOwnersContent).pointers
462
+ }