@zuzuucodes/cli 1.5.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 (198) hide show
  1. package/bin/zuzuu.mjs +12 -3
  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} +46 -116
  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-Dx3ub8Pk.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-DM6oHc7c.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-DflaUwqW.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-wbBeb0oN.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-DRwkDcoK.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/trail.mjs +1 -1
  110. package/zuzuu/commands/act.mjs +1 -1
  111. package/zuzuu/commands/capture.mjs +2 -2
  112. package/zuzuu/commands/code.mjs +2 -2
  113. package/zuzuu/commands/digest.mjs +2 -2
  114. package/zuzuu/commands/distill.mjs +15 -16
  115. package/zuzuu/commands/doctor.mjs +39 -4
  116. package/zuzuu/commands/enable.mjs +1 -1
  117. package/zuzuu/commands/eval.mjs +3 -36
  118. package/zuzuu/commands/faculty.mjs +102 -19
  119. package/zuzuu/commands/generation.mjs +3 -4
  120. package/zuzuu/commands/hook.mjs +7 -7
  121. package/zuzuu/commands/inbox.mjs +1 -6
  122. package/zuzuu/commands/init.mjs +5 -4
  123. package/zuzuu/commands/knowledge.mjs +1 -1
  124. package/zuzuu/commands/migrations/home.mjs +96 -0
  125. package/zuzuu/commands/migrations/index.mjs +48 -0
  126. package/zuzuu/commands/{migrate.mjs → migrations/items.mjs} +34 -246
  127. package/zuzuu/commands/migrations/proposals.mjs +100 -0
  128. package/zuzuu/commands/proposals.mjs +131 -0
  129. package/zuzuu/commands/review.mjs +13 -227
  130. package/zuzuu/commands/session.mjs +8 -2
  131. package/zuzuu/commands/sessions.mjs +159 -0
  132. package/zuzuu/commands/status.mjs +3 -3
  133. package/zuzuu/commands/trace.mjs +1 -1
  134. package/zuzuu/{capture-core.mjs → core/capture-core.mjs} +3 -3
  135. package/zuzuu/{store.mjs → core/store.mjs} +1 -1
  136. package/zuzuu/digest/compose.mjs +96 -0
  137. package/zuzuu/eval/score.mjs +14 -1
  138. package/zuzuu/faculties/actions/index.mjs +283 -0
  139. package/zuzuu/faculties/guardrails/index.mjs +320 -0
  140. package/zuzuu/faculties/instructions/index.mjs +288 -0
  141. package/zuzuu/faculties/knowledge/index.mjs +185 -0
  142. package/zuzuu/{memory/adapter.mjs → faculties/memory/index.mjs} +37 -9
  143. package/zuzuu/faculty/generation/read.mjs +206 -0
  144. package/zuzuu/faculty/generation/write.mjs +207 -0
  145. package/zuzuu/faculty/items.mjs +11 -5
  146. package/zuzuu/faculty/module.mjs +74 -0
  147. package/zuzuu/faculty/pending.mjs +63 -0
  148. package/zuzuu/faculty/registry.mjs +204 -18
  149. package/zuzuu/faculty/render.mjs +59 -0
  150. package/zuzuu/faculty/trail.mjs +1 -1
  151. package/zuzuu/{guardrails.mjs → guardrails/engine.mjs} +1 -1
  152. package/zuzuu/{scaffold.mjs → home/scaffold.mjs} +12 -2
  153. package/zuzuu/live/live-store.mjs +2 -2
  154. package/zuzuu/live/reconcile.mjs +2 -2
  155. package/zuzuu/sessions/git.mjs +47 -0
  156. package/zuzuu/{session-git.mjs → sessions/session-git.mjs} +5 -43
  157. package/web-app/web-dist/assets/DiffTab-BpGp1akx.js +0 -1
  158. package/web-app/web-dist/assets/MonacoFile-CqbVacUZ.js +0 -1
  159. package/web-app/web-dist/assets/angular-html-CmT26mqM.js +0 -1
  160. package/web-app/web-dist/assets/c-BvoqrSVH.js +0 -1
  161. package/web-app/web-dist/assets/cpp-BXsk94m0.js +0 -1
  162. package/web-app/web-dist/assets/css-Z8oOGxII.js +0 -1
  163. package/web-app/web-dist/assets/dist-C6R6xoyX.js +0 -153
  164. package/web-app/web-dist/assets/glsl-KwyfU2aa.js +0 -1
  165. package/web-app/web-dist/assets/graphql-DSeOUAa2.js +0 -1
  166. package/web-app/web-dist/assets/haml-azVoxQRV.js +0 -1
  167. package/web-app/web-dist/assets/html-D_7P5S4m.js +0 -1
  168. package/web-app/web-dist/assets/index-DHpC851f.js +0 -268
  169. package/web-app/web-dist/assets/index-O-t1gyMG.css +0 -2
  170. package/web-app/web-dist/assets/java-D4RbCvBe.js +0 -1
  171. package/web-app/web-dist/assets/javascript-Cb010CKM.js +0 -1
  172. package/web-app/web-dist/assets/json-DWgqV4D1.js +0 -1
  173. package/web-app/web-dist/assets/jsx-CZjSJa1f.js +0 -1
  174. package/web-app/web-dist/assets/lua-TGj_6NzO.js +0 -1
  175. package/web-app/web-dist/assets/r-fCpuAR7u.js +0 -1
  176. package/web-app/web-dist/assets/regexp-B4yxx-Ty.js +0 -1
  177. package/web-app/web-dist/assets/scss-QdjMO_xV.js +0 -1
  178. package/web-app/web-dist/assets/shellscript-BnlgeVVx.js +0 -1
  179. package/web-app/web-dist/assets/sql-DGnQv6iD.js +0 -1
  180. package/web-app/web-dist/assets/tsx-MJ0-9sYG.js +0 -1
  181. package/web-app/web-dist/assets/typescript-C17ZkDe8.js +0 -1
  182. package/web-app/web-dist/assets/xml-CA9lHFQV.js +0 -1
  183. package/web-app/web-dist/assets/yaml-CwRYMJka.js +0 -1
  184. package/zuzuu/actions/adapter.mjs +0 -122
  185. package/zuzuu/digest.mjs +0 -154
  186. package/zuzuu/faculty/generation.mjs +0 -398
  187. package/zuzuu/guardrails/adapter.mjs +0 -103
  188. package/zuzuu/instructions/adapter.mjs +0 -93
  189. package/zuzuu/knowledge/adapter.mjs +0 -99
  190. package/zuzuu/miners/actions.mjs +0 -112
  191. package/zuzuu/miners/guardrails.mjs +0 -176
  192. package/zuzuu/miners/instructions.mjs +0 -157
  193. package/zuzuu/miners/knowledge.mjs +0 -25
  194. package/zuzuu/miners/memory.mjs +0 -27
  195. package/zuzuu/miners/registry.mjs +0 -31
  196. /package/web-app/web-dist/assets/{chunk-QTnfLwEv.js → rolldown-runtime-QTnfLwEv.js} +0 -0
  197. /package/zuzuu/{session.mjs → core/session.mjs} +0 -0
  198. /package/zuzuu/{inject.mjs → home/inject.mjs} +0 -0
