@zuzuucodes/cli 1.4.0 → 1.5.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 +8 -1
- package/package.json +1 -1
- package/web-app/dist/zuzuu-api.js +122 -27
- package/web-app/web-dist/assets/{DiffTab-BuWonUNJ.js → DiffTab-BpGp1akx.js} +1 -1
- package/web-app/web-dist/assets/{MonacoFile-CL3DhFKG.js → MonacoFile-CqbVacUZ.js} +1 -1
- package/web-app/web-dist/assets/{cssMode-B9jnrWOz.js → cssMode-Dx3ub8Pk.js} +1 -1
- package/web-app/web-dist/assets/{dist-ChcDQ_7s.js → dist-C6R6xoyX.js} +1 -1
- package/web-app/web-dist/assets/{htmlMode-Bi8vSvwb.js → htmlMode-DM6oHc7c.js} +1 -1
- package/web-app/web-dist/assets/{index--5yy8RbA.js → index-DHpC851f.js} +25 -24
- package/web-app/web-dist/assets/index-O-t1gyMG.css +2 -0
- package/web-app/web-dist/assets/{jsonMode-C6ELX5GM.js → jsonMode-DflaUwqW.js} +1 -1
- package/web-app/web-dist/assets/{monaco-setup-CsR6EfHe.js → monaco-setup-wbBeb0oN.js} +3 -3
- package/web-app/web-dist/assets/{tsMode-a8OvovQd.js → tsMode-DRwkDcoK.js} +1 -1
- package/web-app/web-dist/index.html +2 -2
- package/zuzuu/actions/adapter.mjs +12 -20
- 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/commands/act-author.mjs +23 -13
- package/zuzuu/commands/act.mjs +3 -5
- package/zuzuu/commands/doctor.mjs +2 -15
- package/zuzuu/commands/explain.mjs +4 -4
- package/zuzuu/commands/faculty.mjs +75 -0
- package/zuzuu/commands/generation.mjs +2 -4
- package/zuzuu/commands/hook.mjs +7 -5
- package/zuzuu/commands/init.mjs +14 -1
- package/zuzuu/commands/migrate.mjs +348 -1
- package/zuzuu/digest.mjs +18 -13
- package/zuzuu/faculty/envelope.mjs +290 -0
- package/zuzuu/faculty/generation.mjs +53 -47
- package/zuzuu/faculty/items.mjs +75 -0
- package/zuzuu/guardrails/adapter.mjs +18 -49
- package/zuzuu/guardrails.mjs +72 -24
- package/zuzuu/instructions/adapter.mjs +30 -30
- package/zuzuu/knowledge/items.mjs +56 -91
- package/zuzuu/live/install.mjs +1 -1
- package/zuzuu/memory/adapter.mjs +27 -52
- package/zuzuu/miners/actions.mjs +14 -20
- package/zuzuu/miners/guardrails.mjs +8 -11
- package/zuzuu/miners/instructions.mjs +10 -10
- package/zuzuu/scaffold.mjs +99 -38
- package/web-app/web-dist/assets/index-BVG4hgk7.css +0 -2
|
@@ -3,17 +3,26 @@
|
|
|
3
3
|
//
|
|
4
4
|
// (default) proposal schema: legacy {candidate, er} → spine {payload, analysis, faculty} (WS2-T5)
|
|
5
5
|
// --home faculty home: visible agent/ → hidden .zuzuu/ (W1, 2026-06-12)
|
|
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).
|
|
6
12
|
//
|
|
7
13
|
// Pure cores: migrateProposals(agentDir) → { scanned, migrated, skipped }
|
|
8
14
|
// migrateHome(root) → { migrated }
|
|
15
|
+
// migrateItems(agentDir) → { knowledge, memory, guardrails, actions, instructions, skipped, errors }
|
|
9
16
|
// CLI surface: migrate(args) — resolves paths, runs the core, prints summary.
|
|
10
17
|
|
|
11
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync, renameSync, rmSync } from 'node:fs';
|
|
18
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync, renameSync, rmSync, statSync, mkdirSync } from 'node:fs';
|
|
12
19
|
import { join } from 'node:path';
|
|
13
20
|
import { paths, repoRoot } from '../store.mjs';
|
|
14
21
|
import { proposalsDir, archiveDir } from '../faculty/contract.mjs';
|
|
15
22
|
import { ensureGitignore } from '../scaffold.mjs';
|
|
16
23
|
import { injectBlock, BLOCK_VERSION } from '../inject.mjs';
|
|
24
|
+
import { serializeEnvelope, deriveTitle } from '../faculty/envelope.mjs';
|
|
25
|
+
import { serializeItem } from '../knowledge/items.mjs';
|
|
17
26
|
|
|
18
27
|
// ---------------------------------------------------------------------------
|
|
19
28
|
// pure core — testable without process.*
|
|
@@ -200,11 +209,349 @@ function reinjectHostBlocks(root) {
|
|
|
200
209
|
}
|
|
201
210
|
}
|
|
202
211
|
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// items migration — the Faculty Standard (W24)
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
/** Does this file's frontmatter already carry the envelope (a `faculty:` key)? */
|
|
217
|
+
function isEnvelopeText(text) {
|
|
218
|
+
const m = String(text).match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
219
|
+
return !!m && /^faculty:/m.test(m[1]);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const unquoteLegacy = (s) => {
|
|
223
|
+
const t = String(s).trim();
|
|
224
|
+
return (t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'")) ? t.slice(1, -1) : t;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Parse the PRE-standard knowledge/memory frontmatter grammar (top-level
|
|
229
|
+
* scalars; ONE nested map `attributes`/`provenance`; arrays of flat maps).
|
|
230
|
+
* Kept here (and only here) — the live parsers are envelope-only (clean break).
|
|
231
|
+
* Throws on violations; the caller fail-softs per item.
|
|
232
|
+
*/
|
|
233
|
+
function parseLegacyFrontmatter(text) {
|
|
234
|
+
const m = String(text).match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
235
|
+
if (!m) throw new Error('no frontmatter block');
|
|
236
|
+
const [, fm, body] = m;
|
|
237
|
+
const item = { scalars: {}, maps: {}, lists: {}, body: body.trim() };
|
|
238
|
+
let section = null; // current nested key
|
|
239
|
+
let mode = null; // 'map' | 'list'
|
|
240
|
+
let current = null;
|
|
241
|
+
for (const raw of fm.split('\n')) {
|
|
242
|
+
if (!raw.trim()) continue;
|
|
243
|
+
const indent = raw.match(/^ */)[0].length;
|
|
244
|
+
const line = raw.trim();
|
|
245
|
+
const kv = line.match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
|
|
246
|
+
if (indent === 0) {
|
|
247
|
+
current = null;
|
|
248
|
+
if (!kv) throw new Error(`bad line: ${line}`);
|
|
249
|
+
const [, key, val] = kv;
|
|
250
|
+
if (val === '') { section = key; mode = null; }
|
|
251
|
+
else { section = null; item.scalars[key] = unquoteLegacy(val); }
|
|
252
|
+
} else if (section) {
|
|
253
|
+
if (line.startsWith('- ')) {
|
|
254
|
+
mode = mode ?? 'list';
|
|
255
|
+
if (!item.lists[section]) item.lists[section] = [];
|
|
256
|
+
const ekv = line.slice(2).match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
|
|
257
|
+
if (ekv) { current = { [ekv[1]]: unquoteLegacy(ekv[2]) }; item.lists[section].push(current); }
|
|
258
|
+
else { current = null; item.lists[section].push(unquoteLegacy(line.slice(2))); }
|
|
259
|
+
} else if (current && kv) {
|
|
260
|
+
current[kv[1]] = unquoteLegacy(kv[2]);
|
|
261
|
+
} else if (kv) {
|
|
262
|
+
mode = mode ?? 'map';
|
|
263
|
+
if (!item.maps[section]) item.maps[section] = {};
|
|
264
|
+
item.maps[section][kv[1]] = unquoteLegacy(kv[2]);
|
|
265
|
+
} else {
|
|
266
|
+
throw new Error(`bad nested line: ${line}`);
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
throw new Error(`unexpected indented line: ${line}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return item;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Parse a legacy inline list: `[a, b]` or an already-array value. */
|
|
276
|
+
function legacyList(v) {
|
|
277
|
+
if (Array.isArray(v)) return v.map(String);
|
|
278
|
+
const t = String(v ?? '').trim();
|
|
279
|
+
if (t.startsWith('[') && t.endsWith(']')) {
|
|
280
|
+
return t.slice(1, -1).split(',').map((s) => unquoteLegacy(s)).filter(Boolean);
|
|
281
|
+
}
|
|
282
|
+
return t ? [t] : [];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** knowledge/items/*.md: legacy keys → envelope (ids unchanged). */
|
|
286
|
+
function migrateKnowledgeItems(agentDir, out) {
|
|
287
|
+
const dir = join(agentDir, 'knowledge', 'items');
|
|
288
|
+
if (!existsSync(dir)) return;
|
|
289
|
+
for (const f of readdirSync(dir).filter((f) => f.endsWith('.md'))) {
|
|
290
|
+
const path = join(dir, f);
|
|
291
|
+
try {
|
|
292
|
+
const text = readFileSync(path, 'utf8');
|
|
293
|
+
if (isEnvelopeText(text)) { out.skipped++; continue; }
|
|
294
|
+
const legacy = parseLegacyFrontmatter(text);
|
|
295
|
+
const item = {
|
|
296
|
+
id: legacy.scalars.id || f.replace(/\.md$/, ''),
|
|
297
|
+
type: legacy.scalars.type,
|
|
298
|
+
created_at: legacy.scalars.created_at,
|
|
299
|
+
updated_at: legacy.scalars.updated_at,
|
|
300
|
+
status: legacy.scalars.status ?? 'active',
|
|
301
|
+
attributes: legacy.maps.attributes ?? {},
|
|
302
|
+
relations: (legacy.lists.relations ?? []).filter((r) => typeof r === 'object'),
|
|
303
|
+
provenance: (legacy.lists.provenance ?? []).filter((r) => typeof r === 'object'),
|
|
304
|
+
body: legacy.body,
|
|
305
|
+
};
|
|
306
|
+
if (!item.type) throw new Error('item missing type');
|
|
307
|
+
writeFileSync(path, serializeItem(item)); // envelope via the knowledge wrapper
|
|
308
|
+
out.knowledge++;
|
|
309
|
+
} catch (e) {
|
|
310
|
+
out.errors.push({ file: `knowledge/items/${f}`, error: e.message });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** memory/entries/*.md: legacy episode keys → envelope (kind: episode). */
|
|
316
|
+
function migrateMemoryEntries(agentDir, out) {
|
|
317
|
+
const dir = join(agentDir, 'memory', 'entries');
|
|
318
|
+
if (!existsSync(dir)) return;
|
|
319
|
+
for (const f of readdirSync(dir).filter((f) => f.endsWith('.md'))) {
|
|
320
|
+
const path = join(dir, f);
|
|
321
|
+
try {
|
|
322
|
+
const text = readFileSync(path, 'utf8');
|
|
323
|
+
if (isEnvelopeText(text)) { out.skipped++; continue; }
|
|
324
|
+
const legacy = parseLegacyFrontmatter(text);
|
|
325
|
+
const id = legacy.scalars.id || f.replace(/\.md$/, '');
|
|
326
|
+
const payload = {};
|
|
327
|
+
const prov = legacy.maps.provenance ?? {};
|
|
328
|
+
const sessions = legacyList(prov.sessions ?? legacy.scalars.sessions ?? '');
|
|
329
|
+
const hosts = legacyList(prov.hosts ?? legacy.scalars.hosts ?? '');
|
|
330
|
+
const tags = legacyList(legacy.scalars.tags ?? '');
|
|
331
|
+
if (sessions.length) payload.sessions = sessions;
|
|
332
|
+
if (hosts.length) payload.hosts = hosts;
|
|
333
|
+
if (tags.length) payload.tags = tags;
|
|
334
|
+
writeFileSync(path, serializeEnvelope({
|
|
335
|
+
id,
|
|
336
|
+
faculty: 'memory',
|
|
337
|
+
kind: 'episode',
|
|
338
|
+
title: legacy.scalars.title ?? deriveTitle(legacy.body, id),
|
|
339
|
+
status: 'active', // curated/proposed lifecycles fold into active
|
|
340
|
+
created_at: legacy.scalars.date ?? legacy.scalars.created_at,
|
|
341
|
+
payload,
|
|
342
|
+
body: legacy.body,
|
|
343
|
+
}));
|
|
344
|
+
out.memory++;
|
|
345
|
+
} catch (e) {
|
|
346
|
+
out.errors.push({ file: `memory/entries/${f}`, error: e.message });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** guardrails/rules.json: EXPLODE into items/<id>.md, then delete rules.json. */
|
|
352
|
+
function migrateGuardrails(agentDir, out) {
|
|
353
|
+
const rulesPath = join(agentDir, 'guardrails', 'rules.json');
|
|
354
|
+
if (!existsSync(rulesPath)) return;
|
|
355
|
+
let data;
|
|
356
|
+
try {
|
|
357
|
+
data = JSON.parse(readFileSync(rulesPath, 'utf8'));
|
|
358
|
+
} catch (e) {
|
|
359
|
+
out.errors.push({ file: 'guardrails/rules.json', error: e.message });
|
|
360
|
+
return; // unreadable → leave the file for the human, never destroy it
|
|
361
|
+
}
|
|
362
|
+
const rules = Array.isArray(data?.rules) ? data.rules : [];
|
|
363
|
+
const itemsDir = join(agentDir, 'guardrails', 'items');
|
|
364
|
+
let failed = 0;
|
|
365
|
+
for (let i = 0; i < rules.length; i++) {
|
|
366
|
+
const r = rules[i] ?? {};
|
|
367
|
+
try {
|
|
368
|
+
const id = String(r.id ?? `rule-${i}`);
|
|
369
|
+
mkdirSync(itemsDir, { recursive: true });
|
|
370
|
+
writeFileSync(join(itemsDir, `${id}.md`), serializeEnvelope({
|
|
371
|
+
id,
|
|
372
|
+
faculty: 'guardrails',
|
|
373
|
+
kind: 'rule',
|
|
374
|
+
title: deriveTitle(r.reason, id),
|
|
375
|
+
status: 'active',
|
|
376
|
+
created_at: new Date().toISOString().replace(/\.\d+Z$/, 'Z'),
|
|
377
|
+
payload: { action: r.action, tool: r.tool || '*', pattern: String(r.pattern ?? ''), reason: String(r.reason ?? '') },
|
|
378
|
+
body: '',
|
|
379
|
+
}));
|
|
380
|
+
out.guardrails++;
|
|
381
|
+
} catch (e) {
|
|
382
|
+
failed++;
|
|
383
|
+
out.errors.push({ file: `guardrails/rules.json#${r.id ?? i}`, error: e.message });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// rules.json goes away only when every rule landed as an item (fail-soft)
|
|
387
|
+
if (failed === 0) {
|
|
388
|
+
try { rmSync(rulesPath, { force: true }); } catch { /* fail-soft */ }
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** One action dir: action.json (+SKILL.md) → ACTION.md; legacy files removed on success. */
|
|
393
|
+
function migrateActionDir(dir, slug, out) {
|
|
394
|
+
const actionMd = join(dir, 'ACTION.md');
|
|
395
|
+
if (existsSync(actionMd)) { out.skipped++; return; }
|
|
396
|
+
const manPath = join(dir, 'action.json');
|
|
397
|
+
const skillPath = join(dir, 'SKILL.md');
|
|
398
|
+
if (!existsSync(manPath) && !existsSync(skillPath)) return; // not an action dir
|
|
399
|
+
try {
|
|
400
|
+
let man = {};
|
|
401
|
+
if (existsSync(manPath)) man = JSON.parse(readFileSync(manPath, 'utf8'));
|
|
402
|
+
let skillFm = {};
|
|
403
|
+
let skillBody = '';
|
|
404
|
+
if (existsSync(skillPath)) {
|
|
405
|
+
const text = readFileSync(skillPath, 'utf8');
|
|
406
|
+
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
407
|
+
if (m) {
|
|
408
|
+
for (const line of m[1].split('\n')) {
|
|
409
|
+
const kv = line.match(/^(\w+):\s*(.*)$/);
|
|
410
|
+
if (kv) skillFm[kv[1]] = kv[2].trim();
|
|
411
|
+
}
|
|
412
|
+
skillBody = m[2].trim();
|
|
413
|
+
} else {
|
|
414
|
+
skillBody = text.trim();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const isScript = existsSync(join(dir, 'run.mjs'));
|
|
418
|
+
const payload = {};
|
|
419
|
+
if (isScript) payload.exec = 'run.mjs';
|
|
420
|
+
// default_args survive as payload.args (flat scalars only — the envelope grammar)
|
|
421
|
+
const args = {};
|
|
422
|
+
for (const [k, v] of Object.entries(man.default_args ?? {})) {
|
|
423
|
+
if (v == null || typeof v === 'object') continue;
|
|
424
|
+
args[k] = String(v);
|
|
425
|
+
}
|
|
426
|
+
if (Object.keys(args).length) payload.args = args;
|
|
427
|
+
const snippet = man.promptSnippet ?? man.description ?? skillFm.description ?? slug;
|
|
428
|
+
const bodyParts = [snippet];
|
|
429
|
+
if (man.description && man.description !== snippet) bodyParts.push('', man.description);
|
|
430
|
+
if (skillBody) bodyParts.push('', skillBody);
|
|
431
|
+
writeFileSync(actionMd, serializeEnvelope({
|
|
432
|
+
id: slug,
|
|
433
|
+
faculty: 'actions',
|
|
434
|
+
kind: isScript ? 'script' : 'runbook',
|
|
435
|
+
title: man.title ?? skillFm.name ?? slug,
|
|
436
|
+
status: 'active',
|
|
437
|
+
created_at: new Date().toISOString().replace(/\.\d+Z$/, 'Z'),
|
|
438
|
+
payload,
|
|
439
|
+
body: bodyParts.join('\n'),
|
|
440
|
+
}));
|
|
441
|
+
// legacy files leave only after ACTION.md landed
|
|
442
|
+
rmSync(manPath, { force: true });
|
|
443
|
+
rmSync(skillPath, { force: true });
|
|
444
|
+
out.actions++;
|
|
445
|
+
} catch (e) {
|
|
446
|
+
out.errors.push({ file: `actions/${slug}`, error: e.message });
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** All action dirs: active + inbox (proposed) — same conversion. */
|
|
451
|
+
function migrateActions(agentDir, out) {
|
|
452
|
+
for (const base of [join(agentDir, 'actions'), join(agentDir, 'actions', 'inbox')]) {
|
|
453
|
+
if (!existsSync(base)) continue;
|
|
454
|
+
for (const name of readdirSync(base)) {
|
|
455
|
+
if (name === 'inbox' || name === 'proposals' || name === '_rolledback') continue;
|
|
456
|
+
const dir = join(base, name);
|
|
457
|
+
let isDir = false;
|
|
458
|
+
try { isDir = statSync(dir).isDirectory(); } catch { continue; }
|
|
459
|
+
if (isDir) migrateActionDir(dir, name, out);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** instructions/project.md → items/steering.md. A customized steering item is
|
|
465
|
+
* never clobbered — project.md then stays put for the human to reconcile. */
|
|
466
|
+
function migrateInstructions(agentDir, out) {
|
|
467
|
+
const projPath = join(agentDir, 'instructions', 'project.md');
|
|
468
|
+
if (!existsSync(projPath)) return;
|
|
469
|
+
const steeringPath = join(agentDir, 'instructions', 'items', 'steering.md');
|
|
470
|
+
try {
|
|
471
|
+
const existing = existsSync(steeringPath) ? readFileSync(steeringPath, 'utf8') : null;
|
|
472
|
+
const placeholder = existing != null && existing.includes('<!-- Fill in:');
|
|
473
|
+
if (existing != null && !placeholder) {
|
|
474
|
+
out.errors.push({ file: 'instructions/project.md', error: 'a customized steering item already exists — merge by hand, then delete project.md' });
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const body = readFileSync(projPath, 'utf8').trim();
|
|
478
|
+
mkdirSync(join(agentDir, 'instructions', 'items'), { recursive: true });
|
|
479
|
+
writeFileSync(steeringPath, serializeEnvelope({
|
|
480
|
+
id: 'steering',
|
|
481
|
+
faculty: 'instructions',
|
|
482
|
+
kind: 'steering',
|
|
483
|
+
title: 'Project steering',
|
|
484
|
+
status: 'active',
|
|
485
|
+
created_at: new Date().toISOString().replace(/\.\d+Z$/, 'Z'),
|
|
486
|
+
payload: { scope: 'project' },
|
|
487
|
+
body,
|
|
488
|
+
}));
|
|
489
|
+
out.instructions++;
|
|
490
|
+
rmSync(projPath, { force: true });
|
|
491
|
+
} catch (e) {
|
|
492
|
+
out.errors.push({ file: 'instructions/project.md', error: e.message });
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Are pre-standard shapes present? Cheap checks gate the init auto-run.
|
|
498
|
+
*/
|
|
499
|
+
export function needsItemsMigration(agentDir) {
|
|
500
|
+
if (existsSync(join(agentDir, 'guardrails', 'rules.json'))) return true;
|
|
501
|
+
if (existsSync(join(agentDir, 'instructions', 'project.md'))) return true;
|
|
502
|
+
for (const base of [join(agentDir, 'actions'), join(agentDir, 'actions', 'inbox')]) {
|
|
503
|
+
if (!existsSync(base)) continue;
|
|
504
|
+
for (const name of readdirSync(base)) {
|
|
505
|
+
if (name === 'inbox' || name === 'proposals' || name === '_rolledback') continue;
|
|
506
|
+
const dir = join(base, name);
|
|
507
|
+
try { if (!statSync(dir).isDirectory()) continue; } catch { continue; }
|
|
508
|
+
if (existsSync(join(dir, 'ACTION.md'))) continue;
|
|
509
|
+
if (existsSync(join(dir, 'action.json')) || existsSync(join(dir, 'SKILL.md'))) return true;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
for (const seg of [['knowledge', 'items'], ['memory', 'entries']]) {
|
|
513
|
+
const dir = join(agentDir, ...seg);
|
|
514
|
+
if (!existsSync(dir)) continue;
|
|
515
|
+
for (const f of readdirSync(dir).filter((f) => f.endsWith('.md'))) {
|
|
516
|
+
try {
|
|
517
|
+
if (!isEnvelopeText(readFileSync(join(dir, f), 'utf8'))) return true;
|
|
518
|
+
} catch { /* unreadable file never forces a migration */ }
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* One-shot Faculty Standard migration for a home. Idempotent (already-envelope
|
|
526
|
+
* files are skipped) and fail-soft per item (an unconvertible item is reported,
|
|
527
|
+
* never fatal; its legacy source is left in place).
|
|
528
|
+
* @returns {{knowledge:number, memory:number, guardrails:number, actions:number,
|
|
529
|
+
* instructions:number, skipped:number, errors:Array<{file,error}>}}
|
|
530
|
+
*/
|
|
531
|
+
export function migrateItems(agentDir) {
|
|
532
|
+
const out = { knowledge: 0, memory: 0, guardrails: 0, actions: 0, instructions: 0, skipped: 0, errors: [] };
|
|
533
|
+
migrateKnowledgeItems(agentDir, out);
|
|
534
|
+
migrateMemoryEntries(agentDir, out);
|
|
535
|
+
migrateGuardrails(agentDir, out);
|
|
536
|
+
migrateActions(agentDir, out);
|
|
537
|
+
migrateInstructions(agentDir, out);
|
|
538
|
+
return out;
|
|
539
|
+
}
|
|
540
|
+
|
|
203
541
|
// ---------------------------------------------------------------------------
|
|
204
542
|
// CLI surface
|
|
205
543
|
// ---------------------------------------------------------------------------
|
|
206
544
|
|
|
207
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
|
+
}
|
|
208
555
|
if (args.home) {
|
|
209
556
|
const root = repoRoot(process.cwd());
|
|
210
557
|
const { migrated } = migrateHome(root);
|
package/zuzuu/digest.mjs
CHANGED
|
@@ -4,31 +4,36 @@
|
|
|
4
4
|
// I/O-free: callers (the CLI + the SessionStart hook) handle output. Every
|
|
5
5
|
// reader is wrapped so a single broken faculty never sinks the whole digest.
|
|
6
6
|
|
|
7
|
-
import { readFileSync } from 'node:fs';
|
|
8
7
|
import { join } from 'node:path';
|
|
9
8
|
import { allItems } from './knowledge/items.mjs';
|
|
10
9
|
import { listProposals } from './knowledge/proposals.mjs';
|
|
11
10
|
import { loadRules } from './guardrails.mjs';
|
|
12
11
|
import { allActions } from './actions/manifest.mjs';
|
|
12
|
+
import { listFacultyItems } from './faculty/items.mjs';
|
|
13
13
|
|
|
14
14
|
const PLACEHOLDER_MARK = '<!-- Fill in:';
|
|
15
15
|
|
|
16
|
-
/** Read instructions
|
|
16
|
+
/** Read the instructions items (steering first, then amendments); classify
|
|
17
|
+
* empty vs steering text. Items are Faculty Standard envelopes (W24). */
|
|
17
18
|
function readInstructions(agentDir) {
|
|
18
|
-
|
|
19
|
-
let raw = '';
|
|
19
|
+
let items = [];
|
|
20
20
|
try {
|
|
21
|
-
|
|
21
|
+
items = listFacultyItems(agentDir, 'instructions').items;
|
|
22
22
|
} catch { /* missing or unreadable → treat as empty */ }
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
// steering pins the top; amendments follow in id order (already sorted)
|
|
24
|
+
items.sort((a, b) => (a.kind === 'steering' ? -1 : 1) - (b.kind === 'steering' ? -1 : 1));
|
|
25
|
+
const bodies = items
|
|
26
|
+
.map((i) => String(i.body ?? ''))
|
|
27
|
+
.map((raw) => (raw.includes(PLACEHOLDER_MARK) ? '' : raw.replace(/^#.*$/gm, '').trim() && raw.trim()))
|
|
28
|
+
.filter(Boolean);
|
|
29
|
+
const text = bodies.join('\n\n');
|
|
30
|
+
return { empty: !text, text };
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
const INTERVIEW = [
|
|
29
34
|
'Project steering is empty. Before substantive work, interview your human',
|
|
30
|
-
'(what is this project, its conventions, its priorities), draft',
|
|
31
|
-
'.zuzuu/instructions/
|
|
35
|
+
'(what is this project, its conventions, its priorities), draft the steering item',
|
|
36
|
+
'.zuzuu/instructions/items/steering.md from their answers, and get their approval.',
|
|
32
37
|
].join(' ');
|
|
33
38
|
|
|
34
39
|
function knowledgeSection(agentDir, limit) {
|
|
@@ -64,10 +69,10 @@ function actionsSection(agentDir, limit) {
|
|
|
64
69
|
|
|
65
70
|
function guardrailsSection(agentDir) {
|
|
66
71
|
try {
|
|
67
|
-
const loaded = loadRules(join(agentDir, 'guardrails'
|
|
68
|
-
return { ok: loaded.ok, count: loaded.ok ? loaded.rules.length : 0 };
|
|
72
|
+
const loaded = loadRules(join(agentDir, 'guardrails'));
|
|
73
|
+
return { ok: loaded.ok, count: loaded.ok ? loaded.rules.length : 0, skipped: loaded.skipped?.length ?? 0 };
|
|
69
74
|
} catch {
|
|
70
|
-
return { ok: false, count: 0 };
|
|
75
|
+
return { ok: false, count: 0, skipped: 0 };
|
|
71
76
|
}
|
|
72
77
|
}
|
|
73
78
|
|