@zuzuucodes/cli 1.4.0 → 1.6.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 (211) hide show
  1. package/bin/zuzuu.mjs +20 -4
  2. package/package.json +1 -1
  3. package/web-app/dist/auth.js +91 -0
  4. package/web-app/dist/server.js +16 -79
  5. package/web-app/dist/zuzuu-cli.js +124 -0
  6. package/web-app/dist/{zuzuu-api.js → zuzuu-routes.js} +158 -133
  7. package/web-app/web-dist/assets/CommandPalette-DhBdR7X3.js +45 -0
  8. package/web-app/web-dist/assets/DiffTab-CqxwSjI2.js +1 -0
  9. package/web-app/web-dist/assets/EditorPane-94QPFR9R.js +41 -0
  10. package/web-app/web-dist/assets/MonacoFile-D76epTrG.js +1 -0
  11. package/web-app/web-dist/assets/angular-html-BVBpGdXr.js +1 -0
  12. package/web-app/web-dist/assets/{angular-ts-CD_OonCa.js → angular-ts-BfdufMKP.js} +1 -1
  13. package/web-app/web-dist/assets/{apl-uOGC3x4e.js → apl-DWBSSoBH.js} +1 -1
  14. package/web-app/web-dist/assets/{astro-B6ybQmWG.js → astro-3LtMP0Sq.js} +1 -1
  15. package/web-app/web-dist/assets/{blade-B1QGRlVx.js → blade-llJRbbtR.js} +1 -1
  16. package/web-app/web-dist/assets/c-Wt1voDr2.js +1 -0
  17. package/web-app/web-dist/assets/{cobol-BgqgtYWn.js → cobol-x_HIyl2P.js} +1 -1
  18. package/web-app/web-dist/assets/{coffee-0wIRKYlr.js → coffee-CThvmt4R.js} +1 -1
  19. package/web-app/web-dist/assets/cpp-NtAeskI3.js +1 -0
  20. package/web-app/web-dist/assets/{crystal-CyTK3qFN.js → crystal-DNu_sX0G.js} +1 -1
  21. package/web-app/web-dist/assets/css-DJp_X0uY.js +1 -0
  22. package/web-app/web-dist/assets/{cssMode-B9jnrWOz.js → cssMode-ByQBaInt.js} +1 -1
  23. package/web-app/web-dist/assets/dist-DQqjtuhV.js +153 -0
  24. package/web-app/web-dist/assets/{edge-CvML9pwC.js → edge-ozw5tpLl.js} +1 -1
  25. package/web-app/web-dist/assets/{editor.api2-BmGoRSl4.js → editor.api2-C7skgoRB.js} +1 -1
  26. package/web-app/web-dist/assets/{elixir-CrjqTiSc.js → elixir-VhA6FeZt.js} +1 -1
  27. package/web-app/web-dist/assets/{elm-C4JtJ0Au.js → elm-dREJmIFz.js} +1 -1
  28. package/web-app/web-dist/assets/{erb-Cmeb-29V.js → erb-CIg6G69l.js} +1 -1
  29. package/web-app/web-dist/assets/{freemarker2-B5LAi19B.js → freemarker2-CBBwP9JV.js} +1 -1
  30. package/web-app/web-dist/assets/{git-rebase-CXqdToiP.js → git-rebase-B44mJPta.js} +1 -1
  31. package/web-app/web-dist/assets/{glimmer-js-Kq-kdTyV.js → glimmer-js-vH_gHG0-.js} +1 -1
  32. package/web-app/web-dist/assets/{glimmer-ts-D0RKLJNf.js → glimmer-ts--abOzSAQ.js} +1 -1
  33. package/web-app/web-dist/assets/glsl-Dv5r7kPw.js +1 -0
  34. package/web-app/web-dist/assets/graphql-CB4jsw2E.js +1 -0
  35. package/web-app/web-dist/assets/{hack-trjVF3Po.js → hack-DvEYX148.js} +1 -1
  36. package/web-app/web-dist/assets/haml-zE6W3STP.js +1 -0
  37. package/web-app/web-dist/assets/{handlebars-B8_x7Zx7.js → handlebars-CzBR2SDs.js} +1 -1
  38. package/web-app/web-dist/assets/{handlebars-g7ZhGhI_.js → handlebars-tXdfxEd6.js} +1 -1
  39. package/web-app/web-dist/assets/html-C8UlPnhE.js +1 -0
  40. package/web-app/web-dist/assets/{html-CfvRMgoC.js → html-DgPn1QYH.js} +1 -1
  41. package/web-app/web-dist/assets/{html-derivative-BYX_F_XH.js → html-derivative-CY6NRz-J.js} +1 -1
  42. package/web-app/web-dist/assets/{htmlMode-Bi8vSvwb.js → htmlMode-BtdIDgA2.js} +1 -1
  43. package/web-app/web-dist/assets/{http-BIVDpHT-.js → http-Cyd7bS_S.js} +1 -1
  44. package/web-app/web-dist/assets/{hurl-CFsshMju.js → hurl-CWPsiEpf.js} +1 -1
  45. package/web-app/web-dist/assets/index-B27_WOhS.css +2 -0
  46. package/web-app/web-dist/assets/index-De6DWTZM.js +7 -0
  47. package/web-app/web-dist/assets/java-CGc3VwQr.js +1 -0
  48. package/web-app/web-dist/assets/{javascript-Bxx2wV4w.js → javascript-5m05n-Be.js} +1 -1
  49. package/web-app/web-dist/assets/javascript-CUt1pgmJ.js +1 -0
  50. package/web-app/web-dist/assets/{jinja-_ZS5zWwe.js → jinja-CD-Z-FLd.js} +1 -1
  51. package/web-app/web-dist/assets/{jison-D8mMEpcs.js → jison-imPNup1l.js} +1 -1
  52. package/web-app/web-dist/assets/json-Bg9ijW3F.js +1 -0
  53. package/web-app/web-dist/assets/{jsonMode-C6ELX5GM.js → jsonMode-BG32YnTY.js} +1 -1
  54. package/web-app/web-dist/assets/jsx-CY6oMTks.js +1 -0
  55. package/web-app/web-dist/assets/{julia-D4h2DZrs.js → julia-Dc3O-irA.js} +1 -1
  56. package/web-app/web-dist/assets/{just-bMqQi3xg.js → just-BhOq_Kbv.js} +1 -1
  57. package/web-app/web-dist/assets/{latex-DThYi3CX.js → latex-Cu4Y1d5w.js} +1 -1
  58. package/web-app/web-dist/assets/lib-KIOQTlcs.js +1 -0
  59. package/web-app/web-dist/assets/{liquid-CUjzzP4r.js → liquid-3ZnQzTbs.js} +1 -1
  60. package/web-app/web-dist/assets/{liquid-CesB-zzl.js → liquid-CvXMrjlQ.js} +1 -1
  61. package/web-app/web-dist/assets/{lspLanguageFeatures-gTnJsses.js → lspLanguageFeatures-6KXALSrl.js} +1 -1
  62. package/web-app/web-dist/assets/lua-BjLEUjKY.js +1 -0
  63. package/web-app/web-dist/assets/{marko-yoGoLK2m.js → marko-DvhNOisQ.js} +1 -1
  64. package/web-app/web-dist/assets/{mdc-BvtXU6eH.js → mdc-Bm9TpL1X.js} +1 -1
  65. package/web-app/web-dist/assets/{mdx-DrXGQbNB.js → mdx-DffTEkNE.js} +1 -1
  66. package/web-app/web-dist/assets/{monaco-setup-CsR6EfHe.js → monaco-setup-DM3A5_VI.js} +3 -3
  67. package/web-app/web-dist/assets/{nginx-DoUz032F.js → nginx-Bhc82uuv.js} +1 -1
  68. package/web-app/web-dist/assets/{nim-B0Pl8B4R.js → nim-DXTVBFnF.js} +1 -1
  69. package/web-app/web-dist/assets/{perl-D2tfAALb.js → perl-C7veXV9z.js} +1 -1
  70. package/web-app/web-dist/assets/{php-BImCcX5X.js → php-BRiuMnnr.js} +1 -1
  71. package/web-app/web-dist/assets/{pug-BcnpC8P_.js → pug-C5hz5LQ7.js} +1 -1
  72. package/web-app/web-dist/assets/{python-ypRCBnvu.js → python-DyLAD3Wt.js} +1 -1
  73. package/web-app/web-dist/assets/{qml-DFDAunHY.js → qml-BdUV3aTS.js} +1 -1
  74. package/web-app/web-dist/assets/r-8R7vtdQc.js +1 -0
  75. package/web-app/web-dist/assets/{razor-aqrhpwqZ.js → razor-C49xQTPQ.js} +1 -1
  76. package/web-app/web-dist/assets/{razor-1_376SZM.js → razor-DRL52XO2.js} +1 -1
  77. package/web-app/web-dist/assets/react-vendor-CCIEwYL0.js +9 -0
  78. package/web-app/web-dist/assets/regexp-Omp9DhTb.js +1 -0
  79. package/web-app/web-dist/assets/{rst-2vG6f11Y.js → rst-BHX71KW9.js} +1 -1
  80. package/web-app/web-dist/assets/{ruby-Dj6bCFXR.js → ruby-B--HzjGU.js} +1 -1
  81. package/web-app/web-dist/assets/{sas-BhVZ4qL2.js → sas-DrLaYOK_.js} +1 -1
  82. package/web-app/web-dist/assets/scss-DdSxiZKl.js +1 -0
  83. package/web-app/web-dist/assets/shellscript-DwcUjJBL.js +1 -0
  84. package/web-app/web-dist/assets/{shellsession-CyO2fnhB.js → shellsession-CPZkydE6.js} +1 -1
  85. package/web-app/web-dist/assets/{soy-DIkw6E88.js → soy-Br5FhD7c.js} +1 -1
  86. package/web-app/web-dist/assets/sql-DNssxck8.js +1 -0
  87. package/web-app/web-dist/assets/{stata-DvkM932O.js → stata-DXn1tqOr.js} +1 -1
  88. package/web-app/web-dist/assets/{surrealql-B4-Q8tqV.js → surrealql-IeLNQw0f.js} +1 -1
  89. package/web-app/web-dist/assets/{svelte-p6yBy-Ki.js → svelte-DOdLCIlh.js} +1 -1
  90. package/web-app/web-dist/assets/{templ-C7EkuiZr.js → templ-CIwIngms.js} +1 -1
  91. package/web-app/web-dist/assets/{tex-DkmD8uFC.js → tex-D8QMumu5.js} +1 -1
  92. package/web-app/web-dist/assets/{ts-tags-U-hncHg4.js → ts-tags-BMVY4q-l.js} +1 -1
  93. package/web-app/web-dist/assets/{tsMode-a8OvovQd.js → tsMode-BndVBac5.js} +1 -1
  94. package/web-app/web-dist/assets/tsx-5Eka4NBX.js +1 -0
  95. package/web-app/web-dist/assets/{twig-CU0OP-IA.js → twig-C8o_5mgw.js} +1 -1
  96. package/web-app/web-dist/assets/{typescript-DnLjiKtn.js → typescript-B1w9vqKF.js} +1 -1
  97. package/web-app/web-dist/assets/typescript-DOu2WMV5.js +1 -0
  98. package/web-app/web-dist/assets/{vue-Db7nY3ba.js → vue-BU18DNDL.js} +1 -1
  99. package/web-app/web-dist/assets/{vue-html-BvAbiAw1.js → vue-html-BeluIYX0.js} +1 -1
  100. package/web-app/web-dist/assets/{vue-vine-BEaIQIlA.js → vue-vine-DGUAbOCX.js} +1 -1
  101. package/web-app/web-dist/assets/{xml-an4Nuuqq.js → xml-D8uAlVv5.js} +1 -1
  102. package/web-app/web-dist/assets/xml-DIqSwXR3.js +1 -0
  103. package/web-app/web-dist/assets/{xsl-D3NQgH22.js → xsl-Ct_-YIAy.js} +1 -1
  104. package/web-app/web-dist/assets/xterm-B1ffpRuj.js +36 -0
  105. package/web-app/web-dist/assets/xterm-addons-psDEiUMC.js +136 -0
  106. package/web-app/web-dist/assets/{yaml-Diiu6O9P.js → yaml-Bb7jXyQv.js} +1 -1
  107. package/web-app/web-dist/assets/yaml-DTtCYNlS.js +1 -0
  108. package/web-app/web-dist/index.html +6 -3
  109. package/zuzuu/actions/convert.mjs +10 -9
  110. package/zuzuu/actions/dispatch.mjs +12 -7
  111. package/zuzuu/actions/inbox.mjs +5 -5
  112. package/zuzuu/actions/manifest.mjs +48 -30
  113. package/zuzuu/actions/schema.mjs +9 -3
  114. package/zuzuu/actions/trail.mjs +1 -1
  115. package/zuzuu/commands/act-author.mjs +23 -13
  116. package/zuzuu/commands/act.mjs +4 -6
  117. package/zuzuu/commands/capture.mjs +2 -2
  118. package/zuzuu/commands/code.mjs +2 -2
  119. package/zuzuu/commands/digest.mjs +2 -2
  120. package/zuzuu/commands/distill.mjs +15 -16
  121. package/zuzuu/commands/doctor.mjs +41 -19
  122. package/zuzuu/commands/enable.mjs +1 -1
  123. package/zuzuu/commands/eval.mjs +3 -36
  124. package/zuzuu/commands/explain.mjs +4 -4
  125. package/zuzuu/commands/faculty.mjs +158 -0
  126. package/zuzuu/commands/generation.mjs +5 -8
  127. package/zuzuu/commands/hook.mjs +14 -12
  128. package/zuzuu/commands/inbox.mjs +1 -6
  129. package/zuzuu/commands/init.mjs +18 -4
  130. package/zuzuu/commands/knowledge.mjs +1 -1
  131. package/zuzuu/commands/migrations/home.mjs +96 -0
  132. package/zuzuu/commands/migrations/index.mjs +48 -0
  133. package/zuzuu/commands/migrations/items.mjs +360 -0
  134. package/zuzuu/commands/migrations/proposals.mjs +100 -0
  135. package/zuzuu/commands/proposals.mjs +131 -0
  136. package/zuzuu/commands/review.mjs +13 -227
  137. package/zuzuu/commands/session.mjs +8 -2
  138. package/zuzuu/commands/sessions.mjs +159 -0
  139. package/zuzuu/commands/status.mjs +3 -3
  140. package/zuzuu/commands/trace.mjs +1 -1
  141. package/zuzuu/{capture-core.mjs → core/capture-core.mjs} +3 -3
  142. package/zuzuu/{store.mjs → core/store.mjs} +1 -1
  143. package/zuzuu/digest/compose.mjs +96 -0
  144. package/zuzuu/eval/score.mjs +14 -1
  145. package/zuzuu/faculties/actions/index.mjs +283 -0
  146. package/zuzuu/faculties/guardrails/index.mjs +320 -0
  147. package/zuzuu/faculties/instructions/index.mjs +288 -0
  148. package/zuzuu/faculties/knowledge/index.mjs +185 -0
  149. package/zuzuu/faculties/memory/index.mjs +124 -0
  150. package/zuzuu/faculty/envelope.mjs +290 -0
  151. package/zuzuu/faculty/generation/read.mjs +206 -0
  152. package/zuzuu/faculty/generation/write.mjs +207 -0
  153. package/zuzuu/faculty/items.mjs +81 -0
  154. package/zuzuu/faculty/module.mjs +74 -0
  155. package/zuzuu/faculty/pending.mjs +63 -0
  156. package/zuzuu/faculty/registry.mjs +204 -18
  157. package/zuzuu/faculty/render.mjs +59 -0
  158. package/zuzuu/faculty/trail.mjs +1 -1
  159. package/zuzuu/guardrails/engine.mjs +137 -0
  160. package/zuzuu/{scaffold.mjs → home/scaffold.mjs} +110 -39
  161. package/zuzuu/knowledge/items.mjs +56 -91
  162. package/zuzuu/live/install.mjs +1 -1
  163. package/zuzuu/live/live-store.mjs +2 -2
  164. package/zuzuu/live/reconcile.mjs +2 -2
  165. package/zuzuu/sessions/git.mjs +47 -0
  166. package/zuzuu/{session-git.mjs → sessions/session-git.mjs} +5 -43
  167. package/web-app/web-dist/assets/DiffTab-BuWonUNJ.js +0 -1
  168. package/web-app/web-dist/assets/MonacoFile-CL3DhFKG.js +0 -1
  169. package/web-app/web-dist/assets/angular-html-CmT26mqM.js +0 -1
  170. package/web-app/web-dist/assets/c-BvoqrSVH.js +0 -1
  171. package/web-app/web-dist/assets/cpp-BXsk94m0.js +0 -1
  172. package/web-app/web-dist/assets/css-Z8oOGxII.js +0 -1
  173. package/web-app/web-dist/assets/dist-ChcDQ_7s.js +0 -153
  174. package/web-app/web-dist/assets/glsl-KwyfU2aa.js +0 -1
  175. package/web-app/web-dist/assets/graphql-DSeOUAa2.js +0 -1
  176. package/web-app/web-dist/assets/haml-azVoxQRV.js +0 -1
  177. package/web-app/web-dist/assets/html-D_7P5S4m.js +0 -1
  178. package/web-app/web-dist/assets/index--5yy8RbA.js +0 -267
  179. package/web-app/web-dist/assets/index-BVG4hgk7.css +0 -2
  180. package/web-app/web-dist/assets/java-D4RbCvBe.js +0 -1
  181. package/web-app/web-dist/assets/javascript-Cb010CKM.js +0 -1
  182. package/web-app/web-dist/assets/json-DWgqV4D1.js +0 -1
  183. package/web-app/web-dist/assets/jsx-CZjSJa1f.js +0 -1
  184. package/web-app/web-dist/assets/lua-TGj_6NzO.js +0 -1
  185. package/web-app/web-dist/assets/r-fCpuAR7u.js +0 -1
  186. package/web-app/web-dist/assets/regexp-B4yxx-Ty.js +0 -1
  187. package/web-app/web-dist/assets/scss-QdjMO_xV.js +0 -1
  188. package/web-app/web-dist/assets/shellscript-BnlgeVVx.js +0 -1
  189. package/web-app/web-dist/assets/sql-DGnQv6iD.js +0 -1
  190. package/web-app/web-dist/assets/tsx-MJ0-9sYG.js +0 -1
  191. package/web-app/web-dist/assets/typescript-C17ZkDe8.js +0 -1
  192. package/web-app/web-dist/assets/xml-CA9lHFQV.js +0 -1
  193. package/web-app/web-dist/assets/yaml-CwRYMJka.js +0 -1
  194. package/zuzuu/actions/adapter.mjs +0 -130
  195. package/zuzuu/commands/migrate.mjs +0 -225
  196. package/zuzuu/digest.mjs +0 -149
  197. package/zuzuu/faculty/generation.mjs +0 -392
  198. package/zuzuu/guardrails/adapter.mjs +0 -134
  199. package/zuzuu/guardrails.mjs +0 -89
  200. package/zuzuu/instructions/adapter.mjs +0 -93
  201. package/zuzuu/knowledge/adapter.mjs +0 -99
  202. package/zuzuu/memory/adapter.mjs +0 -121
  203. package/zuzuu/miners/actions.mjs +0 -118
  204. package/zuzuu/miners/guardrails.mjs +0 -179
  205. package/zuzuu/miners/instructions.mjs +0 -157
  206. package/zuzuu/miners/knowledge.mjs +0 -25
  207. package/zuzuu/miners/memory.mjs +0 -27
  208. package/zuzuu/miners/registry.mjs +0 -31
  209. /package/web-app/web-dist/assets/{chunk-QTnfLwEv.js → rolldown-runtime-QTnfLwEv.js} +0 -0
  210. /package/zuzuu/{session.mjs → core/session.mjs} +0 -0
  211. /package/zuzuu/{inject.mjs → home/inject.mjs} +0 -0
