@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,59 @@
1
+ // zuzuu/faculty/render.mjs — shared proposal card/text rendering for the human
2
+ // gate surfaces (`zuzuu review` cards + `zuzuu proposals` lines). Pure string
3
+ // builders; no I/O beyond the existing-item lookup the knowledge card shows.
4
+
5
+ import { readItem } from '../knowledge/items.mjs';
6
+ import { evalLine } from '../commands/eval.mjs';
7
+
8
+ /**
9
+ * The rich knowledge proposal card (id, type, attrs/relations, evidence, ER
10
+ * verdict + the matched item when enrich/duplicate, eval line).
11
+ * @returns {string}
12
+ */
13
+ export function knowledgeCard(agentDir, p, i, total, scoreResult) {
14
+ const lines = [];
15
+ lines.push(`\n━━ proposal ${i + 1}/${total} ── ${p.id} ── ${p.kind} ── source: ${p.source ?? '-'} ━━`);
16
+ if (p.kind === 'registry') {
17
+ lines.push(` register ${p.registry.slice(0, -1)}: '${p.key}' (seen ${p.evidence?.occurrences}× in candidates)`);
18
+ } else {
19
+ // dual-read: legacy records carry `candidate`/`er`; spine records (e.g.
20
+ // inbox-promoted) carry `payload`/`analysis.er` — both must render.
21
+ const c = p.candidate ?? p.payload ?? {};
22
+ lines.push(` ${c.type}: ${c.body?.slice(0, 100).replace(/\n/g, ' ')}`);
23
+ for (const [k, v] of Object.entries(c.attributes ?? {})) lines.push(` · ${k} = ${v}`);
24
+ for (const r of c.relations ?? []) lines.push(` → ${r.type} ${r.target}`);
25
+ const ev = p.evidence ?? {};
26
+ if (Object.keys(ev).length) lines.push(` evidence: ${Object.entries(ev).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(' ')}`);
27
+ const er = p.er ?? p.analysis?.er ?? {};
28
+ lines.push(` er: ${er.verdict}${er.match ? ` → ${er.match}` : ''} (${(er.confidence ?? 0).toFixed(2)} · ${er.reason ?? ''})`);
29
+ if (er.match) {
30
+ const m = readItem(agentDir, er.match);
31
+ if (m) lines.push(` existing: ${m.body.slice(0, 80).replace(/\n/g, ' ')}`);
32
+ }
33
+ }
34
+ // Eval line — always shown; scoreResult computed by caller from ranked array.
35
+ if (scoreResult) lines.push(` ${evalLine(scoreResult)}`);
36
+ return lines.join('\n');
37
+ }
38
+
39
+ /** The historical knowledge one-liner for `zuzuu proposals list`. */
40
+ export function knowledgeLine(p) {
41
+ const c = p.candidate ?? p.payload ?? {}; // dual-read, same as the card
42
+ const what = p.kind === 'registry'
43
+ ? `register ${p.registry.slice(0, -1)} '${p.key}'`
44
+ : `${c.type}: ${c.body?.slice(0, 60).replace(/\n/g, ' ')}`;
45
+ return ` ${p.id} [${p.er?.verdict ?? p.analysis?.er?.verdict ?? p.kind}] ${what}`;
46
+ }
47
+
48
+ /** Derive the human title for a proposal (the JSON list/table form). */
49
+ export function proposalTitle(adapter, p) {
50
+ let title;
51
+ if (adapter.name === 'knowledge') {
52
+ title = p.kind === 'registry'
53
+ ? `register ${p.registry?.slice(0, -1) ?? ''} '${p.key ?? ''}'`
54
+ : (p.candidate?.body ?? p.payload?.body ?? p.id)?.slice(0, 80);
55
+ } else {
56
+ title = p.title ?? adapter.render(p).line;
57
+ }
58
+ return title ?? p.id;
59
+ }
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { join } from 'node:path';
9
9
  import { mkdirSync, appendFileSync } from 'node:fs';
10
- import { liveDir } from '../store.mjs';
10
+ import { liveDir } from '../core/store.mjs';
11
11
 
