@zuzuucodes/cli 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +5 -5
  2. package/bin/zuzuu.mjs +6 -3
  3. package/package.json +1 -1
  4. package/zuzuu/actions/adapter.mjs +2 -2
  5. package/zuzuu/actions/inbox.mjs +4 -4
  6. package/zuzuu/actions/manifest.mjs +3 -3
  7. package/zuzuu/actions/trail.mjs +1 -1
  8. package/zuzuu/commands/act-author.mjs +3 -3
  9. package/zuzuu/commands/act.mjs +58 -13
  10. package/zuzuu/commands/code.mjs +1 -1
  11. package/zuzuu/commands/digest.mjs +1 -1
  12. package/zuzuu/commands/doctor.mjs +3 -3
  13. package/zuzuu/commands/eval.mjs +44 -19
  14. package/zuzuu/commands/generation.mjs +40 -5
  15. package/zuzuu/commands/hook.mjs +6 -6
  16. package/zuzuu/commands/init.mjs +43 -18
  17. package/zuzuu/commands/knowledge.mjs +1 -1
  18. package/zuzuu/commands/migrate.mjs +109 -9
  19. package/zuzuu/commands/review.mjs +72 -5
  20. package/zuzuu/commands/status.mjs +3 -3
  21. package/zuzuu/commands/web.mjs +75 -0
  22. package/zuzuu/digest.mjs +2 -2
  23. package/zuzuu/faculty/generation.mjs +2 -2
  24. package/zuzuu/faculty/proposal.mjs +1 -1
  25. package/zuzuu/faculty/trail.mjs +2 -2
  26. package/zuzuu/guardrails/adapter.mjs +1 -1
  27. package/zuzuu/guardrails.mjs +1 -1
  28. package/zuzuu/inject.mjs +6 -6
  29. package/zuzuu/instructions/adapter.mjs +1 -1
  30. package/zuzuu/knowledge/inbox.mjs +1 -1
  31. package/zuzuu/knowledge/items.mjs +1 -1
  32. package/zuzuu/knowledge/proposals.mjs +1 -1
  33. package/zuzuu/knowledge/registry.mjs +2 -2
  34. package/zuzuu/live/install.mjs +3 -3
  35. package/zuzuu/live/live-store.mjs +2 -2
  36. package/zuzuu/memory/adapter.mjs +1 -1
  37. package/zuzuu/miners/guardrails.mjs +1 -1
  38. package/zuzuu/miners/instructions.mjs +1 -1
  39. package/zuzuu/miners/memory.mjs +3 -3
  40. package/zuzuu/scaffold.mjs +27 -22
  41. package/zuzuu/store.mjs +6 -5
@@ -1,10 +1,14 @@
1
1
  // `zuzuu init` — git-style, context-aware, idempotent scaffold of the faculty home.
2
2
  //
3
- // empty dir → greenfield: full scaffold + create AGENTS.md/CLAUDE.md
4
- // non-empty, no agent/ → brownfield: scaffold + inject block into existing
5
- // instruction files (user content untouched)
6
- // agent/ exists → "Reinitialized": create missing pieces only (no-op
7
- // on a complete home; never overwrites anything)
3
+ // empty dir → greenfield: full scaffold + create AGENTS.md/CLAUDE.md
4
+ // non-empty, no .zuzuu/ → brownfield: scaffold + inject block into existing
5
+ // instruction files (user content untouched)
6
+ // .zuzuu/ exists → "Reinitialized": create missing pieces only (no-op
7
+ // on a complete home; never overwrites anything)
8
+ //
9
+ // Onboarding contract (W1, 2026-06-12): the output NARRATES what appeared and
10
+ // why — the home is hidden (.zuzuu/, like .git), so the init message is the
11
+ // user's first and main tour of it.
8
12
 
9
13
  import { join, basename } from 'node:path';
10
14
  import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
@@ -12,6 +16,7 @@ import { applyScaffold, ensureGitignore, homeExists } from '../scaffold.mjs';
12
16
  import { injectBlock, facultiesBlock, hasBlock, BLOCK_VERSION } from '../inject.mjs';
13
17
  import { detected } from '../../experiments/experiment-1-trace-capture/adapters/registry.mjs';
14
18
  import { repoRoot } from '../store.mjs';
19
+ import { migrateHome } from './migrate.mjs';
15
20
 
16
21
  const HOST_FILES = ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md'];
17
22
  // dotfiles/dirs that don't make a directory "a project" for emptiness purposes
@@ -51,11 +56,30 @@ function serveInstructions(cwd, { greenfield }) {
51
56
  return { injected, created };
52
57
  }
53
58
 