@@ -1,105 +1,22 @@
1
- // `zuzuu review` — the human gate, as a daily ritual. Walks pending proposals
2
- // one-by-one: shows the candidate, its evidence, and the ER verdict (with the
3
- // matched item when enrich/duplicate) → y approve · n reject · e edit · s skip ·
4
- // q quit. Works piped (answers on stdin) — that's also how it's tested.
5
- // Non-interactive surface: `zuzuu proposals list|show|approve|reject`.
1
+ // `zuzuu review` — the human gate, as a daily ritual (the interactive ceremony).
2
+ // Walks pending proposals one-by-one: shows the candidate, its evidence, and
3
+ // the ER verdict (with the matched item when enrich/duplicate) → y approve ·
4
+ // n reject · e edit · s skip · q quit. Works piped (answers on stdin) — that's
5
+ // also how it's tested. The non-interactive surface lives in proposals.mjs.
6
6
 
7
7
  import { spawnSync } from 'node:child_process';
8
8
  import { join } from 'node:path';
9
9
  import { createInterface } from 'node:readline';
10
- import { paths, readIndex } from '../store.mjs';
10
+ import { paths } from '../core/store.mjs';
11
11
  import { processInbox } from '../knowledge/inbox.mjs';
12
12
  import { getProposal, proposalsDir } from '../knowledge/proposals.mjs';
