@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,96 @@
|
|
|
1
|
+
// zuzuu/commands/migrations/home.mjs — the home migrator (W1, 2026-06-12):
|
|
2
|
+
// visible agent/ → hidden .zuzuu/ (byte-identical inner layout), plus the
|
|
3
|
+
// follow-ups: traceRef rewrite, gitignore + deny-rule swap, host-block re-inject.
|
|
4
|
+
// Pure core: migrateHome(root) → { migrated }. Idempotent + fail-soft.
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, renameSync, rmSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { repoRoot } from '../../core/store.mjs';
|
|
9
|
+
import { ensureGitignore } from '../../home/scaffold.mjs';
|
|
10
|
+
import { injectBlock, BLOCK_VERSION } from '../../home/inject.mjs';
|
|
11
|
+
|
|
12
|
+
// The denies the old visible-agent/ home installed; scrubbed here (NOT kept in
|
|
13
|
+
// install.mjs — clean break) and replaced by the current narrow .zuzuu/ pair.
|
|
14
|
+
const LEGACY_DENY_RULES = ['Read(./agent/.traces/**)', 'Read(./agent/.live/**)'];
|
|
15
|
+
const NEW_DENY_RULES = ['Read(./.zuzuu/.traces/**)', 'Read(./.zuzuu/.live/**)'];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* One-shot HOME migration: visible `agent/` → hidden `.zuzuu/` (byte-identical
|
|
19
|
+
* inner layout). Gated on `agent/agent.json` — `agent/` is a common dir name,
|
|
20
|
+
* so an unrelated agent/ dir in a brownfield repo must NEVER be touched (the
|
|
21
|
+
* one place this differs from the old `.mns→agent` precedent). Idempotent +
|
|
22
|
+
* fail-soft; NEVER clobbers an existing .zuzuu/. Pure FS move (renameSync).
|
|
23
|
+
* @returns {{migrated: boolean}}
|
|
24
|
+
*/
|
|
25
|
+
export function migrateHome(root = repoRoot()) {
|
|
26
|
+
const legacy = join(root, 'agent');
|
|
27
|
+
const home = join(root, '.zuzuu');
|
|
28
|
+
if (existsSync(home) || !existsSync(join(legacy, 'agent.json'))) return { migrated: false };
|
|
29
|
+
|
|
30
|
+
renameSync(legacy, home); // move the whole home (atomic on same filesystem)
|
|
31
|
+
|
|
32
|
+
rewriteTraceRefs(home);
|
|
33
|
+
rewriteGitignore(root);
|
|
34
|
+
scrubLegacyDenies(root);
|
|
35
|
+
// derived index: drop, it rebuilds on the next recall/reindex
|
|
36
|
+
try { rmSync(join(home, 'knowledge', '.index.db'), { force: true }); } catch { /* fail-soft */ }
|
|
37
|
+
return { migrated: true };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** sessions.json stores repo-relative traceRefs (`agent/.traces/…`) — re-point them. */
|
|
41
|
+
function rewriteTraceRefs(home) {
|
|
42
|
+
const index = join(home, 'sessions.json');
|
|
43
|
+
if (!existsSync(index)) return;
|
|
44
|
+
try {
|
|
45
|
+
const idx = JSON.parse(readFileSync(index, 'utf8'));
|
|
46
|
+
for (const s of idx.sessions || []) {
|
|
47
|
+
if (typeof s.traceRef === 'string' && s.traceRef.startsWith('agent/')) {
|
|
48
|
+
s.traceRef = '.zuzuu/' + s.traceRef.slice('agent/'.length);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
writeFileSync(index, JSON.stringify(idx, null, 2) + '\n');
|
|
52
|
+
} catch { /* fail-soft: a bad index never blocks the move */ }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Drop legacy `agent/` ignore lines, then append the canonical .zuzuu/ ones. */
|
|
56
|
+
function rewriteGitignore(root) {
|
|
57
|
+
const path = join(root, '.gitignore');
|
|
58
|
+
if (existsSync(path)) {
|
|
59
|
+
const kept = readFileSync(path, 'utf8')
|
|
60
|
+
.split('\n')
|
|
61
|
+
.filter((l) => !l.trim().startsWith('agent/'))
|
|
62
|
+
.join('\n');
|
|
63
|
+
writeFileSync(path, kept.endsWith('\n') || kept === '' ? kept : kept + '\n');
|
|
64
|
+
}
|
|
65
|
+
ensureGitignore(root); // appends .zuzuu/.traces/, .zuzuu/.live/, .zuzuu/knowledge/.index.db
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Swap the old agent/ deny rules for the .zuzuu/ pair in any .claude settings file. */
|
|
69
|
+
function scrubLegacyDenies(root) {
|
|
70
|
+
for (const f of ['settings.json', 'settings.local.json']) {
|
|
71
|
+
const path = join(root, '.claude', f);
|
|
72
|
+
if (!existsSync(path)) continue;
|
|
73
|
+
try {
|
|
74
|
+
const s = JSON.parse(readFileSync(path, 'utf8'));
|
|
75
|
+
const deny = s?.permissions?.deny;
|
|
76
|
+
if (!Array.isArray(deny)) continue;
|
|
77
|
+
const hadOurs = deny.some((r) => LEGACY_DENY_RULES.includes(r));
|
|
78
|
+
if (!hadOurs) continue;
|
|
79
|
+
s.permissions.deny = deny.filter((r) => !LEGACY_DENY_RULES.includes(r));
|
|
80
|
+
for (const rule of NEW_DENY_RULES) if (!s.permissions.deny.includes(rule)) s.permissions.deny.push(rule);
|
|
81
|
+
writeFileSync(path, JSON.stringify(s, null, 2) + '\n');
|
|
82
|
+
} catch { /* fail-soft: never break settings we can't parse */ }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Re-inject the current faculties block into any existing host instruction files. */
|
|
87
|
+
export function reinjectHostBlocks(root) {
|
|
88
|
+
for (const f of ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md']) {
|
|
89
|
+
const p = join(root, f);
|
|
90
|
+
if (existsSync(p)) {
|
|
91
|
+
const text = readFileSync(p, 'utf8');
|
|
92
|
+
if (!text.includes(`zuzuu:faculties:v${BLOCK_VERSION}`)) writeFileSync(p, injectBlock(text));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// zuzuu/commands/migrations/index.mjs — `zuzuu migrate` dispatch.
|
|
2
|
+
//
|
|
3
|
+
// (default) proposal schema: legacy {candidate, er} → spine {payload, analysis, faculty}
|
|
4
|
+
// --home faculty home: visible agent/ → hidden .zuzuu/
|
|
5
|
+
// --items Faculty Standard: legacy faculty shapes → the envelope standard
|
|
6
|
+
//
|
|
7
|
+
// Pure cores live beside this file (proposals/home/items.mjs); this is the CLI
|
|
8
|
+
// surface — resolves paths, runs a core, prints the summary.
|
|
9
|
+
|
|
10
|
+
import { paths, repoRoot } from '../../core/store.mjs';
|
|
11
|
+
import { BLOCK_VERSION } from '../../home/inject.mjs';
|
|
12
|
+
import { migrateProposals } from './proposals.mjs';
|
|
13
|
+
import { migrateHome, reinjectHostBlocks } from './home.mjs';
|
|
14
|
+
import { migrateItems, needsItemsMigration } from './items.mjs';
|
|
15
|
+
|
|
16
|
+
export { migrateProposals } from './proposals.mjs';
|
|
17
|
+
export { migrateHome } from './home.mjs';
|
|
18
|
+
export { migrateItems, needsItemsMigration } from './items.mjs';
|
|
19
|
+
|
|
20
|
+
export function migrate(args = {}) {
|
|
21
|
+
if (args.items) {
|
|
22
|
+
const agentDir = paths().dir;
|
|
23
|
+
const r = migrateItems(agentDir);
|
|
24
|
+
const total = r.knowledge + r.memory + r.guardrails + r.actions + r.instructions;
|
|
25
|
+
console.log(`migrate --items: ${total} item(s) → the Faculty Standard envelope — knowledge ${r.knowledge} · memory ${r.memory} · guardrails ${r.guardrails} · actions ${r.actions} · instructions ${r.instructions} (${r.skipped} already standard)`);
|
|
26
|
+
if (r.manifests) console.log(` seeded ${r.manifests} faculty manifest(s) (faculty.json — the Faculty Module contract)`);
|
|
27
|
+
for (const e of r.errors) console.log(` ✗ ${e.file}: ${e.error}`);
|
|
28
|
+
if (!total && !r.manifests && !r.errors.length) console.log(' nothing to migrate (the home already speaks the envelope)');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (args.home) {
|
|
32
|
+
const root = repoRoot(process.cwd());
|
|
33
|
+
const { migrated } = migrateHome(root);
|
|
34
|
+
if (!migrated) { console.log('migrate --home: nothing to do (already .zuzuu/, or no zuzuu home at agent/)'); return; }
|
|
35
|
+
try { reinjectHostBlocks(root); } catch { /* fail-open */ }
|
|
36
|
+
console.log(`migrate --home: agent/ → .zuzuu/ (hidden, like .git; block v${BLOCK_VERSION}, gitignore + deny rules rewritten)`);
|
|
37
|
+
console.log(' transparency lives in porcelain now: zuzuu status · explain · digest');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const agentDir = paths().dir;
|
|
41
|
+
const { scanned, migrated, skipped } = migrateProposals(agentDir);
|
|
42
|
+
console.log(`migrate: scanned ${scanned} proposal(s) — migrated ${migrated}, skipped ${skipped}`);
|
|
43
|
+
if (migrated > 0) {
|
|
44
|
+
console.log(' legacy candidate/er keys rewritten to payload/analysis.er + faculty:knowledge');
|
|
45
|
+
} else {
|
|
46
|
+
console.log(' nothing to migrate (all records already in new shape)');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -1,217 +1,15 @@
|
|
|
1
|
-
// zuzuu/commands/
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// --items Faculty Standard (W24): legacy per-faculty shapes → one envelope —
|
|
7
|
-
// knowledge/memory frontmatter keys standardised, rules.json
|
|
8
|
-
// exploded into guardrails/items/, action.json+SKILL.md → ACTION.md,
|
|
9
|
-
// instructions/project.md → items/steering.md. Idempotent,
|
|
10
|
-
// fail-soft per item. Auto-runs from `zuzuu init` when old shapes
|
|
11
|
-
// are detected (like migrateHome).
|
|
12
|
-
//
|
|
13
|
-
// Pure cores: migrateProposals(agentDir) → { scanned, migrated, skipped }
|
|
14
|
-
// migrateHome(root) → { migrated }
|
|
15
|
-
// migrateItems(agentDir) → { knowledge, memory, guardrails, actions, instructions, skipped, errors }
|
|
16
|
-
// CLI surface: migrate(args) — resolves paths, runs the core, prints summary.
|
|
1
|
+
// zuzuu/commands/migrations/items.mjs — the Faculty Standard migrator (W24):
|
|
2
|
+
// legacy per-faculty shapes → one envelope. knowledge/memory frontmatter keys
|
|
3
|
+
// standardised, rules.json exploded into guardrails/items/, action.json+SKILL.md
|
|
4
|
+
// → ACTION.md, instructions/project.md → items/steering.md. Idempotent,
|
|
5
|
+
// fail-soft per item. Auto-runs from `zuzuu init` when old shapes are detected.
|
|
17
6
|
|
|
18
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync,
|
|
7
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync, rmSync, statSync, mkdirSync } from 'node:fs';
|
|
19
8
|
import { join } from 'node:path';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import { serializeEnvelope, deriveTitle } from '../faculty/envelope.mjs';
|
|
25
|
-
import { serializeItem } from '../knowledge/items.mjs';
|
|
26
|
-
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
// pure core — testable without process.*
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Determine whether a parsed JSON record is already in the new shape.
|
|
33
|
-
* A record is "new" when it has `payload` AND `faculty` set.
|
|
34
|
-
* If it only has `candidate` and/or lacks `faculty` it is legacy.
|
|
35
|
-
*/
|
|
36
|
-
function isLegacy(rec) {
|
|
37
|
-
if (!rec || typeof rec !== 'object') return false;
|
|
38
|
-
// already migrated: has payload and faculty
|
|
39
|
-
if (rec.payload !== undefined && rec.faculty !== undefined) return false;
|
|
40
|
-
// legacy if it has candidate or er keys, or is simply missing faculty/payload
|
|
41
|
-
return rec.candidate !== undefined || rec.er !== undefined || rec.faculty === undefined;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Convert a legacy record to the new unified shape.
|
|
46
|
-
* Returns the migrated record (caller writes it back).
|
|
47
|
-
*/
|
|
48
|
-
function migrateRecord(rec) {
|
|
49
|
-
const out = { ...rec };
|
|
50
|
-
|
|
51
|
-
// payload = candidate (drop candidate)
|
|
52
|
-
if (out.candidate !== undefined) {
|
|
53
|
-
if (out.payload === undefined) out.payload = out.candidate;
|
|
54
|
-
delete out.candidate;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// analysis = { er } (drop er)
|
|
58
|
-
if (out.er !== undefined) {
|
|
59
|
-
if (out.analysis === undefined) out.analysis = { er: out.er };
|
|
60
|
-
delete out.er;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// faculty defaults to 'knowledge' (only knowledge proposals exist pre-spine)
|
|
64
|
-
if (!out.faculty) out.faculty = 'knowledge';
|
|
65
|
-
|
|
66
|
-
return out;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Scan one directory of *.json files and migrate legacy records in-place.
|
|
71
|
-
* Fail-soft: bad JSON files are counted as skipped and never throw.
|
|
72
|
-
* Returns { migrated, scanned, skipped } for this directory.
|
|
73
|
-
*/
|
|
74
|
-
function migrateDir(dir) {
|
|
75
|
-
if (!existsSync(dir)) return { migrated: 0, scanned: 0, skipped: 0 };
|
|
76
|
-
|
|
77
|
-
const files = readdirSync(dir).filter((f) => f.endsWith('.json'));
|
|
78
|
-
let migrated = 0;
|
|
79
|
-
let skipped = 0;
|
|
80
|
-
|
|
81
|
-
for (const file of files) {
|
|
82
|
-
const fpath = join(dir, file);
|
|
83
|
-
let rec;
|
|
84
|
-
try {
|
|
85
|
-
rec = JSON.parse(readFileSync(fpath, 'utf8'));
|
|
86
|
-
} catch {
|
|
87
|
-
skipped++;
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (!isLegacy(rec)) {
|
|
92
|
-
skipped++;
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
const migrated_rec = migrateRecord(rec);
|
|
98
|
-
writeFileSync(fpath, JSON.stringify(migrated_rec, null, 2) + '\n');
|
|
99
|
-
migrated++;
|
|
100
|
-
} catch {
|
|
101
|
-
skipped++;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return { migrated, scanned: files.length, skipped };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Scan both pending and archived Knowledge proposals.
|
|
110
|
-
* Returns { scanned, migrated, skipped }.
|
|
111
|
-
*/
|
|
112
|
-
export function migrateProposals(agentDir) {
|
|
113
|
-
const pending = migrateDir(proposalsDir(agentDir, 'knowledge'));
|
|
114
|
-
const archived = migrateDir(archiveDir(agentDir, 'knowledge'));
|
|
115
|
-
|
|
116
|
-
return {
|
|
117
|
-
scanned: pending.scanned + archived.scanned,
|
|
118
|
-
migrated: pending.migrated + archived.migrated,
|
|
119
|
-
skipped: pending.skipped + archived.skipped,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// ---------------------------------------------------------------------------
|
|
124
|
-
// home migration — agent/ → .zuzuu/ (W1, 2026-06-12)
|
|
125
|
-
// ---------------------------------------------------------------------------
|
|
126
|
-
|
|
127
|
-
// The denies the old visible-agent/ home installed; scrubbed here (NOT kept in
|
|
128
|
-
// install.mjs — clean break) and replaced by the current narrow .zuzuu/ pair.
|
|
129
|
-
const LEGACY_DENY_RULES = ['Read(./agent/.traces/**)', 'Read(./agent/.live/**)'];
|
|
130
|
-
const NEW_DENY_RULES = ['Read(./.zuzuu/.traces/**)', 'Read(./.zuzuu/.live/**)'];
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* One-shot HOME migration: visible `agent/` → hidden `.zuzuu/` (byte-identical
|
|
134
|
-
* inner layout). Gated on `agent/agent.json` — `agent/` is a common dir name,
|
|
135
|
-
* so an unrelated agent/ dir in a brownfield repo must NEVER be touched (the
|
|
136
|
-
* one place this differs from the old `.mns→agent` precedent). Idempotent +
|
|
137
|
-
* fail-soft; NEVER clobbers an existing .zuzuu/. Pure FS move (renameSync).
|
|
138
|
-
* @returns {{migrated: boolean}}
|
|
139
|
-
*/
|
|
140
|
-
export function migrateHome(root = repoRoot()) {
|
|
141
|
-
const legacy = join(root, 'agent');
|
|
142
|
-
const home = join(root, '.zuzuu');
|
|
143
|
-
if (existsSync(home) || !existsSync(join(legacy, 'agent.json'))) return { migrated: false };
|
|
144
|
-
|
|
145
|
-
renameSync(legacy, home); // move the whole home (atomic on same filesystem)
|
|
146
|
-
|
|
147
|
-
rewriteTraceRefs(home);
|
|
148
|
-
rewriteGitignore(root);
|
|
149
|
-
scrubLegacyDenies(root);
|
|
150
|
-
// derived index: drop, it rebuilds on the next recall/reindex
|
|
151
|
-
try { rmSync(join(home, 'knowledge', '.index.db'), { force: true }); } catch { /* fail-soft */ }
|
|
152
|
-
return { migrated: true };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** sessions.json stores repo-relative traceRefs (`agent/.traces/…`) — re-point them. */
|
|
156
|
-
function rewriteTraceRefs(home) {
|
|
157
|
-
const index = join(home, 'sessions.json');
|
|
158
|
-
if (!existsSync(index)) return;
|
|
159
|
-
try {
|
|
160
|
-
const idx = JSON.parse(readFileSync(index, 'utf8'));
|
|
161
|
-
for (const s of idx.sessions || []) {
|
|
162
|
-
if (typeof s.traceRef === 'string' && s.traceRef.startsWith('agent/')) {
|
|
163
|
-
s.traceRef = '.zuzuu/' + s.traceRef.slice('agent/'.length);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
writeFileSync(index, JSON.stringify(idx, null, 2) + '\n');
|
|
167
|
-
} catch { /* fail-soft: a bad index never blocks the move */ }
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** Drop legacy `agent/` ignore lines, then append the canonical .zuzuu/ ones. */
|
|
171
|
-
function rewriteGitignore(root) {
|
|
172
|
-
const path = join(root, '.gitignore');
|
|
173
|
-
if (existsSync(path)) {
|
|
174
|
-
const kept = readFileSync(path, 'utf8')
|
|
175
|
-
.split('\n')
|
|
176
|
-
.filter((l) => !l.trim().startsWith('agent/'))
|
|
177
|
-
.join('\n');
|
|
178
|
-
writeFileSync(path, kept.endsWith('\n') || kept === '' ? kept : kept + '\n');
|
|
179
|
-
}
|
|
180
|
-
ensureGitignore(root); // appends .zuzuu/.traces/, .zuzuu/.live/, .zuzuu/knowledge/.index.db
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** Swap the old agent/ deny rules for the .zuzuu/ pair in any .claude settings file. */
|
|
184
|
-
function scrubLegacyDenies(root) {
|
|
185
|
-
for (const f of ['settings.json', 'settings.local.json']) {
|
|
186
|
-
const path = join(root, '.claude', f);
|
|
187
|
-
if (!existsSync(path)) continue;
|
|
188
|
-
try {
|
|
189
|
-
const s = JSON.parse(readFileSync(path, 'utf8'));
|
|
190
|
-
const deny = s?.permissions?.deny;
|
|
191
|
-
if (!Array.isArray(deny)) continue;
|
|
192
|
-
const hadOurs = deny.some((r) => LEGACY_DENY_RULES.includes(r));
|
|
193
|
-
if (!hadOurs) continue;
|
|
194
|
-
s.permissions.deny = deny.filter((r) => !LEGACY_DENY_RULES.includes(r));
|
|
195
|
-
for (const rule of NEW_DENY_RULES) if (!s.permissions.deny.includes(rule)) s.permissions.deny.push(rule);
|
|
196
|
-
writeFileSync(path, JSON.stringify(s, null, 2) + '\n');
|
|
197
|
-
} catch { /* fail-soft: never break settings we can't parse */ }
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/** Re-inject the current faculties block into any existing host instruction files. */
|
|
202
|
-
function reinjectHostBlocks(root) {
|
|
203
|
-
for (const f of ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md']) {
|
|
204
|
-
const p = join(root, f);
|
|
205
|
-
if (existsSync(p)) {
|
|
206
|
-
const text = readFileSync(p, 'utf8');
|
|
207
|
-
if (!text.includes(`zuzuu:faculties:v${BLOCK_VERSION}`)) writeFileSync(p, injectBlock(text));
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// ---------------------------------------------------------------------------
|
|
213
|
-
// items migration — the Faculty Standard (W24)
|
|
214
|
-
// ---------------------------------------------------------------------------
|
|
9
|
+
import { serializeEnvelope, deriveTitle } from '../../faculty/envelope.mjs';
|
|
10
|
+
import { serializeItem } from '../../knowledge/items.mjs';
|
|
11
|
+
import { FACULTIES } from '../../faculty/contract.mjs';
|
|
12
|
+
import { BUILTIN_MODULES } from '../../faculty/registry.mjs';
|
|
215
13
|
|
|
216
14
|
/** Does this file's frontmatter already carry the envelope (a `faculty:` key)? */
|
|
217
15
|
function isEnvelopeText(text) {
|
|
@@ -493,10 +291,31 @@ function migrateInstructions(agentDir, out) {
|
|
|
493
291
|
}
|
|
494
292
|
}
|
|
495
293
|
|
|
294
|
+
/** Seed missing <faculty>/faculty.json manifests into an existing home (the
|
|
295
|
+
* Faculty Module contract, 2026-06-13). Only faculties whose dir exists get
|
|
296
|
+
* one — a migrator repairs, it never scaffolds. Idempotent, fail-soft. */
|
|
297
|
+
function seedFacultyManifests(agentDir, out) {
|
|
298
|
+
for (const f of FACULTIES) {
|
|
299
|
+
try {
|
|
300
|
+
const dir = join(agentDir, f);
|
|
301
|
+
if (!existsSync(dir)) continue;
|
|
302
|
+
const p = join(dir, 'faculty.json');
|
|
303
|
+
if (existsSync(p)) continue;
|
|
304
|
+
writeFileSync(p, JSON.stringify(BUILTIN_MODULES[f].manifest, null, 2) + '\n');
|
|
305
|
+
out.manifests++;
|
|
306
|
+
} catch (e) {
|
|
307
|
+
out.errors.push({ file: `${f}/faculty.json`, error: e.message });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
496
312
|
/**
|
|
497
313
|
* Are pre-standard shapes present? Cheap checks gate the init auto-run.
|
|
498
314
|
*/
|
|
499
315
|
export function needsItemsMigration(agentDir) {
|
|
316
|
+
for (const f of FACULTIES) {
|
|
317
|
+
if (existsSync(join(agentDir, f)) && !existsSync(join(agentDir, f, 'faculty.json'))) return true;
|
|
318
|
+
}
|
|
500
319
|
if (existsSync(join(agentDir, 'guardrails', 'rules.json'))) return true;
|
|
501
320
|
if (existsSync(join(agentDir, 'instructions', 'project.md'))) return true;
|
|
502
321
|
for (const base of [join(agentDir, 'actions'), join(agentDir, 'actions', 'inbox')]) {
|
|
@@ -526,47 +345,16 @@ export function needsItemsMigration(agentDir) {
|
|
|
526
345
|
* files are skipped) and fail-soft per item (an unconvertible item is reported,
|
|
527
346
|
* never fatal; its legacy source is left in place).
|
|
528
347
|
* @returns {{knowledge:number, memory:number, guardrails:number, actions:number,
|
|
529
|
-
* instructions:number, skipped:number, errors:Array<{file,error}>}}
|
|
348
|
+
* instructions:number, manifests:number, skipped:number, errors:Array<{file,error}>}}
|
|
530
349
|
*/
|
|
531
350
|
export function migrateItems(agentDir) {
|
|
532
|
-
const out = { knowledge: 0, memory: 0, guardrails: 0, actions: 0, instructions: 0, skipped: 0, errors: [] };
|
|
351
|
+
const out = { knowledge: 0, memory: 0, guardrails: 0, actions: 0, instructions: 0, manifests: 0, skipped: 0, errors: [] };
|
|
533
352
|
migrateKnowledgeItems(agentDir, out);
|
|
534
353
|
migrateMemoryEntries(agentDir, out);
|
|
535
354
|
migrateGuardrails(agentDir, out);
|
|
536
355
|
migrateActions(agentDir, out);
|
|
537
356
|
migrateInstructions(agentDir, out);
|
|
357
|
+
seedFacultyManifests(agentDir, out);
|
|
538
358
|
return out;
|
|
539
359
|
}
|
|
540
360
|
|
|
541
|
-
// ---------------------------------------------------------------------------
|
|
542
|
-
// CLI surface
|
|
543
|
-
// ---------------------------------------------------------------------------
|
|
544
|
-
|
|
545
|
-
export function migrate(args = {}) {
|
|
546
|
-
if (args.items) {
|
|
547
|
-
const agentDir = paths().dir;
|
|
548
|
-
const r = migrateItems(agentDir);
|
|
549
|
-
const total = r.knowledge + r.memory + r.guardrails + r.actions + r.instructions;
|
|
550
|
-
console.log(`migrate --items: ${total} item(s) → the Faculty Standard envelope — knowledge ${r.knowledge} · memory ${r.memory} · guardrails ${r.guardrails} · actions ${r.actions} · instructions ${r.instructions} (${r.skipped} already standard)`);
|
|
551
|
-
for (const e of r.errors) console.log(` ✗ ${e.file}: ${e.error}`);
|
|
552
|
-
if (!total && !r.errors.length) console.log(' nothing to migrate (the home already speaks the envelope)');
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
if (args.home) {
|
|
556
|
-
const root = repoRoot(process.cwd());
|
|
557
|
-
const { migrated } = migrateHome(root);
|
|
558
|
-
if (!migrated) { console.log('migrate --home: nothing to do (already .zuzuu/, or no zuzuu home at agent/)'); return; }
|
|
559
|
-
try { reinjectHostBlocks(root); } catch { /* fail-open */ }
|
|
560
|
-
console.log(`migrate --home: agent/ → .zuzuu/ (hidden, like .git; block v${BLOCK_VERSION}, gitignore + deny rules rewritten)`);
|
|
561
|
-
console.log(' transparency lives in porcelain now: zuzuu status · explain · digest');
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
const agentDir = paths().dir;
|
|
565
|
-
const { scanned, migrated, skipped } = migrateProposals(agentDir);
|
|
566
|
-
console.log(`migrate: scanned ${scanned} proposal(s) — migrated ${migrated}, skipped ${skipped}`);
|
|
567
|
-
if (migrated > 0) {
|
|
568
|
-
console.log(' legacy candidate/er keys rewritten to payload/analysis.er + faculty:knowledge');
|
|
569
|
-
} else {
|
|
570
|
-
console.log(' nothing to migrate (all records already in new shape)');
|
|
571
|
-
}
|
|
572
|
-
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// zuzuu/commands/migrations/proposals.mjs — legacy proposal-schema migrator.
|
|
2
|
+
// {candidate, er} → spine {payload, analysis, faculty} (WS2-T5). Pure core:
|
|
3
|
+
// migrateProposals(agentDir) → { scanned, migrated, skipped }. Fail-soft per file.
|
|
4
|
+
|
|
5
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { proposalsDir, archiveDir } from '../../faculty/contract.mjs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Determine whether a parsed JSON record is already in the new shape.
|
|
11
|
+
* A record is "new" when it has `payload` AND `faculty` set.
|
|
12
|
+
* If it only has `candidate` and/or lacks `faculty` it is legacy.
|
|
13
|
+
*/
|
|
14
|
+
function isLegacy(rec) {
|
|
15
|
+
if (!rec || typeof rec !== 'object') return false;
|
|
16
|
+
// already migrated: has payload and faculty
|
|
17
|
+
if (rec.payload !== undefined && rec.faculty !== undefined) return false;
|
|
18
|
+
// legacy if it has candidate or er keys, or is simply missing faculty/payload
|
|
19
|
+
return rec.candidate !== undefined || rec.er !== undefined || rec.faculty === undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Convert a legacy record to the new unified shape.
|
|
24
|
+
* Returns the migrated record (caller writes it back).
|
|
25
|
+
*/
|
|
26
|
+
function migrateRecord(rec) {
|
|
27
|
+
const out = { ...rec };
|
|
28
|
+
|
|
29
|
+
// payload = candidate (drop candidate)
|
|
30
|
+
if (out.candidate !== undefined) {
|
|
31
|
+
if (out.payload === undefined) out.payload = out.candidate;
|
|
32
|
+
delete out.candidate;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// analysis = { er } (drop er)
|
|
36
|
+
if (out.er !== undefined) {
|
|
37
|
+
if (out.analysis === undefined) out.analysis = { er: out.er };
|
|
38
|
+
delete out.er;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// faculty defaults to 'knowledge' (only knowledge proposals exist pre-spine)
|
|
42
|
+
if (!out.faculty) out.faculty = 'knowledge';
|
|
43
|
+
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Scan one directory of *.json files and migrate legacy records in-place.
|
|
49
|
+
* Fail-soft: bad JSON files are counted as skipped and never throw.
|
|
50
|
+
* Returns { migrated, scanned, skipped } for this directory.
|
|
51
|
+
*/
|
|
52
|
+
function migrateDir(dir) {
|
|
53
|
+
if (!existsSync(dir)) return { migrated: 0, scanned: 0, skipped: 0 };
|
|
54
|
+
|
|
55
|
+
const files = readdirSync(dir).filter((f) => f.endsWith('.json'));
|
|
56
|
+
let migrated = 0;
|
|
57
|
+
let skipped = 0;
|
|
58
|
+
|
|
59
|
+
for (const file of files) {
|
|
60
|
+
const fpath = join(dir, file);
|
|
61
|
+
let rec;
|
|
62
|
+
try {
|
|
63
|
+
rec = JSON.parse(readFileSync(fpath, 'utf8'));
|
|
64
|
+
} catch {
|
|
65
|
+
skipped++;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!isLegacy(rec)) {
|
|
70
|
+
skipped++;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const migrated_rec = migrateRecord(rec);
|
|
76
|
+
writeFileSync(fpath, JSON.stringify(migrated_rec, null, 2) + '\n');
|
|
77
|
+
migrated++;
|
|
78
|
+
} catch {
|
|
79
|
+
skipped++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { migrated, scanned: files.length, skipped };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Scan both pending and archived Knowledge proposals.
|
|
88
|
+
* Returns { scanned, migrated, skipped }.
|
|
89
|
+
*/
|
|
90
|
+
export function migrateProposals(agentDir) {
|
|
91
|
+
const pending = migrateDir(proposalsDir(agentDir, 'knowledge'));
|
|
92
|
+
const archived = migrateDir(archiveDir(agentDir, 'knowledge'));
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
scanned: pending.scanned + archived.scanned,
|
|
96
|
+
migrated: pending.migrated + archived.migrated,
|
|
97
|
+
skipped: pending.skipped + archived.skipped,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|