59
+ /** The friendly tour of what `init` just created (the home is hidden — narrate it). */
60
+ function narrateHome() {
61
+ console.log('');
62
+ console.log(' .zuzuu/ your agent\'s home — hidden like .git, yours to read & version');
63
+ console.log(' knowledge/ memory/ actions/ instructions/ guardrails/');
64
+ console.log(' the five faculties: what\'s TRUE · what HAPPENED · how to DO · who to BE · what NOT to do');
65
+ console.log(' README.md explains the whole model — start there');
66
+ console.log('');
67
+ }
68
+
54
69
  export function init(args = {}) {
55
70
  // Root at the git toplevel when inside a repo (same base the store uses for
56
- // agent/), falling back to cwd — one project, one home, like .git/.
71
+ // .zuzuu/), falling back to cwd — one project, one home, like .git/.
57
72
  const cwd = repoRoot(process.cwd());
58
73
  if (cwd !== process.cwd()) console.log(`(project root: ${cwd})`);
74
+
75
+ // One-shot home migration: a pre-2026-06-12 visible agent/ home (gated on its
76
+ // agent.json) moves to .zuzuu/. Fail-open — init must never die on migration.
77
+ try {
78
+ if (migrateHome(cwd).migrated) {
79
+ console.log('Migrated agent/ → .zuzuu/ (the faculty home is hidden now, like .git; transparency via `zuzuu status` / `digest` / `explain`)');
80
+ }
81
+ } catch { /* fail-open */ }
82
+
59
83
  const reinit = homeExists(cwd);
60
84
  const greenfield = !reinit && isEmptyDir(cwd);
61
85
 
@@ -66,24 +90,25 @@ export function init(args = {}) {
66
90
  const createdCount = plan.dirs.length + plan.files.length + (plan.manifestMissing ? 1 : 0);
67
91
 
68
92
  if (reinit) {
69
- console.log(`Reinitialized existing zuzuu home in ${join(cwd, 'agent')}/`);
93
+ console.log(`Reinitialized existing zuzuu home in ${join(cwd, '.zuzuu')}/`);
70
94
  if (createdCount) console.log(` restored : ${createdCount} missing piece(s)`);
71
95
  if (injected.length) console.log(` injected : faculty block → ${injected.join(', ')}`);
72
96
  if (!createdCount && !injected.length && !ignoreAdded.length) console.log(' (complete — nothing to do)');
73
- } else if (greenfield) {
74
- console.log(`Initialized empty zuzuu home in ${join(cwd, 'agent')}/`);
75
- console.log(` faculties : knowledge/ memory/ actions/ instructions/ guardrails/ (+ agent.json manifest)`);
76
- console.log(` steering : created ${created.join(' + ')} pointing your agent at its faculties`);
77
- console.log(` next : \`zuzuu enable\` for live capture · \`zuzuu digest\` to preview the grounding your agent opens with · start your agent in ${basename(cwd)}/`);
78
97
  } else {
79
- console.log(`Initialized zuzuu home in existing project ${join(cwd, 'agent')}/`);
80
- console.log(` faculties : knowledge/ memory/ actions/ instructions/ guardrails/ (+ agent.json manifest)`);
98
+ console.log(greenfield
99
+ ? `Initialized empty zuzuu home in ${join(cwd, '.zuzuu')}/`
100
+ : `Initialized zuzuu home in existing project ${basename(cwd)}/`);
101
+ narrateHome();
102
+ // the only visible footprint — name it so nothing feels like it appeared unannounced
103
+ const names = [...injected.map((f) => f.replace(/ \(.*\)$/, '')), ...created];
104
+ console.log(` visible : only a managed zuzuu block in ${names.join(' + ') || 'your instruction files'}${ignoreAdded.length ? ` and ${ignoreAdded.length} .gitignore line(s)` : ''} — everything else lives inside .zuzuu/`);
81
105
  const steer = [];
82
- if (injected.length) steer.push(`injected → ${injected.join(', ')}`);
83
- if (created.length) steer.push(`created ${created.join(' + ')} (read by Codex/OpenCode/pi)`);
84
- if (steer.length) console.log(` steering : ${steer.join(' · ')}`);
106
+ if (injected.length) steer.push(`faculty block → ${injected.join(', ')}`);
107
+ if (created.length) steer.push(`created ${created.join(' + ')}`);
108
+ if (steer.length) console.log(` steering : ${steer.join(' · ')} (read by your agent at session start)`);
85
109
  const hosts = detected().map((a) => a.name).join(', ');
86
- if (hosts) console.log(` hosts : detected ${hosts} — \`zuzuu capture\` works now; \`zuzuu enable\` for live`);
110
+ if (hosts) console.log(` hosts : detected ${hosts}`);
111
+ console.log(' next : `zuzuu enable` (live capture + guardrails gate) → `zuzuu digest` (preview the grounding) → work normally → `zuzuu inbox` / `zuzuu review` when proposals appear');
87
112
  }
88
113
  if (ignoreAdded.length) console.log(` gitignore : +${ignoreAdded.join(' ')}`);
89
114
  }
@@ -50,7 +50,7 @@ export function remember(args) {
50
50
  const unknown = [...v.unknownKeys.attributes, ...v.unknownKeys.relations];
51
51
  if (!v.ok || unknown.length) {
52
52
  for (const e of v.errors) console.error(` ✗ ${e}`);
53
- for (const k of unknown) console.error(` ✗ unregistered key: ${k} (register it in agent/knowledge/registry/ first)`);
53
+ for (const k of unknown) console.error(` ✗ unregistered key: ${k} (register it in .zuzuu/knowledge/registry/ first)`);
54
54
  process.exit(1);
55
55
  }
56
56
  const path = writeItem(agentDir, item);
@@ -1,17 +1,19 @@
1
1
  // zuzuu/commands/migrate.mjs
2
- // `zuzuu migrate` — one-time proposal schema migrator (WS2-T5).
2
+ // `zuzuu migrate` — one-time migrators.
3
3
  //
4
- // Tidies on-disk legacy Knowledge proposals from the old {candidate, er} shape
5
- // to the unified spine shape {payload, analysis, faculty}. The spine already
6
- // dual-reads both formats; this migrator exists so on-disk records are clean.
4
+ // (default) proposal schema: legacy {candidate, er} spine {payload, analysis, faculty} (WS2-T5)
5
+ // --home faculty home: visible agent/ hidden .zuzuu/ (W1, 2026-06-12)
7
6
  //
8
- // Pure core: migrateProposals(agentDir) → { scanned, migrated, skipped }
9
- // CLI surface: migrate(args) resolves agentDir, runs core, prints summary.
7
+ // Pure cores: migrateProposals(agentDir) → { scanned, migrated, skipped }
8
+ // migrateHome(root) { migrated }
9
+ // CLI surface: migrate(args) — resolves paths, runs the core, prints summary.
10
10
 
11
- import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
11
+ import { existsSync, readdirSync, readFileSync, writeFileSync, renameSync, rmSync } from 'node:fs';
12
12
  import { join } from 'node:path';
13
- import { paths } from '../store.mjs';
13
+ import { paths, repoRoot } from '../store.mjs';
14
14
  import { proposalsDir, archiveDir } from '../faculty/contract.mjs';
15
+ import { ensureGitignore } from '../scaffold.mjs';
16
+ import { injectBlock, BLOCK_VERSION } from '../inject.mjs';
15
17
 
16
18
  // ---------------------------------------------------------------------------
17
19
  // pure core — testable without process.*
@@ -109,11 +111,109 @@ export function migrateProposals(agentDir) {
109
111
  };
110
112
  }
111
113
 
114
+ // ---------------------------------------------------------------------------
115
+ // home migration — agent/ → .zuzuu/ (W1, 2026-06-12)
116
+ // ---------------------------------------------------------------------------
117
+
118
+ // The denies the old visible-agent/ home installed; scrubbed here (NOT kept in
119
+ // install.mjs — clean break) and replaced by the current narrow .zuzuu/ pair.
120
+ const LEGACY_DENY_RULES = ['Read(./agent/.traces/**)', 'Read(./agent/.live/**)'];
121
+ const NEW_DENY_RULES = ['Read(./.zuzuu/.traces/**)', 'Read(./.zuzuu/.live/**)'];
122
+
123
+ /**
124
+ * One-shot HOME migration: visible `agent/` → hidden `.zuzuu/` (byte-identical
125
+ * inner layout). Gated on `agent/agent.json` — `agent/` is a common dir name,
126
+ * so an unrelated agent/ dir in a brownfield repo must NEVER be touched (the
127
+ * one place this differs from the old `.mns→agent` precedent). Idempotent +
128
+ * fail-soft; NEVER clobbers an existing .zuzuu/. Pure FS move (renameSync).
129
+ * @returns {{migrated: boolean}}
130
+ */
131
+ export function migrateHome(root = repoRoot()) {
132
+ const legacy = join(root, 'agent');
133
+ const home = join(root, '.zuzuu');
134
+ if (existsSync(home) || !existsSync(join(legacy, 'agent.json'))) return { migrated: false };
135
+
136
+ renameSync(legacy, home); // move the whole home (atomic on same filesystem)
137
+
138
+ rewriteTraceRefs(home);
139
+ rewriteGitignore(root);
140
+ scrubLegacyDenies(root);
141
+ // derived index: drop, it rebuilds on the next recall/reindex
142
+ try { rmSync(join(home, 'knowledge', '.index.db'), { force: true }); } catch { /* fail-soft */ }
143
+ return { migrated: true };
144
+ }
145
+
146
+ /** sessions.json stores repo-relative traceRefs (`agent/.traces/…`) — re-point them. */
147
+ function rewriteTraceRefs(home) {
148
+ const index = join(home, 'sessions.json');
149
+ if (!existsSync(index)) return;
150
+ try {
151
+ const idx = JSON.parse(readFileSync(index, 'utf8'));
152
+ for (const s of idx.sessions || []) {
153
+ if (typeof s.traceRef === 'string' && s.traceRef.startsWith('agent/')) {
154
+ s.traceRef = '.zuzuu/' + s.traceRef.slice('agent/'.length);
155
+ }
156
+ }
157
+ writeFileSync(index, JSON.stringify(idx, null, 2) + '\n');
158
+ } catch { /* fail-soft: a bad index never blocks the move */ }
159
+ }
160
+
161
+ /** Drop legacy `agent/` ignore lines, then append the canonical .zuzuu/ ones. */
162
+ function rewriteGitignore(root) {
163
+ const path = join(root, '.gitignore');
164
+ if (existsSync(path)) {
165
+ const kept = readFileSync(path, 'utf8')
166
+ .split('\n')
167
+ .filter((l) => !l.trim().startsWith('agent/'))
168
+ .join('\n');
169
+ writeFileSync(path, kept.endsWith('\n') || kept === '' ? kept : kept + '\n');
170
+ }
171
+ ensureGitignore(root); // appends .zuzuu/.traces/, .zuzuu/.live/, .zuzuu/knowledge/.index.db
172
+ }
173
+
174
+ /** Swap the old agent/ deny rules for the .zuzuu/ pair in any .claude settings file. */
175
+ function scrubLegacyDenies(root) {
176
+ for (const f of ['settings.json', 'settings.local.json']) {
177
+ const path = join(root, '.claude', f);
178
+ if (!existsSync(path)) continue;
179
+ try {
180
+ const s = JSON.parse(readFileSync(path, 'utf8'));
181
+ const deny = s?.permissions?.deny;
182
+ if (!Array.isArray(deny)) continue;
183
+ const hadOurs = deny.some((r) => LEGACY_DENY_RULES.includes(r));
184
+ if (!hadOurs) continue;
185
+ s.permissions.deny = deny.filter((r) => !LEGACY_DENY_RULES.includes(r));
186
+ for (const rule of NEW_DENY_RULES) if (!s.permissions.deny.includes(rule)) s.permissions.deny.push(rule);
187
+ writeFileSync(path, JSON.stringify(s, null, 2) + '\n');
188
+ } catch { /* fail-soft: never break settings we can't parse */ }
189
+ }
190
+ }
191
+
192
+ /** Re-inject the current faculties block into any existing host instruction files. */
193
+ function reinjectHostBlocks(root) {
194
+ for (const f of ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md']) {
195
+ const p = join(root, f);
196
+ if (existsSync(p)) {
197
+ const text = readFileSync(p, 'utf8');
198
+ if (!text.includes(`zuzuu:faculties:v${BLOCK_VERSION}`)) writeFileSync(p, injectBlock(text));
199
+ }
200
+ }
201
+ }
202
+
112
203
  // ---------------------------------------------------------------------------
113
204
  // CLI surface
114
205
  // ---------------------------------------------------------------------------
115
206
 
116
- export function migrate() {
207
+ export function migrate(args = {}) {
208
+ if (args.home) {
209
+ const root = repoRoot(process.cwd());
210
+ const { migrated } = migrateHome(root);
211
+ if (!migrated) { console.log('migrate --home: nothing to do (already .zuzuu/, or no zuzuu home at agent/)'); return; }
212
+ try { reinjectHostBlocks(root); } catch { /* fail-open */ }
213
+ console.log(`migrate --home: agent/ → .zuzuu/ (hidden, like .git; block v${BLOCK_VERSION}, gitignore + deny rules rewritten)`);
214
+ console.log(' transparency lives in porcelain now: zuzuu status · explain · digest');
215
+ return;
216
+ }
117
217
  const agentDir = paths().dir;
118
218
  const { scanned, migrated, skipped } = migrateProposals(agentDir);
119
219
  console.log(`migrate: scanned ${scanned} proposal(s) — migrated ${migrated}, skipped ${skipped}`);
@@ -246,12 +246,70 @@ function facultyOf(agentDir, id, only) {
246
246
  return 'knowledge';
247
247
  }
248
248
 
249
+ /**
250
+ * Pure: the structured object for `proposals approve --json`.
251
+ * Calls gate.approve and returns the result object the branch prints.
252
+ * @param {string} agentDir
253
+ * @param {string} id
254
+ * @param {string} faculty
255
+ * @returns {object} the gate result (contains ok, action, etc.)
256
+ */
257
+ export function approveData(agentDir, id, faculty) {
258
+ return gate.approve(agentDir, faculty, id);
259
+ }
260
+
261
+ /**
262
+ * Pure: the structured object for `proposals reject --json`.
263
+ * Calls gate.reject and returns the result object the branch prints.
264
+ * @param {string} agentDir
265
+ * @param {string} id
266
+ * @param {string} faculty
267
+ * @param {string} [reason]
268
+ * @returns {object} { ok, id, ... }
269
+ */
270
+ export function rejectData(agentDir, id, faculty, reason = '') {
271
+ const r = gate.reject(agentDir, faculty, id, reason);
272
+ return { ...r, id };
273
+ }
274
+
275
+ /**
276
+ * Pure: list pending proposals as structured data — the zuzuu-web /proposals source.
277
+ * @param {string} agentDir
278
+ * @param {string} [only] optional faculty filter
279
+ * @returns {{ pending: Array<{id, faculty, title}> }}
280
+ */
281
+ export function proposalsListData(agentDir, only) {
282
+ const groups = pendingByFaculty(agentDir).filter((g) => !only || g.adapter.name === only);
283
+ const pending = [];
284
+ for (const { adapter, proposals } of groups) {
285
+ for (const p of proposals) {
286
+ // derive a human title the same way the table does
287
+ let title;
288
+ if (adapter.name === 'knowledge') {
289
+ title = p.kind === 'registry'
290
+ ? `register ${p.registry?.slice(0, -1) ?? ''} '${p.key ?? ''}'`
291
+ : (p.candidate?.body ?? p.payload?.body ?? p.id)?.slice(0, 80);
292
+ } else {
293
+ title = p.title ?? adapter.render(p).line;
294
+ }
295
+ pending.push({ id: p.id, faculty: adapter.name, title: title ?? p.id });
296
+ }
297
+ }
298
+ return { pending };
299
+ }
300
+
249
301
  /** Non-interactive: zuzuu proposals list|show <id>|approve <id>|reject <id> [--reason r] [--faculty f] */
250
302
  export function proposals(args) {
251
303
  const agentDir = paths().dir;
252
304
  const sub = args._[0] || 'list';
253
305
  const only = args.faculty; // optional filter; default = all
254
306
  if (sub === 'list') {
307
+ if (args.json) {
308
+ processInbox(agentDir); // promote plain-text inbox candidates, same as text path
309
+ const d = proposalsListData(agentDir, only);
310
+ console.log(JSON.stringify(d));
311
+ return;
312
+ }
255
313
  const inbox = processInbox(agentDir);
256
314
  if (inbox.processed) console.log(`(processed ${inbox.processed} inbox candidate(s))`);
257
315
  const groups = pendingByFaculty(agentDir).filter((g) => !only || g.adapter.name === only);
@@ -278,20 +336,29 @@ export function proposals(args) {
278
336
  const a = registry.get(faculty);
279
337
  const p = (a && typeof a.getProposal === 'function') ? a.getProposal(agentDir, id) : getProposal(agentDir, id);
280
338
  if (!p) return console.error('not found');
339
+ // show always prints JSON (both with and without --json flag)
281
340
  console.log(JSON.stringify(p, null, 2));
282
341
  return;
283
342
  }
284
343
  if (sub === 'approve') {
285
344
  const faculty = facultyOf(agentDir, id, only);
286
- const r = gate.approve(agentDir, faculty, id);
287
- console.log(r.ok ? `✓ ${r.action}` : `✗ ${(r.errors ?? [r.action]).join('; ')}`);
288
- for (const w of r.warnings ?? []) console.log(`⚠ ${w}`);
345
+ const r = approveData(agentDir, id, faculty);
346
+ if (args.json) {
347
+ console.log(JSON.stringify(r));
348
+ } else {
349
+ console.log(r.ok ? `✓ ${r.action}` : `✗ ${(r.errors ?? [r.action]).join('; ')}`);
350
+ for (const w of r.warnings ?? []) console.log(`⚠ ${w}`);
351
+ }
289
352
  process.exit(r.ok ? 0 : 1);
290
353
  }
291
354
  if (sub === 'reject') {
292
355
  const faculty = facultyOf(agentDir, id, only);
293
- const r = gate.reject(agentDir, faculty, id, args.reason || '');
294
- console.log(r.ok ? '✓ rejected' : '✗ not found');
356
+ const r = rejectData(agentDir, id, faculty, args.reason || '');
357
+ if (args.json) {
358
+ console.log(JSON.stringify(r));
359
+ } else {
360
+ console.log(r.ok ? '✓ rejected' : '✗ not found');
361
+ }
295
362
  process.exit(r.ok ? 0 : 1);
296
363
  }
297
364
  console.error('usage: zuzuu proposals list|show <id>|approve <id>|reject <id> [--reason r] [--faculty f]');
@@ -11,7 +11,7 @@ import { detectDrift } from './doctor.mjs';
11
11
  const fmtDur = (ms) => (ms < 60_000 ? `${(ms / 1000).toFixed(0)}s` : `${(ms / 60_000).toFixed(1)}m`);
12
12
 
13
13
  /** Pure: structured status for a faculty home (the zuzuu-web /status source). Fail-soft per field. */
14
- export function statusData(agentDir) {
14
+ export function statusData(agentDir, { hosts = detected().map((a) => ({ name: a.name })) } = {}) {
15
15
  let active = null, drift = { dirty: false, items: [] };
16
16
  const pending = {};
17
17
  try { active = activeGenerationFn(agentDir); } catch { active = null; }
@@ -23,7 +23,7 @@ export function statusData(agentDir) {
23
23
  const items = Array.isArray(d?.drifted) ? d.drifted : [];
24
24
  drift = { dirty: items.length > 0, items };
25
25
  } catch { /* fail-soft */ }
26
- return { home: existsSync(agentDir), activeGeneration: active, pending, drift };
26
+ return { home: existsSync(agentDir), activeGeneration: active, pending, drift, hosts };
27
27
  }
28
28
 
29
29
  /**
@@ -52,7 +52,7 @@ export function facultiesLine(agentDir) {
52
52
  export function status(args = {}) {
53
53
  if (args.json) { console.log(JSON.stringify(statusData(paths().dir))); return; }
54
54
  const { sessions } = readIndex();
55
- console.log(`this project — recorded sessions (agent/sessions.json): ${sessions.length}`);
55
+ console.log(`this project — recorded sessions (.zuzuu/sessions.json): ${sessions.length}`);
56
56
  if (!sessions.length) {
57
57
  console.log(' none yet — run `zuzuu capture`, or just start your agent (live capture)');
58
58
  } else {
@@ -0,0 +1,75 @@
1
+ // `zuzuu web` — launch the visual workbench (@zuzuucodes/web) as a runtime peer.
2
+ // The workbench opens a browser-based UI and prints its own URL; zuzuu just starts it.
3
+ //
4
+ // Deliberate difference from code.mjs: NO init/enable here. The workbench home
5
+ // owns its own onboarding — this command's only job is: detect, install-on-demand,
6
+ // and launch. Keeping it simple and side-effect-free avoids touching the faculty
7
+ // home in contexts where the workbench UI is the entry point.
8
+ //
9
+ // Zero-dep: @zuzuucodes/web ships the `zuzuu-web` binary as a runtime PEER —
10
+ // detected, and installed on demand if missing — never an npm dependency.
11
+
12
+ import { readSync } from 'node:fs';
13
+ import { resolve } from 'node:path';
14
+ import { spawnSync, spawn } from 'node:child_process';
15
+
16
+ // --- default (real) deps; tests inject fakes for everything external ---
17
+ const realDetect = () => {
18
+ try { return spawnSync('zuzuu-web', ['--version'], { stdio: 'ignore' }).status === 0; }
19
+ catch { return false; }
20
+ };
21
+ const realInstall = () => {
22
+ try { return spawnSync('npm', ['install', '-g', '@zuzuucodes/web'], { stdio: 'inherit' }).status === 0; }
23
+ catch { return false; }
24
+ };
25
+ const realLaunch = ({ cwd }) => {
26
+ spawn('zuzuu-web', [cwd], { detached: true, stdio: 'ignore' }).unref();
27
+ };
28
+ // Synchronous y/n. Only reached when zuzuu-web is missing; the deps seam means
29
+ // tests never call this. Default to 'n' (safe — 'n' is the listed default [y/N]).
30
+ function realPrompt(q) {
31
+ process.stdout.write(`${q} `);
32
+ try {
33
+ const b = Buffer.alloc(8);
34
+ const n = readSync(0, b, 0, 8, null);
35
+ return b.toString('utf8', 0, n).trim().toLowerCase().startsWith('y') ? 'y' : 'n';
36
+ } catch { return 'n'; }
37
+ }
38
+
39
+ /**
40
+ * `zuzuu web [dir]`
41
+ * Launch the visual workbench for the given directory (default: cwd).
42
+ * Installs @zuzuucodes/web on demand if absent.
43
+ */
44
+ export function web(args = {}, deps = {}) {
45
+ const d = {
46
+ detect: realDetect,
47
+ install: realInstall,
48
+ launch: realLaunch,
49
+ prompt: realPrompt,
50
+ log: (...m) => console.log(...m),
51
+ ...deps,
52
+ };
53
+
54
+ // 1. resolve the target directory
55
+ const dir = args._?.[0] ? resolve(String(args._[0])) : process.cwd();
56
+
57
+ // 2. ensure zuzuu-web (detect + install-on-demand)
58
+ if (!d.detect()) {
59
+ d.log("zuzuu-web isn't installed (@zuzuucodes/web).");
60
+ const answer = d.prompt('install @zuzuucodes/web globally? [y/N]');
61
+ if (answer !== 'y') {
62
+ d.log('aborted — install with: npm i -g @zuzuucodes/web');
63
+ return;
64
+ }
65
+ if (!d.install()) {
66
+ d.log('install failed — try: npm i -g @zuzuucodes/web');
67
+ return;
68
+ }
69
+ }
70
+
71
+ // 3. launch — zuzuu-web opens the browser and prints its URL
72
+ d.log(`zuzuu web → launching visual workbench in ${dir} …`);
73
+ d.log(' zuzuu-web will open your browser and print its URL.');
74
+ d.launch({ cwd: dir });
75
+ }
package/zuzuu/digest.mjs CHANGED
@@ -28,7 +28,7 @@ function readInstructions(agentDir) {
28
28
  const INTERVIEW = [
29
29
  'Project steering is empty. Before substantive work, interview your human',
30
30
  '(what is this project, its conventions, its priorities), draft',
31
- 'agent/instructions/project.md from their answers, and get their approval.',
31
+ '.zuzuu/instructions/project.md from their answers, and get their approval.',
32
32
  ].join(' ');
33
33
 
34
34
  function knowledgeSection(agentDir, limit) {
@@ -73,7 +73,7 @@ function guardrailsSection(agentDir) {
73
73
 
74
74
  /**
75
75
  * Compute the digest for a faculty home.
76
- * @param {string} agentDir path to the agent/ directory
76
+ * @param {string} agentDir path to the .zuzuu/ directory
77
77
  * @param {{ knowledgeLimit?: number, budget?: number }} options
78
78
  * @returns {{ text: string, sections: object }}
79
79
  */
@@ -7,7 +7,7 @@
7
7
  // for items that were never committed). Identity: Agent → Generation → Run —
8
8
  // rollback = flip the active pointer + restore content; never `git revert`.
9
9
  //
10
- // Layout under agent/:
10
+ // Layout under .zuzuu/:
11
11
  // generations/active {active: "gen_NNN"} — the live pointer
12
12
  // generations/<id>.json the lockfile (content-addressed manifest)
13
13
  // generations/snapshots/<id>/<faculty>/... pinned item bytes (rollback source)
@@ -126,7 +126,7 @@ export function snapshotFaculties(agentDir) {
126
126
 
127
127
  /** Stable agent id derived from the repo root: agt_<first12 of sha256(root)>. */
128
128
  export function agentId(agentDir) {
129
- // agentDir is the agent/ dir; the repo root is its parent.
129
+ // agentDir is the .zuzuu/ dir; the repo root is its parent.
130
130
  const root = dirname(agentDir);
131
131
  return 'agt_' + sha256(root).slice(0, 12);
132
132
  }
@@ -71,7 +71,7 @@ export function makeProposal({ faculty, kind, source, payload, analysis = {}, ev
71
71
  }
72
72
 
73
73
  /**
74
- * Write a proposal record to `agent/<faculty>/proposals/<id>.json`.
74
+ * Write a proposal record to `.zuzuu/<faculty>/proposals/<id>.json`.
75
75
  * Creates directories as needed. Returns the written path.
76
76
  */
77
77
  export function writeProposal(agentDir, proposal) {
@@ -1,7 +1,7 @@
1
1
  // zuzuu/faculty/trail.mjs
2
2
  // Generalised faculty observability trail (WS2-T1).
3
3
  // Extends the pattern from zuzuu/actions/trail.mjs to any faculty:
4
- // each faculty gets its own agent/.live/<faculty>.jsonl file.
4
+ // each faculty gets its own .zuzuu/.live/<faculty>.jsonl file.
5
5
  //
6
6
  // Fail-soft: a logging failure must never affect the caller.
7
7
 
@@ -11,7 +11,7 @@ import { liveDir } from '../store.mjs';
11
11
 
12
12
  /**
13
13
  * Append a trail entry for a faculty. Never throws.
14
- * @param {string} agentDir - path to the faculty home (agent/)
14
+ * @param {string} agentDir - path to the faculty home (.zuzuu/)
15
15
  * @param {string} faculty - e.g. 'knowledge', 'actions', 'guardrails'
16
16
  * @param {object} entry - arbitrary fields; `at` is stamped automatically
17
17
  */
@@ -7,7 +7,7 @@
7
7
  // A guardrails proposal payload is a single rule record:
8
8
  // { id, action: deny|ask|allow, tool, pattern, reason }
9
9
  //
10
- // apply: loads agent/guardrails/rules.json (seeding {version:1,rules:[]} if
10
+ // apply: loads .zuzuu/guardrails/rules.json (seeding {version:1,rules:[]} if
11
11
  // absent), appends the rule or replaces an existing one with the same id,
12
12
  // then writes the file back.
13
13
  //
@@ -1,6 +1,6 @@
1
1
  // The Guardrails faculty — v1 rule engine (pure; I/O lives in the hook command).
2
2
  //
3
- // Rules are DATA, not code: agent/guardrails/rules.json, ordered, declarative —
3
+ // Rules are DATA, not code: .zuzuu/guardrails/rules.json, ordered, declarative —
4
4
  // a *definition* in the pin-definitions sense (versioned in git, graduates via
5
5
  // proposals like every faculty's contents).
6
6
  //
package/zuzuu/inject.mjs CHANGED
@@ -9,20 +9,20 @@ const END = '<!-- <<< zuzuu:faculties <<< -->';
9
9
  // the current `zuzuu:faculties` block — never duplicated.
10
10
  const BLOCK_RE = /[ \t]*<!-- >>> zuzuu:faculties:v\d+ >>> -->[\s\S]*?<!-- <<< zuzuu:faculties <<< -->[ \t]*\n?/;
11
11
 
12
- export const BLOCK_VERSION = 8;
12
+ export const BLOCK_VERSION = 9;
13
13
 
14
14
  /** The block content served to host agents. Keep short — it's steering, not docs. */
15
15
  export function facultiesBlock(version = BLOCK_VERSION) {
16
16
  return `${BEGIN(version)}
17
17
  ## zuzuu — agent faculty home
18
18
 
19
- This project has a zuzuu faculty home at \`agent/\` (managed by the zuzuu CLI). Work to this contract:
19
+ This project has a zuzuu faculty home at \`.zuzuu/\` (managed by the zuzuu CLI). Work to this contract:
20
20
 
21
- - **Ground.** At session start, read \`agent/.live/digest.md\` if it exists — your *zuzuu digest* (instructions, knowledge, actions, proposals, guardrails), regenerated each session. Trust it as ground truth; don't re-derive what it states or re-read faculty files it already summarized. (On Claude Code the same brief also arrives inline at session start.)
21
+ - **Ground.** At session start, read \`.zuzuu/.live/digest.md\` if it exists — your *zuzuu digest* (instructions, knowledge, actions, proposals, guardrails), regenerated each session. Trust it as ground truth; don't re-derive what it states or re-read faculty files it already summarized. (On Claude Code the same brief also arrives inline at session start.)
22
22
  - **Cite in-flight.** When an answer draws on a stored fact, say \`from knowledge: <id>\`; when you follow a runbook/action, name it. Make the faculty visible.
23
- - **Harvest at close.** Before ending, propose durable learnings as one-fact files in \`agent/knowledge/inbox/\` (plain text is fine), and propose any reusable procedure with \`zuzuu act propose <slug>\` (it lands in \`actions/inbox/\`). A human reviews both via \`zuzuu review\`. Never write \`knowledge/items/\` or active \`actions/\` directly.
24
- - **Respect \`agent/guardrails/\`** — hard rules, *enforced* on tool calls by the zuzuu gate; a refusal there is policy, not preference.
25
- - Do **not** read \`agent/.traces/\` or \`agent/.live/\` (zuzuu observability internals) — **except \`agent/.live/digest.md\`, which is written for you.**
23
+ - **Harvest at close.** Before ending, propose durable learnings as one-fact files in \`.zuzuu/knowledge/inbox/\` (plain text is fine), and propose any reusable procedure with \`zuzuu act propose <slug>\` (it lands in \`actions/inbox/\`). A human reviews both via \`zuzuu review\`. Never write \`knowledge/items/\` or active \`actions/\` directly.
24
+ - **Respect \`.zuzuu/guardrails/\`** — hard rules, *enforced* on tool calls by the zuzuu gate; a refusal there is policy, not preference.
25
+ - Do **not** read \`.zuzuu/.traces/\` or \`.zuzuu/.live/\` (zuzuu observability internals) — **except \`.zuzuu/.live/digest.md\`, which is written for you.**
26
26
  ${END}`;
27
27
  }
28
28
 
@@ -6,7 +6,7 @@
6
6
  // An instructions proposal payload is a steering amendment:
7
7
  // { text } — a line or paragraph to append to project.md
8
8
  //
9
- // apply: appends the text as a line to agent/instructions/project.md (creates
9
+ // apply: appends the text as a line to .zuzuu/instructions/project.md (creates
10
10
  // the file if absent; never duplicates an already-present line).
11
11
  //
12
12
  // Registers itself on import.
@@ -1,5 +1,5 @@
1
1
  // The inbox — where candidates arrive. Agents (per the faculty block) drop one
2
- // fact per file into agent/knowledge/inbox/; `zuzuu distill` drops mined candidates
2
+ // fact per file into .zuzuu/knowledge/inbox/; `zuzuu distill` drops mined candidates
3
3
  // the same way. Processing wraps each into an ER'd proposal (the file's full
4
4
  // content is preserved inside the proposal JSON) and removes the inbox file.
5
5
  //
@@ -1,5 +1,5 @@
1
1
  // Knowledge items — files as truth. One item per markdown file under
2
- // agent/knowledge/items/<id>.md: a constrained-YAML frontmatter (we control both
2
+ // .zuzuu/knowledge/items/<id>.md: a constrained-YAML frontmatter (we control both
3
3
  // writer and reader; grammar below) + a prose body (the fact in your voice).
4
4
  //
5
5
  // ---
@@ -1,6 +1,6 @@
1
1
  // Proposals — the human gate, as files. The first build of DESIGN's Proposal
2
2
  // entity: { candidate, source, evidence, er-verdict, status }. Pending under
3
- // agent/knowledge/proposals/<id>.json; resolved ones move to proposals/archive/
3
+ // .zuzuu/knowledge/proposals/<id>.json; resolved ones move to proposals/archive/
4
4
  // (an auditable history — approvals also show up as item-file diffs in git).
5
5
  //
6
6
  // Registry governance rides the same gate: an unregistered attribute/relation
@@ -4,7 +4,7 @@
4
4
  // are never silently auto-registered — repeated use files a registry *proposal*
5
5
  // (human-gated, like everything in this system).
6
6
  //
7
- // Files (tracked, seeded by `zuzuu init`): agent/knowledge/registry/
7
+ // Files (tracked, seeded by `zuzuu init`): .zuzuu/knowledge/registry/
8
8
  // types.json [{name, description}]
9
9
  // attributes.json [{key, value, description}] value: "string"|"number"|"date"|"url"|{"enum":[...]}
10
10
  // relations.json [{name, inverse, description}]
@@ -42,7 +42,7 @@ export function registryDir(agentDir) {
42
42
  return join(agentDir, 'knowledge', 'registry');
43
43
  }
44
44
 
45
- /** Load the registry from agent/knowledge/registry/. Missing files → empty sets. */
45
+ /** Load the registry from .zuzuu/knowledge/registry/. Missing files → empty sets. */
46
46
  export function loadRegistry(agentDir) {
47
47
  const dir = registryDir(agentDir);
48
48
  const read = (f) => {
@@ -7,13 +7,13 @@
7
7
  export const SIGNATURE = 'zuzuu.mjs'; // appears in every zuzuu hook command path, quote-agnostic
8
8
  const tagged = (cmd) => String(cmd).includes(SIGNATURE);
9
9
  // entire-style: agent can't read its own observability output (feedback loop) —
10
- // but ONLY that. The faculty home (agent/knowledge etc., served by `zuzuu init`)
10
+ // but ONLY that. The faculty home (.zuzuu/knowledge etc., served by `zuzuu init`)
11
11
  // must stay readable, so the deny is narrowed to .traces/ + .live/.
12
- const DENY_RULES = ['Read(./agent/.traces/**)', 'Read(./agent/.live/**)'];
12
+ const DENY_RULES = ['Read(./.zuzuu/.traces/**)', 'Read(./.zuzuu/.live/**)'];
13
13
 
14
14
  // Minimal hook set: lifecycle (Design B re-captures the transcript — no
15
15
  // PostToolUse needed) + the PreToolUse Guardrails GATE (the one place we *do*
16
- // sit on the hot path: it evaluates agent/guardrails/rules.json per tool call,
16
+ // sit on the hot path: it evaluates .zuzuu/guardrails/rules.json per tool call,
17
17
  // fails open, and stays silent unless a rule matches).
18
18
  export const LIFECYCLE_EVENTS = ['SessionStart', 'Stop', 'SessionEnd'];
19
19
  export const GATE_EVENTS = ['PreToolUse'];