13
- import { readItem } from '../knowledge/items.mjs';
14
- import * as registry from '../faculty/registry.mjs';
15
13
  import * as gate from '../faculty/gate.mjs';
16
- import { listProposals as spineListProposals } from '../faculty/proposal.mjs';
17
- import { mintGeneration, activeGeneration } from '../faculty/generation.mjs';
18
- import { rank } from '../eval/rank.mjs';
19
- import { getScorer, mechanicalScore } from '../eval/score.mjs';
14
+ import { pendingByFaculty, buildSessionMtimes } from '../faculty/pending.mjs';
15
+ import { knowledgeCard } from '../faculty/render.mjs';
16
+ import { activeGeneration } from '../faculty/generation/read.mjs';
17
+ import { mintGeneration } from '../faculty/generation/write.mjs';
18
+ import { getScorer } from '../eval/score.mjs';
20
19
  import { evalLine } from './eval.mjs';
21
- import '../knowledge/adapter.mjs'; // self-registers the 'knowledge' adapter
22
- import '../actions/adapter.mjs'; // self-registers the 'actions' adapter
23
- import '../guardrails/adapter.mjs'; // self-registers the 'guardrails' adapter
24
- import '../instructions/adapter.mjs'; // self-registers the 'instructions' adapter
25
- import '../memory/adapter.mjs'; // self-registers the 'memory' adapter
26
-
27
- /** Build sessionMtimes map from the sessions index — best-effort, fail-open. */
28
- function buildSessionMtimes() {
29
- try {
30
- const idx = readIndex();
31
- const map = {};
32
- for (const s of idx.sessions ?? []) {
33
- if (!s.id) continue;
34
- const ms = s.startedAt ? Date.parse(s.startedAt) : 0;
35
- if (!isNaN(ms) && ms > 0) map[s.id] = ms;
36
- }
37
- return map;
38
- } catch {
39
- return {};
40
- }
41
- }
42
-
43
- // Review walks faculties in a fixed order so piped sessions are deterministic
44
- // (the combo smoke test feeds one stdin across the actions pass then knowledge).
45
- const REVIEW_ORDER = ['actions', 'knowledge', 'guardrails', 'instructions', 'memory'];
46
-
47
- /** Ordered list of adapters that have pending proposals to review. */
48
- function pendingByFaculty(agentDir) {
49
- const adapters = registry.all();
50
- const seen = new Set();
51
- const ordered = [];
52
- for (const name of REVIEW_ORDER) {
53
- const a = adapters.find((x) => x.name === name);
54
- if (a) { ordered.push(a); seen.add(name); }
55
- }
56
- for (const a of adapters) if (!seen.has(a.name)) ordered.push(a);
57
- const sessionMtimes = buildSessionMtimes();
58
- const now = Date.now();
59
- const scorer = getScorer();
60
- const out = [];
61
- for (const a of ordered) {
62
- let proposals = facultyPending(agentDir, a);
63
- if (!proposals.length) continue;
64
- // Rank proposals highest-score-first (display only — never changes approval/mint).
65
- const ranked = rank(proposals, scorer, { now, sessionMtimes });
66
- proposals = ranked.map((r) => r.proposal);
67
- out.push({ adapter: a, proposals });
68
- }
69
- return out;
70
- }
71
-
72
- /** Pending proposals for one adapter (dir-shaped adapters override listProposals). */
73
- function facultyPending(agentDir, a) {
74
- if (typeof a.listProposals === 'function') return a.listProposals(agentDir);
75
- // JSON-record faculties: read via the spine (records carry both the spine shape
76
- // and the legacy candidate/er keys the knowledge card renders from).
77
- return spineListProposals(agentDir, a.name);
78
- }
79
-
80
- function card(agentDir, p, i, total, scoreResult) {
81
- const lines = [];
82
- lines.push(`\n━━ proposal ${i + 1}/${total} ── ${p.id} ── ${p.kind} ── source: ${p.source ?? '-'} ━━`);
83
- if (p.kind === 'registry') {
84
- lines.push(` register ${p.registry.slice(0, -1)}: '${p.key}' (seen ${p.evidence?.occurrences}× in candidates)`);
85
- } else {
86
- const c = p.candidate;
87
- lines.push(` ${c.type}: ${c.body?.slice(0, 100).replace(/\n/g, ' ')}`);
88
- for (const [k, v] of Object.entries(c.attributes ?? {})) lines.push(` · ${k} = ${v}`);
89
- for (const r of c.relations ?? []) lines.push(` → ${r.type} ${r.target}`);
90
- const ev = p.evidence ?? {};
91
- if (Object.keys(ev).length) lines.push(` evidence: ${Object.entries(ev).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(' ')}`);
92
- const er = p.er ?? {};
93
- lines.push(` er: ${er.verdict}${er.match ? ` → ${er.match}` : ''} (${(er.confidence ?? 0).toFixed(2)} · ${er.reason ?? ''})`);
94
- if (er.match) {
95
- const m = readItem(agentDir, er.match);
96
- if (m) lines.push(` existing: ${m.body.slice(0, 80).replace(/\n/g, ' ')}`);
97
- }
98
- }
99
- // Eval line — always shown; scoreResult computed by caller from ranked array.
100
- if (scoreResult) lines.push(` ${evalLine(scoreResult)}`);
101
- return lines.join('\n');
102
- }
103
20
 
