@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
@@ -0,0 +1,96 @@
1
+ // zuzuu/digest/compose.mjs
2
+ // The grounding digest — a pure, deterministic, zero-network, no-model brief of
3
+ // the faculty home, injected at session start. Returns { text, sections }.
4
+ // I/O-free: callers (the CLI + the SessionStart hook) handle output.
5
+ //
6
+ // Composition (the Faculty Module contract): each built-in module exports its
7
+ // own digestSection(agentDir, ctx); this file iterates the registry and stacks
8
+ // the sections in the canonical order — instructions → knowledge → actions →
9
+ // proposals (spine-level) → guardrails — then a default "N item(s)" section
10
+ // for every DECLARATIVE faculty. Every hook call rides registry.invoke
11
+ // (fail-soft): a single broken faculty never sinks the whole digest.
12
+
13
+ import { listProposals } from '../knowledge/proposals.mjs';
14
+ import { listFacultyItems } from '../faculty/items.mjs';
15
+ import { facultiesOf, invoke } from '../faculty/registry.mjs';
16
+
17
+ // The canonical section order (instructions/knowledge/actions render above the
18
+ // proposals block; guardrails closes the brief — preserved pre-module layout).
19
+ const HEAD_SECTIONS = ['instructions', 'knowledge', 'actions'];
20
+ const TAIL_SECTIONS = ['guardrails'];
21
+
22
+ function proposalsSection(agentDir) {
23
+ try {
24
+ // count only pending — defensive if listProposals ever returns archived too
25
+ const pending = listProposals(agentDir).filter((p) => p.status === 'pending');
26
+ return { pending: pending.length };
27
+ } catch {
28
+ return { pending: 0 };
29
+ }
30
+ }
31
+
32
+ /** Run one faculty's digestSection hook fail-soft; null = no section. */
33
+ function sectionOf(entry, agentDir, ctx) {
34
+ const r = invoke(entry, 'digestSection', agentDir, ctx);
35
+ if (!r.ok || !r.value || !Array.isArray(r.value.lines)) return null;
36
+ return r.value;
37
+ }
38
+
39
+ /** The default section a faculty WITHOUT a digest hook gets: "N item(s)". */
40
+ function defaultSection(agentDir, entry) {
41
+ let count = 0;
42
+ try {
43
+ count = listFacultyItems(agentDir, entry.id, { itemsDir: entry.manifest?.itemsDir }).items.length;
44
+ } catch { /* unreadable → 0 */ }
45
+ return { lines: [`## ${entry.manifest?.title ?? entry.id}`, `${count} item(s)`], data: { count } };
46
+ }
47
+
48
+ /**
49
+ * Compute the digest for a faculty home.
50
+ * @param {string} agentDir path to the .zuzuu/ directory
51
+ * @param {{ knowledgeLimit?: number, budget?: number }} options
52
+ * @returns {{ text: string, sections: object }}
53
+ */
54
+ export function computeDigest(agentDir, { knowledgeLimit = 5, budget = 1500 } = {}) {
55
+ const charBudget = budget * 4;
56
+ const sections = {};
57
+ const lines = ['# zuzuu faculty digest', ''];
58
+
59
+ const faculties = facultiesOf(agentDir);
60
+ const byId = new Map(faculties.map((f) => [f.id, f]));
61
+ const ctx = () => ({ limit: knowledgeLimit, charBudget, priorLines: lines });
62
+
63
+ for (const id of HEAD_SECTIONS) {
64
+ const s = sectionOf(byId.get(id), agentDir, ctx());
65
+ if (!s) continue;
66
+ sections[id] = s.data;
67
+ if (s.lines.length) lines.push(...s.lines, '');
68
+ }
69
+
70
+ // Proposals — spine-level (cross-faculty pending count lives with the gate).
71
+ const proposals = proposalsSection(agentDir);
72
+ sections.proposals = proposals;
73
+ if (proposals.pending > 0) {
74
+ lines.push('## Proposals');
75
+ lines.push(`${proposals.pending} proposal(s) await your approval — run \`zuzuu review\`; approving mints a generation (your checkpoint).`);
76
+ lines.push('');
77
+ }
78
+
79
+ for (const id of TAIL_SECTIONS) {
80
+ const s = sectionOf(byId.get(id), agentDir, ctx());
81
+ if (!s) continue;
82
+ sections[id] = s.data;
83
+ if (s.lines.length) lines.push(...s.lines, '');
84
+ }
85
+
86
+ // Declarative faculties (manifest-only): the default "N item(s)" line each —
87
+ // a faculty you drop into the home is mentioned in the very next brief.
88
+ for (const entry of faculties) {
89
+ if (!entry.declarative || entry.manifestError) continue;
90
+ const s = sectionOf(entry, agentDir, ctx()) ?? defaultSection(agentDir, entry);
91
+ sections[entry.id] = s.data;
92
+ if (s.lines.length) lines.push(...s.lines, '');
93
+ }
94
+
95
+ return { text: lines.join('\n').trimEnd() + '\n', sections };
96
+ }
@@ -1,8 +1,14 @@
1
1
  // zuzuu/eval/score.mjs
