@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
@@ -0,0 +1,207 @@
1
+ // zuzuu/faculty/generation/write.mjs — the generation WRITE side (WS3-T1,
2
+ // split per the 2026-06-13 overhaul): agent identity repair, minting (freeze +
3
+ // snapshot + flip active) and rollback (restore by content). Read-side paths,
4
+ // enumerators and diffing live in read.mjs.
5
+
6
+ import { join, dirname } from 'node:path';
7
+ import {
8
+ existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync, renameSync,
9
+ } from 'node:fs';
10
+ import { reindex } from '../../knowledge/index.mjs';
11
+ import {
12
+ snapshotsDir, activePath, lockfilePath, agentJsonPath, readJson,
13
+ knowledgeFiles, memoryFiles, actionFiles, guardrailFiles, instructionFiles,
14
+ snapshotFaculties, agentId, listGenerations, readGeneration,
15
+ } from './read.mjs';
16
+
17
+ const writeJson = (p, obj) => {
18
+ mkdirSync(dirname(p), { recursive: true });
19
+ writeFileSync(p, JSON.stringify(obj, null, 2) + '\n');
20
+ };
21
+
22
+ /** Add/repair the agent block in agent.json (bump to v2), preserving other fields. */
23
+ export function ensureAgent(agentDir) {
24
+ const path = agentJsonPath(agentDir);
25
+ const m = existsSync(path) ? readJson(path) : {};
26
+ const id = agentId(agentDir);
27
+ if (!m.agent || !m.agent.id) {
28
+ m.agent = { id, createdAt: new Date().toISOString() };
29
+ }
30
+ m.version = 2;
31
+ writeJson(path, m);
32
+ return m.agent;
33
+ }
34
+
35
+ function nextGenId(agentDir) {
36
+ const ids = listGenerations(agentDir);
37
+ const max = ids.reduce((m, id) => Math.max(m, parseInt(id.slice(4), 10) || 0), 0);
38
+ return 'gen_' + String(max + 1).padStart(3, '0');
39
+ }
40
+
41
+ // --- mint -------------------------------------------------------------------
42
+
43
+ function copySnapshot(agentDir, id) {
44
+ const base = join(snapshotsDir(agentDir), id);
45
+ for (const it of knowledgeFiles(agentDir)) {
46
+ const dest = join(base, 'knowledge', it.rel);
47
+ mkdirSync(dirname(dest), { recursive: true });
48
+ writeFileSync(dest, readFileSync(it.src));
49
+ }
50
+ for (const it of memoryFiles(agentDir)) {
51
+ const dest = join(base, 'memory', it.rel);
52
+ mkdirSync(dirname(dest), { recursive: true });
53
+ writeFileSync(dest, readFileSync(it.src));
54
+ }
55
+ for (const a of actionFiles(agentDir)) {
56
+ for (const rel of a.files) {
57
+ const dest = join(base, 'actions', a.id, rel);
58
+ mkdirSync(dirname(dest), { recursive: true });
59
+ writeFileSync(dest, readFileSync(join(a.adir, rel)));
60
+ }
61
+ }
62
+ for (const it of guardrailFiles(agentDir)) {
63
+ const dest = join(base, 'guardrails', it.rel);
64
+ mkdirSync(dirname(dest), { recursive: true });
65
+ writeFileSync(dest, readFileSync(it.src));
66
+ }
67
+ for (const it of instructionFiles(agentDir)) {
68
+ const dest = join(base, 'instructions', it.rel);
69
+ mkdirSync(dirname(dest), { recursive: true });
70
+ writeFileSync(dest, readFileSync(it.src));
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Mint a new generation: freeze the current faculty state into a content-addressed
76
+ * lockfile + a byte-for-byte snapshot, and make it active.
77
+ */
78
+ export function mintGeneration(agentDir, { forkedFrom = null, mintedFrom = [] } = {}) {
79
+ const agent = ensureAgent(agentDir).id;
80
+ const id = nextGenId(agentDir);
81
+ const lockfile = {
82
+ id,
83
+ agent,
84
+ mintedAt: new Date().toISOString(),
85
+ forkedFrom,
86
+ mintedFrom,
87
+ faculties: snapshotFaculties(agentDir),
88
+ };
89
+ copySnapshot(agentDir, id);
90
+ writeJson(lockfilePath(agentDir, id), lockfile);
91
+ writeJson(activePath(agentDir), { active: id });
92
+ return lockfile;
93
+ }
94
+
95
+ // --- rollback ---------------------------------------------------------------
96
+
97
+ function archive(agentDir, faculty, src) {
98
+ // Park (never delete) under <faculty>/_rolledback/<basename> — by basename so
99
+ // a restore is a simple, flat audit trail of what the rollback displaced.
100
+ const dest = join(agentDir, faculty, '_rolledback', src.slice(dirname(src).length + 1));
101
+ mkdirSync(dirname(dest), { recursive: true });
102
+ renameSync(src, dest);
103
+ }
104
+
105
+ /**
106
+ * Restore a past generation by content: write each snapshotted item back to its
107
+ * live faculty path; MOVE (never delete) active items absent from the target into
108
+ * <faculty>/_rolledback/; reindex knowledge; flip the active pointer.
109
+ */
110
+ export function rollback(agentDir, id) {
111
+ const target = readGeneration(agentDir, id);
112
+ if (!target) throw new Error(`no generation '${id}'`);
113
+ const base = join(snapshotsDir(agentDir), id);
114
+ let restored = 0;
115
+
116
+ // 1) restore snapshotted knowledge items
117
+ const targetKnowledge = new Set((target.faculties.knowledge?.items ?? []).map((i) => i.id));
118
+ for (const i of target.faculties.knowledge?.items ?? []) {
119
+ const snap = join(base, 'knowledge', `${i.id}.md`);
120
+ if (existsSync(snap)) {
121
+ const dest = join(agentDir, 'knowledge', 'items', `${i.id}.md`);
122
+ mkdirSync(dirname(dest), { recursive: true });
123
+ writeFileSync(dest, readFileSync(snap));
124
+ restored++;
125
+ }
126
+ }
127
+ // archive live knowledge items not in the target
128
+ const kdir = join(agentDir, 'knowledge', 'items');
129
+ if (existsSync(kdir)) {
130
+ for (const e of readdirSync(kdir, { withFileTypes: true })) {
131
+ if (e.isFile() && e.name.endsWith('.md') && !targetKnowledge.has(e.name.replace(/\.md$/, ''))) {
132
+ archive(agentDir, 'knowledge', join(kdir, e.name));
133
+ }
134
+ }
135
+ }
136
+
137
+ // 2) restore snapshotted memory items + archive extras
138
+ const targetMemory = new Set((target.faculties.memory?.items ?? []).map((i) => i.id));
139
+ for (const i of target.faculties.memory?.items ?? []) {
140
+ const snap = join(base, 'memory', `${i.id}.md`);
141
+ if (existsSync(snap)) {
142
+ const dest = join(agentDir, 'memory', 'entries', `${i.id}.md`);
143
+ mkdirSync(dirname(dest), { recursive: true });
144
+ writeFileSync(dest, readFileSync(snap));
145
+ restored++;
146
+ }
147
+ }
148
+ const mdir = join(agentDir, 'memory', 'entries');
149
+ if (existsSync(mdir)) {
150
+ for (const e of readdirSync(mdir, { withFileTypes: true })) {
151
+ if (e.isFile() && e.name.endsWith('.md') && !targetMemory.has(e.name.replace(/\.md$/, ''))) {
152
+ archive(agentDir, 'memory', join(mdir, e.name));
153
+ }
154
+ }
155
+ }
156
+
157
+ // 3) restore snapshotted actions + archive extras
158
+ const targetActions = new Set((target.faculties.actions?.items ?? []).map((i) => i.id));
159
+ const asnap = join(base, 'actions');
160
+ if (existsSync(asnap)) {
161
+ for (const slugEnt of readdirSync(asnap, { withFileTypes: true })) {
162
+ if (!slugEnt.isDirectory()) continue;
163
+ const sdir = join(asnap, slugEnt.name);
164
+ for (const f of readdirSync(sdir)) {
165
+ const dest = join(agentDir, 'actions', slugEnt.name, f);
166
+ mkdirSync(dirname(dest), { recursive: true });
167
+ writeFileSync(dest, readFileSync(join(sdir, f)));
168
+ }
169
+ restored++;
170
+ }
171
+ }
172
+ const adir = join(agentDir, 'actions');
173
+ if (existsSync(adir)) {
174
+ for (const e of readdirSync(adir, { withFileTypes: true })) {
175
+ if (e.isDirectory() && e.name !== 'inbox' && e.name !== 'proposals' && e.name !== '_rolledback' && !targetActions.has(e.name)) {
176
+ archive(agentDir, 'actions', join(adir, e.name));
177
+ }
178
+ }
179
+ }
180
+
181
+ // 4) restore guardrails + instructions items (same item-list contract)
182
+ for (const [faculty, liveSeg] of [['guardrails', ['guardrails', 'items']], ['instructions', ['instructions', 'items']]]) {
183
+ const targetIds = new Set((target.faculties[faculty]?.items ?? []).map((i) => i.id));
184
+ for (const i of target.faculties[faculty]?.items ?? []) {
185
+ const snap = join(base, faculty, `${i.id}.md`);
186
+ if (existsSync(snap)) {
187
+ const dest = join(agentDir, ...liveSeg, `${i.id}.md`);
188
+ mkdirSync(dirname(dest), { recursive: true });
189
+ writeFileSync(dest, readFileSync(snap));
190
+ restored++;
191
+ }
192
+ }
193
+ const liveDir = join(agentDir, ...liveSeg);
194
+ if (existsSync(liveDir)) {
195
+ for (const e of readdirSync(liveDir, { withFileTypes: true })) {
196
+ if (e.isFile() && e.name.endsWith('.md') && !targetIds.has(e.name.replace(/\.md$/, ''))) {
197
+ archive(agentDir, faculty, join(liveDir, e.name));
198
+ }
199
+ }
200
+ }
201
+ }
202
+
203
+ // 5) regenerate the derived knowledge index + flip the pointer
204
+ try { reindex(agentDir); } catch { /* derived index; tolerate absence of node:sqlite features */ }
205
+ writeJson(activePath(agentDir), { active: id });
206
+ return { ok: true, restored };
207
+ }
@@ -0,0 +1,81 @@
1
+ // zuzuu/faculty/items.mjs — where each faculty's envelope items live (W24).
2
+ //
3
+ // One standard, five homes:
4
+ // knowledge → knowledge/items/<id>.md
5
+ // memory → memory/entries/<id>.md
6
+ // instructions → instructions/items/<id>.md
7
+ // guardrails → guardrails/items/<id>.md
8
+ // actions → actions/<id>/ACTION.md (dir-shaped: scripts stay siblings)
9
+ //
10
+ // Listing is fail-soft: unparseable files are collected as errors, never thrown
11
+ // (mirrors knowledge allItems — audit surfaces them).
12
+
13
+ import { join, dirname } from 'node:path';
14
+ import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync, statSync } from 'node:fs';
15
+ import { parseEnvelope, serializeEnvelope } from './envelope.mjs';
16
+
17
+ /** Flat item dirs per faculty (actions are dir-shaped — see itemPathFor). */
18
+ const ITEM_DIRS = {
19
+ knowledge: ['knowledge', 'items'],
20
+ memory: ['memory', 'entries'],
21
+ instructions: ['instructions', 'items'],
22
+ guardrails: ['guardrails', 'items'],
23
+ };
24
+
25
+ /** The flat items dir for a faculty, or null for dir-shaped faculties (actions).
26
+ * Unknown (declarative) faculties default to `<faculty>/<itemsDir||'items'>`
27
+ * under the home — manifest-only faculties get items listing for free. */
28
+ export function itemsDirFor(agentDir, faculty, itemsDir) {
29
+ if (faculty === 'actions') return null;
30
+ const rel = ITEM_DIRS[faculty];
31
+ if (rel) return join(agentDir, ...rel);
32
+ const sub = itemsDir && itemsDir !== '.' ? itemsDir : 'items';
33
+ return join(agentDir, faculty, sub);
34
+ }
35
+
36
+ /** Canonical envelope file path for one item. */
37
+ export function itemPathFor(agentDir, faculty, id) {
38
+ if (faculty === 'actions') return join(agentDir, 'actions', id, 'ACTION.md');
39
+ return join(itemsDirFor(agentDir, faculty), `${id}.md`);
40
+ }
41
+
42
+ /**
43
+ * All envelope items of a faculty. Parse errors collected, never thrown.
44
+ * @param {{itemsDir?: string}} [opts] declarative faculties pass manifest.itemsDir
45
+ * @returns {{items: object[], errors: Array<{file: string, error: string}>}}
46
+ */
47
+ export function listFacultyItems(agentDir, faculty, opts = {}) {
48
+ const items = [];
49
+ const errors = [];
50
+ if (faculty === 'actions') {
51
+ const base = join(agentDir, 'actions');
52
+ if (!existsSync(base)) return { items, errors };
53
+ for (const name of readdirSync(base).sort()) {
54
+ if (name === 'inbox' || name === 'proposals' || name === '_rolledback') continue;
55
+ const p = join(base, name, 'ACTION.md');
56
+ let isDir = false;
57
+ try { isDir = statSync(join(base, name)).isDirectory(); } catch { /* skip */ }
58
+ if (!isDir || !existsSync(p)) continue;
59
+ const { ok, item, errors: errs } = parseEnvelope(readFileSync(p, 'utf8'));
60
+ if (ok) items.push(item);
61
+ else errors.push({ file: `${name}/ACTION.md`, error: errs[0] ?? 'parse error' });
62
+ }
63
+ return { items, errors };
64
+ }
65
+ const dir = itemsDirFor(agentDir, faculty, opts.itemsDir);
66
+ if (!dir || !existsSync(dir)) return { items, errors };
67
+ for (const f of readdirSync(dir).filter((f) => f.endsWith('.md')).sort()) {
68
+ const { ok, item, errors: errs } = parseEnvelope(readFileSync(join(dir, f), 'utf8'));
69
+ if (ok) items.push(item);
70
+ else errors.push({ file: f, error: errs[0] ?? 'parse error' });
71
+ }
72
+ return { items, errors };
73
+ }
74
+
75
+ /** Write one envelope item to its canonical path. Returns the path. */
76
+ export function writeFacultyItem(agentDir, item) {
77
+ const path = itemPathFor(agentDir, item.faculty, item.id);
78
+ mkdirSync(dirname(path), { recursive: true });
79
+ writeFileSync(path, serializeEnvelope(item));
80
+ return path;
81
+ }
@@ -0,0 +1,74 @@
1
+ // zuzuu/faculty/module.mjs — the Faculty Module contract (2026-06-13 spec).
2
+ //
3
+ // A faculty = a MANIFEST (<faculty>/faculty.json in the home) + an ITEMS
4
+ // collection (Faculty Standard envelopes under manifest.itemsDir) + a set of
5
+ // named HOOK exports (built-ins: zuzuu/faculties/<id>/index.mjs):
6
+ //
7
+ // manifest — the machine contract (this file's shape)
8
+ // miner = { faculty, aggregate, propose } — REQUIRED for code modules
9
+ // digestSection(agentDir, ctx) — optional; default = "N item(s)" line
10
+ // evalSignals(proposal) — optional; default mechanical scorer
11
+ // gate(toolCall) — optional; ONLY guardrails today
12
+ // applyProposal(agentDir, proposal) — the adapter's apply
13
+ // validate(agentDir, payload) — the adapter's validate
14
+ //
15
+ // Host law (fail-soft everywhere): every hook call is try-wrapped (+ time-boxed
16
+ // for miner-class hooks) by the registry — a broken faculty module degrades to
17
+ // items-only, never crashes the CLI, the gate, or a host hook.
18
+ //
19
+ // Manifest-only folders (faculty.json with NO code) are DECLARATIVE faculties:
20
+ // they get items listing, card UI, schema validation and the default digest
21
+ // line today; third-party CODE loading is deferred (W4).
22
+
23
+ /** The module-API version this host speaks. Majors above this are skipped. */
24
+ export const CONTRACT_VERSION = 1;
25
+
26
+ /**
27
+ * Normalize a raw manifest object into the full contract shape (defaults
28
+ * filled, id forced to the faculty's directory name when absent). Pure.
29
+ * @param {object} raw parsed faculty.json (or {})
30
+ * @param {string} dirName the faculty folder name (fallback id)
31
+ */
32
+ export function normalizeManifest(raw = {}, dirName = 'faculty') {
33
+ const id = typeof raw.id === 'string' && raw.id ? raw.id : dirName;
34
+ const title = typeof raw.title === 'string' && raw.title ? raw.title : id.charAt(0).toUpperCase() + id.slice(1);
35
+ return {
36
+ id,
37
+ title,
38
+ tagline: typeof raw.tagline === 'string' ? raw.tagline : '',
39
+ version: typeof raw.version === 'string' ? raw.version : '1.0.0',
40
+ contract: Number.isFinite(raw.contract) ? raw.contract : CONTRACT_VERSION,
41
+ kinds: Array.isArray(raw.kinds) ? raw.kinds.map(String) : [],
42
+ itemsDir: typeof raw.itemsDir === 'string' && raw.itemsDir ? raw.itemsDir : 'items',
43
+ schema: typeof raw.schema === 'string' && raw.schema ? raw.schema : 'schema.json',
44
+ hooks: {
45
+ miner: !!raw.hooks?.miner,
46
+ digest: !!raw.hooks?.digest,
47
+ eval: !!raw.hooks?.eval,
48
+ gate: !!raw.hooks?.gate,
49
+ },
50
+ ui: {
51
+ icon: typeof raw.ui?.icon === 'string' ? raw.ui.icon : 'folder',
52
+ accent: typeof raw.ui?.accent === 'string' ? raw.ui.accent : 'neutral',
53
+ teaching: typeof raw.ui?.teaching === 'string' ? raw.ui.teaching : '',
54
+ },
55
+ };
56
+ }
57
+
58
+ /** Is a manifest's contract version one this host can serve? (major gate) */
59
+ export function compatibleContract(manifest) {
60
+ return Math.floor(manifest?.contract ?? CONTRACT_VERSION) <= CONTRACT_VERSION;
61
+ }
62
+
63
+ /**
64
+ * Validate a manifest's required fields. Returns {ok, errors} — never throws.
65
+ * (Normalization already fills defaults; this flags the truly broken.)
66
+ */
67
+ export function validateManifest(manifest) {
68
+ const errors = [];
69
+ if (!manifest || typeof manifest !== 'object') return { ok: false, errors: ['not an object'] };
70
+ if (!manifest.id || !/^[a-z0-9][a-z0-9_-]*$/.test(manifest.id)) errors.push(`id must be a slug (got '${manifest.id}')`);
71
+ if (!manifest.title) errors.push('title is required');
72
+ if (!compatibleContract(manifest)) errors.push(`contract ${manifest.contract} > host contract ${CONTRACT_VERSION} — skipped`);
73
+ return { ok: errors.length === 0, errors };
74
+ }
@@ -0,0 +1,63 @@
1
+ // zuzuu/faculty/pending.mjs — pending-proposal collection shared by the gate
2
+ // surfaces (`zuzuu review`, `zuzuu proposals`, `zuzuu eval`). One walk order,
3
+ // one ranked grouping; adapters with their own listProposals (dir-shaped
4
+ // faculties) override the spine read.
5
+
6
+ import * as registry from './registry.mjs';
7
+ import { listProposals as spineListProposals } from './proposal.mjs';
8
+ import { readIndex } from '../core/store.mjs';
9
+ import { rank } from '../eval/rank.mjs';
10
+ import { getScorer } from '../eval/score.mjs';
11
+
12
+ // Review walks faculties in a fixed order so piped sessions are deterministic
13
+ // (the combo smoke test feeds one stdin across the actions pass then knowledge).
14
+ export const REVIEW_ORDER = ['actions', 'knowledge', 'guardrails', 'instructions', 'memory'];
15
+
16
+ /** Build sessionMtimes map from the sessions index — best-effort, fail-open. */
17
+ export function buildSessionMtimes(cwd) {
18
+ try {
19
+ const idx = readIndex(cwd);
20
+ const map = {};
21
+ for (const s of idx.sessions ?? []) {
22
+ if (!s.id) continue;
23
+ const ms = s.startedAt ? Date.parse(s.startedAt) : 0;
24
+ if (!isNaN(ms) && ms > 0) map[s.id] = ms;
25
+ }
26
+ return map;
27
+ } catch {
28
+ return {};
29
+ }
30
+ }
31
+
32
+ /** Pending proposals for one adapter (dir-shaped adapters override listProposals). */
33
+ export function facultyPending(agentDir, a) {
34
+ if (typeof a.listProposals === 'function') return a.listProposals(agentDir);
35
+ // JSON-record faculties: read via the spine (records carry both the spine shape
36
+ // and the legacy candidate/er keys the knowledge card renders from).
37
+ return spineListProposals(agentDir, a.name);
38
+ }
39
+
40
+ /** Ordered list of adapters that have pending proposals to review. */
41
+ export function pendingByFaculty(agentDir) {
42
+ const adapters = registry.all();
43
+ const seen = new Set();
44
+ const ordered = [];
45
+ for (const name of REVIEW_ORDER) {
46
+ const a = adapters.find((x) => x.name === name);
47
+ if (a) { ordered.push(a); seen.add(name); }
48
+ }
49
+ for (const a of adapters) if (!seen.has(a.name)) ordered.push(a);
50
+ const sessionMtimes = buildSessionMtimes();
51
+ const now = Date.now();
52
+ const scorer = getScorer();
53
+ const out = [];
54
+ for (const a of ordered) {
55
+ let proposals = facultyPending(agentDir, a);
56
+ if (!proposals.length) continue;
57
+ // Rank proposals highest-score-first (display only — never changes approval/mint).
58
+ const ranked = rank(proposals, scorer, { now, sessionMtimes });
59
+ proposals = ranked.map((r) => r.proposal);
60
+ out.push({ adapter: a, proposals });
61
+ }
62
+ return out;
63
+ }
@@ -1,33 +1,219 @@
1
- // zuzuu/faculty/registry.mjs
2
- // Faculty adapter registry — a module-level Map keyed by adapter.name.
3
- // Adapters register themselves on import; consumers query by name or list all.
1
+ // zuzuu/faculty/registry.mjs — the Faculty Module registry (2026-06-13 spec).
4
2
  //
5
- // This is the registration surface only. Adapters are added in later work units.
3
+ // Replaces the old scattered wiring (miners/registry self-registration,
4
+ // per-faculty adapter imports at every spine call site, digest's hardcoded
5
+ // sections): the five BUILT-IN modules are imported statically here; the spine
6
+ // (gate/review/proposals/eval/distill/digest/doctor) iterates THIS registry —
7
+ // no faculty names hardcoded outside built-in module files (ordering
8
+ // preferences excepted).
9
+ //
10
+ // Discovery beyond built-ins: any `<home>/<dir>/faculty.json` is parsed and
11
+ // listed as a DECLARATIVE faculty (manifest-only — items listing, card UI,
12
+ // schema validation, default digest line work today). Third-party CODE loading
13
+ // is explicitly deferred to W4.
14
+ //
15
+ // Host law: every hook invocation goes through invoke()/invokeTimeboxed() —
16
+ // try-wrapped (+ 5s time-box on miner-class hooks). A broken module degrades
17
+ // to items-only; failures are recorded and surfaced by `zuzuu doctor`.
18
+
19
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
20
+ import { join } from 'node:path';
21
+ import { FACULTIES } from './contract.mjs';
22
+ import { normalizeManifest, compatibleContract } from './module.mjs';
23
+ import * as knowledge from '../faculties/knowledge/index.mjs';
24
+ import * as memory from '../faculties/memory/index.mjs';
25
+ import * as actions from '../faculties/actions/index.mjs';
26
+ import * as instructions from '../faculties/instructions/index.mjs';
27
+ import * as guardrails from '../faculties/guardrails/index.mjs';
28
+
29
+ export const BUILTIN_MODULES = { knowledge, memory, actions, instructions, guardrails };
30
+
31
+ // Legacy adapter/miner walk order (the pre-module import order at every call
32
+ // site) — preserved so list/eval/distill outputs stay byte-identical.
33
+ const LEGACY_ORDER = ['knowledge', 'actions', 'guardrails', 'instructions', 'memory'];
6
34
 
7
- /** @type {Map<string, object>} */
8
- const _registry = new Map();
35
+ // ---------------------------------------------------------------------------
36
+ // adapter surface (back-compat: gate/review/proposals/eval consume these)
37
+ // ---------------------------------------------------------------------------
38
+
39
+ /** Test/extension overrides layered over the built-ins. */
40
+ const overrides = new Map();
9
41
 
10
42
  /**
11
- * Register an adapter (keyed by adapter.name). Overwrites if already registered.
12
- * @param {{ name: string, [key: string]: any }} adapter
43
+ * Register an adapter override (keyed by adapter.name). Built-ins are always
44
+ * present; this layers replacements/additions on top (tests, future plugins).
13
45
  */
14
46
  export function register(adapter) {
15
- _registry.set(adapter.name, adapter);
47
+ overrides.set(adapter.name, adapter);
48
+ }
49
+
50
+ /** Retrieve an adapter by faculty name (override > built-in). */
51
+ export function get(name) {
52
+ return overrides.get(name) ?? BUILTIN_MODULES[name]?.adapter;
53
+ }
54
+
55
+ /** All adapters, built-ins first in the legacy order, then extra overrides. */
56
+ export function all() {
57
+ const names = [...LEGACY_ORDER];
58
+ for (const n of overrides.keys()) if (!names.includes(n)) names.push(n);
59
+ return names.map(get).filter(Boolean);
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // miner surface (replaces miners/registry.mjs)
64
+ // ---------------------------------------------------------------------------
65
+
66
+ /** All built-in miners in the legacy distill order. */
67
+ export function miners() {
68
+ return LEGACY_ORDER.map((f) => BUILTIN_MODULES[f]?.miner).filter(Boolean);
69
+ }
70
+
71
+ /** The miner for a faculty, or undefined. */
72
+ export function minerOf(faculty) {
73
+ return BUILTIN_MODULES[faculty]?.miner;
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // faculty discovery — built-ins + declarative faculty.json folders
78
+ // ---------------------------------------------------------------------------
79
+
80
+ /** Read + normalize `<home>/<id>/faculty.json` → {manifest|null, error|null}. */
81
+ function readHomeManifest(agentDir, id) {
82
+ const p = join(agentDir, id, 'faculty.json');
83
+ if (!existsSync(p)) return { manifest: null, error: null };
84
+ try {
85
+ const manifest = normalizeManifest(JSON.parse(readFileSync(p, 'utf8')), id);
86
+ if (!compatibleContract(manifest)) {
87
+ return { manifest: null, error: `contract ${manifest.contract} unsupported by this host` };
88
+ }
89
+ return { manifest, error: null };
90
+ } catch (e) {
91
+ return { manifest: null, error: e.message ?? String(e) };
92
+ }
16
93
  }
17
94
 
95
+ // Home dirs that are never faculties.
96
+ const NON_FACULTY_DIRS = new Set(['generations']);
97
+
18
98
  /**
19
- * Retrieve a registered adapter by name. Returns undefined if not found.
20
- * @param {string} name
21
- * @returns {{ name: string, [key: string]: any } | undefined}
99
+ * Every faculty this home serves: the five built-ins (always module code +
100
+ * manifest, home faculty.json overriding the built-in manifest when present)
101
+ * plus declarative manifest-only folders. Fail-soft: a broken faculty.json
102
+ * lists the faculty with `manifestError` (degraded — doctor reports it).
103
+ *
104
+ * @param {string} agentDir
105
+ * @returns {Array<{id, manifest, module, builtin, declarative, manifestSource, manifestError?}>}
22
106
  */
23
- export function get(name) {
24
- return _registry.get(name);
107
+ export function facultiesOf(agentDir) {
108
+ const out = [];
109
+ for (const id of FACULTIES) {
110
+ const mod = BUILTIN_MODULES[id];
111
+ const home = readHomeManifest(agentDir, id);
112
+ out.push({
113
+ id,
114
+ builtin: true,
115
+ declarative: false,
116
+ module: mod,
117
+ manifest: home.manifest ?? mod.manifest,
118
+ manifestSource: home.manifest ? 'home' : 'builtin',
119
+ ...(home.error ? { manifestError: home.error } : {}),
120
+ });
121
+ }
122
+ try {
123
+ for (const e of readdirSync(agentDir, { withFileTypes: true })) {
124
+ if (!e.isDirectory() || e.name.startsWith('.') || e.name.startsWith('_')) continue;
125
+ if (FACULTIES.includes(e.name) || NON_FACULTY_DIRS.has(e.name)) continue;
126
+ const home = readHomeManifest(agentDir, e.name);
127
+ if (!home.manifest && !home.error) continue; // no faculty.json → not a faculty
128
+ out.push({
129
+ id: e.name,
130
+ builtin: false,
131
+ declarative: true,
132
+ module: null, // third-party CODE loading deferred to W4 — manifest-only today
133
+ manifest: home.manifest ?? normalizeManifest({}, e.name),
134
+ manifestSource: 'home',
135
+ ...(home.error ? { manifestError: home.error } : {}),
136
+ });
137
+ }
138
+ } catch { /* no home dir yet → built-ins only */ }
139
+ return out;
140
+ }
141
+
142
+ /** One faculty entry by id (built-in or declarative), or null. */
143
+ export function facultyOf(agentDir, id) {
144
+ return facultiesOf(agentDir).find((f) => f.id === id) ?? null;
145
+ }
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // fail-soft hook invocation (+ degradation record for doctor)
149
+ // ---------------------------------------------------------------------------
150
+
151
+ /** Miner-class hooks get a wall-clock budget (spec: 5s). */
152
+ export const MINER_HOOK_TIMEOUT_MS = 5000;
153
+
154
+ const failures = new Map(); // `${faculty}.${hook}` → { faculty, hook, error, at }
155
+
156
+ function recordFailure(faculty, hook, error) {
157
+ failures.set(`${faculty}.${hook}`, { faculty, hook, error: String(error?.message ?? error), at: new Date().toISOString() });
158
+ }
159
+
160
+ /** Hook failures recorded this process — `zuzuu doctor` surfaces these. */
161
+ export function hookFailures() {
162
+ return [...failures.values()];
163
+ }
164
+
165
+ /** Tests only: forget recorded failures. */
166
+ export function clearHookFailures() {
167
+ failures.clear();
25
168
  }
26
169
 
27
170
  /**
28
- * Return all registered adapters as an array.
29
- * @returns {Array<{ name: string, [key: string]: any }>}
171
+ * Invoke a module hook fail-soft (synchronous spine paths: digest, gate,
172
+ * signals). NEVER throws.
173
+ * @param {{id:string, module:object|null}} entry a facultiesOf() entry (or {id, module})
174
+ * @returns {{ok:true, value:any} | {ok:false, missing?:true, error?:string}}
30
175
  */
31
- export function all() {
32
- return [..._registry.values()];
176
+ export function invoke(entry, hook, ...args) {
177
+ const fn = entry?.module?.[hook];
178
+ if (typeof fn !== 'function') return { ok: false, missing: true };
179
+ try {
180
+ return { ok: true, value: fn(...args) };
181
+ } catch (e) {
182
+ recordFailure(entry.id ?? entry.module?.manifest?.id ?? '?', hook, e);
183
+ return { ok: false, error: String(e?.message ?? e) };
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Invoke a miner-class hook fail-soft WITH a time-box: synchronous throws are
189
+ * caught; an async (Promise-returning) hook is raced against `timeoutMs`
190
+ * (default 5s). NEVER rejects.
191
+ * @returns {Promise<{ok:true, value:any} | {ok:false, missing?:true, timedOut?:true, error?:string}>}
192
+ */
193
+ export async function invokeTimeboxed(entry, hook, args = [], { timeoutMs = MINER_HOOK_TIMEOUT_MS } = {}) {
194
+ const fn = entry?.module?.[hook];
195
+ if (typeof fn !== 'function') return { ok: false, missing: true };
196
+ const faculty = entry.id ?? entry.module?.manifest?.id ?? '?';
197
+ try {
198
+ const r = fn(...args);
199
+ if (!r || typeof r.then !== 'function') return { ok: true, value: r };
200
+ let timer;
201
+ const timeout = new Promise((resolve) => {
202
+ timer = setTimeout(() => resolve({ __zuzuuTimeout: true }), timeoutMs);
203
+ });
204
+ const settled = await Promise.race([Promise.resolve(r).catch((e) => ({ __zuzuuError: e })), timeout]);
205
+ clearTimeout(timer);
206
+ if (settled && settled.__zuzuuTimeout) {
207
+ recordFailure(faculty, hook, `timed out after ${timeoutMs}ms`);
208
+ return { ok: false, timedOut: true, error: `timed out after ${timeoutMs}ms` };
209
+ }
210
+ if (settled && settled.__zuzuuError !== undefined) {
211
+ recordFailure(faculty, hook, settled.__zuzuuError);
212
+ return { ok: false, error: String(settled.__zuzuuError?.message ?? settled.__zuzuuError) };
213
+ }
214
+ return { ok: true, value: settled };
215
+ } catch (e) {
216
+ recordFailure(faculty, hook, e);
217
+ return { ok: false, error: String(e?.message ?? e) };
218
+ }
33
219
  }