104
21
  /**
105
22
  * Pure: the graduation ceremony block shown when a generation is minted.
@@ -177,7 +94,7 @@ export async function review() {
177
94
  try { scoreResult = scorer(p, { now, sessionMtimes }); } catch { /* fail-open */ }
178
95
  // Card: knowledge keeps its rich card (ER + existing-item lookup); other
179
96
  // faculties render through the adapter contract.
180
- if (adapter.name === 'knowledge') console.log(card(agentDir, p, i, proposals.length, scoreResult));
97
+ if (adapter.name === 'knowledge') console.log(knowledgeCard(agentDir, p, i, proposals.length, scoreResult));
181
98
  else {
182
99
  const r = adapter.render(p);
183
100
  const [head, ...rest] = r.card.split('\n');
@@ -210,7 +127,7 @@ export async function review() {
210
127
  proposals[i] = fresh;
211
128
  let freshScore = null;
212
129
  try { freshScore = scorer(fresh, { now, sessionMtimes }); } catch { /* fail-open */ }
213
- console.log(card(agentDir, fresh, i, proposals.length, freshScore));
130
+ console.log(knowledgeCard(agentDir, fresh, i, proposals.length, freshScore));
214
131
  }
215
132
  } else if (a === 's') {
216
133
  skipped++; totalLeft--; acted = true;
@@ -233,134 +150,3 @@ export async function review() {
233
150
  console.log(ceremonyBlock(gen.id, approvedIds, approvedByFaculty));
234
151
  }
235
152
  }
