@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,290 @@
1
+ // zuzuu/faculty/envelope.mjs — the Faculty Standard envelope (W24).
2
+ //
3
+ // ONE storage format across all five faculties: one file per item, markdown
4
+ // prose + a strict constrained-YAML frontmatter we control on both ends (no
5
+ // YAML lib — grown from the knowledge items grammar):
6
+ //
7
+ // ---
8
+ // id: test-command # required, slug
9
+ // faculty: knowledge # required, one of the 5
10
+ // kind: command # required; per-faculty kinds (FACULTY_KINDS)
11
+ // title: "Test command" # required, single line
12
+ // status: active # active | archived
13
+ // created_at: 2026-06-12T00:00:00Z
14
+ // updated_at: … # optional
15
+ // provenance: # optional list of flat maps {session, ref}
16
+ // - session: ses_abc
17
+ // ref: occurrences=12
18
+ // payload: # faculty-typed machine fields
19
+ // type: command # scalar
20
+ // attributes: # one-level map of scalars
21
+ // command: npm test
22
+ // relations: # list of flat maps
23
+ // - type: relates-to
24
+ // target: ci-pipeline
25
+ // ---
26
+ // <markdown prose body>
27
+ //
28
+ // Grammar (deliberately small): top-level scalar keys; `provenance` = a list of
29
+ // flat maps; `payload` = a block of scalars, one-level maps of scalars, lists
30
+ // of flat maps, or lists of scalars. Values are single-line strings (JSON
31
+ // double-quoting when they carry specials — round-trip exact, incl. backslashes
32
+ // in guardrail regexes). Anything outside this grammar is a parse error.
33
+ //
34
+ // API: parseEnvelope(text) → {ok, item, errors} (never throws) ·
35
+ // serializeEnvelope(item) → text · validateEnvelope(item, payloadSchema).
36
+ // Payload validation rides the shared JSON-Schema-subset checker
37
+ // (actions/schema.mjs — type/required/enum/pattern on flat fields).
38
+
39
+ import { FACULTIES } from './contract.mjs';
40
+ import { validate as validateSchema } from '../actions/schema.mjs';
41
+
42
+ /** Per-faculty item kinds. `null` = open set (knowledge kinds are governed by
43
+ * the knowledge registry's types.json, not pinned here). */
44
+ export const FACULTY_KINDS = {
45
+ knowledge: null, // registry-governed (seed: fact|entity|command|decision)
46
+ memory: ['episode'],
47
+ actions: ['runbook', 'script'],
48
+ instructions: ['steering', 'amendment'],
49
+ guardrails: ['rule'],
50
+ };
51
+
52
+ /** Default payload schemas (JSON-Schema subset) — also seeded to
53
+ * .zuzuu/<faculty>/schema.json by `zuzuu init`. We author both ends. */
54
+ export const PAYLOAD_SCHEMAS = {
55
+ knowledge: {
56
+ type: 'object',
57
+ required: ['type'],
58
+ properties: {
59
+ type: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]*$' },
60
+ attributes: { type: 'object' },
61
+ relations: {
62
+ type: 'array',
63
+ items: { type: 'object', required: ['type', 'target'], properties: { type: { type: 'string' }, target: { type: 'string' }, commentary: { type: 'string' } } },
64
+ },
65
+ },
66
+ },
67
+ memory: {
68
+ type: 'object',
69
+ properties: {
70
+ sessions: { type: 'array', items: { type: 'string' } },
71
+ hosts: { type: 'array', items: { type: 'string' } },
72
+ tags: { type: 'array', items: { type: 'string' } },
73
+ },
74
+ },
75
+ actions: {
76
+ type: 'object',
77
+ properties: {
78
+ exec: { type: 'string', pattern: '^[A-Za-z0-9][A-Za-z0-9._-]*$' }, // sibling file name, no path escape
79
+ args: { type: 'object' },
80
+ },
81
+ },
82
+ instructions: {
83
+ type: 'object',
84
+ properties: { scope: { type: 'string' } },
85
+ },
86
+ guardrails: {
87
+ type: 'object',
88
+ required: ['action', 'pattern', 'reason'],
89
+ properties: {
90
+ action: { type: 'string', enum: ['deny', 'ask', 'allow'] },
91
+ tool: { type: 'string' },
92
+ pattern: { type: 'string', minLength: 1 },
93
+ reason: { type: 'string', minLength: 1 },
94
+ },
95
+ },
96
+ };
97
+
98
+ const ID_RE = /^[a-z0-9][a-z0-9_-]*$/; // spec is [a-z0-9-]; `_` tolerated for action slugs
99
+ const ISO_RE = /^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:?\d{2})?)?$/;
100
+ const STATUSES = new Set(['active', 'archived']);
101
+ const TOP_KEYS = ['id', 'faculty', 'kind', 'title', 'status', 'created_at', 'updated_at'];
102
+
103
+ // --- scalar quoting (round-trip exact, incl. backslashes) --------------------
104
+
105
+ const unquote = (s) => {
106
+ const t = s.trim();
107
+ if (t.startsWith('"') && t.endsWith('"') && t.length >= 2) {
108
+ try { return JSON.parse(t); } catch { return t.slice(1, -1); }
109
+ }
110
+ if (t.startsWith("'") && t.endsWith("'") && t.length >= 2) return t.slice(1, -1);
111
+ return t;
112
+ };
113
+
114
+ const quoteIfNeeded = (s) => {
115
+ const t = String(s);
116
+ if (t.includes('\n')) throw new Error('envelope values must be single-line');
117
+ return /[:#'"\\\[\]{}]|^-\s|^\s|\s$/.test(t) || t === '' ? JSON.stringify(t) : t;
118
+ };
119
+
120
+ const KV = /^([A-Za-z_][\w-]*):\s*(.*)$/;
121
+
122
+ // --- parse -------------------------------------------------------------------
123
+
124
+ /**
125
+ * Parse an envelope file's text. Never throws.
126
+ * @returns {{ok: boolean, item: object|null, errors: string[]}}
127
+ */
128
+ export function parseEnvelope(text) {
129
+ const errors = [];
130
+ const m = String(text).match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
131
+ if (!m) return { ok: false, item: null, errors: ['no frontmatter block'] };
132
+ const [, fm, body] = m;
133
+ const item = { provenance: [], payload: {}, body: body.trim() };
134
+
135
+ let section = null; // 'provenance' | 'payload' | null
136
+ let payloadKey = null; // current sub-block key inside payload
137
+ let current = null; // current list entry (flat map) being filled
138
+
139
+ for (const raw of fm.split('\n')) {
140
+ if (!raw.trim()) continue;
141
+ const indent = raw.match(/^ */)[0].length;
142
+ const line = raw.trim();
143
+
144
+ if (indent === 0) {
145
+ section = null; payloadKey = null; current = null;
146
+ const kv = line.match(KV);
147
+ if (!kv) { errors.push(`bad line: ${line}`); continue; }
148
+ const [, key, val] = kv;
149
+ if (key === 'provenance' || key === 'payload') {
150
+ if (val) { errors.push(`${key} must be a block`); continue; }
151
+ section = key;
152
+ } else {
153
+ item[key] = unquote(val);
154
+ }
155
+ } else if (section === 'provenance') {
156
+ if (line.startsWith('- ')) {
157
+ const kv = line.slice(2).match(KV);
158
+ if (!kv) { errors.push(`bad provenance entry: ${line}`); continue; }
159
+ current = { [kv[1]]: unquote(kv[2]) };
160
+ item.provenance.push(current);
161
+ } else {
162
+ const kv = line.match(KV);
163
+ if (!current || !kv) { errors.push(`bad provenance line: ${line}`); continue; }
164
+ current[kv[1]] = unquote(kv[2]);
165
+ }
166
+ } else if (section === 'payload') {
167
+ if (indent === 2 && !line.startsWith('- ')) {
168
+ current = null;
169
+ const kv = line.match(KV);
170
+ if (!kv) { errors.push(`bad payload line: ${line}`); continue; }
171
+ const [, key, val] = kv;
172
+ if (val === '') { payloadKey = key; /* shape resolved by first child */ }
173
+ else { payloadKey = null; item.payload[key] = unquote(val); }
174
+ } else if (indent >= 4 && payloadKey) {
175
+ const slot = item.payload[payloadKey];
176
+ if (line.startsWith('- ')) {
177
+ const rest = line.slice(2);
178
+ if (!Array.isArray(slot)) {
179
+ if (slot !== undefined && typeof slot === 'object') { errors.push(`payload ${payloadKey}: mixed map/list`); continue; }
180
+ item.payload[payloadKey] = [];
181
+ }
182
+ const kv = rest.match(KV);
183
+ if (kv) {
184
+ current = { [kv[1]]: unquote(kv[2]) };
185
+ item.payload[payloadKey].push(current);
186
+ } else {
187
+ current = null;
188
+ item.payload[payloadKey].push(unquote(rest));
189
+ }
190
+ } else if (current && Array.isArray(slot)) {
191
+ const kv = line.match(KV);
192
+ if (!kv) { errors.push(`bad payload ${payloadKey} entry line: ${line}`); continue; }
193
+ current[kv[1]] = unquote(kv[2]);
194
+ } else {
195
+ // one-level map of scalars
196
+ if (slot === undefined) item.payload[payloadKey] = {};
197
+ else if (Array.isArray(item.payload[payloadKey])) { errors.push(`payload ${payloadKey}: mixed list/map`); continue; }
198
+ const kv = line.match(KV);
199
+ if (!kv) { errors.push(`bad payload ${payloadKey} line: ${line}`); continue; }
200
+ item.payload[payloadKey][kv[1]] = unquote(kv[2]);
201
+ }
202
+ } else {
203
+ errors.push(`unexpected indented line: ${line}`);
204
+ }
205
+ } else {
206
+ errors.push(`unexpected indented line: ${line}`);
207
+ }
208
+ }
209
+
210
+ // empty sub-blocks (key with no children) resolve to {} — already the default
211
+ if (!item.id) errors.push('item missing id');
212
+ if (!item.faculty) errors.push('item missing faculty');
213
+ if (!item.kind) errors.push('item missing kind');
214
+ return { ok: errors.length === 0, item, errors };
215
+ }
216
+
217
+ // --- serialize ----------------------------------------------------------------
218
+
219
+ /** Serialize an envelope item → file text (the exact grammar parseEnvelope reads). */
220
+ export function serializeEnvelope(item) {
221
+ const lines = ['---'];
222
+ for (const key of TOP_KEYS) {
223
+ if (item[key] != null && item[key] !== '') lines.push(`${key}: ${quoteIfNeeded(item[key])}`);
224
+ }
225
+ const prov = item.provenance ?? [];
226
+ if (prov.length) {
227
+ lines.push('provenance:');
228
+ for (const entry of prov) {
229
+ Object.keys(entry).forEach((k, i) => lines.push(` ${i === 0 ? '- ' : ' '}${k}: ${quoteIfNeeded(entry[k])}`));
230
+ }
231
+ }
232
+ const payload = item.payload ?? {};
233
+ const pkeys = Object.keys(payload).filter((k) => payload[k] != null);
234
+ if (pkeys.length) {
235
+ lines.push('payload:');
236
+ for (const k of pkeys) {
237
+ const v = payload[k];
238
+ if (Array.isArray(v)) {
239
+ if (!v.length) continue;
240
+ lines.push(` ${k}:`);
241
+ for (const entry of v) {
242
+ if (entry !== null && typeof entry === 'object') {
243
+ Object.keys(entry).forEach((ek, i) => lines.push(` ${i === 0 ? '- ' : ' '}${ek}: ${quoteIfNeeded(entry[ek])}`));
244
+ } else {
245
+ lines.push(` - ${quoteIfNeeded(entry)}`);
246
+ }
247
+ }
248
+ } else if (v !== null && typeof v === 'object') {
249
+ const entries = Object.entries(v);
250
+ if (!entries.length) continue;
251
+ lines.push(` ${k}:`);
252
+ for (const [mk, mv] of entries) lines.push(` ${mk}: ${quoteIfNeeded(mv)}`);
253
+ } else {
254
+ lines.push(` ${k}: ${quoteIfNeeded(v)}`);
255
+ }
256
+ }
257
+ }
258
+ lines.push('---', '');
259
+ return lines.join('\n') + (item.body ? String(item.body).trim() + '\n' : '');
260
+ }
261
+
262
+ // --- validate -----------------------------------------------------------------
263
+
264
+ /**
265
+ * Validate an envelope item: required envelope fields + (optionally) its payload
266
+ * against a JSON-Schema-subset payload schema.
267
+ * @returns {{ok: boolean, errors: string[]}}
268
+ */
269
+ export function validateEnvelope(item, payloadSchema = null) {
270
+ const errors = [];
271
+ if (!item || typeof item !== 'object') return { ok: false, errors: ['not an item'] };
272
+ if (!item.id || !ID_RE.test(item.id)) errors.push(`id must match ${ID_RE} (got '${item.id}')`);
273
+ if (!FACULTIES.includes(item.faculty)) errors.push(`faculty must be one of ${FACULTIES.join('|')} (got '${item.faculty}')`);
274
+ const kinds = FACULTY_KINDS[item.faculty];
275
+ if (!item.kind || !/^[a-z0-9][a-z0-9-]*$/.test(item.kind)) errors.push(`kind must be a slug (got '${item.kind}')`);
276
+ else if (Array.isArray(kinds) && !kinds.includes(item.kind)) errors.push(`kind must be one of ${kinds.join('|')} for ${item.faculty} (got '${item.kind}')`);
277
+ if (!item.title || typeof item.title !== 'string' || item.title.includes('\n')) errors.push('title is required (single line)');
278
+ if (item.status != null && !STATUSES.has(item.status)) errors.push(`status must be active|archived (got '${item.status}')`);
279
+ if (!item.created_at || !ISO_RE.test(String(item.created_at))) errors.push(`created_at must be ISO (got '${item.created_at}')`);
280
+ if (item.updated_at != null && !ISO_RE.test(String(item.updated_at))) errors.push(`updated_at must be ISO (got '${item.updated_at}')`);
281
+ if (item.provenance != null && !Array.isArray(item.provenance)) errors.push('provenance must be a list');
282
+ if (payloadSchema) errors.push(...validateSchema(payloadSchema, item.payload ?? {}, 'payload'));
283
+ return { ok: errors.length === 0, errors };
284
+ }
285
+
286
+ /** Derive a single-line title (first body line, de-markdowned) — fallback id. */
287
+ export function deriveTitle(body, id) {
288
+ const first = String(body ?? '').split('\n').map((l) => l.replace(/^#+\s*/, '').trim()).find(Boolean);
289
+ return (first || String(id || 'item')).slice(0, 80);
290
+ }
@@ -0,0 +1,206 @@
1
+ // zuzuu/faculty/generation/read.mjs — the generation READ side (WS3-T1, split
2
+ // per the 2026-06-13 overhaul): paths, the pinned-set enumerators, snapshot
3
+ // hashing, list/show/diff. Minting + rollback live in write.mjs.
4
+ //
5
+ // A *generation* is an immutable, content-addressed snapshot of the agent's
6
+ // pinned faculties (the lockfile). Minting freezes the current faculty state;
7
+ // rollback restores any past generation by *content* (we copy each pinned item's
8
+ // bytes into generations/snapshots/<id>/ at mint time, so a rollback works even
9
+ // for items that were never committed). Identity: Agent → Generation → Run —
10
+ // rollback = flip the active pointer + restore content; never `git revert`.
11
+ //
12
+ // Layout under .zuzuu/:
13
+ // generations/active {active: "gen_NNN"} — the live pointer
14
+ // generations/<id>.json the lockfile (content-addressed manifest)
15
+ // generations/snapshots/<id>/<faculty>/... pinned item bytes (rollback source)
16
+
17
+ import { createHash } from 'node:crypto';
18
+ import { join, dirname } from 'node:path';
19
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
20
+
21
+ /** Hex sha256 of a string or Buffer. */
22
+ export function sha256(buf) {
23
+ return createHash('sha256').update(buf).digest('hex');
24
+ }
25
+
26
+ const read = (p) => readFileSync(p, 'utf8');
27
+ export const readJson = (p) => JSON.parse(read(p));
28
+
29
+ // --- paths ------------------------------------------------------------------
30
+
31
+ export const generationsDir = (agentDir) => join(agentDir, 'generations');
32
+ export const snapshotsDir = (agentDir) => join(generationsDir(agentDir), 'snapshots');
33
+ export const activePath = (agentDir) => join(generationsDir(agentDir), 'active');
34
+ export const lockfilePath = (agentDir, id) => join(generationsDir(agentDir), `${id}.json`);
35
+ export const agentJsonPath = (agentDir) => join(agentDir, 'agent.json');
36
+
37
+ // --- faculty file enumeration (the pinned set) ------------------------------
38
+ // Each entry: { id, faculty, src (absolute live path), rel (path under the
39
+ // faculty snapshot dir), hash }. `rel` is what we mirror into snapshots/<id>/.
40
+
41
+ function sortDirents(dir) {
42
+ if (!existsSync(dir)) return [];
43
+ return readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
44
+ }
45
+
46
+ export function knowledgeFiles(agentDir) {
47
+ const dir = join(agentDir, 'knowledge', 'items');
48
+ return sortDirents(dir)
49
+ .filter((e) => e.isFile() && e.name.endsWith('.md'))
50
+ .map((e) => {
51
+ const src = join(dir, e.name);
52
+ return { id: e.name.replace(/\.md$/, ''), faculty: 'knowledge', src, rel: e.name, hash: sha256(readFileSync(src)) };
53
+ });
54
+ }
55
+
56
+ export function actionFiles(agentDir) {
57
+ const dir = join(agentDir, 'actions');
58
+ return sortDirents(dir)
59
+ .filter((e) => e.isDirectory() && e.name !== 'inbox' && e.name !== 'proposals' && e.name !== '_rolledback')
60
+ .map((e) => {
61
+ const adir = join(dir, e.name);
62
+ // Hash the dir's defining files concatenated: the ACTION.md envelope
63
+ // (W24) + sibling scripts (*.mjs — run.mjs and any payload.exec module).
64
+ const parts = sortDirents(adir)
65
+ .filter((f) => f.isFile() && (f.name === 'ACTION.md' || f.name.endsWith('.mjs')))
66
+ .map((f) => join(adir, f.name));
67
+ const concat = Buffer.concat(parts.map((p) => readFileSync(p)));
68
+ return {
69
+ id: e.name, faculty: 'actions', files: parts.map((p) => p.slice(adir.length + 1)),
70
+ adir, hash: parts.length ? sha256(concat) : null,
71
+ };
72
+ });
73
+ }
74
+
75
+ /** Flat envelope-item faculties share one enumerator. */
76
+ function mdItemFiles(agentDir, faculty, ...segments) {
77
+ const dir = join(agentDir, ...segments);
78
+ return sortDirents(dir)
79
+ .filter((e) => e.isFile() && e.name.endsWith('.md'))
80
+ .map((e) => {
81
+ const src = join(dir, e.name);
82
+ return { id: e.name.replace(/\.md$/, ''), faculty, src, rel: e.name, hash: sha256(readFileSync(src)) };
83
+ });
84
+ }
85
+
86
+ export const guardrailFiles = (agentDir) => mdItemFiles(agentDir, 'guardrails', 'guardrails', 'items');
87
+ export const instructionFiles = (agentDir) => mdItemFiles(agentDir, 'instructions', 'instructions', 'items');
88
+
89
+ export function memoryFiles(agentDir) {
90
+ const dir = join(agentDir, 'memory', 'entries');
91
+ return sortDirents(dir)
92
+ .filter((e) => e.isFile() && e.name.endsWith('.md'))
93
+ .map((e) => {
94
+ const src = join(dir, e.name);
95
+ return { id: e.name.replace(/\.md$/, ''), faculty: 'memory', src, rel: e.name, hash: sha256(readFileSync(src)) };
96
+ });
97
+ }
98
+
99
+ export function registryHash(agentDir) {
100
+ const dir = join(agentDir, 'knowledge', 'registry');
101
+ const files = sortDirents(dir).filter((e) => e.isFile() && e.name.endsWith('.json'));
102
+ if (!files.length) return null;
103
+ return sha256(Buffer.concat(files.map((e) => readFileSync(join(dir, e.name)))));
104
+ }
105
+
106
+ /**
107
+ * Snapshot the current faculty state → the `faculties` manifest object.
108
+ * Tolerates missing files (empty arrays / null hashes).
109
+ */
110
+ export function snapshotFaculties(agentDir) {
111
+ return {
112
+ knowledge: {
113
+ items: knowledgeFiles(agentDir).map(({ id, hash }) => ({ id, hash })),
114
+ registryHash: registryHash(agentDir),
115
+ },
116
+ actions: {
117
+ items: actionFiles(agentDir).map(({ id, hash }) => ({ id, hash })),
118
+ },
119
+ guardrails: {
120
+ items: guardrailFiles(agentDir).map(({ id, hash }) => ({ id, hash })),
121
+ },
122
+ instructions: {
123
+ items: instructionFiles(agentDir).map(({ id, hash }) => ({ id, hash })),
124
+ },
125
+ memory: {
126
+ items: memoryFiles(agentDir).map(({ id, hash }) => ({ id, hash })),
127
+ },
128
+ };
129
+ }
130
+
131
+ // --- agent identity ---------------------------------------------------------
132
+
133
+ /** Stable agent id derived from the repo root: agt_<first12 of sha256(root)>. */
134
+ export function agentId(agentDir) {
135
+ // agentDir is the .zuzuu/ dir; the repo root is its parent.
136
+ const root = dirname(agentDir);
137
+ return 'agt_' + sha256(root).slice(0, 12);
138
+ }
139
+
140
+ // --- generation read/list ---------------------------------------------------
141
+
142
+ /** The active generation id, or null. */
143
+ export function activeGeneration(agentDir) {
144
+ const p = activePath(agentDir);
145
+ if (!existsSync(p)) return null;
146
+ try { return readJson(p).active ?? null; } catch { return null; }
147
+ }
148
+
149
+ /** All generation ids in ascending order. */
150
+ export function listGenerations(agentDir) {
151
+ const dir = generationsDir(agentDir);
152
+ if (!existsSync(dir)) return [];
153
+ return readdirSync(dir)
154
+ .filter((f) => /^gen_\d+\.json$/.test(f))
155
+ .map((f) => f.replace(/\.json$/, ''))
156
+ .sort();
157
+ }
158
+
159
+ /** Read one lockfile, or null. */
160
+ export function readGeneration(agentDir, id) {
161
+ const p = lockfilePath(agentDir, id);
162
+ return existsSync(p) ? readJson(p) : null;
163
+ }
164
+
165
+ /** Diff two item-manifest arrays → {added, changed, removed} (id lists). */
166
+ function diffItems(parentItems = [], childItems = []) {
167
+ const p = new Map(parentItems.map((i) => [i.id, i.hash]));
168
+ const c = new Map(childItems.map((i) => [i.id, i.hash]));
169
+ const added = [], changed = [], removed = [];
170
+ for (const [id, hash] of c) {
171
+ if (!p.has(id)) added.push(id);
172
+ else if (p.get(id) !== hash) changed.push(id);
173
+ }
174
+ for (const id of p.keys()) if (!c.has(id)) removed.push(id);
175
+ return { added: added.sort(), changed: changed.sort(), removed: removed.sort() };
176
+ }
177
+
178
+ /**
179
+ * Per-faculty diff of generation `id` against its forkedFrom parent (pure).
180
+ * ALL five faculties are item lists under the Faculty Standard (W24) —
181
+ * added/changed/removed id lists per faculty; knowledge additionally reports
182
+ * registryChanged. When there is no parent (forkedFrom null), everything
183
+ * present counts as added. Returns null for an unknown id.
184
+ */
185
+ export function diffGenerations(agentDir, id) {
186
+ const child = readGeneration(agentDir, id);
187
+ if (!child) return null;
188
+ const parent = child.forkedFrom ? readGeneration(agentDir, child.forkedFrom) : null;
189
+ const cf = child.faculties || {};
190
+ const pf = parent?.faculties || {};
191
+ const faculties = {};
192
+ for (const f of ['knowledge', 'actions', 'memory', 'guardrails', 'instructions']) {
193
+ faculties[f] = diffItems(pf[f]?.items, cf[f]?.items);
194
+ // knowledge also has a registry hash
195
+ if (f === 'knowledge') {
196
+ faculties[f].registryChanged = (cf.knowledge?.registryHash ?? null) !== (pf.knowledge?.registryHash ?? null);
197
+ }
198
+ }
199
+ return {
200
+ id,
201
+ forkedFrom: child.forkedFrom ?? null,
202
+ mintedFrom: Array.isArray(child.mintedFrom) ? child.mintedFrom : [],
203
+ mintedAt: child.mintedAt ?? null,
204
+ faculties,
205
+ };
206
+ }