@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.
- package/README.md +5 -5
- package/bin/zuzuu.mjs +6 -3
- package/package.json +1 -1
- package/zuzuu/actions/adapter.mjs +2 -2
- package/zuzuu/actions/inbox.mjs +4 -4
- package/zuzuu/actions/manifest.mjs +3 -3
- package/zuzuu/actions/trail.mjs +1 -1
- package/zuzuu/commands/act-author.mjs +3 -3
- package/zuzuu/commands/act.mjs +58 -13
- package/zuzuu/commands/code.mjs +1 -1
- package/zuzuu/commands/digest.mjs +1 -1
- package/zuzuu/commands/doctor.mjs +3 -3
- package/zuzuu/commands/eval.mjs +44 -19
- package/zuzuu/commands/generation.mjs +40 -5
- package/zuzuu/commands/hook.mjs +6 -6
- package/zuzuu/commands/init.mjs +43 -18
- package/zuzuu/commands/knowledge.mjs +1 -1
- package/zuzuu/commands/migrate.mjs +109 -9
- package/zuzuu/commands/review.mjs +72 -5
- package/zuzuu/commands/status.mjs +3 -3
- package/zuzuu/commands/web.mjs +75 -0
- package/zuzuu/digest.mjs +2 -2
- package/zuzuu/faculty/generation.mjs +2 -2
- package/zuzuu/faculty/proposal.mjs +1 -1
- package/zuzuu/faculty/trail.mjs +2 -2
- package/zuzuu/guardrails/adapter.mjs +1 -1
- package/zuzuu/guardrails.mjs +1 -1
- package/zuzuu/inject.mjs +6 -6
- package/zuzuu/instructions/adapter.mjs +1 -1
- package/zuzuu/knowledge/inbox.mjs +1 -1
- package/zuzuu/knowledge/items.mjs +1 -1
- package/zuzuu/knowledge/proposals.mjs +1 -1
- package/zuzuu/knowledge/registry.mjs +2 -2
- package/zuzuu/live/install.mjs +3 -3
- package/zuzuu/live/live-store.mjs +2 -2
- package/zuzuu/memory/adapter.mjs +1 -1
- package/zuzuu/miners/guardrails.mjs +1 -1
- package/zuzuu/miners/instructions.mjs +1 -1
- package/zuzuu/miners/memory.mjs +3 -3
- package/zuzuu/scaffold.mjs +27 -22
- package/zuzuu/store.mjs +6 -5
package/zuzuu/commands/init.mjs
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
// `zuzuu init` — git-style, context-aware, idempotent scaffold of the faculty home.
|
|
2
2
|
//
|
|
3
|
-
// empty dir
|
|
4
|
-
// non-empty, no
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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
|
-
//
|
|
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, '
|
|
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(
|
|
80
|
-
|
|
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(`
|
|
83
|
-
if (created.length) steer.push(`created ${created.join(' + ')}
|
|
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}
|
|
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
|
|
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
|
|
2
|
+
// `zuzuu migrate` — one-time migrators.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
|
9
|
-
//
|
|
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 =
|
|
287
|
-
|
|
288
|
-
|
|
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 =
|
|
294
|
-
|
|
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 (
|
|
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
|
-
'
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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) {
|
package/zuzuu/faculty/trail.mjs
CHANGED
|
@@ -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
|
|
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 (
|
|
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
|
|
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
|
//
|
package/zuzuu/guardrails.mjs
CHANGED
|
@@ -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:
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
24
|
-
- **Respect
|
|
25
|
-
- Do **not** read
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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`):
|
|
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
|
|
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) => {
|
package/zuzuu/live/install.mjs
CHANGED
|
@@ -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 (
|
|
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(
|
|
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
|
|
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'];
|