@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,288 @@
1
+ // zuzuu/faculties/instructions/index.mjs — the Instructions faculty module.
2
+ //
3
+ // Consolidates the adapter (steering-amendment proposals → envelope items),
4
+ // the miner (recurring corrective user turns → amendment proposals, WS5-T4)
5
+ // and the digest section (the steering text the agent grounds on) behind the
6
+ // Faculty Module contract.
7
+ //
8
+ // An instructions proposal payload is a steering amendment:
9
+ // { id?, text } — a line or paragraph of steering
10
+ //
11
+ // apply: writes the amendment as a Faculty Standard envelope item under
12
+ // .zuzuu/instructions/items/<id>.md (kind: amendment; body = the text).
13
+ // The pinned steering itself lives at items/steering.md; future
14
+ // amendments are MORE items, never edits to steering. Idempotent: a
15
+ // text already present in any instructions item is not duplicated.
16
+
17
+ import { listFacultyItems, writeFacultyItem } from '../../faculty/items.mjs';
18
+ import { deriveTitle } from '../../faculty/envelope.mjs';
19
+ import { makeProposal, writeProposal, listProposals, isArchivedResolved } from '../../faculty/proposal.mjs';
20
+ import { slugify } from '../../knowledge/items.mjs';
21
+
22
+ const name = 'instructions';
23
+
24
+ export const manifest = {
25
+ id: 'instructions',
26
+ title: 'Instructions',
27
+ tagline: 'who to BE — steering and project conventions',
28
+ version: '1.0.0',
29
+ contract: 1,
30
+ kinds: ['steering', 'amendment'],
31
+ itemsDir: 'items',
32
+ schema: 'schema.json',
33
+ hooks: { miner: true, digest: true, eval: false, gate: false },
34
+ ui: { icon: 'compass', accent: 'warning', teaching: 'The pinned steering the agent is told at session start; corrections graduate into amendments here.' },
35
+ };
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // adapter contract
39
+ // ---------------------------------------------------------------------------
40
+
41
+ /**
42
+ * Ingest a raw amendment object. Pass-through: the payload IS the amendment.
43
+ */
44
+ function ingest(_agentDir, raw) {
45
+ const payload = raw?.payload ?? raw ?? {};
46
+ return { payload, analysis: {} };
47
+ }
48
+
49
+ /**
50
+ * Validate an amendment payload.
51
+ * @returns {{ok:boolean, errors:string[], warnings:string[]}}
52
+ */
53
+ export function validate(_agentDir, payload) {
54
+ const errors = [];
55
+ if (!payload?.text || !String(payload.text).trim()) {
56
+ errors.push('text is required (non-empty steering amendment)');
57
+ }
58
+ return { ok: errors.length === 0, errors, warnings: [] };
59
+ }
60
+
61
+ /**
62
+ * Apply an approved amendment: write an amendment item (idempotent on
63
+ * identical text — won't duplicate steering already present in any item).
64
+ * @returns {{ok:boolean, action:string, itemIds:string[]}}
65
+ */
66
+ function apply(agentDir, proposal) {
67
+ const text = String(proposal?.payload?.text ?? '').trim();
68
+
69
+ // Idempotence: skip if the exact text already lives in an instructions item
70
+ const { items } = listFacultyItems(agentDir, 'instructions');
71
+ if (items.some((i) => String(i.body ?? '').includes(text))) {
72
+ return { ok: true, action: 'amended instructions (already present)', itemIds: [] };
73
+ }
74
+
75
+ const id = proposal?.payload?.id || slugify(text, 50);
76
+ writeFacultyItem(agentDir, {
77
+ id,
78
+ faculty: name,
79
+ kind: 'amendment',
80
+ title: deriveTitle(text, id),
81
+ status: 'active',
82
+ created_at: new Date().toISOString().replace(/\.\d+Z$/, 'Z'),
83
+ provenance: Array.isArray(proposal?.provenance) ? proposal.provenance : [],
84
+ payload: { scope: 'project' },
85
+ body: text,
86
+ });
87
+
88
+ return { ok: true, action: 'amended instructions', itemIds: [id] };
89
+ }
90
+
91
+ export const applyProposal = apply;
92
+
93
+ /**
94
+ * Render an amendment proposal for the human gate.
95
+ * @returns {{line:string, card:string}}
96
+ */
97
+ function render(proposal) {
98
+ const text = proposal?.payload?.text ?? '';
99
+ const preview = text.slice(0, 80).replace(/\n/g, ' ');
100
+ return {
101
+ line: `[amendment] ${preview}`,
102
+ card: text,
103
+ };
104
+ }
105
+
106
+ export const adapter = { name, ingest, validate, apply, render };
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // miner (WS5-T4 — recurring corrective turns → amendment proposals, unchanged)
110
+ // ---------------------------------------------------------------------------
111
+
112
+ /**
113
+ * Normalise a correction text for grouping:
114
+ * lowercase, collapse whitespace, truncate to 200 chars.
115
+ *
116
+ * v1 grouping: near-identical normalised text (exact key match). Simple and
117
+ * deterministic; a fuzzy grouper can be earned later.
118
+ *
119
+ * @param {string} text
120
+ * @returns {string}
121
+ */
122
+ function normText(text) {
123
+ return String(text).toLowerCase().replace(/\s+/g, ' ').trim().slice(0, 200);
124
+ }
125
+
126
+ /**
127
+ * Derive a proposal id fragment from a normalised text key.
128
+ * Keep it stable, short, and filesystem-safe.
129
+ */
130
+ function instrId(normKey) {
131
+ // Use a slugified version of the first 60 chars of the normalised text,
132
+ // prefixed to make collisions with other faculties impossible.
133
+ const slug = normKey.slice(0, 60).replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 50) || 'instr';
134
+ return 'instr-' + slug;
135
+ }
136
+
137
+ /**
138
+ * Group correctionTurns from mined sessions; propose when a similar correction
139
+ * recurs across ≥minSessions (default 2) distinct sessions.
140
+ *
141
+ * @param {Array<{sessionId:string, correctionTurns:{text:string}[]}>} sessions
142
+ * @param {object} opts
143
+ * @param {number} [opts.minSessions=2] min distinct sessions with the same normalised correction
144
+ * @returns {Array<{payload:{text:string}, evidence:{occurrences:number, sessions:number}}>}
145
+ */
146
+ export function aggregate(sessions, { minSessions = 2 } = {}) {
147
+ // normalised text → { count, sessions: Set<sessionId>, rawText: string }
148
+ const stats = new Map();
149
+
150
+ for (const s of sessions) {
151
+ if (!Array.isArray(s.correctionTurns)) continue;
152
+ // Track distinct normalised texts per session to avoid double-counting
153
+ // the same session for the same correction text.
154
+ const seenInSession = new Set();
155
+ for (const { text } of s.correctionTurns) {
156
+ const key = normText(text);
157
+ if (!key) continue;
158
+ const st = stats.get(key) ?? { count: 0, sessions: new Set(), rawText: text };
159
+ st.count++;
160
+ st.sessions.add(s.sessionId);
161
+ seenInSession.add(key);
162
+ stats.set(key, st);
163
+ }
164
+ }
165
+
166
+ const candidates = [];
167
+ for (const [, st] of stats) {
168
+ if (st.sessions.size < minSessions) continue;
169
+
170
+ // Phrase the raw correction as an instruction for the steering amendment.
171
+ // The corrective turn text already reads like user guidance; use it directly
172
+ // (trimmed to 500 chars to match mineTranscript's cap).
173
+ const amendmentText = st.rawText.slice(0, 500).trim();
174
+ const id = instrId(normText(amendmentText));
175
+
176
+ candidates.push({
177
+ payload: { id, text: amendmentText },
178
+ evidence: { occurrences: st.count, sessions: st.sessions.size },
179
+ });
180
+ }
181
+
182
+ return candidates;
183
+ }
184
+
185
+ /**
186
+ * Write an instructions proposal into .zuzuu/instructions/proposals/ for each
187
+ * candidate.
188
+ *
189
+ * Idempotent:
190
+ * - skips if an instructions proposal with the same derived id already exists
191
+ * - skips if the text is already present in an instructions item (steering
192
+ * or a prior amendment)
193
+ * - skips if the id is already resolved in proposals/archive/ — a rejection
194
+ * is remembered; re-distilling never resurrects it
195
+ *
196
+ * @param {string} agentDir
197
+ * @param {ReturnType<typeof aggregate>} aggregated
198
+ * @returns {number} count of new proposals written
199
+ */
200
+ export function propose(agentDir, aggregated) {
201
+ // Collect ids of existing pending proposals for this faculty.
202
+ const existing = listProposals(agentDir, 'instructions');
203
+ const existingIds = new Set(existing.map((p) => p.payload?.id).filter(Boolean));
204
+
205
+ // Read the instructions items (steering + amendments) to skip applied text.
206
+ let appliedText = '';
207
+ try {
208
+ appliedText = listFacultyItems(agentDir, 'instructions').items.map((i) => i.body ?? '').join('\n');
209
+ } catch { appliedText = ''; }
210
+
211
+ let count = 0;
212
+ for (const c of aggregated) {
213
+ const { payload, evidence } = c;
214
+
215
+ // Idempotent: skip if already proposed.
216
+ if (existingIds.has(payload.id)) continue;
217
+
218
+ // Idempotent: skip if text already present in an instructions item.
219
+ if (appliedText.includes(payload.text)) continue;
220
+
221
+ const proposal = makeProposal({
222
+ faculty: 'instructions',
223
+ kind: 'block',
224
+ source: 'distill',
225
+ payload,
226
+ evidence,
227
+ });
228
+
229
+ // A rejection is remembered: never resurrect an archive-resolved id.
230
+ if (isArchivedResolved(agentDir, 'instructions', proposal.id)) continue;
231
+
232
+ writeProposal(agentDir, proposal);
233
+ count++;
234
+ }
235
+
236
+ return count;
237
+ }
238
+
239
+ export const miner = { faculty: name, aggregate, propose };
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // digest section (moved from the pre-module digest, byte-identical output)
243
+ // ---------------------------------------------------------------------------
244
+
245
+ const PLACEHOLDER_MARK = '<!-- Fill in:';
246
+
247
+ const INTERVIEW = [
248
+ 'Project steering is empty. Before substantive work, interview your human',
249
+ '(what is this project, its conventions, its priorities), draft the steering item',
250
+ '.zuzuu/instructions/items/steering.md from their answers, and get their approval.',
251
+ ].join(' ');
252
+
253
+ /** Read the instructions items (steering first, then amendments); classify
254
+ * empty vs steering text. Items are Faculty Standard envelopes (W24). */
255
+ function readInstructions(agentDir) {
256
+ let items = [];
257
+ try {
258
+ items = listFacultyItems(agentDir, 'instructions').items;
259
+ } catch { /* missing or unreadable → treat as empty */ }
260
+ // steering pins the top; amendments follow in id order (already sorted)
261
+ items.sort((a, b) => (a.kind === 'steering' ? -1 : 1) - (b.kind === 'steering' ? -1 : 1));
262
+ const bodies = items
263
+ .map((i) => String(i.body ?? ''))
264
+ .map((raw) => (raw.includes(PLACEHOLDER_MARK) ? '' : raw.replace(/^#.*$/gm, '').trim() && raw.trim()))
265
+ .filter(Boolean);
266
+ const text = bodies.join('\n\n');
267
+ return { empty: !text, text };
268
+ }
269
+
270
+ /**
271
+ * The Instructions digest section: the pinned steering (or the interview
272
+ * prompt when empty). Always renders — grounding starts here.
273
+ * @param {string} agentDir
274
+ * @returns {{lines: string[], data: object}}
275
+ */
276
+ export function digestSection(agentDir) {
277
+ const instr = readInstructions(agentDir);
278
+ return { lines: ['## Instructions', instr.empty ? INTERVIEW : instr.text], data: instr };
279
+ }
280
+
281
+ // ---------------------------------------------------------------------------
282
+ // session signals (the observability surface — `zuzuu session inspect`)
283
+ // ---------------------------------------------------------------------------
284
+
285
+ /** Counts of the mined-signal superset slices this faculty grows from. */
286
+ export function sessionSignals(signals = {}) {
287
+ return { correctionTurns: signals.correctionTurns?.length ?? 0 };
288
+ }
@@ -0,0 +1,185 @@
1
+ // zuzuu/faculties/knowledge/index.mjs — the Knowledge faculty module.
2
+ //
3
+ // Consolidates the faculty's spine surface behind the Faculty Module contract
4
+ // (faculty.json manifest + hook exports): the adapter (ingest/validate/apply/
5
+ // render — wraps the EXISTING Knowledge pipeline: proposals/ER/registry/items/
6
+ // index), the miner (the source-A distill path), and the digest section.
7
+ // Substrate code stays in zuzuu/knowledge/ — this module is the contract face.
8
+
9
+ import { resolve as erResolve } from '../../knowledge/er.mjs';
10
+ import { loadRegistry, validateItem } from '../../knowledge/registry.mjs';
11
+ import { allItems, slugify } from '../../knowledge/items.mjs';
12
+ import { applyKnowledgeProposal, createProposal } from '../../knowledge/proposals.mjs';
13
+ import { aggregate } from '../../knowledge/distill.mjs';
14
+
15
+ const name = 'knowledge';
16
+
17
+ export const manifest = {
18
+ id: 'knowledge',
19
+ title: 'Knowledge',
20
+ tagline: "what's TRUE — facts about this project",
21
+ version: '1.0.0',
22
+ contract: 1,
23
+ kinds: ['fact', 'entity', 'command', 'decision'],
24
+ itemsDir: 'items',
25
+ schema: 'schema.json',
26
+ hooks: { miner: true, digest: true, eval: false, gate: false },
27
+ ui: { icon: 'book', accent: 'info', teaching: 'Facts zuzuu learns from your sessions land here after your approval.' },
28
+ };
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // adapter (WS2-T2 — unchanged behaviour)
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /**
35
+ * Ingest a raw candidate: run ER against existing items and return the
36
+ * normalised payload + analysis. Mirrors what createProposal computes today.
37
+ * @param {string} agentDir
38
+ * @param {{candidate:object, source?:string, evidence?:object}} raw
39
+ */
40
+ function ingest(agentDir, raw) {
41
+ const { items } = allItems(agentDir);
42
+ const candidate = { ...raw.candidate };
43
+ candidate.id = candidate.id || slugify(candidate.body);
44
+ const er = erResolve(candidate, items);
45
+ return { payload: candidate, analysis: { er }, dedupeKey: candidate.id };
46
+ }
47
+
48
+ /**
49
+ * Validate an item against the Knowledge registry.
50
+ * @returns {{ok:boolean, errors:string[], warnings:string[]}}
51
+ */
52
+ export function validate(agentDir, payload) {
53
+ const reg = loadRegistry(agentDir);
54
+ const v = validateItem(reg, payload);
55
+ const warnings = [
56
+ ...v.unknownKeys.attributes.map((k) => `unregistered attribute '${k}'`),
57
+ ...v.unknownKeys.relations.map((t) => `unregistered relation type '${t}'`),
58
+ ];
59
+ return { ok: v.ok, errors: v.errors, warnings };
60
+ }
61
+
62
+ /**
63
+ * Apply an approved proposal — delegates to the extracted approve apply body.
64
+ * @returns {{ok:boolean, action:string, itemIds:string[], warnings:string[]}}
65
+ */
66
+ function apply(agentDir, proposal) {
67
+ // Bridge spine-shaped records (payload/analysis.er) onto applyKnowledgeProposal's
68
+ // legacy shape (candidate/er). Records that still carry candidate/er pass through.
69
+ const legacy = {
70
+ ...proposal,
71
+ candidate: proposal.candidate ?? proposal.payload,
72
+ er: proposal.er ?? proposal.analysis?.er,
73
+ };
74
+ const r = applyKnowledgeProposal(agentDir, legacy);
75
+ return {
76
+ ok: r.ok,
77
+ action: r.action,
78
+ itemIds: r.item ? [r.item] : [],
79
+ warnings: r.warnings ?? [],
80
+ };
81
+ }
82
+
83
+ export const applyProposal = apply;
84
+
85
+ /**
86
+ * Render a proposal for the human gate. `card` mirrors the multi-line summary
87
+ * `zuzuu review` shows for knowledge proposals (id, type, attrs/relations, ER
88
+ * verdict); `line` is the one-line list form (`zuzuu proposals list`).
89
+ * @returns {{line:string, card:string}}
90
+ */
91
+ function render(proposal) {
92
+ if (proposal.kind === 'registry') {
93
+ const what = `register ${String(proposal.registry).slice(0, -1)} '${proposal.key}'`;
94
+ return {
95
+ line: `${proposal.id} [${proposal.kind}] ${what}`,
96
+ card: `${what} (seen ${proposal.evidence?.occurrences}× in candidates)`,
97
+ };
98
+ }
99
+ const c = proposal.candidate ?? {};
100
+ const er = proposal.er ?? {};
101
+ const lines = [];
102
+ lines.push(`${c.id ?? ''} ── ${c.type}: ${c.body?.slice(0, 100).replace(/\n/g, ' ')}`);
103
+ for (const [k, v] of Object.entries(c.attributes ?? {})) lines.push(` · ${k} = ${v}`);
104
+ for (const r of c.relations ?? []) lines.push(` → ${r.type} ${r.target}`);
105
+ lines.push(` er: ${er.verdict}${er.match ? ` → ${er.match}` : ''} (${(er.confidence ?? 0).toFixed(2)} · ${er.reason ?? ''})`);
106
+ return {
107
+ line: `${proposal.id} [${er.verdict ?? proposal.kind}] ${c.type}: ${c.body?.slice(0, 60).replace(/\n/g, ' ')}`,
108
+ card: lines.join('\n'),
109
+ };
110
+ }
111
+
112
+ export const adapter = { name, ingest, validate, apply, render };
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // miner (WS5-T1 — the golden source-A distill path, unchanged)
116
+ // ---------------------------------------------------------------------------
117
+
118
+ /** File one knowledge proposal per aggregated candidate; return the count of
119
+ * actually-filed proposals (archive-resolved ids are skipped, not re-filed). */
120
+ export function propose(agentDir, aggregated) {
121
+ let count = 0;
122
+ for (const c of aggregated) {
123
+ const p = createProposal(agentDir, { candidate: c.candidate, source: 'distill', evidence: c.evidence });
124
+ if (p && p.status !== 'archived-skip') count++;
125
+ }
126
+ return count;
127
+ }
128
+
129
+ export const miner = { faculty: name, aggregate, propose };
130
+
131
+ export { aggregate };
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // digest section (moved from the pre-module digest, byte-identical output)
135
+ // ---------------------------------------------------------------------------
136
+
137
+ /**
138
+ * The Knowledge digest section: most-recent items under the shared char budget.
139
+ * @param {string} agentDir
140
+ * @param {{limit:number, charBudget:number, priorLines:string[]}} ctx
141
+ * @returns {{lines: string[], data: object}}
142
+ */
143
+ export function digestSection(agentDir, { limit, charBudget, priorLines }) {
144
+ let knowledge;
145
+ try {
146
+ const { items } = allItems(agentDir);
147
+ const ranked = [...items]
148
+ .sort((a, b) => String(b.created_at).localeCompare(String(a.created_at)))
149
+ .slice(0, limit);
150
+ knowledge = { count: items.length, shown: ranked.map((i) => ({ id: i.id, type: i.type, body: i.body })) };
151
+ } catch {
152
+ knowledge = { count: 0, shown: [] };
153
+ }
154
+ const lines = ['## Knowledge'];
155
+ if (!knowledge.count) {
156
+ lines.push('(no items yet — propose facts to knowledge/inbox/)');
157
+ return { lines, data: { ...knowledge, renderedCount: 0 } };
158
+ }
159
+ lines.push(`${knowledge.count} item(s); most recent:`);
160
+ let shown = 0;
161
+ for (const it of knowledge.shown) {
162
+ const line = `- ${it.id} · ${it.type} · ${it.body.split('\n')[0].slice(0, 80)}`;
163
+ // join is O(items²) but trivial: once-per-session, limit default 5
164
+ if ([...priorLines, ...lines].join('\n').length + line.length > charBudget && shown > 0) break;
165
+ lines.push(line);
166
+ shown++;
167
+ }
168
+ const dropped = knowledge.count - shown;
169
+ if (dropped > 0) lines.push(`- … (${dropped} more — \`zuzuu recall\`)`);
170
+ // `shown` = items actually rendered (after budget); `count` = total available
171
+ return { lines, data: { ...knowledge, shown: knowledge.shown.slice(0, shown), renderedCount: shown } };
172
+ }
173
+
174
+ // ---------------------------------------------------------------------------
175
+ // session signals (the observability surface — `zuzuu session inspect`)
176
+ // ---------------------------------------------------------------------------
177
+
178
+ /** Counts of the mined-signal superset slices this faculty grows from. */
179
+ export function sessionSignals(signals = {}) {
180
+ return {
181
+ commands: signals.commands?.length ?? 0,
182
+ files: signals.files?.length ?? 0,
183
+ failures: signals.failures?.length ?? 0,
184
+ };
185
+ }
@@ -0,0 +1,124 @@
1
+ // zuzuu/faculties/memory/index.mjs — the Memory faculty module.
2
+ //
3
+ // Consolidates the adapter (episode proposals → envelope entries) and the
4
+ // miner STUB (WS5-T4, deferred — see the WHAT IT WOULD MINE note below) behind
5
+ // the Faculty Module contract. No digest section today (the pre-module digest
6
+ // never rendered one — preserved).
7
+ //
8
+ // A memory proposal payload is an episode record:
9
+ // { id, date, title, provenance: {sessions, hosts}, tags, body }
10
+ // id format: mem-<YYYY-MM-DD>-<slug>
11
+ //
12
+ // apply: writes .zuzuu/memory/entries/<id>.md as a Faculty Standard envelope
13
+ // (kind: episode; payload = {sessions, hosts, tags}; body = the
14
+ // Attempted / Resulted / Remember-next-time sections).
15
+ //
16
+ // MINER (when implemented): completed-session episodes — a Run that reached
17
+ // `completed` in .zuzuu/sessions.json distilled into curated entries, emitted
18
+ // as kind-'episode' proposals for `zuzuu review`. Deferred until the Memory
19
+ // substrate decision lands and completed runs carry rich enough trace data.
20
+
21
+ import { writeFacultyItem } from '../../faculty/items.mjs';
22
+
23
+ const name = 'memory';
24
+
25
+ export const manifest = {
26
+ id: 'memory',
27
+ title: 'Memory',
28
+ tagline: 'what HAPPENED — curated episodes from past sessions',
29
+ version: '1.0.0',
30
+ contract: 1,
31
+ kinds: ['episode'],
32
+ itemsDir: 'entries',
33
+ schema: 'schema.json',
34
+ hooks: { miner: true, digest: false, eval: false, gate: false },
35
+ ui: { icon: 'clock', accent: 'neutral', teaching: 'Curated episodes from past sessions — what was attempted, what resulted, what to remember.' },
36
+ };
37
+
38
+ // mem-<YYYY-MM-DD>-<slug>: the id must START with "mem-"
39
+ const MEM_ID_RE = /^mem-/;
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // adapter contract
43
+ // ---------------------------------------------------------------------------
44
+
45
+ /**
46
+ * Ingest a raw episode. Pass-through: the payload IS the episode.
47
+ */
48
+ function ingest(_agentDir, raw) {
49
+ const payload = raw?.payload ?? raw ?? {};
50
+ return { payload, analysis: {}, dedupeKey: payload.id };
51
+ }
52
+
53
+ /**
54
+ * Validate an episode payload.
55
+ * @returns {{ok:boolean, errors:string[], warnings:string[]}}
56
+ */
57
+ export function validate(_agentDir, payload) {
58
+ const errors = [];
59
+ if (!payload?.id || typeof payload.id !== 'string') {
60
+ errors.push('id is required');
61
+ } else if (!MEM_ID_RE.test(payload.id)) {
62
+ errors.push(`id must match mem-<YYYY-MM-DD>-<slug> format (got '${payload.id}')`);
63
+ }
64
+ if (!payload?.title || !String(payload.title).trim()) {
65
+ errors.push('title is required');
66
+ }
67
+ return { ok: errors.length === 0, errors, warnings: [] };
68
+ }
69
+
70
+ /**
71
+ * Apply an approved episode proposal: write the envelope entry file.
72
+ * @returns {{ok:boolean, action:string, itemIds:string[]}}
73
+ */
74
+ function apply(agentDir, proposal) {
75
+ const p = proposal?.payload ?? {};
76
+ const envPayload = {};
77
+ if (Array.isArray(p.provenance?.sessions) && p.provenance.sessions.length) envPayload.sessions = p.provenance.sessions.map(String);
78
+ if (Array.isArray(p.provenance?.hosts) && p.provenance.hosts.length) envPayload.hosts = p.provenance.hosts.map(String);
79
+ if (Array.isArray(p.tags) && p.tags.length) envPayload.tags = p.tags.map(String);
80
+
81
+ writeFacultyItem(agentDir, {
82
+ id: p.id,
83
+ faculty: name,
84
+ kind: 'episode',
85
+ title: p.title,
86
+ status: 'active',
87
+ created_at: p.date || new Date().toISOString().replace(/\.\d+Z$/, 'Z'),
88
+ provenance: Array.isArray(proposal?.provenance) ? proposal.provenance : [],
89
+ payload: envPayload,
90
+ body: p.body ?? '',
91
+ });
92
+
93
+ return { ok: true, action: `wrote memory ${p.id}`, itemIds: [p.id] };
94
+ }
95
+
96
+ export const applyProposal = apply;
97
+
98
+ /**
99
+ * Render an episode proposal for the human gate.
100
+ * @returns {{line:string, card:string}}
101
+ */
102
+ function render(proposal) {
103
+ const p = proposal?.payload ?? {};
104
+ const title = p.title ?? '';
105
+ const date = p.date ?? '';
106
+ const id = p.id ?? '';
107
+ return {
108
+ line: `${id} [episode] ${title} (${date})`,
109
+ card: `${title}\n id: ${id} date: ${date}`,
110
+ };
111
+ }
112
+
113
+ export const adapter = { name, ingest, validate, apply, render };
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // miner (registered no-op stub — deferred)
117
+ // ---------------------------------------------------------------------------
118
+
119
+ export const miner = {
120
+ faculty: name,
121
+ stub: true,
122
+ aggregate: () => [],
123
+ propose: () => 0,
124
+ };