@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.
- package/bin/zuzuu.mjs +12 -3
- 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} +46 -116
- 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-Dx3ub8Pk.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-DM6oHc7c.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-DflaUwqW.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-wbBeb0oN.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-DRwkDcoK.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/trail.mjs +1 -1
- package/zuzuu/commands/act.mjs +1 -1
- 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 +39 -4
- package/zuzuu/commands/enable.mjs +1 -1
- package/zuzuu/commands/eval.mjs +3 -36
- package/zuzuu/commands/faculty.mjs +102 -19
- package/zuzuu/commands/generation.mjs +3 -4
- package/zuzuu/commands/hook.mjs +7 -7
- package/zuzuu/commands/inbox.mjs +1 -6
- package/zuzuu/commands/init.mjs +5 -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/{migrate.mjs → migrations/items.mjs} +34 -246
- 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/{memory/adapter.mjs → faculties/memory/index.mjs} +37 -9
- package/zuzuu/faculty/generation/read.mjs +206 -0
- package/zuzuu/faculty/generation/write.mjs +207 -0
- package/zuzuu/faculty/items.mjs +11 -5
- 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.mjs → guardrails/engine.mjs} +1 -1
- package/zuzuu/{scaffold.mjs → home/scaffold.mjs} +12 -2
- 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-BpGp1akx.js +0 -1
- package/web-app/web-dist/assets/MonacoFile-CqbVacUZ.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-C6R6xoyX.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-DHpC851f.js +0 -268
- package/web-app/web-dist/assets/index-O-t1gyMG.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 -122
- package/zuzuu/digest.mjs +0 -154
- package/zuzuu/faculty/generation.mjs +0 -398
- package/zuzuu/guardrails/adapter.mjs +0 -103
- package/zuzuu/instructions/adapter.mjs +0 -93
- package/zuzuu/knowledge/adapter.mjs +0 -99
- package/zuzuu/miners/actions.mjs +0 -112
- package/zuzuu/miners/guardrails.mjs +0 -176
- 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,185 @@
|
|
|
1
|
+
// zuzuu/faculties/knowledge/index.mjs — the Knowledge faculty module.
|
|
2
|
+
//
|
|
3
|
+
// Consolidates the faculty's spine surface behind the Faculty Module contract
|
|
4
|
+
// (faculty.json manifest + hook exports): the adapter (ingest/validate/apply/
|
|
5
|
+
// render — wraps the EXISTING Knowledge pipeline: proposals/ER/registry/items/
|
|
6
|
+
// index), the miner (the source-A distill path), and the digest section.
|
|
7
|
+
// Substrate code stays in zuzuu/knowledge/ — this module is the contract face.
|
|
8
|
+
|
|
9
|
+
import { resolve as erResolve } from '../../knowledge/er.mjs';
|
|
10
|
+
import { loadRegistry, validateItem } from '../../knowledge/registry.mjs';
|
|
11
|
+
import { allItems, slugify } from '../../knowledge/items.mjs';
|
|
12
|
+
import { applyKnowledgeProposal, createProposal } from '../../knowledge/proposals.mjs';
|
|
13
|
+
import { aggregate } from '../../knowledge/distill.mjs';
|
|
14
|
+
|
|
15
|
+
const name = 'knowledge';
|
|
16
|
+
|
|
17
|
+
export const manifest = {
|
|
18
|
+
id: 'knowledge',
|
|
19
|
+
title: 'Knowledge',
|
|
20
|
+
tagline: "what's TRUE — facts about this project",
|
|
21
|
+
version: '1.0.0',
|
|
22
|
+
contract: 1,
|
|
23
|
+
kinds: ['fact', 'entity', 'command', 'decision'],
|
|
24
|
+
itemsDir: 'items',
|
|
25
|
+
schema: 'schema.json',
|
|
26
|
+
hooks: { miner: true, digest: true, eval: false, gate: false },
|
|
27
|
+
ui: { icon: 'book', accent: 'info', teaching: 'Facts zuzuu learns from your sessions land here after your approval.' },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// adapter (WS2-T2 — unchanged behaviour)
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Ingest a raw candidate: run ER against existing items and return the
|
|
36
|
+
* normalised payload + analysis. Mirrors what createProposal computes today.
|
|
37
|
+
* @param {string} agentDir
|
|
38
|
+
* @param {{candidate:object, source?:string, evidence?:object}} raw
|
|
39
|
+
*/
|
|
40
|
+
function ingest(agentDir, raw) {
|
|
41
|
+
const { items } = allItems(agentDir);
|
|
42
|
+
const candidate = { ...raw.candidate };
|
|
43
|
+
candidate.id = candidate.id || slugify(candidate.body);
|
|
44
|
+
const er = erResolve(candidate, items);
|
|
45
|
+
return { payload: candidate, analysis: { er }, dedupeKey: candidate.id };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Validate an item against the Knowledge registry.
|
|
50
|
+
* @returns {{ok:boolean, errors:string[], warnings:string[]}}
|
|
51
|
+
*/
|
|
52
|
+
export function validate(agentDir, payload) {
|
|
53
|
+
const reg = loadRegistry(agentDir);
|
|
54
|
+
const v = validateItem(reg, payload);
|
|
55
|
+
const warnings = [
|
|
56
|
+
...v.unknownKeys.attributes.map((k) => `unregistered attribute '${k}'`),
|
|
57
|
+
...v.unknownKeys.relations.map((t) => `unregistered relation type '${t}'`),
|
|
58
|
+
];
|
|
59
|
+
return { ok: v.ok, errors: v.errors, warnings };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Apply an approved proposal — delegates to the extracted approve apply body.
|
|
64
|
+
* @returns {{ok:boolean, action:string, itemIds:string[], warnings:string[]}}
|
|
65
|
+
*/
|
|
66
|
+
function apply(agentDir, proposal) {
|
|
67
|
+
// Bridge spine-shaped records (payload/analysis.er) onto applyKnowledgeProposal's
|
|
68
|
+
// legacy shape (candidate/er). Records that still carry candidate/er pass through.
|
|
69
|
+
const legacy = {
|
|
70
|
+
...proposal,
|
|
71
|
+
candidate: proposal.candidate ?? proposal.payload,
|
|
72
|
+
er: proposal.er ?? proposal.analysis?.er,
|
|
73
|
+
};
|
|
74
|
+
const r = applyKnowledgeProposal(agentDir, legacy);
|
|
75
|
+
return {
|
|
76
|
+
ok: r.ok,
|
|
77
|
+
action: r.action,
|
|
78
|
+
itemIds: r.item ? [r.item] : [],
|
|
79
|
+
warnings: r.warnings ?? [],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const applyProposal = apply;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Render a proposal for the human gate. `card` mirrors the multi-line summary
|
|
87
|
+
* `zuzuu review` shows for knowledge proposals (id, type, attrs/relations, ER
|
|
88
|
+
* verdict); `line` is the one-line list form (`zuzuu proposals list`).
|
|
89
|
+
* @returns {{line:string, card:string}}
|
|
90
|
+
*/
|
|
91
|
+
function render(proposal) {
|
|
92
|
+
if (proposal.kind === 'registry') {
|
|
93
|
+
const what = `register ${String(proposal.registry).slice(0, -1)} '${proposal.key}'`;
|
|
94
|
+
return {
|
|
95
|
+
line: `${proposal.id} [${proposal.kind}] ${what}`,
|
|
96
|
+
card: `${what} (seen ${proposal.evidence?.occurrences}× in candidates)`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const c = proposal.candidate ?? {};
|
|
100
|
+
const er = proposal.er ?? {};
|
|
101
|
+
const lines = [];
|
|
102
|
+
lines.push(`${c.id ?? ''} ── ${c.type}: ${c.body?.slice(0, 100).replace(/\n/g, ' ')}`);
|
|
103
|
+
for (const [k, v] of Object.entries(c.attributes ?? {})) lines.push(` · ${k} = ${v}`);
|
|
104
|
+
for (const r of c.relations ?? []) lines.push(` → ${r.type} ${r.target}`);
|
|
105
|
+
lines.push(` er: ${er.verdict}${er.match ? ` → ${er.match}` : ''} (${(er.confidence ?? 0).toFixed(2)} · ${er.reason ?? ''})`);
|
|
106
|
+
return {
|
|
107
|
+
line: `${proposal.id} [${er.verdict ?? proposal.kind}] ${c.type}: ${c.body?.slice(0, 60).replace(/\n/g, ' ')}`,
|
|
108
|
+
card: lines.join('\n'),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const adapter = { name, ingest, validate, apply, render };
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// miner (WS5-T1 — the golden source-A distill path, unchanged)
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
/** File one knowledge proposal per aggregated candidate; return the count of
|
|
119
|
+
* actually-filed proposals (archive-resolved ids are skipped, not re-filed). */
|
|
120
|
+
export function propose(agentDir, aggregated) {
|
|
121
|
+
let count = 0;
|
|
122
|
+
for (const c of aggregated) {
|
|
123
|
+
const p = createProposal(agentDir, { candidate: c.candidate, source: 'distill', evidence: c.evidence });
|
|
124
|
+
if (p && p.status !== 'archived-skip') count++;
|
|
125
|
+
}
|
|
126
|
+
return count;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const miner = { faculty: name, aggregate, propose };
|
|
130
|
+
|
|
131
|
+
export { aggregate };
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// digest section (moved from the pre-module digest, byte-identical output)
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The Knowledge digest section: most-recent items under the shared char budget.
|
|
139
|
+
* @param {string} agentDir
|
|
140
|
+
* @param {{limit:number, charBudget:number, priorLines:string[]}} ctx
|
|
141
|
+
* @returns {{lines: string[], data: object}}
|
|
142
|
+
*/
|
|
143
|
+
export function digestSection(agentDir, { limit, charBudget, priorLines }) {
|
|
144
|
+
let knowledge;
|
|
145
|
+
try {
|
|
146
|
+
const { items } = allItems(agentDir);
|
|
147
|
+
const ranked = [...items]
|
|
148
|
+
.sort((a, b) => String(b.created_at).localeCompare(String(a.created_at)))
|
|
149
|
+
.slice(0, limit);
|
|
150
|
+
knowledge = { count: items.length, shown: ranked.map((i) => ({ id: i.id, type: i.type, body: i.body })) };
|
|
151
|
+
} catch {
|
|
152
|
+
knowledge = { count: 0, shown: [] };
|
|
153
|
+
}
|
|
154
|
+
const lines = ['## Knowledge'];
|
|
155
|
+
if (!knowledge.count) {
|
|
156
|
+
lines.push('(no items yet — propose facts to knowledge/inbox/)');
|
|
157
|
+
return { lines, data: { ...knowledge, renderedCount: 0 } };
|
|
158
|
+
}
|
|
159
|
+
lines.push(`${knowledge.count} item(s); most recent:`);
|
|
160
|
+
let shown = 0;
|
|
161
|
+
for (const it of knowledge.shown) {
|
|
162
|
+
const line = `- ${it.id} · ${it.type} · ${it.body.split('\n')[0].slice(0, 80)}`;
|
|
163
|
+
// join is O(items²) but trivial: once-per-session, limit default 5
|
|
164
|
+
if ([...priorLines, ...lines].join('\n').length + line.length > charBudget && shown > 0) break;
|
|
165
|
+
lines.push(line);
|
|
166
|
+
shown++;
|
|
167
|
+
}
|
|
168
|
+
const dropped = knowledge.count - shown;
|
|
169
|
+
if (dropped > 0) lines.push(`- … (${dropped} more — \`zuzuu recall\`)`);
|
|
170
|
+
// `shown` = items actually rendered (after budget); `count` = total available
|
|
171
|
+
return { lines, data: { ...knowledge, shown: knowledge.shown.slice(0, shown), renderedCount: shown } };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// session signals (the observability surface — `zuzuu session inspect`)
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
/** Counts of the mined-signal superset slices this faculty grows from. */
|
|
179
|
+
export function sessionSignals(signals = {}) {
|
|
180
|
+
return {
|
|
181
|
+
commands: signals.commands?.length ?? 0,
|
|
182
|
+
files: signals.files?.length ?? 0,
|
|
183
|
+
failures: signals.failures?.length ?? 0,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
// zuzuu/memory/
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// zuzuu/faculties/memory/index.mjs — the Memory faculty module.
|
|
2
|
+
//
|
|
3
|
+
// Consolidates the adapter (episode proposals → envelope entries) and the
|
|
4
|
+
// miner STUB (WS5-T4, deferred — see the WHAT IT WOULD MINE note below) behind
|
|
5
|
+
// the Faculty Module contract. No digest section today (the pre-module digest
|
|
6
|
+
// never rendered one — preserved).
|
|
5
7
|
//
|
|
6
8
|
// A memory proposal payload is an episode record:
|
|
7
9
|
// { id, date, title, provenance: {sessions, hosts}, tags, body }
|
|
@@ -11,13 +13,28 @@
|
|
|
11
13
|
// (kind: episode; payload = {sessions, hosts, tags}; body = the
|
|
12
14
|
// Attempted / Resulted / Remember-next-time sections).
|
|
13
15
|
//
|
|
14
|
-
//
|
|
16
|
+
// MINER (when implemented): completed-session episodes — a Run that reached
|
|
17
|
+
// `completed` in .zuzuu/sessions.json distilled into curated entries, emitted
|
|
18
|
+
// as kind-'episode' proposals for `zuzuu review`. Deferred until the Memory
|
|
19
|
+
// substrate decision lands and completed runs carry rich enough trace data.
|
|
15
20
|
|
|
16
|
-
import
|
|
17
|
-
import { writeFacultyItem } from '../faculty/items.mjs';
|
|
21
|
+
import { writeFacultyItem } from '../../faculty/items.mjs';
|
|
18
22
|
|
|
19
23
|
const name = 'memory';
|
|
20
24
|
|
|
25
|
+
export const manifest = {
|
|
26
|
+
id: 'memory',
|
|
27
|
+
title: 'Memory',
|
|
28
|
+
tagline: 'what HAPPENED — curated episodes from past sessions',
|
|
29
|
+
version: '1.0.0',
|
|
30
|
+
contract: 1,
|
|
31
|
+
kinds: ['episode'],
|
|
32
|
+
itemsDir: 'entries',
|
|
33
|
+
schema: 'schema.json',
|
|
34
|
+
hooks: { miner: true, digest: false, eval: false, gate: false },
|
|
35
|
+
ui: { icon: 'clock', accent: 'neutral', teaching: 'Curated episodes from past sessions — what was attempted, what resulted, what to remember.' },
|
|
36
|
+
};
|
|
37
|
+
|
|
21
38
|
// mem-<YYYY-MM-DD>-<slug>: the id must START with "mem-"
|
|
22
39
|
const MEM_ID_RE = /^mem-/;
|
|
23
40
|
|
|
@@ -37,7 +54,7 @@ function ingest(_agentDir, raw) {
|
|
|
37
54
|
* Validate an episode payload.
|
|
38
55
|
* @returns {{ok:boolean, errors:string[], warnings:string[]}}
|
|
39
56
|
*/
|
|
40
|
-
function validate(_agentDir, payload) {
|
|
57
|
+
export function validate(_agentDir, payload) {
|
|
41
58
|
const errors = [];
|
|
42
59
|
if (!payload?.id || typeof payload.id !== 'string') {
|
|
43
60
|
errors.push('id is required');
|
|
@@ -76,6 +93,8 @@ function apply(agentDir, proposal) {
|
|
|
76
93
|
return { ok: true, action: `wrote memory ${p.id}`, itemIds: [p.id] };
|
|
77
94
|
}
|
|
78
95
|
|
|
96
|
+
export const applyProposal = apply;
|
|
97
|
+
|
|
79
98
|
/**
|
|
80
99
|
* Render an episode proposal for the human gate.
|
|
81
100
|
* @returns {{line:string, card:string}}
|
|
@@ -93,4 +112,13 @@ function render(proposal) {
|
|
|
93
112
|
|
|
94
113
|
export const adapter = { name, ingest, validate, apply, render };
|
|
95
114
|
|
|
96
|
-
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// miner (registered no-op stub — deferred)
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
export const miner = {
|
|
120
|
+
faculty: name,
|
|
121
|
+
stub: true,
|
|
122
|
+
aggregate: () => [],
|
|
123
|
+
propose: () => 0,
|
|
124
|
+
};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// zuzuu/faculty/generation/read.mjs — the generation READ side (WS3-T1, split
|
|
2
|
+
// per the 2026-06-13 overhaul): paths, the pinned-set enumerators, snapshot
|
|
3
|
+
// hashing, list/show/diff. Minting + rollback live in write.mjs.
|
|
4
|
+
//
|
|
5
|
+
// A *generation* is an immutable, content-addressed snapshot of the agent's
|
|
6
|
+
// pinned faculties (the lockfile). Minting freezes the current faculty state;
|
|
7
|
+
// rollback restores any past generation by *content* (we copy each pinned item's
|
|
8
|
+
// bytes into generations/snapshots/<id>/ at mint time, so a rollback works even
|
|
9
|
+
// for items that were never committed). Identity: Agent → Generation → Run —
|
|
10
|
+
// rollback = flip the active pointer + restore content; never `git revert`.
|
|
11
|
+
//
|
|
12
|
+
// Layout under .zuzuu/:
|
|
13
|
+
// generations/active {active: "gen_NNN"} — the live pointer
|
|
14
|
+
// generations/<id>.json the lockfile (content-addressed manifest)
|
|
15
|
+
// generations/snapshots/<id>/<faculty>/... pinned item bytes (rollback source)
|
|
16
|
+
|
|
17
|
+
import { createHash } from 'node:crypto';
|
|
18
|
+
import { join, dirname } from 'node:path';
|
|
19
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
20
|
+
|
|
21
|
+
/** Hex sha256 of a string or Buffer. */
|
|
22
|
+
export function sha256(buf) {
|
|
23
|
+
return createHash('sha256').update(buf).digest('hex');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const read = (p) => readFileSync(p, 'utf8');
|
|
27
|
+
export const readJson = (p) => JSON.parse(read(p));
|
|
28
|
+
|
|
29
|
+
// --- paths ------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
export const generationsDir = (agentDir) => join(agentDir, 'generations');
|
|
32
|
+
export const snapshotsDir = (agentDir) => join(generationsDir(agentDir), 'snapshots');
|
|
33
|
+
export const activePath = (agentDir) => join(generationsDir(agentDir), 'active');
|
|
34
|
+
export const lockfilePath = (agentDir, id) => join(generationsDir(agentDir), `${id}.json`);
|
|
35
|
+
export const agentJsonPath = (agentDir) => join(agentDir, 'agent.json');
|
|
36
|
+
|
|
37
|
+
// --- faculty file enumeration (the pinned set) ------------------------------
|
|
38
|
+
// Each entry: { id, faculty, src (absolute live path), rel (path under the
|
|
39
|
+
// faculty snapshot dir), hash }. `rel` is what we mirror into snapshots/<id>/.
|
|
40
|
+
|
|
41
|
+
function sortDirents(dir) {
|
|
42
|
+
if (!existsSync(dir)) return [];
|
|
43
|
+
return readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function knowledgeFiles(agentDir) {
|
|
47
|
+
const dir = join(agentDir, 'knowledge', 'items');
|
|
48
|
+
return sortDirents(dir)
|
|
49
|
+
.filter((e) => e.isFile() && e.name.endsWith('.md'))
|
|
50
|
+
.map((e) => {
|
|
51
|
+
const src = join(dir, e.name);
|
|
52
|
+
return { id: e.name.replace(/\.md$/, ''), faculty: 'knowledge', src, rel: e.name, hash: sha256(readFileSync(src)) };
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function actionFiles(agentDir) {
|
|
57
|
+
const dir = join(agentDir, 'actions');
|
|
58
|
+
return sortDirents(dir)
|
|
59
|
+
.filter((e) => e.isDirectory() && e.name !== 'inbox' && e.name !== 'proposals' && e.name !== '_rolledback')
|
|
60
|
+
.map((e) => {
|
|
61
|
+
const adir = join(dir, e.name);
|
|
62
|
+
// Hash the dir's defining files concatenated: the ACTION.md envelope
|
|
63
|
+
// (W24) + sibling scripts (*.mjs — run.mjs and any payload.exec module).
|
|
64
|
+
const parts = sortDirents(adir)
|
|
65
|
+
.filter((f) => f.isFile() && (f.name === 'ACTION.md' || f.name.endsWith('.mjs')))
|
|
66
|
+
.map((f) => join(adir, f.name));
|
|
67
|
+
const concat = Buffer.concat(parts.map((p) => readFileSync(p)));
|
|
68
|
+
return {
|
|
69
|
+
id: e.name, faculty: 'actions', files: parts.map((p) => p.slice(adir.length + 1)),
|
|
70
|
+
adir, hash: parts.length ? sha256(concat) : null,
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Flat envelope-item faculties share one enumerator. */
|
|
76
|
+
function mdItemFiles(agentDir, faculty, ...segments) {
|
|
77
|
+
const dir = join(agentDir, ...segments);
|
|
78
|
+
return sortDirents(dir)
|
|
79
|
+
.filter((e) => e.isFile() && e.name.endsWith('.md'))
|
|
80
|
+
.map((e) => {
|
|
81
|
+
const src = join(dir, e.name);
|
|
82
|
+
return { id: e.name.replace(/\.md$/, ''), faculty, src, rel: e.name, hash: sha256(readFileSync(src)) };
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const guardrailFiles = (agentDir) => mdItemFiles(agentDir, 'guardrails', 'guardrails', 'items');
|
|
87
|
+
export const instructionFiles = (agentDir) => mdItemFiles(agentDir, 'instructions', 'instructions', 'items');
|
|
88
|
+
|
|
89
|
+
export function memoryFiles(agentDir) {
|
|
90
|
+
const dir = join(agentDir, 'memory', 'entries');
|
|
91
|
+
return sortDirents(dir)
|
|
92
|
+
.filter((e) => e.isFile() && e.name.endsWith('.md'))
|
|
93
|
+
.map((e) => {
|
|
94
|
+
const src = join(dir, e.name);
|
|
95
|
+
return { id: e.name.replace(/\.md$/, ''), faculty: 'memory', src, rel: e.name, hash: sha256(readFileSync(src)) };
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function registryHash(agentDir) {
|
|
100
|
+
const dir = join(agentDir, 'knowledge', 'registry');
|
|
101
|
+
const files = sortDirents(dir).filter((e) => e.isFile() && e.name.endsWith('.json'));
|
|
102
|
+
if (!files.length) return null;
|
|
103
|
+
return sha256(Buffer.concat(files.map((e) => readFileSync(join(dir, e.name)))));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Snapshot the current faculty state → the `faculties` manifest object.
|
|
108
|
+
* Tolerates missing files (empty arrays / null hashes).
|
|
109
|
+
*/
|
|
110
|
+
export function snapshotFaculties(agentDir) {
|
|
111
|
+
return {
|
|
112
|
+
knowledge: {
|
|
113
|
+
items: knowledgeFiles(agentDir).map(({ id, hash }) => ({ id, hash })),
|
|
114
|
+
registryHash: registryHash(agentDir),
|
|
115
|
+
},
|
|
116
|
+
actions: {
|
|
117
|
+
items: actionFiles(agentDir).map(({ id, hash }) => ({ id, hash })),
|
|
118
|
+
},
|
|
119
|
+
guardrails: {
|
|
120
|
+
items: guardrailFiles(agentDir).map(({ id, hash }) => ({ id, hash })),
|
|
121
|
+
},
|
|
122
|
+
instructions: {
|
|
123
|
+
items: instructionFiles(agentDir).map(({ id, hash }) => ({ id, hash })),
|
|
124
|
+
},
|
|
125
|
+
memory: {
|
|
126
|
+
items: memoryFiles(agentDir).map(({ id, hash }) => ({ id, hash })),
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// --- agent identity ---------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
/** Stable agent id derived from the repo root: agt_<first12 of sha256(root)>. */
|
|
134
|
+
export function agentId(agentDir) {
|
|
135
|
+
// agentDir is the .zuzuu/ dir; the repo root is its parent.
|
|
136
|
+
const root = dirname(agentDir);
|
|
137
|
+
return 'agt_' + sha256(root).slice(0, 12);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// --- generation read/list ---------------------------------------------------
|
|
141
|
+
|
|
142
|
+
/** The active generation id, or null. */
|
|
143
|
+
export function activeGeneration(agentDir) {
|
|
144
|
+
const p = activePath(agentDir);
|
|
145
|
+
if (!existsSync(p)) return null;
|
|
146
|
+
try { return readJson(p).active ?? null; } catch { return null; }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** All generation ids in ascending order. */
|
|
150
|
+
export function listGenerations(agentDir) {
|
|
151
|
+
const dir = generationsDir(agentDir);
|
|
152
|
+
if (!existsSync(dir)) return [];
|
|
153
|
+
return readdirSync(dir)
|
|
154
|
+
.filter((f) => /^gen_\d+\.json$/.test(f))
|
|
155
|
+
.map((f) => f.replace(/\.json$/, ''))
|
|
156
|
+
.sort();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Read one lockfile, or null. */
|
|
160
|
+
export function readGeneration(agentDir, id) {
|
|
161
|
+
const p = lockfilePath(agentDir, id);
|
|
162
|
+
return existsSync(p) ? readJson(p) : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Diff two item-manifest arrays → {added, changed, removed} (id lists). */
|
|
166
|
+
function diffItems(parentItems = [], childItems = []) {
|
|
167
|
+
const p = new Map(parentItems.map((i) => [i.id, i.hash]));
|
|
168
|
+
const c = new Map(childItems.map((i) => [i.id, i.hash]));
|
|
169
|
+
const added = [], changed = [], removed = [];
|
|
170
|
+
for (const [id, hash] of c) {
|
|
171
|
+
if (!p.has(id)) added.push(id);
|
|
172
|
+
else if (p.get(id) !== hash) changed.push(id);
|
|
173
|
+
}
|
|
174
|
+
for (const id of p.keys()) if (!c.has(id)) removed.push(id);
|
|
175
|
+
return { added: added.sort(), changed: changed.sort(), removed: removed.sort() };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Per-faculty diff of generation `id` against its forkedFrom parent (pure).
|
|
180
|
+
* ALL five faculties are item lists under the Faculty Standard (W24) —
|
|
181
|
+
* added/changed/removed id lists per faculty; knowledge additionally reports
|
|
182
|
+
* registryChanged. When there is no parent (forkedFrom null), everything
|
|
183
|
+
* present counts as added. Returns null for an unknown id.
|
|
184
|
+
*/
|
|
185
|
+
export function diffGenerations(agentDir, id) {
|
|
186
|
+
const child = readGeneration(agentDir, id);
|
|
187
|
+
if (!child) return null;
|
|
188
|
+
const parent = child.forkedFrom ? readGeneration(agentDir, child.forkedFrom) : null;
|
|
189
|
+
const cf = child.faculties || {};
|
|
190
|
+
const pf = parent?.faculties || {};
|
|
191
|
+
const faculties = {};
|
|
192
|
+
for (const f of ['knowledge', 'actions', 'memory', 'guardrails', 'instructions']) {
|
|
193
|
+
faculties[f] = diffItems(pf[f]?.items, cf[f]?.items);
|
|
194
|
+
// knowledge also has a registry hash
|
|
195
|
+
if (f === 'knowledge') {
|
|
196
|
+
faculties[f].registryChanged = (cf.knowledge?.registryHash ?? null) !== (pf.knowledge?.registryHash ?? null);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
id,
|
|
201
|
+
forkedFrom: child.forkedFrom ?? null,
|
|
202
|
+
mintedFrom: Array.isArray(child.mintedFrom) ? child.mintedFrom : [],
|
|
203
|
+
mintedAt: child.mintedAt ?? null,
|
|
204
|
+
faculties,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
@@ -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
|
+
}
|