236
-
237
- /**
238
- * Resolve which faculty owns a given proposal id (used when --faculty is omitted).
239
- * Defaults to 'knowledge' (the historical path) when no other faculty claims it.
240
- */
241
- function facultyOf(agentDir, id, only) {
242
- if (only) return only;
243
- for (const { adapter, proposals } of pendingByFaculty(agentDir)) {
244
- if (proposals.some((p) => p.id === id)) return adapter.name;
245
- }
246
- return 'knowledge';
247
- }
248
-
249
- /**
250
- * Pure: the structured object for `proposals approve --json`.
251
- * Calls gate.approve and returns the result object the branch prints.
252
- * @param {string} agentDir
253
- * @param {string} id
254
- * @param {string} faculty
255
- * @returns {object} the gate result (contains ok, action, etc.)
256
- */
257
- export function approveData(agentDir, id, faculty) {
258
- return gate.approve(agentDir, faculty, id);
259
- }
260
-
261
- /**
262
- * Pure: the structured object for `proposals reject --json`.
263
- * Calls gate.reject and returns the result object the branch prints.
264
- * @param {string} agentDir
265
- * @param {string} id
266
- * @param {string} faculty
267
- * @param {string} [reason]
268
- * @returns {object} { ok, id, ... }
269
- */
270
- export function rejectData(agentDir, id, faculty, reason = '') {
271
- const r = gate.reject(agentDir, faculty, id, reason);
272
- return { ...r, id };
273
- }
274
-
275
- /**
276
- * Pure: list pending proposals as structured data — the zuzuu-web /proposals source.
277
- * @param {string} agentDir
278
- * @param {string} [only] optional faculty filter
279
- * @returns {{ pending: Array<{id, faculty, title}> }}
280
- */
281
- export function proposalsListData(agentDir, only) {
282
- const groups = pendingByFaculty(agentDir).filter((g) => !only || g.adapter.name === only);
283
- const pending = [];
284
- for (const { adapter, proposals } of groups) {
285
- for (const p of proposals) {
286
- // derive a human title the same way the table does
287
- let title;
288
- if (adapter.name === 'knowledge') {
289
- title = p.kind === 'registry'
290
- ? `register ${p.registry?.slice(0, -1) ?? ''} '${p.key ?? ''}'`
291
- : (p.candidate?.body ?? p.payload?.body ?? p.id)?.slice(0, 80);
292
- } else {
293
- title = p.title ?? adapter.render(p).line;
294
- }
295
- pending.push({ id: p.id, faculty: adapter.name, title: title ?? p.id });
296
- }
297
- }
298
- return { pending };
299
- }
300
-
301
- /** Non-interactive: zuzuu proposals list|show <id>|approve <id>|reject <id> [--reason r] [--faculty f] */
302
- export function proposals(args) {
303
- const agentDir = paths().dir;
304
- const sub = args._[0] || 'list';
305
- const only = args.faculty; // optional filter; default = all
306
- if (sub === 'list') {
307
- if (args.json) {
308
- processInbox(agentDir); // promote plain-text inbox candidates, same as text path
309
- const d = proposalsListData(agentDir, only);
310
- console.log(JSON.stringify(d));
311
- return;
312
- }
313
- const inbox = processInbox(agentDir);
314
- if (inbox.processed) console.log(`(processed ${inbox.processed} inbox candidate(s))`);
315
- const groups = pendingByFaculty(agentDir).filter((g) => !only || g.adapter.name === only);
316
- const any = groups.some((g) => g.proposals.length);
317
- if (!any) return console.log('no pending proposals');
318
- for (const { adapter, proposals } of groups) {
319
- for (const p of proposals) {
320
- // knowledge keeps its historical one-liner; other faculties use adapter.render
321
- if (adapter.name === 'knowledge') {
322
- const what = p.kind === 'registry'
323
- ? `register ${p.registry.slice(0, -1)} '${p.key}'`
324
- : `${p.candidate.type}: ${p.candidate.body?.slice(0, 60).replace(/\n/g, ' ')}`;
325
- console.log(` ${p.id} [${p.er?.verdict ?? p.kind}] ${what}`);
326
- } else {
327
- console.log(` ${adapter.render(p).line}`);
328
- }
329
- }
330
- }
331
- return;
332
- }
333
- const id = args._[1];
334
- if (sub === 'show') {
335
- const faculty = facultyOf(agentDir, id, only);
336
- const a = registry.get(faculty);
337
- const p = (a && typeof a.getProposal === 'function') ? a.getProposal(agentDir, id) : getProposal(agentDir, id);
338
- if (!p) return console.error('not found');
339
- // show always prints JSON (both with and without --json flag)
340
- console.log(JSON.stringify(p, null, 2));
341
- return;
342
- }
343
- if (sub === 'approve') {
344
- const faculty = facultyOf(agentDir, id, only);
345
- const r = approveData(agentDir, id, faculty);
346
- if (args.json) {
347
- console.log(JSON.stringify(r));
348
- } else {
349
- console.log(r.ok ? `✓ ${r.action}` : `✗ ${(r.errors ?? [r.action]).join('; ')}`);
350
- for (const w of r.warnings ?? []) console.log(`⚠ ${w}`);
351
- }
352
- process.exit(r.ok ? 0 : 1);
353
- }
354
- if (sub === 'reject') {
355
- const faculty = facultyOf(agentDir, id, only);
356
- const r = rejectData(agentDir, id, faculty, args.reason || '');
357
- if (args.json) {
358
- console.log(JSON.stringify(r));
359
- } else {
360
- console.log(r.ok ? '✓ rejected' : '✗ not found');
361
- }
362
- process.exit(r.ok ? 0 : 1);
363
- }
364
- console.error('usage: zuzuu proposals list|show <id>|approve <id>|reject <id> [--reason r] [--faculty f]');
365
- process.exit(1);
366
- }
@@ -8,7 +8,8 @@
8
8
  // All git mutation lives in session-git.mjs (fail-soft, never throws); this is
