@zuzuucodes/cli 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/zuzuu.mjs +20 -4
- package/package.json +1 -1
- package/web-app/dist/auth.js +91 -0
- package/web-app/dist/server.js +16 -79
- package/web-app/dist/zuzuu-cli.js +124 -0
- package/web-app/dist/{zuzuu-api.js → zuzuu-routes.js} +158 -133
- package/web-app/web-dist/assets/CommandPalette-DhBdR7X3.js +45 -0
- package/web-app/web-dist/assets/DiffTab-CqxwSjI2.js +1 -0
- package/web-app/web-dist/assets/EditorPane-94QPFR9R.js +41 -0
- package/web-app/web-dist/assets/MonacoFile-D76epTrG.js +1 -0
- package/web-app/web-dist/assets/angular-html-BVBpGdXr.js +1 -0
- package/web-app/web-dist/assets/{angular-ts-CD_OonCa.js → angular-ts-BfdufMKP.js} +1 -1
- package/web-app/web-dist/assets/{apl-uOGC3x4e.js → apl-DWBSSoBH.js} +1 -1
- package/web-app/web-dist/assets/{astro-B6ybQmWG.js → astro-3LtMP0Sq.js} +1 -1
- package/web-app/web-dist/assets/{blade-B1QGRlVx.js → blade-llJRbbtR.js} +1 -1
- package/web-app/web-dist/assets/c-Wt1voDr2.js +1 -0
- package/web-app/web-dist/assets/{cobol-BgqgtYWn.js → cobol-x_HIyl2P.js} +1 -1
- package/web-app/web-dist/assets/{coffee-0wIRKYlr.js → coffee-CThvmt4R.js} +1 -1
- package/web-app/web-dist/assets/cpp-NtAeskI3.js +1 -0
- package/web-app/web-dist/assets/{crystal-CyTK3qFN.js → crystal-DNu_sX0G.js} +1 -1
- package/web-app/web-dist/assets/css-DJp_X0uY.js +1 -0
- package/web-app/web-dist/assets/{cssMode-B9jnrWOz.js → cssMode-ByQBaInt.js} +1 -1
- package/web-app/web-dist/assets/dist-DQqjtuhV.js +153 -0
- package/web-app/web-dist/assets/{edge-CvML9pwC.js → edge-ozw5tpLl.js} +1 -1
- package/web-app/web-dist/assets/{editor.api2-BmGoRSl4.js → editor.api2-C7skgoRB.js} +1 -1
- package/web-app/web-dist/assets/{elixir-CrjqTiSc.js → elixir-VhA6FeZt.js} +1 -1
- package/web-app/web-dist/assets/{elm-C4JtJ0Au.js → elm-dREJmIFz.js} +1 -1
- package/web-app/web-dist/assets/{erb-Cmeb-29V.js → erb-CIg6G69l.js} +1 -1
- package/web-app/web-dist/assets/{freemarker2-B5LAi19B.js → freemarker2-CBBwP9JV.js} +1 -1
- package/web-app/web-dist/assets/{git-rebase-CXqdToiP.js → git-rebase-B44mJPta.js} +1 -1
- package/web-app/web-dist/assets/{glimmer-js-Kq-kdTyV.js → glimmer-js-vH_gHG0-.js} +1 -1
- package/web-app/web-dist/assets/{glimmer-ts-D0RKLJNf.js → glimmer-ts--abOzSAQ.js} +1 -1
- package/web-app/web-dist/assets/glsl-Dv5r7kPw.js +1 -0
- package/web-app/web-dist/assets/graphql-CB4jsw2E.js +1 -0
- package/web-app/web-dist/assets/{hack-trjVF3Po.js → hack-DvEYX148.js} +1 -1
- package/web-app/web-dist/assets/haml-zE6W3STP.js +1 -0
- package/web-app/web-dist/assets/{handlebars-B8_x7Zx7.js → handlebars-CzBR2SDs.js} +1 -1
- package/web-app/web-dist/assets/{handlebars-g7ZhGhI_.js → handlebars-tXdfxEd6.js} +1 -1
- package/web-app/web-dist/assets/html-C8UlPnhE.js +1 -0
- package/web-app/web-dist/assets/{html-CfvRMgoC.js → html-DgPn1QYH.js} +1 -1
- package/web-app/web-dist/assets/{html-derivative-BYX_F_XH.js → html-derivative-CY6NRz-J.js} +1 -1
- package/web-app/web-dist/assets/{htmlMode-Bi8vSvwb.js → htmlMode-BtdIDgA2.js} +1 -1
- package/web-app/web-dist/assets/{http-BIVDpHT-.js → http-Cyd7bS_S.js} +1 -1
- package/web-app/web-dist/assets/{hurl-CFsshMju.js → hurl-CWPsiEpf.js} +1 -1
- package/web-app/web-dist/assets/index-B27_WOhS.css +2 -0
- package/web-app/web-dist/assets/index-De6DWTZM.js +7 -0
- package/web-app/web-dist/assets/java-CGc3VwQr.js +1 -0
- package/web-app/web-dist/assets/{javascript-Bxx2wV4w.js → javascript-5m05n-Be.js} +1 -1
- package/web-app/web-dist/assets/javascript-CUt1pgmJ.js +1 -0
- package/web-app/web-dist/assets/{jinja-_ZS5zWwe.js → jinja-CD-Z-FLd.js} +1 -1
- package/web-app/web-dist/assets/{jison-D8mMEpcs.js → jison-imPNup1l.js} +1 -1
- package/web-app/web-dist/assets/json-Bg9ijW3F.js +1 -0
- package/web-app/web-dist/assets/{jsonMode-C6ELX5GM.js → jsonMode-BG32YnTY.js} +1 -1
- package/web-app/web-dist/assets/jsx-CY6oMTks.js +1 -0
- package/web-app/web-dist/assets/{julia-D4h2DZrs.js → julia-Dc3O-irA.js} +1 -1
- package/web-app/web-dist/assets/{just-bMqQi3xg.js → just-BhOq_Kbv.js} +1 -1
- package/web-app/web-dist/assets/{latex-DThYi3CX.js → latex-Cu4Y1d5w.js} +1 -1
- package/web-app/web-dist/assets/lib-KIOQTlcs.js +1 -0
- package/web-app/web-dist/assets/{liquid-CUjzzP4r.js → liquid-3ZnQzTbs.js} +1 -1
- package/web-app/web-dist/assets/{liquid-CesB-zzl.js → liquid-CvXMrjlQ.js} +1 -1
- package/web-app/web-dist/assets/{lspLanguageFeatures-gTnJsses.js → lspLanguageFeatures-6KXALSrl.js} +1 -1
- package/web-app/web-dist/assets/lua-BjLEUjKY.js +1 -0
- package/web-app/web-dist/assets/{marko-yoGoLK2m.js → marko-DvhNOisQ.js} +1 -1
- package/web-app/web-dist/assets/{mdc-BvtXU6eH.js → mdc-Bm9TpL1X.js} +1 -1
- package/web-app/web-dist/assets/{mdx-DrXGQbNB.js → mdx-DffTEkNE.js} +1 -1
- package/web-app/web-dist/assets/{monaco-setup-CsR6EfHe.js → monaco-setup-DM3A5_VI.js} +3 -3
- package/web-app/web-dist/assets/{nginx-DoUz032F.js → nginx-Bhc82uuv.js} +1 -1
- package/web-app/web-dist/assets/{nim-B0Pl8B4R.js → nim-DXTVBFnF.js} +1 -1
- package/web-app/web-dist/assets/{perl-D2tfAALb.js → perl-C7veXV9z.js} +1 -1
- package/web-app/web-dist/assets/{php-BImCcX5X.js → php-BRiuMnnr.js} +1 -1
- package/web-app/web-dist/assets/{pug-BcnpC8P_.js → pug-C5hz5LQ7.js} +1 -1
- package/web-app/web-dist/assets/{python-ypRCBnvu.js → python-DyLAD3Wt.js} +1 -1
- package/web-app/web-dist/assets/{qml-DFDAunHY.js → qml-BdUV3aTS.js} +1 -1
- package/web-app/web-dist/assets/r-8R7vtdQc.js +1 -0
- package/web-app/web-dist/assets/{razor-aqrhpwqZ.js → razor-C49xQTPQ.js} +1 -1
- package/web-app/web-dist/assets/{razor-1_376SZM.js → razor-DRL52XO2.js} +1 -1
- package/web-app/web-dist/assets/react-vendor-CCIEwYL0.js +9 -0
- package/web-app/web-dist/assets/regexp-Omp9DhTb.js +1 -0
- package/web-app/web-dist/assets/{rst-2vG6f11Y.js → rst-BHX71KW9.js} +1 -1
- package/web-app/web-dist/assets/{ruby-Dj6bCFXR.js → ruby-B--HzjGU.js} +1 -1
- package/web-app/web-dist/assets/{sas-BhVZ4qL2.js → sas-DrLaYOK_.js} +1 -1
- package/web-app/web-dist/assets/scss-DdSxiZKl.js +1 -0
- package/web-app/web-dist/assets/shellscript-DwcUjJBL.js +1 -0
- package/web-app/web-dist/assets/{shellsession-CyO2fnhB.js → shellsession-CPZkydE6.js} +1 -1
- package/web-app/web-dist/assets/{soy-DIkw6E88.js → soy-Br5FhD7c.js} +1 -1
- package/web-app/web-dist/assets/sql-DNssxck8.js +1 -0
- package/web-app/web-dist/assets/{stata-DvkM932O.js → stata-DXn1tqOr.js} +1 -1
- package/web-app/web-dist/assets/{surrealql-B4-Q8tqV.js → surrealql-IeLNQw0f.js} +1 -1
- package/web-app/web-dist/assets/{svelte-p6yBy-Ki.js → svelte-DOdLCIlh.js} +1 -1
- package/web-app/web-dist/assets/{templ-C7EkuiZr.js → templ-CIwIngms.js} +1 -1
- package/web-app/web-dist/assets/{tex-DkmD8uFC.js → tex-D8QMumu5.js} +1 -1
- package/web-app/web-dist/assets/{ts-tags-U-hncHg4.js → ts-tags-BMVY4q-l.js} +1 -1
- package/web-app/web-dist/assets/{tsMode-a8OvovQd.js → tsMode-BndVBac5.js} +1 -1
- package/web-app/web-dist/assets/tsx-5Eka4NBX.js +1 -0
- package/web-app/web-dist/assets/{twig-CU0OP-IA.js → twig-C8o_5mgw.js} +1 -1
- package/web-app/web-dist/assets/{typescript-DnLjiKtn.js → typescript-B1w9vqKF.js} +1 -1
- package/web-app/web-dist/assets/typescript-DOu2WMV5.js +1 -0
- package/web-app/web-dist/assets/{vue-Db7nY3ba.js → vue-BU18DNDL.js} +1 -1
- package/web-app/web-dist/assets/{vue-html-BvAbiAw1.js → vue-html-BeluIYX0.js} +1 -1
- package/web-app/web-dist/assets/{vue-vine-BEaIQIlA.js → vue-vine-DGUAbOCX.js} +1 -1
- package/web-app/web-dist/assets/{xml-an4Nuuqq.js → xml-D8uAlVv5.js} +1 -1
- package/web-app/web-dist/assets/xml-DIqSwXR3.js +1 -0
- package/web-app/web-dist/assets/{xsl-D3NQgH22.js → xsl-Ct_-YIAy.js} +1 -1
- package/web-app/web-dist/assets/xterm-B1ffpRuj.js +36 -0
- package/web-app/web-dist/assets/xterm-addons-psDEiUMC.js +136 -0
- package/web-app/web-dist/assets/{yaml-Diiu6O9P.js → yaml-Bb7jXyQv.js} +1 -1
- package/web-app/web-dist/assets/yaml-DTtCYNlS.js +1 -0
- package/web-app/web-dist/index.html +6 -3
- package/zuzuu/actions/convert.mjs +10 -9
- package/zuzuu/actions/dispatch.mjs +12 -7
- package/zuzuu/actions/inbox.mjs +5 -5
- package/zuzuu/actions/manifest.mjs +48 -30
- package/zuzuu/actions/schema.mjs +9 -3
- package/zuzuu/actions/trail.mjs +1 -1
- package/zuzuu/commands/act-author.mjs +23 -13
- package/zuzuu/commands/act.mjs +4 -6
- package/zuzuu/commands/capture.mjs +2 -2
- package/zuzuu/commands/code.mjs +2 -2
- package/zuzuu/commands/digest.mjs +2 -2
- package/zuzuu/commands/distill.mjs +15 -16
- package/zuzuu/commands/doctor.mjs +41 -19
- package/zuzuu/commands/enable.mjs +1 -1
- package/zuzuu/commands/eval.mjs +3 -36
- package/zuzuu/commands/explain.mjs +4 -4
- package/zuzuu/commands/faculty.mjs +158 -0
- package/zuzuu/commands/generation.mjs +5 -8
- package/zuzuu/commands/hook.mjs +14 -12
- package/zuzuu/commands/inbox.mjs +1 -6
- package/zuzuu/commands/init.mjs +18 -4
- package/zuzuu/commands/knowledge.mjs +1 -1
- package/zuzuu/commands/migrations/home.mjs +96 -0
- package/zuzuu/commands/migrations/index.mjs +48 -0
- package/zuzuu/commands/migrations/items.mjs +360 -0
- package/zuzuu/commands/migrations/proposals.mjs +100 -0
- package/zuzuu/commands/proposals.mjs +131 -0
- package/zuzuu/commands/review.mjs +13 -227
- package/zuzuu/commands/session.mjs +8 -2
- package/zuzuu/commands/sessions.mjs +159 -0
- package/zuzuu/commands/status.mjs +3 -3
- package/zuzuu/commands/trace.mjs +1 -1
- package/zuzuu/{capture-core.mjs → core/capture-core.mjs} +3 -3
- package/zuzuu/{store.mjs → core/store.mjs} +1 -1
- package/zuzuu/digest/compose.mjs +96 -0
- package/zuzuu/eval/score.mjs +14 -1
- package/zuzuu/faculties/actions/index.mjs +283 -0
- package/zuzuu/faculties/guardrails/index.mjs +320 -0
- package/zuzuu/faculties/instructions/index.mjs +288 -0
- package/zuzuu/faculties/knowledge/index.mjs +185 -0
- package/zuzuu/faculties/memory/index.mjs +124 -0
- package/zuzuu/faculty/envelope.mjs +290 -0
- package/zuzuu/faculty/generation/read.mjs +206 -0
- package/zuzuu/faculty/generation/write.mjs +207 -0
- package/zuzuu/faculty/items.mjs +81 -0
- package/zuzuu/faculty/module.mjs +74 -0
- package/zuzuu/faculty/pending.mjs +63 -0
- package/zuzuu/faculty/registry.mjs +204 -18
- package/zuzuu/faculty/render.mjs +59 -0
- package/zuzuu/faculty/trail.mjs +1 -1
- package/zuzuu/guardrails/engine.mjs +137 -0
- package/zuzuu/{scaffold.mjs → home/scaffold.mjs} +110 -39
- package/zuzuu/knowledge/items.mjs +56 -91
- package/zuzuu/live/install.mjs +1 -1
- package/zuzuu/live/live-store.mjs +2 -2
- package/zuzuu/live/reconcile.mjs +2 -2
- package/zuzuu/sessions/git.mjs +47 -0
- package/zuzuu/{session-git.mjs → sessions/session-git.mjs} +5 -43
- package/web-app/web-dist/assets/DiffTab-BuWonUNJ.js +0 -1
- package/web-app/web-dist/assets/MonacoFile-CL3DhFKG.js +0 -1
- package/web-app/web-dist/assets/angular-html-CmT26mqM.js +0 -1
- package/web-app/web-dist/assets/c-BvoqrSVH.js +0 -1
- package/web-app/web-dist/assets/cpp-BXsk94m0.js +0 -1
- package/web-app/web-dist/assets/css-Z8oOGxII.js +0 -1
- package/web-app/web-dist/assets/dist-ChcDQ_7s.js +0 -153
- package/web-app/web-dist/assets/glsl-KwyfU2aa.js +0 -1
- package/web-app/web-dist/assets/graphql-DSeOUAa2.js +0 -1
- package/web-app/web-dist/assets/haml-azVoxQRV.js +0 -1
- package/web-app/web-dist/assets/html-D_7P5S4m.js +0 -1
- package/web-app/web-dist/assets/index--5yy8RbA.js +0 -267
- package/web-app/web-dist/assets/index-BVG4hgk7.css +0 -2
- package/web-app/web-dist/assets/java-D4RbCvBe.js +0 -1
- package/web-app/web-dist/assets/javascript-Cb010CKM.js +0 -1
- package/web-app/web-dist/assets/json-DWgqV4D1.js +0 -1
- package/web-app/web-dist/assets/jsx-CZjSJa1f.js +0 -1
- package/web-app/web-dist/assets/lua-TGj_6NzO.js +0 -1
- package/web-app/web-dist/assets/r-fCpuAR7u.js +0 -1
- package/web-app/web-dist/assets/regexp-B4yxx-Ty.js +0 -1
- package/web-app/web-dist/assets/scss-QdjMO_xV.js +0 -1
- package/web-app/web-dist/assets/shellscript-BnlgeVVx.js +0 -1
- package/web-app/web-dist/assets/sql-DGnQv6iD.js +0 -1
- package/web-app/web-dist/assets/tsx-MJ0-9sYG.js +0 -1
- package/web-app/web-dist/assets/typescript-C17ZkDe8.js +0 -1
- package/web-app/web-dist/assets/xml-CA9lHFQV.js +0 -1
- package/web-app/web-dist/assets/yaml-CwRYMJka.js +0 -1
- package/zuzuu/actions/adapter.mjs +0 -130
- package/zuzuu/commands/migrate.mjs +0 -225
- package/zuzuu/digest.mjs +0 -149
- package/zuzuu/faculty/generation.mjs +0 -392
- package/zuzuu/guardrails/adapter.mjs +0 -134
- package/zuzuu/guardrails.mjs +0 -89
- package/zuzuu/instructions/adapter.mjs +0 -93
- package/zuzuu/knowledge/adapter.mjs +0 -99
- package/zuzuu/memory/adapter.mjs +0 -121
- package/zuzuu/miners/actions.mjs +0 -118
- package/zuzuu/miners/guardrails.mjs +0 -179
- package/zuzuu/miners/instructions.mjs +0 -157
- package/zuzuu/miners/knowledge.mjs +0 -25
- package/zuzuu/miners/memory.mjs +0 -27
- package/zuzuu/miners/registry.mjs +0 -31
- /package/web-app/web-dist/assets/{chunk-QTnfLwEv.js → rolldown-runtime-QTnfLwEv.js} +0 -0
- /package/zuzuu/{session.mjs → core/session.mjs} +0 -0
- /package/zuzuu/{inject.mjs → home/inject.mjs} +0 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// zuzuu/faculty/generation/write.mjs — the generation WRITE side (WS3-T1,
|
|
2
|
+
// split per the 2026-06-13 overhaul): agent identity repair, minting (freeze +
|
|
3
|
+
// snapshot + flip active) and rollback (restore by content). Read-side paths,
|
|
4
|
+
// enumerators and diffing live in read.mjs.
|
|
5
|
+
|
|
6
|
+
import { join, dirname } from 'node:path';
|
|
7
|
+
import {
|
|
8
|
+
existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync, renameSync,
|
|
9
|
+
} from 'node:fs';
|
|
10
|
+
import { reindex } from '../../knowledge/index.mjs';
|
|
11
|
+
import {
|
|
12
|
+
snapshotsDir, activePath, lockfilePath, agentJsonPath, readJson,
|
|
13
|
+
knowledgeFiles, memoryFiles, actionFiles, guardrailFiles, instructionFiles,
|
|
14
|
+
snapshotFaculties, agentId, listGenerations, readGeneration,
|
|
15
|
+
} from './read.mjs';
|
|
16
|
+
|
|
17
|
+
const writeJson = (p, obj) => {
|
|
18
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
19
|
+
writeFileSync(p, JSON.stringify(obj, null, 2) + '\n');
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Add/repair the agent block in agent.json (bump to v2), preserving other fields. */
|
|
23
|
+
export function ensureAgent(agentDir) {
|
|
24
|
+
const path = agentJsonPath(agentDir);
|
|
25
|
+
const m = existsSync(path) ? readJson(path) : {};
|
|
26
|
+
const id = agentId(agentDir);
|
|
27
|
+
if (!m.agent || !m.agent.id) {
|
|
28
|
+
m.agent = { id, createdAt: new Date().toISOString() };
|
|
29
|
+
}
|
|
30
|
+
m.version = 2;
|
|
31
|
+
writeJson(path, m);
|
|
32
|
+
return m.agent;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function nextGenId(agentDir) {
|
|
36
|
+
const ids = listGenerations(agentDir);
|
|
37
|
+
const max = ids.reduce((m, id) => Math.max(m, parseInt(id.slice(4), 10) || 0), 0);
|
|
38
|
+
return 'gen_' + String(max + 1).padStart(3, '0');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// --- mint -------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
function copySnapshot(agentDir, id) {
|
|
44
|
+
const base = join(snapshotsDir(agentDir), id);
|
|
45
|
+
for (const it of knowledgeFiles(agentDir)) {
|
|
46
|
+
const dest = join(base, 'knowledge', it.rel);
|
|
47
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
48
|
+
writeFileSync(dest, readFileSync(it.src));
|
|
49
|
+
}
|
|
50
|
+
for (const it of memoryFiles(agentDir)) {
|
|
51
|
+
const dest = join(base, 'memory', it.rel);
|
|
52
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
53
|
+
writeFileSync(dest, readFileSync(it.src));
|
|
54
|
+
}
|
|
55
|
+
for (const a of actionFiles(agentDir)) {
|
|
56
|
+
for (const rel of a.files) {
|
|
57
|
+
const dest = join(base, 'actions', a.id, rel);
|
|
58
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
59
|
+
writeFileSync(dest, readFileSync(join(a.adir, rel)));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
for (const it of guardrailFiles(agentDir)) {
|
|
63
|
+
const dest = join(base, 'guardrails', it.rel);
|
|
64
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
65
|
+
writeFileSync(dest, readFileSync(it.src));
|
|
66
|
+
}
|
|
67
|
+
for (const it of instructionFiles(agentDir)) {
|
|
68
|
+
const dest = join(base, 'instructions', it.rel);
|
|
69
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
70
|
+
writeFileSync(dest, readFileSync(it.src));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Mint a new generation: freeze the current faculty state into a content-addressed
|
|
76
|
+
* lockfile + a byte-for-byte snapshot, and make it active.
|
|
77
|
+
*/
|
|
78
|
+
export function mintGeneration(agentDir, { forkedFrom = null, mintedFrom = [] } = {}) {
|
|
79
|
+
const agent = ensureAgent(agentDir).id;
|
|
80
|
+
const id = nextGenId(agentDir);
|
|
81
|
+
const lockfile = {
|
|
82
|
+
id,
|
|
83
|
+
agent,
|
|
84
|
+
mintedAt: new Date().toISOString(),
|
|
85
|
+
forkedFrom,
|
|
86
|
+
mintedFrom,
|
|
87
|
+
faculties: snapshotFaculties(agentDir),
|
|
88
|
+
};
|
|
89
|
+
copySnapshot(agentDir, id);
|
|
90
|
+
writeJson(lockfilePath(agentDir, id), lockfile);
|
|
91
|
+
writeJson(activePath(agentDir), { active: id });
|
|
92
|
+
return lockfile;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// --- rollback ---------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
function archive(agentDir, faculty, src) {
|
|
98
|
+
// Park (never delete) under <faculty>/_rolledback/<basename> — by basename so
|
|
99
|
+
// a restore is a simple, flat audit trail of what the rollback displaced.
|
|
100
|
+
const dest = join(agentDir, faculty, '_rolledback', src.slice(dirname(src).length + 1));
|
|
101
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
102
|
+
renameSync(src, dest);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Restore a past generation by content: write each snapshotted item back to its
|
|
107
|
+
* live faculty path; MOVE (never delete) active items absent from the target into
|
|
108
|
+
* <faculty>/_rolledback/; reindex knowledge; flip the active pointer.
|
|
109
|
+
*/
|
|
110
|
+
export function rollback(agentDir, id) {
|
|
111
|
+
const target = readGeneration(agentDir, id);
|
|
112
|
+
if (!target) throw new Error(`no generation '${id}'`);
|
|
113
|
+
const base = join(snapshotsDir(agentDir), id);
|
|
114
|
+
let restored = 0;
|
|
115
|
+
|
|
116
|
+
// 1) restore snapshotted knowledge items
|
|
117
|
+
const targetKnowledge = new Set((target.faculties.knowledge?.items ?? []).map((i) => i.id));
|
|
118
|
+
for (const i of target.faculties.knowledge?.items ?? []) {
|
|
119
|
+
const snap = join(base, 'knowledge', `${i.id}.md`);
|
|
120
|
+
if (existsSync(snap)) {
|
|
121
|
+
const dest = join(agentDir, 'knowledge', 'items', `${i.id}.md`);
|
|
122
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
123
|
+
writeFileSync(dest, readFileSync(snap));
|
|
124
|
+
restored++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// archive live knowledge items not in the target
|
|
128
|
+
const kdir = join(agentDir, 'knowledge', 'items');
|
|
129
|
+
if (existsSync(kdir)) {
|
|
130
|
+
for (const e of readdirSync(kdir, { withFileTypes: true })) {
|
|
131
|
+
if (e.isFile() && e.name.endsWith('.md') && !targetKnowledge.has(e.name.replace(/\.md$/, ''))) {
|
|
132
|
+
archive(agentDir, 'knowledge', join(kdir, e.name));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 2) restore snapshotted memory items + archive extras
|
|
138
|
+
const targetMemory = new Set((target.faculties.memory?.items ?? []).map((i) => i.id));
|
|
139
|
+
for (const i of target.faculties.memory?.items ?? []) {
|
|
140
|
+
const snap = join(base, 'memory', `${i.id}.md`);
|
|
141
|
+
if (existsSync(snap)) {
|
|
142
|
+
const dest = join(agentDir, 'memory', 'entries', `${i.id}.md`);
|
|
143
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
144
|
+
writeFileSync(dest, readFileSync(snap));
|
|
145
|
+
restored++;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const mdir = join(agentDir, 'memory', 'entries');
|
|
149
|
+
if (existsSync(mdir)) {
|
|
150
|
+
for (const e of readdirSync(mdir, { withFileTypes: true })) {
|
|
151
|
+
if (e.isFile() && e.name.endsWith('.md') && !targetMemory.has(e.name.replace(/\.md$/, ''))) {
|
|
152
|
+
archive(agentDir, 'memory', join(mdir, e.name));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 3) restore snapshotted actions + archive extras
|
|
158
|
+
const targetActions = new Set((target.faculties.actions?.items ?? []).map((i) => i.id));
|
|
159
|
+
const asnap = join(base, 'actions');
|
|
160
|
+
if (existsSync(asnap)) {
|
|
161
|
+
for (const slugEnt of readdirSync(asnap, { withFileTypes: true })) {
|
|
162
|
+
if (!slugEnt.isDirectory()) continue;
|
|
163
|
+
const sdir = join(asnap, slugEnt.name);
|
|
164
|
+
for (const f of readdirSync(sdir)) {
|
|
165
|
+
const dest = join(agentDir, 'actions', slugEnt.name, f);
|
|
166
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
167
|
+
writeFileSync(dest, readFileSync(join(sdir, f)));
|
|
168
|
+
}
|
|
169
|
+
restored++;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const adir = join(agentDir, 'actions');
|
|
173
|
+
if (existsSync(adir)) {
|
|
174
|
+
for (const e of readdirSync(adir, { withFileTypes: true })) {
|
|
175
|
+
if (e.isDirectory() && e.name !== 'inbox' && e.name !== 'proposals' && e.name !== '_rolledback' && !targetActions.has(e.name)) {
|
|
176
|
+
archive(agentDir, 'actions', join(adir, e.name));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 4) restore guardrails + instructions items (same item-list contract)
|
|
182
|
+
for (const [faculty, liveSeg] of [['guardrails', ['guardrails', 'items']], ['instructions', ['instructions', 'items']]]) {
|
|
183
|
+
const targetIds = new Set((target.faculties[faculty]?.items ?? []).map((i) => i.id));
|
|
184
|
+
for (const i of target.faculties[faculty]?.items ?? []) {
|
|
185
|
+
const snap = join(base, faculty, `${i.id}.md`);
|
|
186
|
+
if (existsSync(snap)) {
|
|
187
|
+
const dest = join(agentDir, ...liveSeg, `${i.id}.md`);
|
|
188
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
189
|
+
writeFileSync(dest, readFileSync(snap));
|
|
190
|
+
restored++;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const liveDir = join(agentDir, ...liveSeg);
|
|
194
|
+
if (existsSync(liveDir)) {
|
|
195
|
+
for (const e of readdirSync(liveDir, { withFileTypes: true })) {
|
|
196
|
+
if (e.isFile() && e.name.endsWith('.md') && !targetIds.has(e.name.replace(/\.md$/, ''))) {
|
|
197
|
+
archive(agentDir, faculty, join(liveDir, e.name));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 5) regenerate the derived knowledge index + flip the pointer
|
|
204
|
+
try { reindex(agentDir); } catch { /* derived index; tolerate absence of node:sqlite features */ }
|
|
205
|
+
writeJson(activePath(agentDir), { active: id });
|
|
206
|
+
return { ok: true, restored };
|
|
207
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// zuzuu/faculty/items.mjs — where each faculty's envelope items live (W24).
|
|
2
|
+
//
|
|
3
|
+
// One standard, five homes:
|
|
4
|
+
// knowledge → knowledge/items/<id>.md
|
|
5
|
+
// memory → memory/entries/<id>.md
|
|
6
|
+
// instructions → instructions/items/<id>.md
|
|
7
|
+
// guardrails → guardrails/items/<id>.md
|
|
8
|
+
// actions → actions/<id>/ACTION.md (dir-shaped: scripts stay siblings)
|
|
9
|
+
//
|
|
10
|
+
// Listing is fail-soft: unparseable files are collected as errors, never thrown
|
|
11
|
+
// (mirrors knowledge allItems — audit surfaces them).
|
|
12
|
+
|
|
13
|
+
import { join, dirname } from 'node:path';
|
|
14
|
+
import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync, statSync } from 'node:fs';
|
|
15
|
+
import { parseEnvelope, serializeEnvelope } from './envelope.mjs';
|
|
16
|
+
|
|
17
|
+
/** Flat item dirs per faculty (actions are dir-shaped — see itemPathFor). */
|
|
18
|
+
const ITEM_DIRS = {
|
|
19
|
+
knowledge: ['knowledge', 'items'],
|
|
20
|
+
memory: ['memory', 'entries'],
|
|
21
|
+
instructions: ['instructions', 'items'],
|
|
22
|
+
guardrails: ['guardrails', 'items'],
|
|
23
|
+
};
|
|
24
|
+
|
|
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;
|
|
30
|
+
const rel = ITEM_DIRS[faculty];
|
|
31
|
+
if (rel) return join(agentDir, ...rel);
|
|
32
|
+
const sub = itemsDir && itemsDir !== '.' ? itemsDir : 'items';
|
|
33
|
+
return join(agentDir, faculty, sub);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Canonical envelope file path for one item. */
|
|
37
|
+
export function itemPathFor(agentDir, faculty, id) {
|
|
38
|
+
if (faculty === 'actions') return join(agentDir, 'actions', id, 'ACTION.md');
|
|
39
|
+
return join(itemsDirFor(agentDir, faculty), `${id}.md`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* All envelope items of a faculty. Parse errors collected, never thrown.
|
|
44
|
+
* @param {{itemsDir?: string}} [opts] declarative faculties pass manifest.itemsDir
|
|
45
|
+
* @returns {{items: object[], errors: Array<{file: string, error: string}>}}
|
|
46
|
+
*/
|
|
47
|
+
export function listFacultyItems(agentDir, faculty, opts = {}) {
|
|
48
|
+
const items = [];
|
|
49
|
+
const errors = [];
|
|
50
|
+
if (faculty === 'actions') {
|
|
51
|
+
const base = join(agentDir, 'actions');
|
|
52
|
+
if (!existsSync(base)) return { items, errors };
|
|
53
|
+
for (const name of readdirSync(base).sort()) {
|
|
54
|
+
if (name === 'inbox' || name === 'proposals' || name === '_rolledback') continue;
|
|
55
|
+
const p = join(base, name, 'ACTION.md');
|
|
56
|
+
let isDir = false;
|
|
57
|
+
try { isDir = statSync(join(base, name)).isDirectory(); } catch { /* skip */ }
|
|
58
|
+
if (!isDir || !existsSync(p)) continue;
|
|
59
|
+
const { ok, item, errors: errs } = parseEnvelope(readFileSync(p, 'utf8'));
|
|
60
|
+
if (ok) items.push(item);
|
|
61
|
+
else errors.push({ file: `${name}/ACTION.md`, error: errs[0] ?? 'parse error' });
|
|
62
|
+
}
|
|
63
|
+
return { items, errors };
|
|
64
|
+
}
|
|
65
|
+
const dir = itemsDirFor(agentDir, faculty, opts.itemsDir);
|
|
66
|
+
if (!dir || !existsSync(dir)) return { items, errors };
|
|
67
|
+
for (const f of readdirSync(dir).filter((f) => f.endsWith('.md')).sort()) {
|
|
68
|
+
const { ok, item, errors: errs } = parseEnvelope(readFileSync(join(dir, f), 'utf8'));
|
|
69
|
+
if (ok) items.push(item);
|
|
70
|
+
else errors.push({ file: f, error: errs[0] ?? 'parse error' });
|
|
71
|
+
}
|
|
72
|
+
return { items, errors };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Write one envelope item to its canonical path. Returns the path. */
|
|
76
|
+
export function writeFacultyItem(agentDir, item) {
|
|
77
|
+
const path = itemPathFor(agentDir, item.faculty, item.id);
|
|
78
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
79
|
+
writeFileSync(path, serializeEnvelope(item));
|
|
80
|
+
return path;
|
|
81
|
+
}
|
|
@@ -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
|
-
//
|
|
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
|
-
|
|
8
|
-
|
|
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).
|
|
12
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
|
24
|
-
|
|
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
|
-
*
|
|
29
|
-
*
|
|
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
|
|
32
|
-
|
|
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
|
}
|