@@ -22,10 +22,15 @@ const ITEM_DIRS = {
22
22
  guardrails: ['guardrails', 'items'],
23
23
  };
24
24
 
25
- /** The flat items dir for a faculty, or null for dir-shaped faculties (actions). */
26
- export function itemsDirFor(agentDir, faculty) {
25
+ /** The flat items dir for a faculty, or null for dir-shaped faculties (actions).
26
+ * Unknown (declarative) faculties default to `<faculty>/<itemsDir||'items'>`
27
+ * under the home — manifest-only faculties get items listing for free. */
28
+ export function itemsDirFor(agentDir, faculty, itemsDir) {
29
+ if (faculty === 'actions') return null;
27
30
  const rel = ITEM_DIRS[faculty];
28
- return rel ? join(agentDir, ...rel) : null;
31
+ if (rel) return join(agentDir, ...rel);
32
+ const sub = itemsDir && itemsDir !== '.' ? itemsDir : 'items';
33
+ return join(agentDir, faculty, sub);
29
34
  }
30
35
 
31
36
  /** Canonical envelope file path for one item. */
@@ -36,9 +41,10 @@ export function itemPathFor(agentDir, faculty, id) {
36
41
 
37
42
  /**
38
43
  * All envelope items of a faculty. Parse errors collected, never thrown.
44
+ * @param {{itemsDir?: string}} [opts] declarative faculties pass manifest.itemsDir
39
45
  * @returns {{items: object[], errors: Array<{file: string, error: string}>}}
40
46
  */
41
- export function listFacultyItems(agentDir, faculty) {
47
+ export function listFacultyItems(agentDir, faculty, opts = {}) {
42
48
  const items = [];
43
49
  const errors = [];
44
50
  if (faculty === 'actions') {
@@ -56,7 +62,7 @@ export function listFacultyItems(agentDir, faculty) {
56
62
  }
57
63
  return { items, errors };
58
64
  }
59
- const dir = itemsDirFor(agentDir, faculty);
65
+ const dir = itemsDirFor(agentDir, faculty, opts.itemsDir);
60
66
  if (!dir || !existsSync(dir)) return { items, errors };
61
67
  for (const f of readdirSync(dir).filter((f) => f.endsWith('.md')).sort()) {
62
68
  const { ok, item, errors: errs } = parseEnvelope(readFileSync(join(dir, f), 'utf8'));
@@ -0,0 +1,74 @@
1
+ // zuzuu/faculty/module.mjs — the Faculty Module contract (2026-06-13 spec).
2
+ //
3
+ // A faculty = a MANIFEST (<faculty>/faculty.json in the home) + an ITEMS
4
+ // collection (Faculty Standard envelopes under manifest.itemsDir) + a set of
5
+ // named HOOK exports (built-ins: zuzuu/faculties/<id>/index.mjs):
6
+ //
7
+ // manifest — the machine contract (this file's shape)
8
+ // miner = { faculty, aggregate, propose } — REQUIRED for code modules
9
+ // digestSection(agentDir, ctx) — optional; default = "N item(s)" line
10
+ // evalSignals(proposal) — optional; default mechanical scorer
11
+ // gate(toolCall) — optional; ONLY guardrails today
12
+ // applyProposal(agentDir, proposal) — the adapter's apply
13
+ // validate(agentDir, payload) — the adapter's validate
14
+ //
15
+ // Host law (fail-soft everywhere): every hook call is try-wrapped (+ time-boxed
16
+ // for miner-class hooks) by the registry — a broken faculty module degrades to
17
+ // items-only, never crashes the CLI, the gate, or a host hook.
18
+ //
19
+ // Manifest-only folders (faculty.json with NO code) are DECLARATIVE faculties:
20
+ // they get items listing, card UI, schema validation and the default digest
21
+ // line today; third-party CODE loading is deferred (W4).
22
+
23
+ /** The module-API version this host speaks. Majors above this are skipped. */
24
+ export const CONTRACT_VERSION = 1;
25
+
26
+ /**
27
+ * Normalize a raw manifest object into the full contract shape (defaults
28
+ * filled, id forced to the faculty's directory name when absent). Pure.
29
+ * @param {object} raw parsed faculty.json (or {})
30
+ * @param {string} dirName the faculty folder name (fallback id)
31
+ */
32
+ export function normalizeManifest(raw = {}, dirName = 'faculty') {
33
+ const id = typeof raw.id === 'string' && raw.id ? raw.id : dirName;
34
+ const title = typeof raw.title === 'string' && raw.title ? raw.title : id.charAt(0).toUpperCase() + id.slice(1);
35
+ return {
36
+ id,
37
+ title,
38
+ tagline: typeof raw.tagline === 'string' ? raw.tagline : '',
39
+ version: typeof raw.version === 'string' ? raw.version : '1.0.0',
40
+ contract: Number.isFinite(raw.contract) ? raw.contract : CONTRACT_VERSION,
41
+ kinds: Array.isArray(raw.kinds) ? raw.kinds.map(String) : [],
42
+ itemsDir: typeof raw.itemsDir === 'string' && raw.itemsDir ? raw.itemsDir : 'items',
43
+ schema: typeof raw.schema === 'string' && raw.schema ? raw.schema : 'schema.json',
44
+ hooks: {
45
+ miner: !!raw.hooks?.miner,
46
+ digest: !!raw.hooks?.digest,
47
+ eval: !!raw.hooks?.eval,
48
+ gate: !!raw.hooks?.gate,
49
+ },
50
+ ui: {
51
+ icon: typeof raw.ui?.icon === 'string' ? raw.ui.icon : 'folder',
52
+ accent: typeof raw.ui?.accent === 'string' ? raw.ui.accent : 'neutral',
53
+ teaching: typeof raw.ui?.teaching === 'string' ? raw.ui.teaching : '',
54
+ },
55
+ };
56
+ }
57
+
58
+ /** Is a manifest's contract version one this host can serve? (major gate) */
59
+ export function compatibleContract(manifest) {
60
+ return Math.floor(manifest?.contract ?? CONTRACT_VERSION) <= CONTRACT_VERSION;
61
+ }
62
+
63
+ /**
64
+ * Validate a manifest's required fields. Returns {ok, errors} — never throws.
65
+ * (Normalization already fills defaults; this flags the truly broken.)
66
+ */
67
+ export function validateManifest(manifest) {
68
+ const errors = [];
69
+ if (!manifest || typeof manifest !== 'object') return { ok: false, errors: ['not an object'] };
70
+ if (!manifest.id || !/^[a-z0-9][a-z0-9_-]*$/.test(manifest.id)) errors.push(`id must be a slug (got '${manifest.id}')`);
71
+ if (!manifest.title) errors.push('title is required');
72
+ if (!compatibleContract(manifest)) errors.push(`contract ${manifest.contract} > host contract ${CONTRACT_VERSION} — skipped`);
73
+ return { ok: errors.length === 0, errors };
74
+ }
@@ -0,0 +1,63 @@
1
+ // zuzuu/faculty/pending.mjs — pending-proposal collection shared by the gate
2
+ // surfaces (`zuzuu review`, `zuzuu proposals`, `zuzuu eval`). One walk order,
3
+ // one ranked grouping; adapters with their own listProposals (dir-shaped
4
+ // faculties) override the spine read.
5
+
6
+ import * as registry from './registry.mjs';
7
+ import { listProposals as spineListProposals } from './proposal.mjs';
8
+ import { readIndex } from '../core/store.mjs';
9
+ import { rank } from '../eval/rank.mjs';
10
+ import { getScorer } from '../eval/score.mjs';
11
+
12
+ // Review walks faculties in a fixed order so piped sessions are deterministic
13
+ // (the combo smoke test feeds one stdin across the actions pass then knowledge).
14
+ export const REVIEW_ORDER = ['actions', 'knowledge', 'guardrails', 'instructions', 'memory'];
15
+
16
+ /** Build sessionMtimes map from the sessions index — best-effort, fail-open. */
17
+ export function buildSessionMtimes(cwd) {
18
+ try {
19
+ const idx = readIndex(cwd);
20
+ const map = {};
21
+ for (const s of idx.sessions ?? []) {
22
+ if (!s.id) continue;
23
+ const ms = s.startedAt ? Date.parse(s.startedAt) : 0;
24
+ if (!isNaN(ms) && ms > 0) map[s.id] = ms;
25
+ }
26
+ return map;
27
+ } catch {
28
+ return {};
29
+ }
30
+ }
31
+
32
+ /** Pending proposals for one adapter (dir-shaped adapters override listProposals). */
33
+ export function facultyPending(agentDir, a) {
34
+ if (typeof a.listProposals === 'function') return a.listProposals(agentDir);
35
+ // JSON-record faculties: read via the spine (records carry both the spine shape
36
+ // and the legacy candidate/er keys the knowledge card renders from).
37
+ return spineListProposals(agentDir, a.name);
38
+ }
39
+
40
+ /** Ordered list of adapters that have pending proposals to review. */
41
+ export function pendingByFaculty(agentDir) {
42
+ const adapters = registry.all();
43
+ const seen = new Set();
44
+ const ordered = [];
45
+ for (const name of REVIEW_ORDER) {
46
+ const a = adapters.find((x) => x.name === name);
47
+ if (a) { ordered.push(a); seen.add(name); }
48
+ }
49
+ for (const a of adapters) if (!seen.has(a.name)) ordered.push(a);
50
+ const sessionMtimes = buildSessionMtimes();
51
+ const now = Date.now();
52
+ const scorer = getScorer();
53
+ const out = [];
54
+ for (const a of ordered) {
55
+ let proposals = facultyPending(agentDir, a);
56
+ if (!proposals.length) continue;
57
+ // Rank proposals highest-score-first (display only — never changes approval/mint).
58
+ const ranked = rank(proposals, scorer, { now, sessionMtimes });
59
+ proposals = ranked.map((r) => r.proposal);
60
+ out.push({ adapter: a, proposals });
61
+ }
62
+ return out;
63
+ }
@@ -1,33 +1,219 @@
1
- // zuzuu/faculty/registry.mjs
2
- // Faculty adapter registry — a module-level Map keyed by adapter.name.
3
- // Adapters register themselves on import; consumers query by name or list all.
1
+ // zuzuu/faculty/registry.mjs — the Faculty Module registry (2026-06-13 spec).
4
2
  //
5
- // This is the registration surface only. Adapters are added in later work units.
3
+ // Replaces the old scattered wiring (miners/registry self-registration,
4
+ // per-faculty adapter imports at every spine call site, digest's hardcoded
5
+ // sections): the five BUILT-IN modules are imported statically here; the spine
6
+ // (gate/review/proposals/eval/distill/digest/doctor) iterates THIS registry —
7
+ // no faculty names hardcoded outside built-in module files (ordering
8
+ // preferences excepted).
9
+ //
10
+ // Discovery beyond built-ins: any `<home>/<dir>/faculty.json` is parsed and
11
+ // listed as a DECLARATIVE faculty (manifest-only — items listing, card UI,
12
+ // schema validation, default digest line work today). Third-party CODE loading
13
+ // is explicitly deferred to W4.
14
+ //
15
+ // Host law: every hook invocation goes through invoke()/invokeTimeboxed() —
16
+ // try-wrapped (+ 5s time-box on miner-class hooks). A broken module degrades
17
+ // to items-only; failures are recorded and surfaced by `zuzuu doctor`.
18
+
19
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
20
+ import { join } from 'node:path';
21
+ import { FACULTIES } from './contract.mjs';
22
+ import { normalizeManifest, compatibleContract } from './module.mjs';
23
+ import * as knowledge from '../faculties/knowledge/index.mjs';
24
+ import * as memory from '../faculties/memory/index.mjs';
25
+ import * as actions from '../faculties/actions/index.mjs';
26
+ import * as instructions from '../faculties/instructions/index.mjs';
27
+ import * as guardrails from '../faculties/guardrails/index.mjs';
28
+
29
+ export const BUILTIN_MODULES = { knowledge, memory, actions, instructions, guardrails };
30
+
31
+ // Legacy adapter/miner walk order (the pre-module import order at every call
32
+ // site) — preserved so list/eval/distill outputs stay byte-identical.
33
+ const LEGACY_ORDER = ['knowledge', 'actions', 'guardrails', 'instructions', 'memory'];
6
34
 
7
- /** @type {Map<string, object>} */
8
- const _registry = new Map();
35
+ // ---------------------------------------------------------------------------
36
+ // adapter surface (back-compat: gate/review/proposals/eval consume these)
37
+ // ---------------------------------------------------------------------------
38
+
39
+ /** Test/extension overrides layered over the built-ins. */
40
+ const overrides = new Map();
9
41
 
10
42
  /**
11
- * Register an adapter (keyed by adapter.name). Overwrites if already registered.
12
- * @param {{ name: string, [key: string]: any }} adapter
43
+ * Register an adapter override (keyed by adapter.name). Built-ins are always
44
+ * present; this layers replacements/additions on top (tests, future plugins).
13
45
  */
14
46
  export function register(adapter) {
15
- _registry.set(adapter.name, adapter);
47
+ overrides.set(adapter.name, adapter);
48
+ }
49
+
50
+ /** Retrieve an adapter by faculty name (override > built-in). */
51
+ export function get(name) {
52
+ return overrides.get(name) ?? BUILTIN_MODULES[name]?.adapter;
53
+ }
54
+
55
+ /** All adapters, built-ins first in the legacy order, then extra overrides. */
56
+ export function all() {
57
+ const names = [...LEGACY_ORDER];
58
+ for (const n of overrides.keys()) if (!names.includes(n)) names.push(n);
59
+ return names.map(get).filter(Boolean);
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // miner surface (replaces miners/registry.mjs)
64
+ // ---------------------------------------------------------------------------
65
+
66
+ /** All built-in miners in the legacy distill order. */
67
+ export function miners() {
68
+ return LEGACY_ORDER.map((f) => BUILTIN_MODULES[f]?.miner).filter(Boolean);
69
+ }
70
+
71
+ /** The miner for a faculty, or undefined. */
72
+ export function minerOf(faculty) {
73
+ return BUILTIN_MODULES[faculty]?.miner;
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // faculty discovery — built-ins + declarative faculty.json folders
78
+ // ---------------------------------------------------------------------------
79
+
80
+ /** Read + normalize `<home>/<id>/faculty.json` → {manifest|null, error|null}. */
81
+ function readHomeManifest(agentDir, id) {
82
+ const p = join(agentDir, id, 'faculty.json');
83
+ if (!existsSync(p)) return { manifest: null, error: null };
84
+ try {
85
+ const manifest = normalizeManifest(JSON.parse(readFileSync(p, 'utf8')), id);
86
+ if (!compatibleContract(manifest)) {
87
+ return { manifest: null, error: `contract ${manifest.contract} unsupported by this host` };
88
+ }
89
+ return { manifest, error: null };
90
+ } catch (e) {
91
+ return { manifest: null, error: e.message ?? String(e) };
92
+ }
16
93
  }
17
94
 
95
+ // Home dirs that are never faculties.
96
+ const NON_FACULTY_DIRS = new Set(['generations']);
97
+
18
98
  /**
19
- * Retrieve a registered adapter by name. Returns undefined if not found.
20
- * @param {string} name
21
- * @returns {{ name: string, [key: string]: any } | undefined}
99
+ * Every faculty this home serves: the five built-ins (always module code +
100
+ * manifest, home faculty.json overriding the built-in manifest when present)
101
+ * plus declarative manifest-only folders. Fail-soft: a broken faculty.json
102
+ * lists the faculty with `manifestError` (degraded — doctor reports it).
103
+ *
104
+ * @param {string} agentDir
105
+ * @returns {Array<{id, manifest, module, builtin, declarative, manifestSource, manifestError?}>}
22
106
  */
23
- export function get(name) {
24
- return _registry.get(name);
107
+ export function facultiesOf(agentDir) {
108
+ const out = [];
109
+ for (const id of FACULTIES) {
110
+ const mod = BUILTIN_MODULES[id];
111
+ const home = readHomeManifest(agentDir, id);
112
+ out.push({
113
+ id,
114
+ builtin: true,
115
+ declarative: false,
116
+ module: mod,
117
+ manifest: home.manifest ?? mod.manifest,
118
+ manifestSource: home.manifest ? 'home' : 'builtin',
119
+ ...(home.error ? { manifestError: home.error } : {}),
120
+ });
121
+ }
122
+ try {
123
+ for (const e of readdirSync(agentDir, { withFileTypes: true })) {
124
+ if (!e.isDirectory() || e.name.startsWith('.') || e.name.startsWith('_')) continue;
125
+ if (FACULTIES.includes(e.name) || NON_FACULTY_DIRS.has(e.name)) continue;
126
+ const home = readHomeManifest(agentDir, e.name);
127
+ if (!home.manifest && !home.error) continue; // no faculty.json → not a faculty
128
+ out.push({
129
+ id: e.name,
130
+ builtin: false,
131
+ declarative: true,
132
+ module: null, // third-party CODE loading deferred to W4 — manifest-only today
133
+ manifest: home.manifest ?? normalizeManifest({}, e.name),
134
+ manifestSource: 'home',
135
+ ...(home.error ? { manifestError: home.error } : {}),
136
+ });
137
+ }
138
+ } catch { /* no home dir yet → built-ins only */ }
139
+ return out;
140
+ }
141
+
142
+ /** One faculty entry by id (built-in or declarative), or null. */
143
+ export function facultyOf(agentDir, id) {
144
+ return facultiesOf(agentDir).find((f) => f.id === id) ?? null;
145
+ }
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // fail-soft hook invocation (+ degradation record for doctor)
149
+ // ---------------------------------------------------------------------------
150
+
151
+ /** Miner-class hooks get a wall-clock budget (spec: 5s). */
152
+ export const MINER_HOOK_TIMEOUT_MS = 5000;
153
+
154
+ const failures = new Map(); // `${faculty}.${hook}` → { faculty, hook, error, at }
155
+
156
+ function recordFailure(faculty, hook, error) {
157
+ failures.set(`${faculty}.${hook}`, { faculty, hook, error: String(error?.message ?? error), at: new Date().toISOString() });
158
+ }
159
+
160
+ /** Hook failures recorded this process — `zuzuu doctor` surfaces these. */
161
+ export function hookFailures() {
162
+ return [...failures.values()];
163
+ }
164
+
165
+ /** Tests only: forget recorded failures. */
166
+ export function clearHookFailures() {
167
+ failures.clear();
25
168
  }
26
169
 
27
170
  /**
28
- * Return all registered adapters as an array.
29
- * @returns {Array<{ name: string, [key: string]: any }>}
171
+ * Invoke a module hook fail-soft (synchronous spine paths: digest, gate,
172
+ * signals). NEVER throws.
173
+ * @param {{id:string, module:object|null}} entry a facultiesOf() entry (or {id, module})
174
+ * @returns {{ok:true, value:any} | {ok:false, missing?:true, error?:string}}
30
175
  */
31
- export function all() {
32
- return [..._registry.values()];
176
+ export function invoke(entry, hook, ...args) {
177
+ const fn = entry?.module?.[hook];
178
+ if (typeof fn !== 'function') return { ok: false, missing: true };
179
+ try {
180
+ return { ok: true, value: fn(...args) };
181
+ } catch (e) {
182
+ recordFailure(entry.id ?? entry.module?.manifest?.id ?? '?', hook, e);
183
+ return { ok: false, error: String(e?.message ?? e) };
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Invoke a miner-class hook fail-soft WITH a time-box: synchronous throws are
189
+ * caught; an async (Promise-returning) hook is raced against `timeoutMs`
190
+ * (default 5s). NEVER rejects.
191
+ * @returns {Promise<{ok:true, value:any} | {ok:false, missing?:true, timedOut?:true, error?:string}>}
192
+ */
193
+ export async function invokeTimeboxed(entry, hook, args = [], { timeoutMs = MINER_HOOK_TIMEOUT_MS } = {}) {
194
+ const fn = entry?.module?.[hook];
195
+ if (typeof fn !== 'function') return { ok: false, missing: true };
196
+ const faculty = entry.id ?? entry.module?.manifest?.id ?? '?';
197
+ try {
198
+ const r = fn(...args);
199
+ if (!r || typeof r.then !== 'function') return { ok: true, value: r };
200
+ let timer;
201
+ const timeout = new Promise((resolve) => {
202
+ timer = setTimeout(() => resolve({ __zuzuuTimeout: true }), timeoutMs);
203
+ });
204
+ const settled = await Promise.race([Promise.resolve(r).catch((e) => ({ __zuzuuError: e })), timeout]);
205
+ clearTimeout(timer);
206
+ if (settled && settled.__zuzuuTimeout) {
207
+ recordFailure(faculty, hook, `timed out after ${timeoutMs}ms`);
208
+ return { ok: false, timedOut: true, error: `timed out after ${timeoutMs}ms` };
209
+ }
210
+ if (settled && settled.__zuzuuError !== undefined) {
211
+ recordFailure(faculty, hook, settled.__zuzuuError);
212
+ return { ok: false, error: String(settled.__zuzuuError?.message ?? settled.__zuzuuError) };
213
+ }
214
+ return { ok: true, value: settled };
215
+ } catch (e) {
216
+ recordFailure(faculty, hook, e);
217
+ return { ok: false, error: String(e?.message ?? e) };
218
+ }
33
219
  }
@@ -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.
@@ -31,7 +31,7 @@
31
31
 
32
32
  import { join } from 'node:path';
33
33
  import { readFileSync, readdirSync, statSync } from 'node:fs';
34
- import { parseEnvelope } from './faculty/envelope.mjs';
34
+ import { parseEnvelope } from '../faculty/envelope.mjs';
35
35
 
36
36
  const SEVERITY = { deny: 3, ask: 2, allow: 1 };
37
37
  const ACTIONS = new Set(Object.keys(SEVERITY));
@@ -15,8 +15,9 @@
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';
19
- import { serializeEnvelope, PAYLOAD_SCHEMAS, FACULTY_KINDS } from './faculty/envelope.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';
20
21
 
21
22
  export const MANIFEST_VERSION = 4;
22
23
 
@@ -194,6 +195,10 @@ const ENVELOPE_SPEC = JSON.stringify(
194
195
 
195
196
  const payloadSchemaSeed = (f) => JSON.stringify(PAYLOAD_SCHEMAS[f], null, 2) + '\n';
196
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';
201
+
197
202
  /** The layout contract: dirs + seed files (relative to the project root). */
198
203
  export const LAYOUT = {
199
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'],
@@ -202,15 +207,20 @@ export const LAYOUT = {
202
207
  '.zuzuu/schema.json': ENVELOPE_SPEC,
203
208
  '.zuzuu/knowledge/README.md': KNOWLEDGE_README,
204
209
  '.zuzuu/knowledge/schema.json': payloadSchemaSeed('knowledge'),
210
+ '.zuzuu/knowledge/faculty.json': manifestSeed('knowledge'),
205
211
  '.zuzuu/memory/README.md': MEMORY_README,
206
212
  '.zuzuu/memory/schema.json': payloadSchemaSeed('memory'),
213
+ '.zuzuu/memory/faculty.json': manifestSeed('memory'),
207
214
  '.zuzuu/actions/README.md': ACTIONS_README,
208
215
  '.zuzuu/actions/schema.json': payloadSchemaSeed('actions'),
216
+ '.zuzuu/actions/faculty.json': manifestSeed('actions'),
209
217
  '.zuzuu/instructions/README.md': INSTRUCTIONS_README,
210
218
  '.zuzuu/instructions/schema.json': payloadSchemaSeed('instructions'),
219
+ '.zuzuu/instructions/faculty.json': manifestSeed('instructions'),
211
220
  '.zuzuu/instructions/items/steering.md': STEERING_SEED,
212
221
  '.zuzuu/guardrails/README.md': GUARDRAILS_README,
213
222
  '.zuzuu/guardrails/schema.json': payloadSchemaSeed('guardrails'),
223
+ '.zuzuu/guardrails/faculty.json': manifestSeed('guardrails'),
214
224
  '.zuzuu/guardrails/items/no-root-wipe.md': RULE_SEEDS['no-root-wipe'],
215
225
  '.zuzuu/guardrails/items/no-secret-reads.md': RULE_SEEDS['no-secret-reads'],
216
226
  '.zuzuu/guardrails/items/confirm-force-push.md': RULE_SEEDS['confirm-force-push'],
@@ -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
 
@@ -0,0 +1,47 @@
1
+ // zuzuu/sessions/git.mjs — git PLUMBING for session-git (no session policy here).
2
+ //
3
+ // Every helper is safe-by-construction: argv arrays only (no shell strings),
4
+ // never throws, returns plain data. Session POLICY (open/checkpoint/close/
5
+ // status and the safety gates) lives in session-git.mjs.
6
+
7
+ import { spawnSync } from 'node:child_process';
8
+ import { rmSync } from 'node:fs';
9
+ import { join, isAbsolute, resolve } from 'node:path';
10
+
11
+ /** One git call — argv array only (no shell), never throws. */
12
+ export function git(args, cwd, input) {
13
+ try {
14
+ const r = spawnSync('git', args, { cwd, encoding: 'utf8', input });
15
+ return { ok: r.status === 0 && !r.error, out: (r.stdout ?? '').trim(), err: (r.stderr ?? '').trim() };
16
+ } catch (e) {
17
+ return { ok: false, out: '', err: String(e) };
18
+ }
19
+ }
20
+
21
+ export function gitDir(cwd) {
22
+ const r = git(['rev-parse', '--git-dir'], cwd);
23
+ if (!r.ok || !r.out) return null;
24
+ return isAbsolute(r.out) ? r.out : resolve(cwd, r.out);
25
+ }
26
+
27
+ /** Current branch name, or null when detached / not a repo. */
28
+ export function currentBranch(cwd) {
29
+ const r = git(['symbolic-ref', '--short', '-q', 'HEAD'], cwd);
30
+ return r.ok && r.out ? r.out : null;
31
+ }
32
+
33
+ export const branchExists = (cwd, name) => git(['rev-parse', '-q', '--verify', `refs/heads/${name}`], cwd).ok;
34
+ export const isDirty = (cwd) => !!git(['status', '--porcelain'], cwd).out;
35
+
36
+ /** Best-effort: drop squash leftovers so they can't leak into the user's next commit. */
37
+ export function cleanupSquashState(cwd) {
38
+ const gd = gitDir(cwd);
39
+ if (!gd) return;
40
+ for (const f of ['SQUASH_MSG', 'MERGE_MSG']) {
41
+ try {
42
+ rmSync(join(gd, f), { force: true });
43
+ } catch {
44
+ /* best-effort */
45
+ }
46
+ }
47
+ }