@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/README.md
CHANGED
|
@@ -6,20 +6,20 @@
|
|
|
6
6
|
|
|
7
7
|
Your host agent — Claude Code, Codex, Gemini CLI, OpenCode — supplies the *brain* (the reasoning loop + the model). zuzuu wraps the host you already pay for: it **serves** faculties to it, **observes** every session as an OpenTelemetry trace, and (the end-game) **evolves** the faculties from those traces — human-gated, across versioned generations. We never run a competing agent loop and never drive the host headlessly.
|
|
8
8
|
|
|
9
|
-
>
|
|
9
|
+
> Install `npm i -g @zuzuucodes/cli` — the command is **`zz`** (or `zuzuu`). Published with provenance; releases auto-publish from `main` via GitHub OIDC.
|
|
10
10
|
|
|
11
11
|
> **Status (honest):** early build, moving fast. **Observe** works (5 real hosts, verified). **Serve** delivers the faculty home (`zuzuu init`), a session digest to every host, an **enforced guardrails gate** on all 5, and five faculties sharing one proposal/review spine. **Evolve** is now **wired and tested** — trace miners → a mechanical eval lens → human-gated `zuzuu review` → versioned **generations** (mint / rollback / drift-check) — but **not yet proven on a real graduation corpus** (the loop runs + passes hermetic tests; it hasn't yet improved an agent from real sessions end-to-end). Full design: [`docs/DESIGN.md`](docs/DESIGN.md).
|
|
12
12
|
|
|
13
13
|
## What works today
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npm install -g @zuzuucodes/cli # zero dependencies — installs the `zuzuu` command
|
|
16
|
+
npm install -g @zuzuucodes/cli # zero dependencies — installs the `zuzuu` command (short alias: `zz`)
|
|
17
17
|
|
|
18
18
|
# no coding agent yet? one command gives you a fully faculty-equipped one:
|
|
19
19
|
zuzuu code # scaffold the faculty home, install + wire OpenCode, launch it (capture + gate + grounding)
|
|
20
20
|
|
|
21
21
|
# already run Claude Code / Gemini / Codex / OpenCode / pi? wrap the one you have:
|
|
22
|
-
zuzuu init # scaffold your project's agent home (
|
|
22
|
+
zuzuu init # scaffold your project's agent home (.zuzuu/) — git-style, hidden like .git
|
|
23
23
|
zuzuu explain # the 5 faculties + how graduation works (you're always in the loop)
|
|
24
24
|
zuzuu inbox # what's pending your approval · zuzuu review to approve/reject
|
|
25
25
|
zuzuu capture # turn your latest agent session into an OpenTelemetry trace
|
|
@@ -42,11 +42,11 @@ All five verified against **real sessions** — never fixtures; every host's liv
|
|
|
42
42
|
|
|
43
43
|
**Prerequisites:** Node ≥ 22 — that's it. You need at least one supported agent you've already used, so a session exists to capture. (Hacking on zuzuu itself? `git clone https://github.com/h1902y/zuzuu && cd zuzuu && npm link`.)
|
|
44
44
|
|
|
45
|
-
**`zuzuu init`** behaves like `git init`: empty dir → scaffolds the agent home + `AGENTS.md`/`CLAUDE.md`; existing project → adds
|
|
45
|
+
**`zuzuu init`** behaves like `git init`: empty dir → scaffolds the agent home + `AGENTS.md`/`CLAUDE.md`; existing project → adds the home and injects a small delimiter-marked block into your existing instruction files (your text is never touched); already initialized → restores missing pieces only. The home is **hidden like `.git` and self-explaining** — a `.zuzuu/` dir you can read and version in git (the only visible footprint is the managed block + three `.gitignore` lines; transparency lives in `zuzuu status` / `explain` / `digest`): `.zuzuu/README.md` (the explainer) · `knowledge/` (verified facts) · `memory/` (curated episodes) · `actions/` (runbooks) · `instructions/` (steering) · `guardrails/` (enforced rules), plus `generations/` (your checkpoints). Machine internals are dot-prefixed + git-ignored (`.zuzuu/.traces/`, `.zuzuu/.live/`). *(Pre-2026-06-12 homes at `agent/` migrate automatically on `zuzuu init`, or via `zuzuu migrate --home`.)*
|
|
46
46
|
|
|
47
47
|
**Live capture** (`zuzuu enable`) is invisible by design: a minimal lifecycle hook set (Claude Code, Gemini CLI, Codex), a bus plugin (OpenCode), or an extension (pi) — each wrapped so it **always exits 0 / fails open — it can never break your agent**. The same hook carries the guardrails gate, applied in each host's own idiom (Claude/Codex `hookSpecificOutput`, Gemini `{decision:"deny"}`, OpenCode throws from `tool.execute.before`, pi returns `{block:true}` from `tool_call`). Most hosts emit no clean end-signal when a terminal is killed, so `zuzuu doctor` *reconciles* lost sessions afterward from the transcript still on disk (nothing lost).
|
|
48
48
|
|
|
49
|
-
**Where your data lives:** transcripts are read **read-only**; output is git-native in your repo —
|
|
49
|
+
**Where your data lives:** transcripts are read **read-only**; output is git-native in your repo — `.zuzuu/sessions.json` (small tracked index, each session linked to a commit) + `.zuzuu/.traces/*.otlp.jsonl` (local, git-ignored). **Nothing is uploaded**; no raw tool input/output on the trace (byte sizes only).
|
|
50
50
|
|
|
51
51
|
**Verify / troubleshoot:** `npm test` (hermetic) · `npm run playground` (⏭️ skip = that host isn't on *your* machine, not a failure) · `zuzuu doctor` (env + session health). "No host detected" → use a supported agent once in the repo, then retry.
|
|
52
52
|
|
package/bin/zuzuu.mjs
CHANGED
|
@@ -29,6 +29,7 @@ import { migrate } from '../zuzuu/commands/migrate.mjs';
|
|
|
29
29
|
import { generation } from '../zuzuu/commands/generation.mjs';
|
|
30
30
|
import { evalCmd } from '../zuzuu/commands/eval.mjs';
|
|
31
31
|
import { code } from '../zuzuu/commands/code.mjs';
|
|
32
|
+
import { web } from '../zuzuu/commands/web.mjs';
|
|
32
33
|
import { explain } from '../zuzuu/commands/explain.mjs';
|
|
33
34
|
import { inbox } from '../zuzuu/commands/inbox.mjs';
|
|
34
35
|
|
|
@@ -59,9 +60,10 @@ function help() {
|
|
|
59
60
|
usage: zuzuu <command> [options]
|
|
60
61
|
|
|
61
62
|
code [dir] launch OpenCode as the bundled default host (faculty home + capture + gate + digest)
|
|
62
|
-
|
|
63
|
+
web [dir] launch the visual workbench (installs @zuzuucodes/web on demand)
|
|
64
|
+
init scaffold the faculty home (.zuzuu/) — git-style, idempotent
|
|
63
65
|
status detected hosts + recorded sessions
|
|
64
|
-
capture [--host NAME] capture a session →
|
|
66
|
+
capture [--host NAME] capture a session → .zuzuu/.traces + .zuzuu/sessions.json
|
|
65
67
|
[--session ID] [--file PATH]
|
|
66
68
|
trace [--last | FILE] print a captured trace's span tree
|
|
67
69
|
remember "fact" [--type t] [--attr k=v] [--rel type=target]
|
|
@@ -88,7 +90,7 @@ usage: zuzuu <command> [options]
|
|
|
88
90
|
enable background hooks: invisible live capture + guardrails gate
|
|
89
91
|
disable remove the background hooks
|
|
90
92
|
eval [--faculty f] rank pending proposals by eval score, highest first
|
|
91
|
-
migrate
|
|
93
|
+
migrate [--home] one-time migrators: proposal schema · --home moves agent/ → .zuzuu/
|
|
92
94
|
doctor environment + session health (reconciles lost sessions)
|
|
93
95
|
explain [topic] the 5 faculties + how graduation works
|
|
94
96
|
version print version
|
|
@@ -103,6 +105,7 @@ const args = parseArgs(rest);
|
|
|
103
105
|
|
|
104
106
|
switch (cmd) {
|
|
105
107
|
case 'code': process.exit(code(args)); break;
|
|
108
|
+
case 'web': web(args); break;
|
|
106
109
|
case 'init': init(args); break;
|
|
107
110
|
case 'remember': remember(args); break;
|
|
108
111
|
case 'recall': await recall(args); break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// zuzuu/actions/adapter.mjs
|
|
2
2
|
// The Actions faculty adapter (WS2-T3). Wraps the EXISTING Actions inbox gate
|
|
3
|
-
// (proposed dirs under
|
|
3
|
+
// (proposed dirs under .zuzuu/actions/inbox/<slug>/) behind the faculty-spine
|
|
4
4
|
// adapter contract — { name, ingest, validate, apply, render } — so the generic
|
|
5
5
|
// `zuzuu review` gate can drive Actions the same way it drives Knowledge.
|
|
6
6
|
//
|
|
@@ -42,7 +42,7 @@ function recordFor(a) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
* Pending action proposals (dirs in
|
|
45
|
+
* Pending action proposals (dirs in .zuzuu/actions/inbox/), surfaced as
|
|
46
46
|
* spine-shaped records so the gate can render/approve/reject them uniformly.
|
|
47
47
|
*/
|
|
48
48
|
function listProposals(agentDir) {
|
package/zuzuu/actions/inbox.mjs
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
// zuzuu/actions/inbox.mjs
|
|
2
2
|
// The Actions crystallization gate (the same governed pipeline as Knowledge
|
|
3
3
|
// promotion, kept out of the knowledge ER/registry machinery). A proposed action
|
|
4
|
-
// is a real dir under
|
|
5
|
-
//
|
|
4
|
+
// is a real dir under .zuzuu/actions/inbox/<slug>/. A human activates it (move to
|
|
5
|
+
// .zuzuu/actions/<slug>/) or rejects it (remove). Never auto-activates.
|
|
6
6
|
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { existsSync, readFileSync, renameSync, mkdirSync, rmSync } from 'node:fs';
|
|
9
9
|
import { actionsDir, inboxDir, listActions, isSafeSlug } from './manifest.mjs';
|
|
10
10
|
|
|
11
|
-
/** Archive dir for rejected action proposals:
|
|
11
|
+
/** Archive dir for rejected action proposals: .zuzuu/actions/proposals/archive/. */
|
|
12
12
|
const archiveBaseDir = (agentDir) => join(actionsDir(agentDir), 'proposals', 'archive');
|
|
13
13
|
|
|
14
|
-
/** Proposed actions awaiting review (in
|
|
14
|
+
/** Proposed actions awaiting review (in .zuzuu/actions/inbox/). */
|
|
15
15
|
export function listProposedActions(agentDir) {
|
|
16
16
|
return listActions(inboxDir(agentDir));
|
|
17
17
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// zuzuu/actions/manifest.mjs
|
|
2
|
-
// Reads the Actions faculty off disk: one action per dir under
|
|
2
|
+
// Reads the Actions faculty off disk: one action per dir under .zuzuu/actions/.
|
|
3
3
|
// Two kinds — `script` (has run.mjs + action.json) and `runbook` (SKILL.md prose).
|
|
4
4
|
// Pure-ish: filesystem reads only, no logging, no process control.
|
|
5
5
|
|
|
@@ -7,7 +7,7 @@ import { join } from 'node:path';
|
|
|
7
7
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
8
8
|
|
|
9
9
|
// Action slugs: letters/digits start, then letters/digits/-/_. No dots or slashes
|
|
10
|
-
// → cannot escape
|
|
10
|
+
// → cannot escape .zuzuu/actions/ via path traversal.
|
|
11
11
|
export const SAFE_SLUG = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
|
|
12
12
|
export function isSafeSlug(slug) {
|
|
13
13
|
return typeof slug === 'string' && SAFE_SLUG.test(slug);
|
|
@@ -66,7 +66,7 @@ export function listActions(baseDir) {
|
|
|
66
66
|
return out;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
/** Active actions under
|
|
69
|
+
/** Active actions under .zuzuu/actions/ (the inbox subdir is excluded). */
|
|
70
70
|
export function allActions(agentDir) {
|
|
71
71
|
return listActions(actionsDir(agentDir)).filter((a) => a.slug !== 'inbox');
|
|
72
72
|
}
|
package/zuzuu/actions/trail.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// zuzuu/actions/trail.mjs
|
|
2
2
|
// The actions observability trail (A7): every `zuzuu act` run appends an outcome
|
|
3
|
-
// record to
|
|
3
|
+
// record to .zuzuu/.live/actions.jsonl. This is the "details" side of the result —
|
|
4
4
|
// the agent sees the marker value; the trace keeps the metadata. Fail-soft: a
|
|
5
5
|
// logging failure must never affect the action (mirrors the guardrails decision log).
|
|
6
6
|
|
|
@@ -46,12 +46,12 @@ function scaffoldInto(baseDir, slug) {
|
|
|
46
46
|
return { created };
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/** Scaffold a live action (
|
|
49
|
+
/** Scaffold a live action (.zuzuu/actions/<slug>/). Humans author here directly. */
|
|
50
50
|
export function scaffoldAction(agentDir, slug) {
|
|
51
51
|
return scaffoldInto(actionsDir(agentDir), slug);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
/** Scaffold a PROPOSED action (
|
|
54
|
+
/** Scaffold a PROPOSED action (.zuzuu/actions/inbox/<slug>/) — agents propose here. */
|
|
55
55
|
export function proposeAction(agentDir, slug) {
|
|
56
56
|
return scaffoldInto(inboxDir(agentDir), slug);
|
|
57
57
|
}
|
|
@@ -59,7 +59,7 @@ export function proposeAction(agentDir, slug) {
|
|
|
59
59
|
export function newAction(agentDir, slug) {
|
|
60
60
|
if (!slug) { console.error('usage: zuzuu act new <slug>'); process.exit(1); }
|
|
61
61
|
const { created } = scaffoldAction(agentDir, slug);
|
|
62
|
-
if (created.length) console.log(`scaffolded action '${slug}' → ${created.join(', ')} in
|
|
62
|
+
if (created.length) console.log(`scaffolded action '${slug}' → ${created.join(', ')} in .zuzuu/actions/${slug}/`);
|
|
63
63
|
else console.log(`action '${slug}' already complete — nothing to do`);
|
|
64
64
|
}
|
|
65
65
|
|
package/zuzuu/commands/act.mjs
CHANGED
|
@@ -60,11 +60,48 @@ function run(agentDir, slug, args) {
|
|
|
60
60
|
|
|
61
61
|
function propose(agentDir, slug) {
|
|
62
62
|
const { created } = proposeAction(agentDir, slug);
|
|
63
|
-
if (created.length) console.log(`proposed action '${slug}' → ${created.join(', ')} in
|
|
63
|
+
if (created.length) console.log(`proposed action '${slug}' → ${created.join(', ')} in .zuzuu/actions/inbox/${slug}/ (review with \`zuzuu review\`)`);
|
|
64
64
|
else console.log(`proposed action '${slug}' already complete — nothing to do`);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Pure: data for `act inbox --json`.
|
|
69
|
+
* @param {string} agentDir
|
|
70
|
+
* @returns {{ pending: Array }}
|
|
71
|
+
*/
|
|
72
|
+
export function actInboxData(agentDir) {
|
|
73
|
+
return { pending: listProposedActions(agentDir) };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Pure: data for `act approve --json`.
|
|
78
|
+
* Calls activateAction and returns the printed object.
|
|
79
|
+
* @param {string} agentDir
|
|
80
|
+
* @param {string} slug
|
|
81
|
+
* @returns {{ ok: boolean, action: string, slug: string }}
|
|
82
|
+
*/
|
|
83
|
+
export function actApproveData(agentDir, slug) {
|
|
84
|
+
const r = activateAction(agentDir, slug);
|
|
85
|
+
return { ok: r.ok, action: r.ok ? `activated ${slug}` : r.error, slug };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Pure: data for `act reject --json`.
|
|
90
|
+
* Calls rejectAction and returns the printed object.
|
|
91
|
+
* @param {string} agentDir
|
|
92
|
+
* @param {string} slug
|
|
93
|
+
* @returns {{ ok: boolean, action: string, slug: string }}
|
|
94
|
+
*/
|
|
95
|
+
export function actRejectData(agentDir, slug) {
|
|
96
|
+
const r = rejectAction(agentDir, slug);
|
|
97
|
+
return { ok: r.ok, action: r.ok ? `rejected ${slug}` : r.error, slug };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function inbox(agentDir, args = {}) {
|
|
101
|
+
if (args.json) {
|
|
102
|
+
console.log(JSON.stringify(actInboxData(agentDir)));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
68
105
|
const pending = listProposedActions(agentDir);
|
|
69
106
|
if (!pending.length) return console.log('no proposed actions — inbox empty');
|
|
70
107
|
for (const a of pending.sort((x, y) => x.slug.localeCompare(y.slug))) {
|
|
@@ -72,16 +109,24 @@ function inbox(agentDir) {
|
|
|
72
109
|
}
|
|
73
110
|
}
|
|
74
111
|
|
|
75
|
-
function approve(agentDir, slug) {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
112
|
+
function approve(agentDir, slug, args = {}) {
|
|
113
|
+
const result = actApproveData(agentDir, slug);
|
|
114
|
+
if (args.json) {
|
|
115
|
+
console.log(JSON.stringify(result));
|
|
116
|
+
} else {
|
|
117
|
+
console.log(result.ok ? `✓ activated '${slug}'` : `✗ ${result.action}`);
|
|
118
|
+
}
|
|
119
|
+
process.exit(result.ok ? 0 : 1);
|
|
79
120
|
}
|
|
80
121
|
|
|
81
|
-
function reject(agentDir, slug) {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
122
|
+
function reject(agentDir, slug, args = {}) {
|
|
123
|
+
const result = actRejectData(agentDir, slug);
|
|
124
|
+
if (args.json) {
|
|
125
|
+
console.log(JSON.stringify(result));
|
|
126
|
+
} else {
|
|
127
|
+
console.log(result.ok ? `✓ rejected '${slug}'` : `✗ ${result.action}`);
|
|
128
|
+
}
|
|
129
|
+
process.exit(result.ok ? 0 : 1);
|
|
85
130
|
}
|
|
86
131
|
|
|
87
132
|
export function act(args) {
|
|
@@ -92,9 +137,9 @@ export function act(args) {
|
|
|
92
137
|
if (sub === 'new') return newAction(agentDir, requireSlug(args._[1], 'usage: zuzuu act new <slug>'));
|
|
93
138
|
if (sub === 'schema') return schemaCmd(agentDir, requireSlug(args._[1], 'usage: zuzuu act schema <slug> [--openai|--anthropic]'), args);
|
|
94
139
|
if (sub === 'propose') return propose(agentDir, requireSlug(args._[1], 'usage: zuzuu act propose <slug>'));
|
|
95
|
-
if (sub === 'inbox') return inbox(agentDir);
|
|
96
|
-
if (sub === 'approve') return approve(agentDir, requireSlug(args._[1], 'usage: zuzuu act approve <slug>'));
|
|
97
|
-
if (sub === 'reject') return reject(agentDir, requireSlug(args._[1], 'usage: zuzuu act reject <slug>'));
|
|
140
|
+
if (sub === 'inbox') return inbox(agentDir, args);
|
|
141
|
+
if (sub === 'approve') return approve(agentDir, requireSlug(args._[1], 'usage: zuzuu act approve <slug>'), args);
|
|
142
|
+
if (sub === 'reject') return reject(agentDir, requireSlug(args._[1], 'usage: zuzuu act reject <slug>'), args);
|
|
98
143
|
// future-reserved guard: extend RESERVED + add a handler above in tandem
|
|
99
144
|
if (RESERVED.has(sub)) { console.error(`unknown: zuzuu act ${sub}`); process.exit(1); }
|
|
100
145
|
return run(agentDir, requireSlug(sub, 'usage: zuzuu act <slug> [--args JSON]'), args);
|
package/zuzuu/commands/code.mjs
CHANGED
|
@@ -76,7 +76,7 @@ export function code(args = {}, deps = {}) {
|
|
|
76
76
|
|
|
77
77
|
// a clean one-screen summary of what the newcomer just got (vs. the verbose enable output)
|
|
78
78
|
d.log('zuzuu code → OpenCode, faculty-equipped');
|
|
79
|
-
d.log(` ✓ faculty home (
|
|
79
|
+
d.log(` ✓ faculty home (.zuzuu/) ${wired ? '✓ capture + guardrails gate ✓ session grounding' : '⚠ plugin not wired (degraded)'}`);
|
|
80
80
|
d.log(` → launching OpenCode in ${dir} …`);
|
|
81
81
|
|
|
82
82
|
// 5. launch the real OpenCode (configure + launch, never drive)
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { paths } from '../store.mjs';
|
|
7
7
|
import { computeDigest } from '../digest.mjs';
|
|
8
8
|
|
|
9
|
-
/** Pure: the digest payload — the zuzuu-web /digest source (the daemon also reads
|
|
9
|
+
/** Pure: the digest payload — the zuzuu-web /digest source (the daemon also reads .zuzuu/.live/digest.md directly). */
|
|
10
10
|
export function digestData(agentDir, opts = {}) {
|
|
11
11
|
const d = computeDigest(agentDir, opts);
|
|
12
12
|
return { text: d.text ?? '' };
|
|
@@ -123,14 +123,14 @@ export async function doctor() {
|
|
|
123
123
|
if (commit) ok(`git repo on ${branch} @ ${commit.slice(0, 8)}`);
|
|
124
124
|
else info("not a git repo — capture works; sessions just won’t link to a commit");
|
|
125
125
|
|
|
126
|
-
//
|
|
126
|
+
// .zuzuu/ writable
|
|
127
127
|
const { dir } = paths();
|
|
128
128
|
try {
|
|
129
129
|
mkdirSync(dir, { recursive: true });
|
|
130
130
|
accessSync(dir, constants.W_OK);
|
|
131
|
-
ok(
|
|
131
|
+
ok(`.zuzuu/ writable (${dir})`);
|
|
132
132
|
} catch {
|
|
133
|
-
bad(
|
|
133
|
+
bad(`.zuzuu/ not writable (${dir})`);
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
// faculty home (served by `zuzuu init`)
|
package/zuzuu/commands/eval.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// Also exports `evalLine` — a small pure helper used by `zuzuu review` to render
|
|
7
7
|
// a one-line eval annotation per proposal card.
|
|
8
8
|
|
|
9
|
+
import { join } from 'node:path';
|
|
9
10
|
import { paths, readIndex } from '../store.mjs';
|
|
10
11
|
import * as registry from '../faculty/registry.mjs';
|
|
11
12
|
import { listProposals as spineListProposals } from '../faculty/proposal.mjs';
|
|
@@ -58,20 +59,21 @@ function collectProposals(agentDir, adapter) {
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
/**
|
|
61
|
-
*
|
|
62
|
+
* Pure: gather + rank all pending proposals, returning structured data for JSON output.
|
|
63
|
+
* The zuzuu-web /eval source.
|
|
64
|
+
* Touches FS via buildSessionMtimes (fail-open) and collectProposals.
|
|
62
65
|
*
|
|
63
|
-
* @param {
|
|
64
|
-
* @param {
|
|
66
|
+
* @param {string} agentDir Resolved .zuzuu/ path.
|
|
67
|
+
* @param {object} [opts]
|
|
68
|
+
* @param {string} [opts.faculty] Filter to a single faculty name.
|
|
69
|
+
* @returns {{ ranked: Array<{id,faculty,title,score,confidence,rationale}> }}
|
|
65
70
|
*/
|
|
66
|
-
export function
|
|
67
|
-
const agentDir = paths().dir;
|
|
68
|
-
const onlyFaculty = args?.faculty ?? null;
|
|
71
|
+
export function evalData(agentDir, { faculty: onlyFaculty = null } = {}) {
|
|
69
72
|
const adapters = registry.all();
|
|
70
|
-
const sessionMtimes = buildSessionMtimes();
|
|
73
|
+
const sessionMtimes = buildSessionMtimes(join(agentDir, '..'));
|
|
71
74
|
const now = Date.now();
|
|
72
75
|
const scorer = getScorer();
|
|
73
76
|
|
|
74
|
-
// Gather proposals from all (or the one filtered) faculty adapters.
|
|
75
77
|
const allEntries = [];
|
|
76
78
|
for (const adapter of adapters) {
|
|
77
79
|
if (onlyFaculty && adapter.name !== onlyFaculty) continue;
|
|
@@ -81,21 +83,44 @@ export function evalCmd(args, log = console.log) {
|
|
|
81
83
|
}
|
|
82
84
|
}
|
|
83
85
|
|
|
84
|
-
if (!allEntries.length) {
|
|
85
|
-
log('no pending proposals');
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
86
|
+
if (!allEntries.length) return { ranked: [] };
|
|
88
87
|
|
|
89
|
-
// Rank all entries together.
|
|
90
88
|
const rawProposals = allEntries.map((e) => e.proposal);
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
// Build faculty lookup for display: proposal.id → faculty name.
|
|
89
|
+
const rankResults = rank(rawProposals, scorer, { now, sessionMtimes });
|
|
94
90
|
const facultyByProposalId = new Map(allEntries.map((e) => [e.proposal.id, e.faculty]));
|
|
95
91
|
|
|
96
|
-
|
|
97
|
-
const
|
|
92
|
+
const ranked = rankResults.map(({ proposal, score, confidence, rationale }) => {
|
|
93
|
+
const fac = facultyByProposalId.get(proposal.id) ?? '?';
|
|
94
|
+
const title = proposal.title
|
|
95
|
+
?? proposal.candidate?.body?.slice(0, 80)
|
|
96
|
+
?? proposal.payload?.body?.slice(0, 80)
|
|
97
|
+
?? proposal.id;
|
|
98
|
+
return { id: proposal.id, faculty: fac, title, score, confidence, rationale };
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return { ranked };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Core of `zuzuu eval` — exported so tests can inject a custom log fn.
|
|
106
|
+
*
|
|
107
|
+
* @param {object} args Parsed CLI args.
|
|
108
|
+
* @param {Function} [log=console.log] Output sink (injectable for tests).
|
|
109
|
+
*/
|
|
110
|
+
export function evalCmd(args, log = console.log) {
|
|
111
|
+
const agentDir = paths().dir;
|
|
112
|
+
const onlyFaculty = args?.faculty ?? null;
|
|
113
|
+
|
|
114
|
+
if (args?.json) {
|
|
115
|
+
const d = evalData(agentDir, { faculty: onlyFaculty });
|
|
116
|
+
log(JSON.stringify(d));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const { ranked } = evalData(agentDir, { faculty: onlyFaculty });
|
|
121
|
+
if (!ranked.length) { log('no pending proposals'); return; }
|
|
122
|
+
for (const { id, faculty, score, confidence, rationale } of ranked) {
|
|
98
123
|
const warn = confidence === 'low' ? ' ⚠' : '';
|
|
99
|
-
log(`${String(score).padEnd(6)} [${confidence}] ${faculty}/${
|
|
124
|
+
log(`${String(score).padEnd(6)} [${confidence}] ${faculty}/${id} — ${rationale}${warn}`);
|
|
100
125
|
}
|
|
101
126
|
}
|
|
@@ -25,9 +25,39 @@ function list(dir) {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Pure: mint a new generation and return structured data.
|
|
30
|
+
* @param {string} dir
|
|
31
|
+
* @param {object} [opts]
|
|
32
|
+
* @param {string[]} [opts.mintedFrom] proposal ids this generation is built from
|
|
33
|
+
* @returns {{ id: string, mintedFrom: string[], forkedFrom: string|null }}
|
|
34
|
+
*/
|
|
35
|
+
export function mintGenerationData(dir, { mintedFrom = [] } = {}) {
|
|
29
36
|
const forkedFrom = activeGeneration(dir);
|
|
30
|
-
const lf = mintGeneration(dir, { forkedFrom });
|
|
37
|
+
const lf = mintGeneration(dir, { forkedFrom, mintedFrom });
|
|
38
|
+
return { id: lf.id, mintedFrom: lf.mintedFrom ?? [], forkedFrom: lf.forkedFrom ?? null };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Pure: rollback to a generation and return structured data.
|
|
43
|
+
* @param {string} dir
|
|
44
|
+
* @param {string} id
|
|
45
|
+
* @returns {{ ok: boolean, restored: number, active: string }}
|
|
46
|
+
*/
|
|
47
|
+
export function rollbackData(dir, id) {
|
|
48
|
+
const r = rollback(dir, id);
|
|
49
|
+
return { ok: r.ok, restored: r.restored, active: id };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function mint(dir, args = {}) {
|
|
53
|
+
const mintedFrom = args.from ? String(args.from).split(',').map((s) => s.trim()).filter(Boolean) : [];
|
|
54
|
+
if (args.json) {
|
|
55
|
+
const d = mintGenerationData(dir, { mintedFrom });
|
|
56
|
+
console.log(JSON.stringify(d));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const forkedFrom = activeGeneration(dir);
|
|
60
|
+
const lf = mintGeneration(dir, { forkedFrom, mintedFrom });
|
|
31
61
|
console.log(`✓ minted ${lf.id}${forkedFrom ? ` (forkedFrom ${forkedFrom})` : ''} — now active`);
|
|
32
62
|
}
|
|
33
63
|
|
|
@@ -78,9 +108,14 @@ function show(dir, id) {
|
|
|
78
108
|
console.log(out);
|
|
79
109
|
}
|
|
80
110
|
|
|
81
|
-
function doRollback(dir, id) {
|
|
111
|
+
function doRollback(dir, id, args = {}) {
|
|
82
112
|
if (!id) { console.error('usage: zuzuu generation rollback <id>'); process.exit(1); }
|
|
83
113
|
if (!readGeneration(dir, id)) { console.error(`no generation '${id}'`); process.exit(1); }
|
|
114
|
+
if (args.json) {
|
|
115
|
+
const d = rollbackData(dir, id);
|
|
116
|
+
console.log(JSON.stringify(d));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
84
119
|
const r = rollback(dir, id);
|
|
85
120
|
console.log(`✓ rolled back to ${id} — restored ${r.restored} item(s); active=${id}`);
|
|
86
121
|
}
|
|
@@ -92,7 +127,7 @@ export function generation(args) {
|
|
|
92
127
|
if (args.json) { console.log(JSON.stringify(generationListData(dir))); return; }
|
|
93
128
|
return list(dir);
|
|
94
129
|
}
|
|
95
|
-
if (sub === 'mint') return mint(dir);
|
|
130
|
+
if (sub === 'mint') return mint(dir, args);
|
|
96
131
|
if (sub === 'show') {
|
|
97
132
|
if (args.json) {
|
|
98
133
|
const d = generationShowData(dir, args._[1]);
|
|
@@ -101,7 +136,7 @@ export function generation(args) {
|
|
|
101
136
|
}
|
|
102
137
|
return show(dir, args._[1]);
|
|
103
138
|
}
|
|
104
|
-
if (sub === 'rollback') return doRollback(dir, args._[1]);
|
|
139
|
+
if (sub === 'rollback') return doRollback(dir, args._[1], args);
|
|
105
140
|
console.error(`unknown: zuzuu generation ${sub}\nusage: zuzuu generation [list|show <id>|mint|rollback <id>]`);
|
|
106
141
|
process.exit(1);
|
|
107
142
|
}
|
package/zuzuu/commands/hook.mjs
CHANGED
|
@@ -73,7 +73,7 @@ export function handleHook({ event, payload = {}, cwd = process.cwd(), now = Dat
|
|
|
73
73
|
openLive({ id, host, transcriptPath: ref, startedAt: new Date(now).toISOString(), now, generation }, cwd);
|
|
74
74
|
safeCapture(adapter, ref, SessionState.ACTIVE, cwd, generation);
|
|
75
75
|
} catch { /* live/capture hiccup must not block grounding below */ }
|
|
76
|
-
writeLiveDigest(cwd); // universal grounding channel — every host reads
|
|
76
|
+
writeLiveDigest(cwd); // universal grounding channel — every host reads .zuzuu/.live/digest.md
|
|
77
77
|
} else if (TURN.has(event)) {
|
|
78
78
|
touchLive({ id, host, transcriptPath: ref, now }, cwd);
|
|
79
79
|
safeCapture(adapter, ref, SessionState.ACTIVE, cwd);
|
|
@@ -88,7 +88,7 @@ export function handleHook({ event, payload = {}, cwd = process.cwd(), now = Dat
|
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
90
|
* The Guardrails gate (PreToolUse). Evaluates the tool call against
|
|
91
|
-
*
|
|
91
|
+
* .zuzuu/guardrails/rules.json and prints Claude's hookSpecificOutput decision —
|
|
92
92
|
* or NOTHING (exit 0, no JSON = defer to the host's normal permission flow).
|
|
93
93
|
* That silence is the fail-open: engine errors and rule-file problems can slow
|
|
94
94
|
* nothing down and block nothing. Matched decisions are logged for the trace.
|
|
@@ -132,11 +132,11 @@ export function gateDecision({ host = 'claude-code', payload = {}, cwd = process
|
|
|
132
132
|
|
|
133
133
|
/**
|
|
134
134
|
* Universal digest delivery (Design B side effect, not a span builder). Computes
|
|
135
|
-
* the faculty digest and writes it to
|
|
135
|
+
* the faculty digest and writes it to `.zuzuu/.live/digest.md` — the ONE channel
|
|
136
136
|
* every host can read at session start (the faculty block points here). Claude
|
|
137
137
|
* also gets it inline via sessionStartContext; the other 4 hosts rely on this
|
|
138
138
|
* file. Fail-open: any error is swallowed (never break the host).
|
|
139
|
-
* @param {string} cwd repo cwd; paths() resolves the
|
|
139
|
+
* @param {string} cwd repo cwd; paths() resolves the .zuzuu/ home under it
|
|
140
140
|
*/
|
|
141
141
|
export function writeLiveDigest(cwd = process.cwd()) {
|
|
142
142
|
try {
|
|
@@ -155,7 +155,7 @@ export function writeLiveDigest(cwd = process.cwd()) {
|
|
|
155
155
|
* Build Claude Code's SessionStart additionalContext payload from the faculty
|
|
156
156
|
* digest. Returns null on ANY failure (fail-open: the session proceeds with no
|
|
157
157
|
* injected context, never a broken hook).
|
|
158
|
-
* @param {string} cwd repo cwd; paths() resolves the
|
|
158
|
+
* @param {string} cwd repo cwd; paths() resolves the .zuzuu/ home under it
|
|
159
159
|
*/
|
|
160
160
|
export function sessionStartContext(cwd = process.cwd()) {
|
|
161
161
|
try {
|
|
@@ -193,7 +193,7 @@ export function runHook(event, { host = 'claude-code', session } = {}) {
|
|
|
193
193
|
} else {
|
|
194
194
|
try { handleHook({ event, payload, host }); } catch { /* capture failure is silent — never blocks the digest or the host */ }
|
|
195
195
|
// Claude consumes additionalContext inline; the other hosts read
|
|
196
|
-
//
|
|
196
|
+
// .zuzuu/.live/digest.md (written by handleHook's OPEN branch). Scoping the
|
|
197
197
|
// stdout push to Claude avoids emitting an unread schema to Gemini/Codex.
|
|
198
198
|
if (event === 'SessionStart' && host === 'claude-code') {
|
|
199
199
|
try {
|