9
9
  // the thin print layer (xxxData pattern — pure data fns + --json everywhere).
10
10
 
11
- import { sessionStatus, closeSession, continueSession, discardSession } from '../session-git.mjs';
11
+ import { sessionStatus, closeSession, continueSession, discardSession } from '../sessions/session-git.mjs';
12
+ import { sessionInspect } from './sessions.mjs';
12
13
 
13
14
  /** Pure: structured session-git state (the leftover detector included). */
14
15
  export function sessionStatusData(cwd = process.cwd()) {
@@ -98,6 +99,11 @@ export function session(args = {}) {
98
99
  process.exit(1);
99
100
  }
100
101
 
101
- console.error(`unknown: zuzuu session ${sub}\nusage: zuzuu session [status|merge [--title t]|continue|discard --yes]`);
102
+ if (sub === 'inspect') {
103
+ sessionInspect(args);
104
+ return;
105
+ }
106
+
107
+ console.error(`unknown: zuzuu session ${sub}\nusage: zuzuu session [status|merge [--title t]|continue|discard --yes|inspect <id>]`);
102
108
  process.exit(1);
103
109
  }
@@ -0,0 +1,159 @@
1
+ // zuzuu/commands/sessions.mjs — the sessions observability surface (overhaul
2
+ // Part A, 2026-06-13).
3
+ //
4
+ // zuzuu sessions [--json] recorded sessions w/ state labels
5
+ // zuzuu session inspect <id> [--json] one session: trace summary +
6
+ // per-faculty mined signals
7
+ //
8
+ // inspect = { session, trace: {spans, tools, duration}, signals: {<faculty>:
9
+ // counts} } — the span count reads the stored OTLP blob; the signals re-mine
10
+ // the HOST transcript through the proven adapters' mineSignals and map the
11
+ // superset onto faculties via each module's sessionSignals hook. Fail-soft
12
+ // throughout: a gone blob/transcript degrades to warnings, never a throw.
13
+
14
+ import { readFileSync } from 'node:fs';
15
+ import { readIndex, resolveTrace, paths } from '../core/store.mjs';
16
+ import { transcriptsFor, mineHostSession } from '../knowledge/distill.mjs';
17
+ import { facultiesOf, invoke } from '../faculty/registry.mjs';
18
+
19
+ /** Pure: the sessions list with state labels — the web Sessions section source. */
20
+ export function sessionsListData(cwd = process.cwd()) {
21
+ const { sessions } = readIndex(cwd);
22
+ return {
23
+ sessions: sessions.map((s) => ({
24
+ id: s.id,
25
+ host: s.host,
26
+ state: s.status, // active | completed | abandoned | crashed | captured | opening
27
+ startedAt: s.startedAt ?? null,
28
+ endedAt: s.endedAt ?? null,
29
+ durationMs: s.durationMs ?? 0,
30
+ counts: s.counts ?? { turns: 0, tools: 0, errors: 0 },
31
+ generation: s.generation ?? null,
32
+ git: s.git ?? { commit: null, branch: null },
33
+ })),
34
+ };
35
+ }
36
+
37
+ /** Count spans across an OTLP/JSON NDJSON blob (one export request per line). */
38
+ function countTraceSpans(file) {
39
+ let spans = 0;
40
+ for (const line of readFileSync(file, 'utf8').split('\n')) {
41
+ if (!line.trim()) continue;
42
+ const req = JSON.parse(line);
43
+ for (const rs of req.resourceSpans ?? []) {
44
+ for (const ss of rs.scopeSpans ?? []) spans += (ss.spans ?? []).length;
45
+ }
46
+ }
47
+ return spans;
48
+ }
49
+
50
+ /**
51
+ * Pure-ish: one session's observability document, or null for an unknown id.
52
+ * Accepts a unique id prefix (the table shows 8-char ids).
53
+ * @param {string} cwd
54
+ * @param {string} idArg
55
+ * @param {{transcripts?: Array<{host,ref,sessionId}>}} [opts] injectable for hermetic tests
56
+ * @returns {{session, trace:{spans,tools,duration}, signals:object, warnings:string[]} | null}
57
+ */
58
+ export function sessionInspectData(cwd, idArg, { transcripts } = {}) {
59
+ if (!idArg) return null;
60
+ const { sessions } = readIndex(cwd);
61
+ const matches = sessions.filter((s) => s.id === idArg);
62
+ const byPrefix = matches.length ? matches : sessions.filter((s) => String(s.id).startsWith(idArg));
63
+ if (!byPrefix.length) return null;
64
+ const s = byPrefix[0];
65
+ const warnings = [];
66
+
67
+ // trace summary — span count from the stored OTLP blob (fail-soft)
68
+ let spans = null;
69
+ try {
70
+ spans = countTraceSpans(resolveTrace(s.traceRef, cwd));
71
+ } catch {
72
+ warnings.push('trace blob unavailable — span count unknown');
73
+ }
74
+ const trace = { spans, tools: s.counts?.tools ?? 0, duration: s.durationMs ?? 0 };
75
+
76
+ // per-faculty mined signals — re-mine the host transcript (fail-soft when gone)
77
+ const signals = {};
78
+ try {
79
+ const pairs = transcripts ?? transcriptsFor({ scope: 'all', cwd });
80
+ const pair = pairs.find((p) => p.host === s.host && String(p.sessionId) === String(s.id));
81
+ const mined = pair ? mineHostSession(pair) : null;
82
+ if (!mined) {
83
+ warnings.push('host transcript unavailable — signals empty');
84
+ } else {
85
+ const agentDir = paths(cwd).dir;
86
+ for (const entry of facultiesOf(agentDir)) {
87
+ const r = invoke(entry, 'sessionSignals', mined);
88
+ if (r.ok && r.value && typeof r.value === 'object') signals[entry.id] = r.value;
89
+ }
90
+ }
91
+ } catch {
92
+ warnings.push('signal mining failed — signals empty');
93
+ }
94
+
95
+ return {
96
+ session: {
97
+ id: s.id,
98
+ host: s.host,
99
+ state: s.status,
100
+ startedAt: s.startedAt ?? null,
101
+ endedAt: s.endedAt ?? null,
102
+ durationMs: s.durationMs ?? 0,
103
+ counts: s.counts ?? { turns: 0, tools: 0, errors: 0 },
104
+ generation: s.generation ?? null,
105
+ git: s.git ?? { commit: null, branch: null },
106
+ traceRef: s.traceRef ?? null,
107
+ },
108
+ trace,
109
+ signals,
110
+ warnings,
111
+ };
112
+ }
113
+
114
+ const fmtDur = (ms) => (ms < 60_000 ? `${(ms / 1000).toFixed(0)}s` : `${(ms / 60_000).toFixed(1)}m`);
115
+
116
+ /** `zuzuu sessions [--json]` — the recorded-sessions list with state labels. */
117
+ export function sessions(args = {}) {
118
+ const cwd = process.cwd();
119
+ const d = sessionsListData(cwd);
120
+ if (args.json) { console.log(JSON.stringify(d)); return; }
121
+ if (!d.sessions.length) {
122
+ console.log('no recorded sessions yet — run `zuzuu capture`, or just start your agent (live capture)');
123
+ return;
124
+ }
125
+ console.log(' STATE HOST DUR T/TOOLS/ERR STARTED SESSION');
126
+ for (const s of d.sessions) {
127
+ const dur = fmtDur(s.durationMs || 0).padStart(6);
128
+ const cnt = `${s.counts.turns}/${s.counts.tools}/${s.counts.errors}`.padEnd(11);
129
+ const started = (s.startedAt ?? '').slice(0, 19).padEnd(20);
130
+ console.log(` ${s.state.padEnd(10)} ${s.host.padEnd(13)} ${dur} ${cnt} ${started} ${String(s.id).slice(0, 8)}`);
131
+ }
132
+ console.log(`\n${d.sessions.length} session(s) — inspect one: zuzuu session inspect <id>`);
133
+ }
134
+
135
+ /** `zuzuu session inspect <id> [--json]` — print one session's document. */
136
+ export function sessionInspect(args = {}) {
137
+ const cwd = process.cwd();
138
+ const id = args._?.[1];
139
+ const d = sessionInspectData(cwd, id);
140
+ if (!d) {
141
+ console.error(id ? `no recorded session matching '${id}'` : 'usage: zuzuu session inspect <id> [--json]');
142
+ process.exit(1);
143
+ }
144
+ if (args.json) { console.log(JSON.stringify(d)); return; }
145
+ const s = d.session;
146
+ console.log(`${s.id} — ${s.host} · ${s.state}`);
147
+ console.log(` started: ${s.startedAt ?? '?'} ended: ${s.endedAt ?? '?'} dur: ${fmtDur(s.durationMs || 0)}`);
148
+ console.log(` git: ${s.git?.commit ? s.git.commit.slice(0, 8) : '-'} (${s.git?.branch ?? '-'}) generation: ${s.generation ?? '-'}`);
149
+ console.log(` trace: ${d.trace.spans ?? '?'} span(s) · ${d.trace.tools} tool(s) · ${fmtDur(d.trace.duration || 0)}`);
150
+ const sigs = Object.entries(d.signals);
151
+ if (sigs.length) {
152
+ console.log(' signals:');
153
+ for (const [faculty, counts] of sigs) {
154
+ const parts = Object.entries(counts).map(([k, v]) => `${k} ${v}`).join(' · ');
155
+ console.log(` ${faculty.padEnd(13)} ${parts}`);
156
+ }
157
+ }
158
+ for (const w of d.warnings) console.log(` ⚠ ${w}`);
159
+ }
@@ -3,11 +3,11 @@
3
3
  import { existsSync } from 'node:fs';
