@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
@@ -1,112 +0,0 @@
1
- // zuzuu/miners/actions.mjs
2
- // Actions miner (WS5-T2) — detect recurring Bash 2-gram sequences across
3
- // sessions and scaffold runbook proposals into actions/inbox/<slug>/.
4
- //
5
- // Shape: { faculty:'actions', aggregate(sessions, opts), propose(agentDir, aggregated) }
6
- // Self-registers on import.
7
-
8
- import { join } from 'node:path';
9
- import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
10
- import { slugify } from '../knowledge/items.mjs';
11
- import { isSafeSlug, actionsDir, inboxDir } from '../actions/manifest.mjs';
12
- import { serializeEnvelope } from '../faculty/envelope.mjs';
13
- import { register } from './registry.mjs';
14
-
15
- // Must match the constant in knowledge/distill.mjs (adjacent Bash separator).
16
- const SEQ_SEP = ' && ';
17
-
18
- /**
19
- * Derive a safe slug from a raw sequence string (bounded, safe chars only).
20
- * e.g. "npm ci && npm test" → "npm-ci-npm-test" (max 50 chars).
21
- */
22
- function slugFromSequence(seq) {
23
- const raw = slugify(seq.replace(/ && /g, ' '), 50);
24
- // slugify already returns safe chars [a-z0-9-]; isSafeSlug allows upper too,
25
- // but we keep lower for readability. Force-safe just in case.
26
- return raw || 'action-sequence';
27
- }
28
-
29
- /**
30
- * Aggregate recurring Bash 2-gram sequences from mined sessions.
31
- *
32
- * @param {Array<{sessionId:string, sequences:string[]}>} sessions
33
- * The per-session mineTranscript output array.
34
- * @param {object} opts
35
- * @param {number} [opts.minSeqCount=3] min total occurrences across all sessions
36
- * @param {number} [opts.minSeqSessions=2] min distinct sessions the sequence appears in
37
- * @returns {Array<{payload:{slug,title,steps,promptSnippet,sequence}, evidence:{occurrences,sessions,sequence}}>}
38
- */
39
- export function aggregate(sessions, { minSeqCount = 3, minSeqSessions = 2 } = {}) {
40
- // Count occurrences per sequence string, tracking distinct session ids.
41
- const stats = new Map(); // rawSeq → { count, sessions: Set<sessionId> }
42
- for (const s of sessions) {
43
- if (!Array.isArray(s.sequences)) continue;
44
- for (const seq of s.sequences) {
45
- const st = stats.get(seq) ?? { count: 0, sessions: new Set() };
46
- st.count++;
47
- st.sessions.add(s.sessionId);
48
- stats.set(seq, st);
49
- }
50
- }
51
-
52
- const candidates = [];
53
- for (const [seq, st] of stats) {
54
- if (st.count < minSeqCount || st.sessions.size < minSeqSessions) continue;
55
- const steps = seq.split(SEQ_SEP);
56
- const slug = slugFromSequence(seq);
57
- // Make sure the slug is safe; if not, skip rather than emit a bad slug.
58
- if (!isSafeSlug(slug)) continue;
59
- const title = `Run sequence: ${steps.join(' → ')}`;
60
- const promptSnippet = `Runs: ${steps.join(' then ')}`;
61
- candidates.push({
62
- payload: { slug, title, steps, promptSnippet, sequence: seq },
63
- evidence: { occurrences: st.count, sessions: st.sessions.size, sequence: seq },
64
- });
65
- }
66
- return candidates;
67
- }
68
-
69
- /**
70
- * Write a runbook action proposal into actions/inbox/<slug>/ for each candidate.
71
- * Idempotent: skips if inbox/<slug>/ OR active actions/<slug>/ already exists.
72
- *
73
- * @param {string} agentDir
74
- * @param {ReturnType<typeof aggregate>} aggregated
75
- * @returns {number} count of new proposals written
76
- */
77
- export function propose(agentDir, aggregated) {
78
- const actDir = actionsDir(agentDir);
79
- const ibDir = inboxDir(agentDir);
80
- let count = 0;
81
- for (const c of aggregated) {
82
- const { slug, title, steps, promptSnippet } = c.payload;
83
- const inboxSlug = join(ibDir, slug);
84
- const activeSlug = join(actDir, slug);
85
- // Idempotent: skip if already proposed or already active.
86
- if (existsSync(inboxSlug) || existsSync(activeSlug)) continue;
87
-
88
- mkdirSync(inboxSlug, { recursive: true });
89
-
90
- // ACTION.md — a runbook envelope (no run.mjs; the body IS the procedure).
91
- // The body's first line is the digest one-liner (promptSnippet).
92
- const stepsBlock = steps.map((cmd, i) => `${i + 1}. \`${cmd}\``).join('\n');
93
- writeFileSync(join(inboxSlug, 'ACTION.md'), serializeEnvelope({
94
- id: slug,
95
- faculty: 'actions',
96
- kind: 'runbook',
97
- title,
98
- status: 'active',
99
- created_at: new Date().toISOString().replace(/\.\d+Z$/, 'Z'),
100
- provenance: [],
101
- payload: {},
102
- body: `${promptSnippet}\n\nRecurring command sequence detected from session traces.\n\n## Steps\n\n${stepsBlock}`,
103
- }));
104
-
105
- count++;
106
- }
107
- return count;
108
- }
109
-
110
- export const miner = { faculty: 'actions', aggregate, propose };
111
-
112
- register(miner);
@@ -1,176 +0,0 @@
1
- // zuzuu/miners/guardrails.mjs
2
- // Guardrails miner (WS5-T3) — detect repeated destructive-command failures
3
- // across sessions and propose ask-only guardrail rules.
4
- //
5
- // MANDATORY SAFETY PROPERTIES (enforced in aggregate):
6
- // 1. action is ALWAYS 'ask' — never 'deny'. Auto-proposed rules only escalate
7
- // to the human prompt, they never hard-block.
8
- // 2. Patterns are LITERAL-ESCAPED from the observed command — never a broad/
9
- // free regex. escapeRegex() handles this.
10
- // 3. Cross-session corroboration required — a destructive command must fail
11
- // ≥minFailures (default 3) times across ≥minSessions (default 2) DISTINCT
12
- // sessions. A single session — no matter how many failures — produces
13
- // NOTHING.
14
- //
15
- // Shape: { faculty:'guardrails', aggregate(sessions, opts), propose(agentDir, aggregated) }
16
- // Self-registers on import.
17
-
18
- import { join } from 'node:path';
19
- import { slugify } from '../knowledge/items.mjs';
20
- import { makeProposal, writeProposal, listProposals, isArchivedResolved } from '../faculty/proposal.mjs';
21
- import { loadRules as loadRuleItems } from '../guardrails.mjs';
22
- import { register } from './registry.mjs';
23
-
24
- // ---------------------------------------------------------------------------
25
- // escapeRegex — the ONLY safe way to build a pattern from a literal command.
26
- // Escapes all RegExp metacharacters so the pattern matches the exact string.
27
-
28
- /**
29
- * Escape all regex metacharacters in `s` so that `new RegExp(escapeRegex(s))`
30
- * matches exactly the string `s` and nothing broader.
31
- *
32
- * @param {string} s
33
- * @returns {string}
34
- */
35
- export function escapeRegex(s) {
36
- // Standard set of regex metacharacters that need escaping.
37
- return String(s).replace(/[.*+?^${}()|[\]\\\/\-]/g, '\\$&');
38
- }
39
-
40
- // ---------------------------------------------------------------------------
41
- // helpers
42
-
43
- /** Normalise a command string (trim + collapse whitespace). */
44
- const norm = (cmd) => String(cmd).trim().replace(/\s+/g, ' ').slice(0, 200);
45
-
46
- /** Derive a guardrails-miner id for a command. */
47
- function guardId(cmd) {
48
- return 'guard-' + slugify(cmd, 50);
49
- }
50
-
51
- /** Ids of live rule items (guardrails/items/*.md); absent/unreadable → none. */
52
- function liveRuleIds(agentDir) {
53
- try {
54
- return new Set(loadRuleItems(join(agentDir, 'guardrails')).rules.map((r) => r.id));
55
- } catch {
56
- return new Set();
57
- }
58
- }
59
-
60
- // ---------------------------------------------------------------------------
61
- // aggregate
62
-
63
- /**
64
- * Group destructiveFailures by normalised command; emit a candidate ONLY when
65
- * both the occurrence count and distinct-session count meet their thresholds.
66
- *
67
- * SAFETY: a single-session cluster, no matter how large, produces NOTHING.
68
- *
69
- * @param {Array<{sessionId:string, destructiveFailures:{cmd:string,tool:string}[]}>} sessions
70
- * @param {object} opts
71
- * @param {number} [opts.minFailures=3] min total failures across all sessions
72
- * @param {number} [opts.minSessions=2] min distinct sessions with ≥1 failure each
73
- * @returns {Array<{payload:{id,action,tool,pattern,reason}, evidence:{occurrences,sessions}}>}
74
- */
75
- export function aggregate(sessions, { minFailures = 3, minSessions = 2 } = {}) {
76
- // cmd (normalized) → { count: number, sessions: Set<sessionId>, tool: string }
77
- const stats = new Map();
78
-
79
- for (const s of sessions) {
80
- if (!Array.isArray(s.destructiveFailures)) continue;
81
- for (const { cmd, tool } of s.destructiveFailures) {
82
- const key = norm(cmd);
83
- const st = stats.get(key) ?? { count: 0, sessions: new Set(), tool: tool ?? 'Bash' };
84
- st.count++;
85
- st.sessions.add(s.sessionId);
86
- // Keep first observed tool name (they should all be 'Bash' for destructive cmds).
87
- stats.set(key, st);
88
- }
89
- }
90
-
91
- const candidates = [];
92
- for (const [cmd, st] of stats) {
93
- // SAFETY: enforce BOTH thresholds — cross-session gate is the key one.
94
- if (st.count < minFailures) continue;
95
- if (st.sessions.size < minSessions) continue; // ← single-session always rejected here
96
-
97
- const id = guardId(cmd);
98
- const pattern = escapeRegex(cmd);
99
- const tool = st.tool ?? 'Bash';
100
-
101
- candidates.push({
102
- payload: {
103
- id,
104
- // SAFETY: ALWAYS 'ask', never 'deny'.
105
- action: 'ask',
106
- tool,
107
- pattern,
108
- reason: `auto-proposed: '${cmd}' failed repeatedly across sessions — confirm before running`,
109
- },
110
- evidence: {
111
- occurrences: st.count,
112
- sessions: st.sessions.size,
113
- },
114
- });
115
- }
116
-
117
- return candidates;
118
- }
119
-
120
- // ---------------------------------------------------------------------------
121
- // propose
122
-
123
- /**
124
- * Write a guardrails proposal into .zuzuu/guardrails/proposals/ for each candidate.
125
- * Idempotent:
126
- * - skips if a guardrails proposal with the same payload.id already exists
127
- * - skips if a live rule item (guardrails/items/<id>.md) already exists
128
- * - skips if the id is already resolved in proposals/archive/ — a rejection
129
- * is remembered; re-distilling never resurrects it
130
- *
131
- * The proposals flow through `zuzuu review` → guardrails adapter on approval.
132
- *
133
- * @param {string} agentDir
134
- * @param {ReturnType<typeof aggregate>} aggregated
135
- * @returns {number} count of new proposals written
136
- */
137
- export function propose(agentDir, aggregated) {
138
- // Load existing proposals (ids already pending).
139
- const existing = listProposals(agentDir, 'guardrails');
140
- const existingIds = new Set(existing.map((p) => p.payload?.id).filter(Boolean));
141
-
142
- // Live rule items (ids already applied).
143
- const rulesIds = liveRuleIds(agentDir);
144
-
145
- let count = 0;
146
- for (const c of aggregated) {
147
- const { payload, evidence } = c;
148
-
149
- // Idempotent: skip if already proposed or already a live rule.
150
- if (existingIds.has(payload.id)) continue;
151
- if (rulesIds.has(payload.id)) continue;
152
-
153
- const proposal = makeProposal({
154
- faculty: 'guardrails',
155
- kind: 'rule',
156
- source: 'distill',
157
- payload,
158
- evidence,
159
- });
160
-
161
- // A rejection is remembered: never resurrect an archive-resolved id.
162
- if (isArchivedResolved(agentDir, 'guardrails', proposal.id)) continue;
163
-
164
- writeProposal(agentDir, proposal);
165
- count++;
166
- }
167
-
168
- return count;
169
- }
170
-
171
- // ---------------------------------------------------------------------------
172
- // self-register
173
-
174
- export const miner = { faculty: 'guardrails', aggregate, propose };
175
-
176
- register(miner);
@@ -1,157 +0,0 @@
1
- // zuzuu/miners/instructions.mjs
2
- // Instructions miner (WS5-T4) — detect recurring corrective user turns across
3
- // sessions and propose steering-amendment blocks for the Instructions faculty.
4
- //
5
- // Corrective turns are captured by mineTranscript as:
6
- // correctionTurns: [{ text }] — user turns following an assistant tool action
7
- // that contain corrective lexicon (e.g. "always", "never", "don't", etc.)
8
- //
9
- // Shape: { faculty:'instructions', aggregate(sessions, opts), propose(agentDir, aggregated) }
10
- // Self-registers on import.
11
-
12
- import { makeProposal, writeProposal, listProposals, isArchivedResolved } from '../faculty/proposal.mjs';
13
- import { listFacultyItems } from '../faculty/items.mjs';
14
- import { register } from './registry.mjs';
15
-
16
- // ---------------------------------------------------------------------------
17
- // helpers
18
-
19
- /**
20
- * Normalise a correction text for grouping:
21
- * lowercase, collapse whitespace, truncate to 200 chars.
22
- *
23
- * v1 grouping: near-identical normalised text (exact key match). Simple and
24
- * deterministic; a fuzzy grouper can be earned later.
25
- *
26
- * @param {string} text
27
- * @returns {string}
28
- */
29
- function normText(text) {
30
- return String(text).toLowerCase().replace(/\s+/g, ' ').trim().slice(0, 200);
31
- }
32
-
33
- /**
34
- * Derive a proposal id fragment from a normalised text key.
35
- * Keep it stable, short, and filesystem-safe.
36
- */
37
- function instrId(normKey) {
38
- // Use a slugified version of the first 60 chars of the normalised text,
39
- // prefixed to make collisions with other faculties impossible.
40
- const slug = normKey.slice(0, 60).replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 50) || 'instr';
41
- return 'instr-' + slug;
42
- }
43
-
44
- // ---------------------------------------------------------------------------
45
- // aggregate
46
-
47
- /**
48
- * Group correctionTurns from mined sessions; propose when a similar correction
49
- * recurs across ≥minSessions (default 2) distinct sessions.
50
- *
51
- * @param {Array<{sessionId:string, correctionTurns:{text:string}[]}>} sessions
52
- * @param {object} opts
53
- * @param {number} [opts.minSessions=2] min distinct sessions with the same normalised correction
54
- * @returns {Array<{payload:{text:string}, evidence:{occurrences:number, sessions:number}}>}
55
- */
56
- export function aggregate(sessions, { minSessions = 2 } = {}) {
57
- // normalised text → { count, sessions: Set<sessionId>, rawText: string }
58
- const stats = new Map();
59
-
60
- for (const s of sessions) {
61
- if (!Array.isArray(s.correctionTurns)) continue;
62
- // Track distinct normalised texts per session to avoid double-counting
63
- // the same session for the same correction text.
64
- const seenInSession = new Set();
65
- for (const { text } of s.correctionTurns) {
66
- const key = normText(text);
67
- if (!key) continue;
68
- const st = stats.get(key) ?? { count: 0, sessions: new Set(), rawText: text };
69
- st.count++;
70
- st.sessions.add(s.sessionId);
71
- seenInSession.add(key);
72
- stats.set(key, st);
73
- }
74
- }
75
-
76
- const candidates = [];
77
- for (const [, st] of stats) {
78
- if (st.sessions.size < minSessions) continue;
79
-
80
- // Phrase the raw correction as an instruction for the steering amendment.
81
- // The corrective turn text already reads like user guidance; use it directly
82
- // (trimmed to 500 chars to match mineTranscript's cap).
83
- const amendmentText = st.rawText.slice(0, 500).trim();
84
- const id = instrId(normText(amendmentText));
85
-
86
- candidates.push({
87
- payload: { id, text: amendmentText },
88
- evidence: { occurrences: st.count, sessions: st.sessions.size },
89
- });
90
- }
91
-
92
- return candidates;
93
- }
94
-
95
- // ---------------------------------------------------------------------------
96
- // propose
97
-
98
- /**
99
- * Write an instructions proposal into .zuzuu/instructions/proposals/ for each
100
- * candidate.
101
- *
102
- * Idempotent:
103
- * - skips if an instructions proposal with the same derived id already exists
104
- * - skips if the text is already present in an instructions item (steering
105
- * or a prior amendment)
106
- * - skips if the id is already resolved in proposals/archive/ — a rejection
107
- * is remembered; re-distilling never resurrects it
108
- *
109
- * @param {string} agentDir
110
- * @param {ReturnType<typeof aggregate>} aggregated
111
- * @returns {number} count of new proposals written
112
- */
113
- export function propose(agentDir, aggregated) {
114
- // Collect ids of existing pending proposals for this faculty.
115
- const existing = listProposals(agentDir, 'instructions');
116
- const existingIds = new Set(existing.map((p) => p.payload?.id).filter(Boolean));
117
-
118
- // Read the instructions items (steering + amendments) to skip applied text.
119
- let appliedText = '';
120
- try {
121
- appliedText = listFacultyItems(agentDir, 'instructions').items.map((i) => i.body ?? '').join('\n');
122
- } catch { appliedText = ''; }
123
-
124
- let count = 0;
125
- for (const c of aggregated) {
126
- const { payload, evidence } = c;
127
-
128
- // Idempotent: skip if already proposed.
129
- if (existingIds.has(payload.id)) continue;
130
-
131
- // Idempotent: skip if text already present in an instructions item.
132
- if (appliedText.includes(payload.text)) continue;
133
-
134
- const proposal = makeProposal({
135
- faculty: 'instructions',
136
- kind: 'block',
137
- source: 'distill',
138
- payload,
139
- evidence,
140
- });
141
-
142
- // A rejection is remembered: never resurrect an archive-resolved id.
143
- if (isArchivedResolved(agentDir, 'instructions', proposal.id)) continue;
144
-
145
- writeProposal(agentDir, proposal);
146
- count++;
147
- }
148
-
149
- return count;
150
- }
151
-
152
- // ---------------------------------------------------------------------------
153
- // self-register
154
-
155
- export const miner = { faculty: 'instructions', aggregate, propose };
156
-
157
- register(miner);
@@ -1,25 +0,0 @@
1
- // Knowledge miner (WS5-T1) — the existing source-A distill path, wrapped as a
2
- // registry miner. NO behavior change: `aggregate` is the same function the
3
- // golden distill test asserts on; `propose` mirrors `distillSessions` (one
4
- // `createProposal` per aggregated candidate). Self-registers on import.
5
-
6
- import { aggregate } from '../knowledge/distill.mjs';
7
- import { createProposal } from '../knowledge/proposals.mjs';
8
- import { register } from './registry.mjs';
9
-
10
- /** File one knowledge proposal per aggregated candidate; return the count of
11
- * actually-filed proposals (archive-resolved ids are skipped, not re-filed). */
12
- export function propose(agentDir, aggregated) {
13
- let count = 0;
14
- for (const c of aggregated) {
15
- const p = createProposal(agentDir, { candidate: c.candidate, source: 'distill', evidence: c.evidence });
16
- if (p && p.status !== 'archived-skip') count++;
17
- }
18
- return count;
19
- }
20
-
21
- export const miner = { faculty: 'knowledge', aggregate, propose };
22
-
23
- register(miner);
24
-
25
- export { aggregate };
@@ -1,27 +0,0 @@
1
- // zuzuu/miners/memory.mjs
2
- // Memory miner STUB (WS5-T4) — registered no-op; deferred this pass.
3
- //
4
- // WHAT IT WOULD MINE (when implemented):
5
- // Completed-session episodes — a Run that reached `completed` status in
6
- // .zuzuu/sessions.json — would be distilled into curated episode entries at
7
- // .zuzuu/memory/entries/<id>.md. Each entry captures: what was attempted,
8
- // key decisions made, outcome, and a set of durable learnings. The miner
9
- // would emit `memory` proposals of kind 'episode' into
10
- // .zuzuu/memory/proposals/ for human review via `zuzuu review`. Deferred until
11
- // the Memory substrate (off-edge Postgres/Neon or local Markdown) is
12
- // established and the session lifecycle state machine is stable enough to
13
- // reliably produce `completed` runs with rich enough trace data to distill.
14
- //
15
- // Shape: { faculty:'memory', aggregate, propose, stub:true }
16
- // Self-registers on import (no-op).
17
-
18
- import { register } from './registry.mjs';
19
-
20
- export const miner = {
21
- faculty: 'memory',
22
- stub: true,
23
- aggregate: () => [],
24
- propose: () => 0,
25
- };
26
-
27
- register(miner);
@@ -1,31 +0,0 @@
1
- // Miner registry (WS5-T1) — the faculty-mining plugin table.
2
- //
3
- // Each miner is `{ faculty, aggregate(sessions, opts) -> candidates,
4
- // propose(agentDir, candidates) -> count }`. The `--all-faculties` distill driver
5
- // mines every transcript once into a shared `sessions` array, then runs each
6
- // registered miner's aggregate + propose. Miners self-register on import.
7
-
8
- const miners = [];
9
-
10
- /** Register a miner. Re-registering the same faculty replaces it (idempotent import). */
11
- export function register(miner) {
12
- const i = miners.findIndex((m) => m.faculty === miner.faculty);
13
- if (i >= 0) miners[i] = miner;
14
- else miners.push(miner);
15
- return miner;
16
- }
17
-
18
- /** All registered miners. */
19
- export function all() {
20
- return miners.slice();
21
- }
22
-
23
- /** The miner for a faculty, or undefined. */
24
- export function get(faculty) {
25
- return miners.find((m) => m.faculty === faculty);
26
- }
27
-
28
- /** Clear the registry — tests only. */
29
- export function reset() {
30
- miners.length = 0;
31
- }
File without changes
File without changes