2
2
  // Mechanical scorer — weighted sum of normalized signals → { score, confidence, rationale, signals }.
3
3
  // Pure; deterministic; no FS, no Date.now(), no Math.random().
4
+ //
5
+ // Faculty Module hook: a module may export evalSignals(proposal) → PARTIAL
6
+ // signals; those overlay the mechanical extraction (fail-soft via the
7
+ // registry's invoke — a broken hook leaves the default scorer untouched).
8
+ // No built-in implements it today, so behavior is unchanged.
4
9
 
5
10
  import { extractSignals } from './signals.mjs';
11
+ import { BUILTIN_MODULES, invoke } from '../faculty/registry.mjs';
6
12
 
7
13
  // Weight vector (must sum to 1.0).
8
14
  const W = {
@@ -60,7 +66,14 @@ function buildRationale(s) {
60
66
  * @returns {{ score: number, confidence: string, rationale: string, signals: object }}
61
67
  */
62
68
  export function mechanicalScore(proposal, opts = {}) {
63
- const s = extractSignals(proposal, opts);
69
+ let s = extractSignals(proposal, opts);
70
+
71
+ // Optional per-faculty evalSignals hook — partial overlay, fail-soft.
72
+ const mod = BUILTIN_MODULES[proposal?.faculty];
73
+ if (mod && typeof mod.evalSignals === 'function') {
74
+ const r = invoke({ id: proposal.faculty, module: mod }, 'evalSignals', proposal);
75
+ if (r.ok && r.value && typeof r.value === 'object') s = { ...s, ...r.value };
76
+ }
64
77
 
65
78
  const raw =
66
79
  W.occurrence * s.occurrence +
@@ -0,0 +1,283 @@
1
+ // zuzuu/faculties/actions/index.mjs — the Actions faculty module.
2
+ //
3
+ // Consolidates the adapter (the inbox gate over dir-shaped proposals, WS2-T3),
4
+ // the miner (recurring Bash 2-gram sequences → runbook proposals, WS5-T2) and
5
+ // the digest section behind the Faculty Module contract. Substrate code stays
6
+ // in zuzuu/actions/ — this module is the contract face.
7
+ //
8
+ // Actions payloads are DIRECTORIES (ACTION.md + sibling scripts), not JSON.
9
+ // Strategy (lowest-risk): the inbox stays a dir; this adapter emits/reads a
10
+ // spine-shaped proposal RECORD that REFERENCES the dir
11
+ // (payload = { slug, kind, dir:'inbox/<slug>' }). The gate resolves a single
12
+ // record via `getProposal`, lists pending via `listProposals`, and — because
13
+ // the payload is dir-shaped — archives rejections via `rejectDir` (a dir move
14
+ // into actions/proposals/archive/, not a JSON archive).
15
+
16
+ import { join } from 'node:path';
17
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
18
+ import { listActions, allActions, inboxDir, actionsDir, isSafeSlug } from '../../actions/manifest.mjs';
19
+ import { activateAction, rejectAction } from '../../actions/inbox.mjs';
20
+ import { parseEnvelope, validateEnvelope, serializeEnvelope, PAYLOAD_SCHEMAS } from '../../faculty/envelope.mjs';
21
+ import { slugify } from '../../knowledge/items.mjs';
22
+
23
+ const name = 'actions';
24
+
25
+ export const manifest = {
26
+ id: 'actions',
27
+ title: 'Actions',
28
+ tagline: 'how to DO things — runbooks + runnable scripts',
29
+ version: '1.0.0',
30
+ contract: 1,
31
+ kinds: ['runbook', 'script'],
32
+ itemsDir: '.',
33
+ schema: 'schema.json',
34
+ hooks: { miner: true, digest: true, eval: false, gate: false },
35
+ ui: { icon: 'play', accent: 'success', teaching: 'Reusable runbooks and scripts, mined from how you actually work and approved by you.' },
36
+ };
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // adapter contract
40
+ // ---------------------------------------------------------------------------
41
+
42
+ /** Build a spine-shaped proposal record for one proposed action. */
43
+ function recordFor(a) {
44
+ return {
45
+ id: a.slug,
46
+ faculty: name,
47
+ kind: 'action',
48
+ status: 'pending',
49
+ source: 'agent',
50
+ payload: { slug: a.slug, kind: a.kind, dir: `inbox/${a.slug}` },
51
+ // carry render hints alongside the payload (cheap, dir read already done)
52
+ title: a.title,
53
+ promptSnippet: a.promptSnippet,
54
+ analysis: {},
55
+ evidence: {},
56
+ provenance: [],
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Pending action proposals (dirs in .zuzuu/actions/inbox/), surfaced as
62
+ * spine-shaped records so the gate can render/approve/reject them uniformly.
63
+ */
64
+ function listProposals(agentDir) {
65
+ return listActions(inboxDir(agentDir)).map(recordFor);
66
+ }
67
+
68
+ /** Resolve a single proposed action by slug → spine-shaped record, or null. */
69
+ function getProposal(agentDir, slug) {
70
+ if (!isSafeSlug(slug)) return null;
71
+ return listProposals(agentDir).find((p) => p.id === slug) ?? null;
72
+ }
73
+
74
+ /**
75
+ * Ingest is a pass-through for Actions: proposing scaffolds a dir
76
+ * (zuzuu act propose / act-author). Kept for adapter-contract symmetry.
77
+ */
78
+ function ingest(_agentDir, raw) {
79
+ return { payload: raw?.payload ?? raw ?? {}, analysis: {} };
80
+ }
81
+
82
+ /**
83
+ * Validate a proposed action's ACTION.md envelope (id matches the dir; the
84
+ * payload validates against the actions schema). Missing ACTION.md → accept
85
+ * (slug fallback, mirrors the historical missing-manifest tolerance).
86
+ * @returns {{ok:boolean, errors:string[], warnings:string[]}}
87
+ */
88
+ export function validate(agentDir, payload) {
89
+ const slug = payload?.slug;
90
+ if (!isSafeSlug(slug)) return { ok: false, errors: [`invalid slug '${slug}'`], warnings: [] };
91
+ const manPath = join(inboxDir(agentDir), slug, 'ACTION.md');
92
+ if (!existsSync(manPath)) return { ok: true, errors: [], warnings: [] };
93
+ const { ok, item, errors: parseErrors } = parseEnvelope(readFileSync(manPath, 'utf8'));
94
+ if (!ok) return { ok: false, errors: [`ACTION.md is not a valid envelope: ${parseErrors[0]}`], warnings: [] };
95
+ if (item.id && item.id !== slug) return { ok: false, errors: [`ACTION.md id '${item.id}' ≠ dir '${slug}'`], warnings: [] };
96
+ if (item.faculty !== 'actions') return { ok: false, errors: [`ACTION.md faculty must be 'actions' (got '${item.faculty}')`], warnings: [] };
97
+ const v = validateEnvelope(item, PAYLOAD_SCHEMAS.actions);
98
+ return { ok: v.ok, errors: v.errors, warnings: [] };
99
+ }
100
+
101
+ /**
102
+ * Apply an approved action proposal: activate it (move inbox/<slug> → <slug>).
103
+ * Preserves the "already exists" guard from activateAction.
104
+ * @returns {{ok:boolean, action:string, itemIds:string[], warnings:string[]}}
105
+ */
106
+ function apply(agentDir, proposal) {
107
+ const slug = proposal?.payload?.slug ?? proposal?.id;
108
+ const r = activateAction(agentDir, slug);
109
+ if (!r.ok) return { ok: false, action: r.error, itemIds: [], warnings: [] };
110
+ return { ok: true, action: `activated ${slug}`, itemIds: [slug], warnings: [] };
111
+ }
112
+
113
+ export const applyProposal = apply;
114
+
115
+ /**
116
+ * Reject path: dir-shaped, so the gate calls this instead of the JSON archive.
117
+ * Moves inbox/<slug> → actions/proposals/archive/<slug> (archive, not delete).
118
+ */
119
+ function rejectDir(agentDir, slug, _reason = '') {
120
+ return rejectAction(agentDir, slug);
121
+ }
122
+
123
+ /**
124
+ * Render a proposed action for the human gate. `card` mirrors the current review
125
+ * card (slug ── kind, then the prompt snippet); `line` is the one-line list form.
126
+ * @returns {{line:string, card:string}}
127
+ */
128
+ function render(proposal) {
129
+ const slug = proposal?.id ?? proposal?.payload?.slug ?? '';
130
+ const kind = proposal?.payload?.kind ?? proposal?.kind ?? 'action';
131
+ const snippet = proposal?.promptSnippet ?? '';
132
+ return {
133
+ line: `${slug} [${kind}] ${snippet}`,
134
+ card: `${slug} ── ${kind}\n ${snippet}`,
135
+ };
136
+ }
137
+
138
+ export const adapter = { name, ingest, validate, apply, render, listProposals, getProposal, rejectDir };
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // miner (WS5-T2 — recurring Bash 2-grams → runbook proposals, unchanged)
142
+ // ---------------------------------------------------------------------------
143
+
144
+ // Must match the constant in knowledge/distill.mjs (adjacent Bash separator).
145
+ const SEQ_SEP = ' && ';
146
+
147
+ /**
148
+ * Derive a safe slug from a raw sequence string (bounded, safe chars only).
149
+ * e.g. "npm ci && npm test" → "npm-ci-npm-test" (max 50 chars).
150
+ */
151
+ function slugFromSequence(seq) {
152
+ const raw = slugify(seq.replace(/ && /g, ' '), 50);
153
+ // slugify already returns safe chars [a-z0-9-]; isSafeSlug allows upper too,
154
+ // but we keep lower for readability. Force-safe just in case.
155
+ return raw || 'action-sequence';
156
+ }
157
+
158
+ /**
159
+ * Aggregate recurring Bash 2-gram sequences from mined sessions.
160
+ *
161
+ * @param {Array<{sessionId:string, sequences:string[]}>} sessions
162
+ * The per-session mineTranscript output array.
163
+ * @param {object} opts
164
+ * @param {number} [opts.minSeqCount=3] min total occurrences across all sessions
165
+ * @param {number} [opts.minSeqSessions=2] min distinct sessions the sequence appears in
166
+ * @returns {Array<{payload:{slug,title,steps,promptSnippet,sequence}, evidence:{occurrences,sessions,sequence}}>}
167
+ */
168
+ export function aggregate(sessions, { minSeqCount = 3, minSeqSessions = 2 } = {}) {
169
+ // Count occurrences per sequence string, tracking distinct session ids.
170
+ const stats = new Map(); // rawSeq → { count, sessions: Set<sessionId> }
171
+ for (const s of sessions) {
172
+ if (!Array.isArray(s.sequences)) continue;
173
+ for (const seq of s.sequences) {
174
+ const st = stats.get(seq) ?? { count: 0, sessions: new Set() };
175
+ st.count++;
176
+ st.sessions.add(s.sessionId);
177
+ stats.set(seq, st);
178
+ }
179
+ }
180
+
181
+ const candidates = [];
182
+ for (const [seq, st] of stats) {
183
+ if (st.count < minSeqCount || st.sessions.size < minSeqSessions) continue;
184
+ const steps = seq.split(SEQ_SEP);
185
+ const slug = slugFromSequence(seq);
186
+ // Make sure the slug is safe; if not, skip rather than emit a bad slug.
187
+ if (!isSafeSlug(slug)) continue;
188
+ const title = `Run sequence: ${steps.join(' → ')}`;
189
+ const promptSnippet = `Runs: ${steps.join(' then ')}`;
190
+ candidates.push({
191
+ payload: { slug, title, steps, promptSnippet, sequence: seq },
192
+ evidence: { occurrences: st.count, sessions: st.sessions.size, sequence: seq },
193
+ });
194
+ }
195
+ return candidates;
196
+ }
197
+
198
+ /**
199
+ * Write a runbook action proposal into actions/inbox/<slug>/ for each candidate.
200
+ * Idempotent: skips if inbox/<slug>/ OR active actions/<slug>/ already exists.
201
+ *
202
+ * @param {string} agentDir
203
+ * @param {ReturnType<typeof aggregate>} aggregated
204
+ * @returns {number} count of new proposals written
205
+ */
206
+ export function propose(agentDir, aggregated) {
207
+ const actDir = actionsDir(agentDir);
208
+ const ibDir = inboxDir(agentDir);
209
+ let count = 0;
210
+ for (const c of aggregated) {
211
+ const { slug, title, steps, promptSnippet } = c.payload;
212
+ const inboxSlug = join(ibDir, slug);
213
+ const activeSlug = join(actDir, slug);
214
+ // Idempotent: skip if already proposed or already active.
215
+ if (existsSync(inboxSlug) || existsSync(activeSlug)) continue;
216
+
217
+ mkdirSync(inboxSlug, { recursive: true });
218
+
219
+ // ACTION.md — a runbook envelope (no run.mjs; the body IS the procedure).
220
+ // The body's first line is the digest one-liner (promptSnippet).
221
+ const stepsBlock = steps.map((cmd, i) => `${i + 1}. \`${cmd}\``).join('\n');
222
+ writeFileSync(join(inboxSlug, 'ACTION.md'), serializeEnvelope({
223
+ id: slug,
224
+ faculty: 'actions',
225
+ kind: 'runbook',
226
+ title,
227
+ status: 'active',
228
+ created_at: new Date().toISOString().replace(/\.\d+Z$/, 'Z'),
229
+ provenance: [],
230
+ payload: {},
231
+ body: `${promptSnippet}\n\nRecurring command sequence detected from session traces.\n\n## Steps\n\n${stepsBlock}`,
232
+ }));
233
+
234
+ count++;
235
+ }
236
+ return count;
237
+ }
238
+
239
+ export const miner = { faculty: name, aggregate, propose };
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // digest section (moved from the pre-module digest, byte-identical output)
243
+ // ---------------------------------------------------------------------------
244
+
245
+ /**
246
+ * The Actions digest section: available actions under the shared char budget.
247
+ * Renders NOTHING when no actions exist (preserved pre-module behaviour).
248
+ * @param {string} agentDir
249
+ * @param {{limit:number, charBudget:number, priorLines:string[]}} ctx
250
+ * @returns {{lines: string[], data: object}}
251
+ */
252
+ export function digestSection(agentDir, { limit, charBudget, priorLines }) {
253
+ let actions;
254
+ try {
255
+ const list = allActions(agentDir);
256
+ actions = { count: list.length, shown: list.slice(0, limit).map((a) => ({ slug: a.slug, kind: a.kind, promptSnippet: a.promptSnippet })) };
257
+ } catch {
258
+ actions = { count: 0, shown: [] };
259
+ }
260
+ if (!actions.count) return { lines: [], data: { ...actions, renderedCount: 0 } };
261
+ const lines = ['## Actions'];
262
+ lines.push(`${actions.count} available; run with \`zuzuu act <slug>\`:`);
263
+ let shownA = 0;
264
+ for (const a of actions.shown) {
265
+ const line = `- ${a.slug} · ${a.promptSnippet}`;
266
+ if ([...priorLines, ...lines].join('\n').length + line.length > charBudget && shownA > 0) break;
267
+ lines.push(line);
268
+ shownA++;
269
+ }
270
+ const droppedA = actions.count - shownA;
271
+ if (droppedA > 0) lines.push(`- … (${droppedA} more — \`zuzuu act list\`)`);
272
+ // mirror the Knowledge contract: shown reflects what actually rendered
273
+ return { lines, data: { ...actions, shown: actions.shown.slice(0, shownA), renderedCount: shownA } };
274
+ }
275
+
276
+ // ---------------------------------------------------------------------------
277
+ // session signals (the observability surface — `zuzuu session inspect`)
278
+ // ---------------------------------------------------------------------------
279
+
280
+ /** Counts of the mined-signal superset slices this faculty grows from. */
281
+ export function sessionSignals(signals = {}) {
282
+ return { sequences: signals.sequences?.length ?? 0 };
283
+ }