4
4
  import { dirname } from 'node:path';
5
5
  import { detected } from '../capture/adapters/registry.mjs';
6
- import { sessionStatus } from '../session-git.mjs';
7
- import { readIndex, paths } from '../store.mjs';
6
+ import { sessionStatus } from '../sessions/session-git.mjs';
7
+ import { readIndex, paths } from '../core/store.mjs';
8
8
  import { FACULTIES } from '../faculty/contract.mjs';
9
9
  import { listProposals } from '../faculty/proposal.mjs';
10
- import { activeGeneration as activeGenerationFn } from '../faculty/generation.mjs';
10
+ import { activeGeneration as activeGenerationFn } from '../faculty/generation/read.mjs';
11
11
  import { detectDrift } from './doctor.mjs';
12
12
 
13
13
  const fmtDur = (ms) => (ms < 60_000 ? `${(ms / 1000).toFixed(0)}s` : `${(ms / 60_000).toFixed(1)}m`);
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { existsSync } from 'node:fs';
4
4
  import { loadSpans, renderTree } from '../capture/core/render.mjs';
5
- import { lastTrace } from '../store.mjs';
5
+ import { lastTrace } from '../core/store.mjs';
6
6
 
7
7
  export function trace(args) {
8
8
  let file = args._[0];
@@ -3,9 +3,9 @@
3
3
  // (`hook`/`reconcile`, statuses active/completed/abandoned). One proven path —
4
4
  // Design B: the hook never builds spans, it re-runs THIS.
5
5
 
6
- import { eventsToSpans } from './capture/core/spans.mjs';
7
- import { toExportRequest } from './capture/core/otlp.mjs';
8
- import { EventKind, Status } from './capture/core/event.mjs';
6
+ import { eventsToSpans } from '../capture/core/spans.mjs';
7
+ import { toExportRequest } from '../capture/core/otlp.mjs';
8
+ import { EventKind, Status } from '../capture/core/event.mjs';
9
9
  import { makeSession, SessionState } from './session.mjs';
10
10
  import { writeTrace, upsertSession, gitInfo } from './store.mjs';
11
11
 
@@ -11,7 +11,7 @@
11
11
  import { join, relative, resolve, isAbsolute } from 'node:path';
12
12
  import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, writeFileSync, renameSync } from 'node:fs';
13
13
  import { spawnSync } from 'node:child_process';
14
- import { writeNdjson } from './capture/core/otlp.mjs';
14
+ import { writeNdjson } from '../capture/core/otlp.mjs';
15
15
 
16
16
  const INDEX_VERSION = 1;
17
17
 