12
12
  /**
13
13
  * Append a trail entry for a faculty. Never throws.
@@ -0,0 +1,137 @@
1
+ // The Guardrails faculty — v1 rule engine (pure; I/O lives in the hook command).
2
+ //
3
+ // Rules are DATA, not code: one envelope item per rule under
4
+ // .zuzuu/guardrails/items/<id>.md (the Faculty Standard, W24) — *definitions*
5
+ // in the pin-definitions sense (versioned in git, graduate via proposals like
6
+ // every faculty's contents).
7
+ //
8
+ // ---
9
+ // id: no-root-wipe
10
+ // faculty: guardrails
11
+ // kind: rule
12
+ // title: …
13
+ // payload:
14
+ // action: deny # deny | ask | allow
15
+ // tool: Bash # exact tool name, or "*"
16
+ // pattern: "rm\\s+-rf\\s+/" # regex over the tool INPUT (stringified)
17
+ // reason: destructive root delete
18
+ // ---
19
+ // (optional rationale prose)
20
+ //
21
+ // Evaluation: collect every matching rule, then severity wins — deny > ask >
22
+ // allow (an explicit allow can whitelist past a later ask/deny only if it is
23
+ // NOT outweighed; severity beats file order so a sloppy rule ordering can never
24
+ // silently disarm a deny).
25
+ //
26
+ // FAIL-OPEN, per item: a malformed rule file is SKIPPED (and counted in
27
+ // `skipped`), never a crash and never a block — the other rules still apply.
28
+ // The gate runs per tool call, so loads are cached on the items dir's stat
29
+ // signature (names+mtimes+sizes): re-parse only when something changed. No
30
+ // derived file — the item files stay the single source of truth.
31
+
32
+ import { join } from 'node:path';
33
+ import { readFileSync, readdirSync, statSync } from 'node:fs';
34
+ import { parseEnvelope } from '../faculty/envelope.mjs';
35
+
36
+ const SEVERITY = { deny: 3, ask: 2, allow: 1 };
37
+ const ACTIONS = new Set(Object.keys(SEVERITY));
38
+
39
+ // dir → { sig, result } — tiny in-memory cache (per process; the spawned gate
40
+ // pays one cold load, long-lived processes like `zuzuu web` skip re-parses).
41
+ const cache = new Map();
42
+
43
+ /** Compile one parsed envelope item into a rule, or null if malformed. */
44
+ function compileRule(item) {
45
+ const p = item?.payload ?? {};
46
+ if (!item?.id || !ACTIONS.has(p.action) || typeof p.pattern !== 'string' || !p.pattern) return null;
47
+ try {
48
+ return { id: String(item.id), action: p.action, tool: p.tool || '*', re: new RegExp(p.pattern, 'i'), reason: String(p.reason ?? '') };
49
+ } catch {
50
+ return null; // uncompilable pattern → skip this rule only
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Load the rules from a guardrails faculty dir (…/guardrails) by composing the
56
+ * envelope items under its items/ subdir. FAIL-OPEN: a missing dir is zero
57
+ * rules; a malformed item is skipped + counted, never a crash.
58
+ * @param {string} guardrailsDir the faculty dir (e.g. <home>/guardrails)
59
+ * @returns {{ok: boolean, rules: Array, skipped: Array<{file: string, error: string}>}}
60
+ */
61
+ export function loadRules(guardrailsDir) {
62
+ try {
63
+ const dir = join(guardrailsDir, 'items');
64
+ let names;
65
+ try {
66
+ names = readdirSync(dir).filter((f) => f.endsWith('.md')).sort();
67
+ } catch {
68
+ return { ok: true, rules: [], skipped: [] }; // no items dir → no rules (normal flow)
69
+ }
70
+ let sig = null;
71
+ try {
72
+ sig = names.map((n) => { const s = statSync(join(dir, n)); return `${n}:${s.mtimeMs}:${s.size}`; }).join('|');
73
+ const hit = cache.get(dir);
74
+ if (hit && hit.sig === sig) return hit.result;
75
+ } catch { sig = null; /* stat race → just load uncached */ }
76
+
77
+ const rules = [];
78
+ const skipped = [];
79
+ for (const f of names) {
80
+ try {
81
+ const { ok, item, errors } = parseEnvelope(readFileSync(join(dir, f), 'utf8'));
82
+ const rule = ok ? compileRule(item) : null;
83
+ if (rule) rules.push(rule);
84
+ else skipped.push({ file: f, error: ok ? 'malformed rule payload' : (errors[0] ?? 'parse error') });
85
+ } catch (e) {
86
+ skipped.push({ file: f, error: e.message });
87
+ }
88
+ }
89
+ const result = { ok: true, rules, skipped };
90
+ if (sig != null) cache.set(dir, { sig, result });
91
+ return result;
92
+ } catch (e) {
93
+ return { ok: false, rules: [], skipped: [], error: e.message }; // engine trouble → fail open
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Evaluate a tool call against loaded rules.
99
+ * @param {Array} rules from loadRules().rules
100
+ * @param {{tool:string, input:any}} call
101
+ * @returns {null | {action:'deny'|'ask'|'allow', rule:string, reason:string}}
102
+ * null = no rule matched → defer to the host's normal permission flow
103
+ */
104
+ export function evaluate(rules, { tool, input }) {
105
+ const haystack = typeof input === 'string' ? input : JSON.stringify(input ?? {});
106
+ let winner = null;
107
+ for (const r of rules) {
108
+ if (r.tool !== '*' && r.tool !== tool) continue;
109
+ if (!r.re.test(haystack)) continue;
110
+ if (!winner || SEVERITY[r.action] > SEVERITY[winner.action]) {
111
+ winner = { action: r.action, rule: r.id, reason: r.reason || `matched guardrail ${r.id}` };
112
+ }
113
+ }
114
+ return winner;
115
+ }
116
+
117
+ /**
118
+ * Gemini CLI block shape: stdout JSON { decision: "deny", reason } (exit 0).
119
+ * Gemini has no "ask" decision → defer (null) so its own approval flow runs.
120
+ * Only an explicit deny blocks.
121
+ */
122
+ export function toGeminiDecision(verdict) {
123
+ if (!verdict || verdict.action !== 'deny') return null;
124
+ return { decision: 'deny', reason: `guardrail ${verdict.rule}: ${verdict.reason}` };
125
+ }
126
+
127
+ /** Map a verdict to Claude Code's PreToolUse hookSpecificOutput (verified schema). */
128
+ export function toPreToolUseDecision(verdict) {
129
+ if (!verdict || verdict.action === 'allow') return null; // no output → normal flow (fail-open / explicit allow)
130
+ return {
131
+ hookSpecificOutput: {
132
+ hookEventName: 'PreToolUse',
133
+ permissionDecision: verdict.action, // 'deny' | 'ask'
134
+ permissionDecisionReason: `guardrail ${verdict.rule}: ${verdict.reason}`,
135
+ },
136
+ };
137
+ }
@@ -15,9 +15,15 @@
15
15
 
16
16
  import { join } from 'node:path';
17
17
  import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
18
- import { SEED_TYPES, SEED_ATTRIBUTES, SEED_RELATIONS } from './knowledge/registry.mjs';
18
+ import { SEED_TYPES, SEED_ATTRIBUTES, SEED_RELATIONS } from '../knowledge/registry.mjs';
19
+ import { serializeEnvelope, PAYLOAD_SCHEMAS, FACULTY_KINDS } from '../faculty/envelope.mjs';
20
+ import { BUILTIN_MODULES } from '../faculty/registry.mjs';
19
21
 
20
- export const MANIFEST_VERSION = 3;
22
+ export const MANIFEST_VERSION = 4;
23
+
24
+ // Deterministic seed timestamp (the Faculty Standard date) — seeds are pinned
25
+ // definitions, so idempotent re-inits must produce byte-identical files.
26
+ const SEED_AT = '2026-06-12T00:00:00Z';
21
27
 
22
28
  const AGENT_README = `# .zuzuu/ — your agent's home (hidden, like .git — yours to read & version)
23
29
 
@@ -70,17 +76,23 @@ Curated recollections of past sessions, distilled from the observability traces
70
76
  - **Who writes:** zuzuu (distillation — *not built yet*), human (curation). Raw traces stay in traces/ — this is the *curated* layer.
71
77
  - **Where:** one Markdown file per entry under \`entries/\`, named \`<id>.md\`.
72
78
 
73
- ## Record schema (Markdown + YAML frontmatter)
79
+ ## Record schema (the Faculty Standard envelope)
74
80
  \`\`\`markdown
75
81
  ---
76
82
  id: mem-2026-06-11-flaky-ci-retry # mem-<YYYY-MM-DD>-<slug>, stable
77
- date: 2026-06-11 # ISO date the episode occurred
83
+ faculty: memory
84
+ kind: episode
78
85
  title: Flaky CI fixed by pinning node 22
79
- provenance: # links back to observability
80
- sessions: [ses_abc123] # ids that exist in .zuzuu/sessions.json
81
- hosts: [claude-code]
82
- tags: [ci, flaky-test] # optional
83
- status: curated # curated (human) | proposed (reserved — future distiller)
86
+ status: active
87
+ created_at: 2026-06-11 # ISO date the episode occurred
88
+ payload:
89
+ sessions: # ids that exist in .zuzuu/sessions.json
90
+ - ses_abc123
91
+ hosts:
92
+ - claude-code
93
+ tags: # optional
94
+ - ci
95
+ - flaky-test
84
96
  ---
85
97
  ## Attempted
86
98
  What was tried.
@@ -89,7 +101,6 @@ What happened (outcome / error / fix).
89
101
  ## Remember next time
90
102
  The durable lesson.
91
103
  \`\`\`
92
- \`status: proposed\` and the distiller→review pipeline are **reserved** (not built this pass).
93
104
  `;
94
105
 
95
106
  const ACTIONS_README = `# actions/ — procedural faculty (how to DO things)
@@ -104,55 +115,115 @@ const INSTRUCTIONS_README = `# instructions/ — the Instructions faculty (direc
104
115
 
105
116
  Cognition steering: identity, conventions, priorities — the project-level seed of
106
117
  the pinned system prompt. The host agent reads and follows this.
107
- - \`project.md\` — project-specific steering (what this is, conventions, priorities).
118
+ - \`items/steering.md\` — the pinned steering item (what this is, conventions, priorities).
119
+ - Approved amendments land as further items in \`items/\` (kind: amendment).
108
120
  - Hard *enforced* rules live in \`../guardrails/\` (a separate faculty), not here.
109
121
  `;
110
122
 
111
- const PROJECT_SEED = `# Project steering
112
-
113
- <!-- Fill in: what this project is, conventions, priorities. The host agent reads this. -->
114
- `;
123
+ const STEERING_SEED = serializeEnvelope({
124
+ id: 'steering',
125
+ faculty: 'instructions',
126
+ kind: 'steering',
127
+ title: 'Project steering',
128
+ status: 'active',
129
+ created_at: SEED_AT,
130
+ payload: { scope: 'project' },
131
+ body: '<!-- Fill in: what this project is, conventions, priorities. The host agent reads this. -->',
132
+ });
115
133
 
116
134
  const GUARDRAILS_README = `# guardrails/ — the Guardrails faculty (enforced, not advisory)
117
135
 
118
- Declarative rules in \`rules.json\`, evaluated on every tool call by the zuzuu
119
- PreToolUse gate (installed by \`zuzuu enable\`). Severity wins: deny > ask > allow;
120
- no match the host's normal permission flow. The engine FAILS OPEN — a
121
- guardrail bug can block nothing and matched decisions are logged for the trace.
122
-
123
- Rule shape: \`{ id, action: deny|ask|allow, tool: "Bash"|"*", pattern: <regex
124
- over the tool input>, reason }\`. Edit, commit, done — rules are definitions,
136
+ One rule per envelope item in \`items/\` (markdown + frontmatter; payload =
137
+ \`{ action: deny|ask|allow, tool: "Bash"|"*", pattern: <regex over the tool
138
+ input>, reason }\`; the body is optional rationale prose). Every tool call is
139
+ evaluated by the zuzuu PreToolUse gate (installed by \`zuzuu enable\`). Severity
140
+ wins: deny > ask > allow; no match → the host's normal permission flow. The
141
+ engine FAILS OPEN a malformed item is skipped, never a block — and matched
142
+ decisions are logged for the trace. Edit, commit, done — rules are definitions,
125
143
  versioned in git like everything else.
126
144
  `;
127
145
 
128
- const RULES_SEED =
129
- JSON.stringify(
130
- {
131
- version: 1,
132
- rules: [
133
- { id: 'no-root-wipe', action: 'deny', tool: 'Bash', pattern: 'rm\\s+-[a-z]*r[a-z]*\\s+/(\\s|$)', reason: 'destructive delete at filesystem root' },
134
- { id: 'no-secret-reads', action: 'deny', tool: '*', pattern: '\\.env(\\.|\\b)|id_rsa|\\.pem\\b', reason: 'secret material should not enter the context' },
135
- // \b.*\bpush, not push adjacent to git: a real session bypassed the
136
- // adjacent form with `git -C /path push --force-with-lease` (exp-8).
137
- { id: 'confirm-force-push', action: 'ask', tool: 'Bash', pattern: 'git\\b.*\\bpush\\b.*--force', reason: 'force-push rewrites shared history' },
138
- ],
146
+ /** Seeded rules, one envelope item each (the Faculty Standard, W24). */
147
+ const ruleSeed = ({ id, title, action, tool, pattern, reason, body }) =>
148
+ serializeEnvelope({
149
+ id, faculty: 'guardrails', kind: 'rule', title, status: 'active', created_at: SEED_AT,
150
+ payload: { action, tool, pattern, reason }, body,
151
+ });
152
+
153
+ const RULE_SEEDS = {
154
+ 'no-root-wipe': ruleSeed({
155
+ id: 'no-root-wipe', title: 'No destructive delete at filesystem root', action: 'deny', tool: 'Bash',
156
+ pattern: 'rm\\s+-[a-z]*r[a-z]*\\s+/(\\s|$)', reason: 'destructive delete at filesystem root',
157
+ body: 'Blocks `rm -rf /` and flag-order variants targeting the filesystem root.',
158
+ }),
159
+ 'no-secret-reads': ruleSeed({
160
+ id: 'no-secret-reads', title: 'No reading secret material', action: 'deny', tool: '*',
161
+ pattern: '\\.env(\\.|\\b)|id_rsa|\\.pem\\b', reason: 'secret material should not enter the context',
162
+ body: 'Keys and env files must not enter the model context — across every tool, not just Bash.',
163
+ }),
164
+ 'confirm-force-push': ruleSeed({
165
+ id: 'confirm-force-push', title: 'Confirm before force-push', action: 'ask', tool: 'Bash',
166
+ pattern: 'git\\b.*\\bpush\\b.*--force', reason: 'force-push rewrites shared history',
167
+ // \b.*\bpush, not push adjacent to git: a real session bypassed the
168
+ // adjacent form with `git -C /path push --force-with-lease` (exp-8).
169
+ body: 'Asks (never blocks) on any force-push. The loose `git\\b.*\\bpush` form catches the real exp-8 bypass: `git -C /path push --force-with-lease`.',
170
+ }),
171
+ };
172
+
173
+ /** Envelope spec seed (.zuzuu/schema.json) — descriptive, for humans + tools. */
174
+ const ENVELOPE_SPEC = JSON.stringify(
175
+ {
176
+ standard: 'zuzuu-faculty-envelope',
177
+ version: 1,
178
+ description: 'One file per item: markdown body + strict frontmatter. One rigid envelope across all five faculties; payload is faculty-typed and validated by <faculty>/schema.json.',
179
+ envelope: {
180
+ id: 'required — slug [a-z0-9-]',
181
+ faculty: 'required — knowledge|memory|actions|instructions|guardrails',
182
+ kind: 'required — per-faculty kinds (see kinds)',
183
+ title: 'required — single line',
184
+ status: 'active|archived (default active)',
185
+ created_at: 'required — ISO date/datetime',
186
+ updated_at: 'optional — ISO date/datetime',
187
+ provenance: 'optional list of {session, ref}',
188
+ payload: 'faculty-typed machine fields (see <faculty>/schema.json)',
139
189
  },
140
- null,
141
- 2,
142
- ) + '\n';
190
+ kinds: { ...FACULTY_KINDS, knowledge: 'registry-governed (knowledge/registry/types.json)' },
191
+ },
192
+ null,
193
+ 2,
194
+ ) + '\n';
195
+
196
+ const payloadSchemaSeed = (f) => JSON.stringify(PAYLOAD_SCHEMAS[f], null, 2) + '\n';
197
+
198
+ /** Faculty Module manifest seed (faculty.json) — the built-in module's canonical
199
+ * manifest, serialized. Pinned definitions: byte-identical on re-init. */
200
+ export const manifestSeed = (f) => JSON.stringify(BUILTIN_MODULES[f].manifest, null, 2) + '\n';
143
201
 
144
202
  /** The layout contract: dirs + seed files (relative to the project root). */
145
203
  export const LAYOUT = {
146
- dirs: ['.zuzuu', '.zuzuu/knowledge', '.zuzuu/knowledge/registry', '.zuzuu/knowledge/items', '.zuzuu/knowledge/inbox', '.zuzuu/knowledge/proposals', '.zuzuu/memory', '.zuzuu/memory/entries', '.zuzuu/memory/inbox', '.zuzuu/memory/proposals', '.zuzuu/actions', '.zuzuu/actions/inbox', '.zuzuu/instructions', '.zuzuu/instructions/inbox', '.zuzuu/instructions/proposals', '.zuzuu/guardrails', '.zuzuu/guardrails/inbox', '.zuzuu/guardrails/proposals', '.zuzuu/generations', '.zuzuu/generations/snapshots'],
204
+ dirs: ['.zuzuu', '.zuzuu/knowledge', '.zuzuu/knowledge/registry', '.zuzuu/knowledge/items', '.zuzuu/knowledge/inbox', '.zuzuu/knowledge/proposals', '.zuzuu/memory', '.zuzuu/memory/entries', '.zuzuu/memory/inbox', '.zuzuu/memory/proposals', '.zuzuu/actions', '.zuzuu/actions/inbox', '.zuzuu/instructions', '.zuzuu/instructions/items', '.zuzuu/instructions/inbox', '.zuzuu/instructions/proposals', '.zuzuu/guardrails', '.zuzuu/guardrails/items', '.zuzuu/guardrails/inbox', '.zuzuu/guardrails/proposals', '.zuzuu/generations', '.zuzuu/generations/snapshots'],
147
205
  files: {
148
206
  '.zuzuu/README.md': AGENT_README,
207
+ '.zuzuu/schema.json': ENVELOPE_SPEC,
149
208
  '.zuzuu/knowledge/README.md': KNOWLEDGE_README,
209
+ '.zuzuu/knowledge/schema.json': payloadSchemaSeed('knowledge'),
210
+ '.zuzuu/knowledge/faculty.json': manifestSeed('knowledge'),
150
211
  '.zuzuu/memory/README.md': MEMORY_README,
212
+ '.zuzuu/memory/schema.json': payloadSchemaSeed('memory'),
213
+ '.zuzuu/memory/faculty.json': manifestSeed('memory'),
151
214
  '.zuzuu/actions/README.md': ACTIONS_README,
215
+ '.zuzuu/actions/schema.json': payloadSchemaSeed('actions'),
216
+ '.zuzuu/actions/faculty.json': manifestSeed('actions'),
152
217
  '.zuzuu/instructions/README.md': INSTRUCTIONS_README,
153
- '.zuzuu/instructions/project.md': PROJECT_SEED,
218
+ '.zuzuu/instructions/schema.json': payloadSchemaSeed('instructions'),
219
+ '.zuzuu/instructions/faculty.json': manifestSeed('instructions'),
220
+ '.zuzuu/instructions/items/steering.md': STEERING_SEED,
154
221
  '.zuzuu/guardrails/README.md': GUARDRAILS_README,
155
- '.zuzuu/guardrails/rules.json': RULES_SEED,
222
+ '.zuzuu/guardrails/schema.json': payloadSchemaSeed('guardrails'),
223
+ '.zuzuu/guardrails/faculty.json': manifestSeed('guardrails'),
224
+ '.zuzuu/guardrails/items/no-root-wipe.md': RULE_SEEDS['no-root-wipe'],
225
+ '.zuzuu/guardrails/items/no-secret-reads.md': RULE_SEEDS['no-secret-reads'],
226
+ '.zuzuu/guardrails/items/confirm-force-push.md': RULE_SEEDS['confirm-force-push'],
156
227
  '.zuzuu/knowledge/registry/types.json': JSON.stringify(SEED_TYPES, null, 2) + '\n',
157
228
  '.zuzuu/knowledge/registry/attributes.json': JSON.stringify(SEED_ATTRIBUTES, null, 2) + '\n',
158
229
  '.zuzuu/knowledge/registry/relations.json': JSON.stringify(SEED_RELATIONS, null, 2) + '\n',
@@ -1,31 +1,35 @@
1
- // Knowledge items — files as truth. One item per markdown file under
2
- // .zuzuu/knowledge/items/<id>.md: a constrained-YAML frontmatter (we control both
3
- // writer and reader; grammar below) + a prose body (the fact in your voice).
1
+ // Knowledge items — files as truth, in the Faculty Standard envelope (W24).
2
+ // One item per markdown file under .zuzuu/knowledge/items/<id>.md:
4
3
  //
5
4
  // ---
6
5
  // id: test-command
7
- // type: command
8
- // created_at: 2026-06-10T12:00:00Z
6
+ // faculty: knowledge
7
+ // kind: command
8
+ // title: The test command
9
9
  // status: active
10
- // attributes:
11
- // command: npm test
12
- // relations:
13
- // - type: relates-to
14
- // target: ci-pipeline
15
- // commentary: optional
10
+ // created_at: 2026-06-10T12:00:00Z
16
11
  // provenance:
17
12
  // - session: ses_abc
18
13
  // ref: occurrences=12
14
+ // payload:
15
+ // type: command
16
+ // attributes:
17
+ // command: npm test
18
+ // relations:
19
+ // - type: relates-to
20
+ // target: ci-pipeline
19
21
  // ---
20
22
  // Body prose.
21
23
  //
22
- // Grammar (deliberately small): top-level scalar keys; ONE nested map
23
- // (`attributes`); arrays of flat maps (`relations`, `provenance`). Values are
24
- // single-line strings (quotes optional). Anything outside this grammar is a
25
- // parse error git-diffable simplicity beats YAML completeness here.
24
+ // This module is a thin wrapper over faculty/envelope.mjs: the ON-DISK format
25
+ // is the envelope; the IN-MEMORY item shape stays the historical knowledge one
26
+ // ({id, type, created_at, status, attributes, relations, provenance, body}) so
27
+ // the registry/ER/index/digest pipeline is untouched. Ids are unchanged by the
28
+ // standard — only frontmatter keys moved (type/attributes/relations → payload).
26
29
 
27
30
  import { join } from 'node:path';
28
31
  import { existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync } from 'node:fs';
32
+ import { parseEnvelope, serializeEnvelope, deriveTitle } from '../faculty/envelope.mjs';
29
33
 
30
34
  export const itemsDir = (agentDir) => join(agentDir, 'knowledge', 'items');
31
35
 
@@ -38,88 +42,49 @@ export function slugify(text, max = 60) {
38
42
  .replace(/-+$/, '') || 'item';
39
43
  }
40
44
 
41
- const unquote = (s) => {
42
- const t = s.trim();
43
- return (t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'")) ? t.slice(1, -1) : t;
44
- };
45
- const quoteIfNeeded = (s) => {
46
- const t = String(s);
47
- if (t.includes('\n')) throw new Error('item values must be single-line');
48
- return /[:#'"\[\]{}]|^\s|\s$/.test(t) ? JSON.stringify(t) : t;
49
- };
50
-
51
- /** Parse an item file's text → item object. Throws on grammar violations. */
45
+ /**
46
+ * Parse an item file's text → the in-memory knowledge item. Throws on grammar
47
+ * violations (callers catch allItems collects, inbox falls back to prose).
48
+ */
52
49
  export function parseItem(text) {
53
- const m = text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
54
- if (!m) throw new Error('no frontmatter block');
55
- const [, fm, body] = m;
56
- const item = { attributes: {}, relations: [], provenance: [], body: body.trim() };
57
- let section = null; // 'attributes' | 'relations' | 'provenance'
58
- let current = null; // current array entry
59
- for (const raw of fm.split('\n')) {
60
- if (!raw.trim()) continue;
61
- const indent = raw.match(/^ */)[0].length;
62
- const line = raw.trim();
63
- if (indent === 0) {
64
- current = null;
65
- const kv = line.match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
66
- if (!kv) throw new Error(`bad line: ${line}`);
67
- const [, key, val] = kv;
68
- if (['attributes', 'relations', 'provenance'].includes(key)) {
69
- section = key;
70
- if (val) throw new Error(`${key} must be a block`);
71
- } else {
72
- section = null;
73
- item[key] = unquote(val);
74
- }
75
- } else if (section === 'attributes') {
76
- const kv = line.match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
77
- if (!kv) throw new Error(`bad attribute line: ${line}`);
78
- item.attributes[kv[1]] = unquote(kv[2]);
79
- } else if (section === 'relations' || section === 'provenance') {
80
- if (line.startsWith('- ')) {
81
- current = {};
82
- item[section].push(current);
83
- const kv = line.slice(2).match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
84
- if (!kv) throw new Error(`bad ${section} entry: ${line}`);
85
- current[kv[1]] = unquote(kv[2]);
86
- } else {
87
- if (!current) throw new Error(`${section} entry continuation without "-": ${line}`);
88
- const kv = line.match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
89
- if (!kv) throw new Error(`bad ${section} line: ${line}`);
90
- current[kv[1]] = unquote(kv[2]);
91
- }
92
- } else {
93
- throw new Error(`unexpected indented line: ${line}`);
94
- }
95
- }
96
- if (!item.id) throw new Error('item missing id');
50
+ const { ok, item: env, errors } = parseEnvelope(text);
51
+ if (!ok) throw new Error(errors[0] ?? 'invalid envelope');
52
+ if (env.faculty !== 'knowledge') throw new Error(`not a knowledge item (faculty: ${env.faculty})`);
53
+ const p = env.payload ?? {};
54
+ const item = {
55
+ attributes: p.attributes ?? {},
56
+ relations: p.relations ?? [],
57
+ provenance: env.provenance ?? [],
58
+ body: env.body,
59
+ };
60
+ item.id = env.id;
61
+ item.type = p.type ?? env.kind;
62
+ if (env.created_at != null) item.created_at = env.created_at;
63
+ if (env.updated_at != null) item.updated_at = env.updated_at;
64
+ if (env.status != null) item.status = env.status;
97
65
  if (!item.type) throw new Error('item missing type');
98
66
  return item;
99
67
  }
100
68
 
101
- /** Serialize an item object → file text (the exact grammar parseItem reads). */
69
+ /** Serialize an in-memory knowledge item → envelope file text. */
102
70
  export function serializeItem(item) {
103
- const lines = ['---'];
104
- for (const key of ['id', 'type', 'created_at', 'status']) {
105
- if (item[key] != null) lines.push(`${key}: ${quoteIfNeeded(item[key])}`);
106
- }
107
- const attrs = Object.entries(item.attributes ?? {});
108
- if (attrs.length) {
109
- lines.push('attributes:');
110
- for (const [k, v] of attrs) lines.push(` ${k}: ${quoteIfNeeded(v)}`);
111
- }
112
- for (const section of ['relations', 'provenance']) {
113
- const arr = item[section] ?? [];
114
- if (!arr.length) continue;
115
- lines.push(`${section}:`);
116
- for (const entry of arr) {
117
- const keys = Object.keys(entry);
118
- keys.forEach((k, i) => lines.push(` ${i === 0 ? '- ' : ' '}${k}: ${quoteIfNeeded(entry[k])}`));
119
- }
120
- }
121
- lines.push('---', '');
122
- return lines.join('\n') + (item.body ? item.body.trim() + '\n' : '');
71
+ if (!item.id) throw new Error('item missing id');
72
+ if (!item.type) throw new Error('item missing type');
73
+ const payload = { type: item.type };
74
+ if (Object.keys(item.attributes ?? {}).length) payload.attributes = item.attributes;
75
+ if ((item.relations ?? []).length) payload.relations = item.relations;
76
+ return serializeEnvelope({
77
+ id: item.id,
78
+ faculty: 'knowledge',
79
+ kind: item.type,
80
+ title: item.title ?? deriveTitle(item.body, item.id),
81
+ status: item.status,
82
+ created_at: item.created_at,
83
+ updated_at: item.updated_at,
84
+ provenance: item.provenance ?? [],
85
+ payload,
86
+ body: item.body,
87
+ });
123
88
  }
124
89
 
125
90
  /** Write an item to its canonical file. Returns the path. */
@@ -13,7 +13,7 @@ const DENY_RULES = ['Read(./.zuzuu/.traces/**)', 'Read(./.zuzuu/.live/**)'];
13
13
 
14
14
  // Minimal hook set: lifecycle (Design B re-captures the transcript — no
15
15
  // PostToolUse needed) + the PreToolUse Guardrails GATE (the one place we *do*
16
- // sit on the hot path: it evaluates .zuzuu/guardrails/rules.json per tool call,
16
+ // sit on the hot path: it evaluates the .zuzuu/guardrails/items/ rules per tool call,
17
17
  // fails open, and stays silent unless a rule matches).
18
18
  export const LIFECYCLE_EVENTS = ['SessionStart', 'Stop', 'SessionEnd'];
19
19
  export const GATE_EVENTS = ['PreToolUse'];
@@ -7,8 +7,8 @@
7
7
 
8
8
  import { join } from 'node:path';
9
9
  import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
10
- import { paths, liveDir as liveDirOf } from '../store.mjs';
11
- import { SessionState } from '../session.mjs';
10
+ import { paths, liveDir as liveDirOf } from '../core/store.mjs';
11
+ import { SessionState } from '../core/session.mjs';
12
12
 
13
13
  const liveDir = (cwd) => liveDirOf(paths(cwd).dir);
14
14
  // Some hosts pass a file PATH as the session id (pi → the session-file path).
@@ -5,8 +5,8 @@
5
5
 
6
6
  import { listLive, isStale, closeLive } from './live-store.mjs';
7
7
  import { byName } from '../capture/adapters/registry.mjs';
8
- import { captureTrace } from '../capture-core.mjs';
9
- import { SessionState } from '../session.mjs';
8
+ import { captureTrace } from '../core/capture-core.mjs';
9
+ import { SessionState } from '../core/session.mjs';
10
10
 
11
11
  export const DEFAULT_STALE_MS = 15 * 60 * 1000; // 15 min without a heartbeat → abandoned
12
12