@@ -0,0 +1,96 @@
1
+ // zuzuu/digest/compose.mjs
2
+ // The grounding digest — a pure, deterministic, zero-network, no-model brief of
3
+ // the faculty home, injected at session start. Returns { text, sections }.
4
+ // I/O-free: callers (the CLI + the SessionStart hook) handle output.
5
+ //
6
+ // Composition (the Faculty Module contract): each built-in module exports its
7
+ // own digestSection(agentDir, ctx); this file iterates the registry and stacks
8
+ // the sections in the canonical order — instructions → knowledge → actions →
9
+ // proposals (spine-level) → guardrails — then a default "N item(s)" section
10
+ // for every DECLARATIVE faculty. Every hook call rides registry.invoke
11
+ // (fail-soft): a single broken faculty never sinks the whole digest.
12
+
13
+ import { listProposals } from '../knowledge/proposals.mjs';
14
+ import { listFacultyItems } from '../faculty/items.mjs';
15
+ import { facultiesOf, invoke } from '../faculty/registry.mjs';
16
+
17
+ // The canonical section order (instructions/knowledge/actions render above the
18
+ // proposals block; guardrails closes the brief — preserved pre-module layout).
19
+ const HEAD_SECTIONS = ['instructions', 'knowledge', 'actions'];
20
+ const TAIL_SECTIONS = ['guardrails'];
21
+
22
+ function proposalsSection(agentDir) {
23
+ try {
24
+ // count only pending — defensive if listProposals ever returns archived too
25
+ const pending = listProposals(agentDir).filter((p) => p.status === 'pending');
26
+ return { pending: pending.length };
27
+ } catch {
28
+ return { pending: 0 };
29
+ }
30
+ }
31
+
32
+ /** Run one faculty's digestSection hook fail-soft; null = no section. */
33
+ function sectionOf(entry, agentDir, ctx) {
34
+ const r = invoke(entry, 'digestSection', agentDir, ctx);
35
+ if (!r.ok || !r.value || !Array.isArray(r.value.lines)) return null;
36
+ return r.value;
37
+ }
38
+
39
+ /** The default section a faculty WITHOUT a digest hook gets: "N item(s)". */
40
+ function defaultSection(agentDir, entry) {
41
+ let count = 0;
42
+ try {
43
+ count = listFacultyItems(agentDir, entry.id, { itemsDir: entry.manifest?.itemsDir }).items.length;
44
+ } catch { /* unreadable → 0 */ }
45
+ return { lines: [`## ${entry.manifest?.title ?? entry.id}`, `${count} item(s)`], data: { count } };
46
+ }
47
+
48
+ /**
49
+ * Compute the digest for a faculty home.
50
+ * @param {string} agentDir path to the .zuzuu/ directory
51
+ * @param {{ knowledgeLimit?: number, budget?: number }} options
52
+ * @returns {{ text: string, sections: object }}
53
+ */
54
+ export function computeDigest(agentDir, { knowledgeLimit = 5, budget = 1500 } = {}) {
55
+ const charBudget = budget * 4;
56
+ const sections = {};
57
+ const lines = ['# zuzuu faculty digest', ''];
58
+
59
+ const faculties = facultiesOf(agentDir);
60
+ const byId = new Map(faculties.map((f) => [f.id, f]));
61
+ const ctx = () => ({ limit: knowledgeLimit, charBudget, priorLines: lines });
62
+
63
+ for (const id of HEAD_SECTIONS) {
64
+ const s = sectionOf(byId.get(id), agentDir, ctx());
65
+ if (!s) continue;
66
+ sections[id] = s.data;
67
+ if (s.lines.length) lines.push(...s.lines, '');
68
+ }
69
+
70
+ // Proposals — spine-level (cross-faculty pending count lives with the gate).
71
+ const proposals = proposalsSection(agentDir);
72
+ sections.proposals = proposals;
73
+ if (proposals.pending > 0) {
74
+ lines.push('## Proposals');
75
+ lines.push(`${proposals.pending} proposal(s) await your approval — run \`zuzuu review\`; approving mints a generation (your checkpoint).`);
76
+ lines.push('');
77
+ }
78
+
79
+ for (const id of TAIL_SECTIONS) {
80
+ const s = sectionOf(byId.get(id), agentDir, ctx());
81
+ if (!s) continue;
82
+ sections[id] = s.data;
83
+ if (s.lines.length) lines.push(...s.lines, '');
84
+ }
85
+
86
+ // Declarative faculties (manifest-only): the default "N item(s)" line each —
87
+ // a faculty you drop into the home is mentioned in the very next brief.
88
+ for (const entry of faculties) {
89
+ if (!entry.declarative || entry.manifestError) continue;
90
+ const s = sectionOf(entry, agentDir, ctx()) ?? defaultSection(agentDir, entry);
91
+ sections[entry.id] = s.data;
92
+ if (s.lines.length) lines.push(...s.lines, '');
93
+ }
94
+
95
+ return { text: lines.join('\n').trimEnd() + '\n', sections };
96
+ }
@@ -1,8 +1,14 @@
1
1
  // zuzuu/eval/score.mjs
2
2
  // Mechanical scorer — weighted sum of normalized signals → { score, confidence, rationale, signals }.
3
3
  // Pure; deterministic; no FS, no Date.now(), no Math.random().
4
+ //
5
+ // Faculty Module hook: a module may export evalSignals(proposal) → PARTIAL
6
+ // signals; those overlay the mechanical extraction (fail-soft via the
7
+ // registry's invoke — a broken hook leaves the default scorer untouched).
8
+ // No built-in implements it today, so behavior is unchanged.
4
9
 
5
10
  import { extractSignals } from './signals.mjs';
11
+ import { BUILTIN_MODULES, invoke } from '../faculty/registry.mjs';
6
12
 
7
13
  // Weight vector (must sum to 1.0).
8
14
  const W = {
@@ -60,7 +66,14 @@ function buildRationale(s) {
60
66
  * @returns {{ score: number, confidence: string, rationale: string, signals: object }}
61
67
  */
62
68
  export function mechanicalScore(proposal, opts = {}) {
63
- const s = extractSignals(proposal, opts);
69
+ let s = extractSignals(proposal, opts);
70
+
71
+ // Optional per-faculty evalSignals hook — partial overlay, fail-soft.
72
+ const mod = BUILTIN_MODULES[proposal?.faculty];
73
+ if (mod && typeof mod.evalSignals === 'function') {
74
+ const r = invoke({ id: proposal.faculty, module: mod }, 'evalSignals', proposal);
75
+ if (r.ok && r.value && typeof r.value === 'object') s = { ...s, ...r.value };
76
+ }
64
77
 
65
78
  const raw =
66
79
  W.occurrence * s.occurrence +