@zuzuucodes/cli 1.3.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/zuzuu.mjs +8 -1
- package/package.json +1 -1
- package/web-app/dist/zuzuu-api.js +125 -20
- package/web-app/web-dist/assets/{DiffTab-CihRJjzf.js → DiffTab-BpGp1akx.js} +1 -1
- package/web-app/web-dist/assets/{MonacoFile-DJvpGyW2.js → MonacoFile-CqbVacUZ.js} +1 -1
- package/web-app/web-dist/assets/{cssMode-R1Bks9TO.js → cssMode-Dx3ub8Pk.js} +1 -1
- package/web-app/web-dist/assets/{dist-jCnX6g-O.js → dist-C6R6xoyX.js} +1 -1
- package/web-app/web-dist/assets/{htmlMode-Csqnn3yv.js → htmlMode-DM6oHc7c.js} +1 -1
- package/web-app/web-dist/assets/index-DHpC851f.js +268 -0
- package/web-app/web-dist/assets/index-O-t1gyMG.css +2 -0
- package/web-app/web-dist/assets/{jsonMode-DRBg9jwi.js → jsonMode-DflaUwqW.js} +1 -1
- package/web-app/web-dist/assets/{monaco-setup-Dszx738Y.js → monaco-setup-wbBeb0oN.js} +3 -3
- package/web-app/web-dist/assets/{tsMode-9YOHYiVQ.js → tsMode-DRwkDcoK.js} +1 -1
- package/web-app/web-dist/index.html +2 -2
- package/zuzuu/actions/adapter.mjs +12 -20
- package/zuzuu/actions/convert.mjs +10 -9
- package/zuzuu/actions/dispatch.mjs +12 -7
- package/zuzuu/actions/inbox.mjs +5 -5
- package/zuzuu/actions/manifest.mjs +48 -30
- package/zuzuu/actions/schema.mjs +9 -3
- package/zuzuu/commands/act-author.mjs +23 -13
- package/zuzuu/commands/act.mjs +3 -5
- package/zuzuu/commands/doctor.mjs +2 -15
- package/zuzuu/commands/explain.mjs +4 -4
- package/zuzuu/commands/faculty.mjs +75 -0
- package/zuzuu/commands/generation.mjs +2 -4
- package/zuzuu/commands/hook.mjs +7 -5
- package/zuzuu/commands/init.mjs +14 -1
- package/zuzuu/commands/migrate.mjs +348 -1
- package/zuzuu/digest.mjs +18 -13
- package/zuzuu/faculty/envelope.mjs +290 -0
- package/zuzuu/faculty/generation.mjs +53 -47
- package/zuzuu/faculty/items.mjs +75 -0
- package/zuzuu/guardrails/adapter.mjs +18 -49
- package/zuzuu/guardrails.mjs +72 -24
- package/zuzuu/instructions/adapter.mjs +30 -30
- package/zuzuu/knowledge/items.mjs +56 -91
- package/zuzuu/live/install.mjs +1 -1
- package/zuzuu/memory/adapter.mjs +27 -52
- package/zuzuu/miners/actions.mjs +14 -20
- package/zuzuu/miners/guardrails.mjs +8 -11
- package/zuzuu/miners/instructions.mjs +10 -10
- package/zuzuu/scaffold.mjs +99 -38
- package/web-app/web-dist/assets/index-D_MPtALn.css +0 -2
- package/web-app/web-dist/assets/index-Ye54YyTn.js +0 -267
package/zuzuu/guardrails.mjs
CHANGED
|
@@ -1,48 +1,96 @@
|
|
|
1
1
|
// The Guardrails faculty — v1 rule engine (pure; I/O lives in the hook command).
|
|
2
2
|
//
|
|
3
|
-
// Rules are DATA, not code:
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// Rules are DATA, not code: one envelope item per rule under
|
|
4
|
+
// .zuzuu/guardrails/items/<id>.md (the Faculty Standard, W24) — *definitions*
|
|
5
|
+
// in the pin-definitions sense (versioned in git, graduate via proposals like
|
|
6
|
+
// every faculty's contents).
|
|
6
7
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// ---
|
|
9
|
+
// id: no-root-wipe
|
|
10
|
+
// faculty: guardrails
|
|
11
|
+
// kind: rule
|
|
12
|
+
// title: …
|
|
13
|
+
// payload:
|
|
14
|
+
// action: deny # deny | ask | allow
|
|
15
|
+
// tool: Bash # exact tool name, or "*"
|
|
16
|
+
// pattern: "rm\\s+-rf\\s+/" # regex over the tool INPUT (stringified)
|
|
17
|
+
// reason: destructive root delete
|
|
18
|
+
// ---
|
|
19
|
+
// (optional rationale prose)
|
|
12
20
|
//
|
|
13
21
|
// Evaluation: collect every matching rule, then severity wins — deny > ask >
|
|
14
22
|
// allow (an explicit allow can whitelist past a later ask/deny only if it is
|
|
15
23
|
// NOT outweighed; severity beats file order so a sloppy rule ordering can never
|
|
16
24
|
// silently disarm a deny).
|
|
17
25
|
//
|
|
18
|
-
// FAIL-OPEN:
|
|
19
|
-
//
|
|
20
|
-
//
|
|
26
|
+
// FAIL-OPEN, per item: a malformed rule file is SKIPPED (and counted in
|
|
27
|
+
// `skipped`), never a crash and never a block — the other rules still apply.
|
|
28
|
+
// The gate runs per tool call, so loads are cached on the items dir's stat
|
|
29
|
+
// signature (names+mtimes+sizes): re-parse only when something changed. No
|
|
30
|
+
// derived file — the item files stay the single source of truth.
|
|
21
31
|
|
|
22
|
-
import {
|
|
32
|
+
import { join } from 'node:path';
|
|
33
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
34
|
+
import { parseEnvelope } from './faculty/envelope.mjs';
|
|
23
35
|
|
|
24
36
|
const SEVERITY = { deny: 3, ask: 2, allow: 1 };
|
|
25
37
|
const ACTIONS = new Set(Object.keys(SEVERITY));
|
|
26
38
|
|
|
27
|
-
|
|
28
|
-
|
|
39
|
+
// dir → { sig, result } — tiny in-memory cache (per process; the spawned gate
|
|
40
|
+
// pays one cold load, long-lived processes like `zuzuu web` skip re-parses).
|
|
41
|
+
const cache = new Map();
|
|
42
|
+
|
|
43
|
+
/** Compile one parsed envelope item into a rule, or null if malformed. */
|
|
44
|
+
function compileRule(item) {
|
|
45
|
+
const p = item?.payload ?? {};
|
|
46
|
+
if (!item?.id || !ACTIONS.has(p.action) || typeof p.pattern !== 'string' || !p.pattern) return null;
|
|
47
|
+
try {
|
|
48
|
+
return { id: String(item.id), action: p.action, tool: p.tool || '*', re: new RegExp(p.pattern, 'i'), reason: String(p.reason ?? '') };
|
|
49
|
+
} catch {
|
|
50
|
+
return null; // uncompilable pattern → skip this rule only
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Load the rules from a guardrails faculty dir (…/guardrails) by composing the
|
|
56
|
+
* envelope items under its items/ subdir. FAIL-OPEN: a missing dir is zero
|
|
57
|
+
* rules; a malformed item is skipped + counted, never a crash.
|
|
58
|
+
* @param {string} guardrailsDir the faculty dir (e.g. <home>/guardrails)
|
|
59
|
+
* @returns {{ok: boolean, rules: Array, skipped: Array<{file: string, error: string}>}}
|
|
60
|
+
*/
|
|
61
|
+
export function loadRules(guardrailsDir) {
|
|
29
62
|
try {
|
|
30
|
-
const
|
|
31
|
-
|
|
63
|
+
const dir = join(guardrailsDir, 'items');
|
|
64
|
+
let names;
|
|
65
|
+
try {
|
|
66
|
+
names = readdirSync(dir).filter((f) => f.endsWith('.md')).sort();
|
|
67
|
+
} catch {
|
|
68
|
+
return { ok: true, rules: [], skipped: [] }; // no items dir → no rules (normal flow)
|
|
69
|
+
}
|
|
70
|
+
let sig = null;
|
|
71
|
+
try {
|
|
72
|
+
sig = names.map((n) => { const s = statSync(join(dir, n)); return `${n}:${s.mtimeMs}:${s.size}`; }).join('|');
|
|
73
|
+
const hit = cache.get(dir);
|
|
74
|
+
if (hit && hit.sig === sig) return hit.result;
|
|
75
|
+
} catch { sig = null; /* stat race → just load uncached */ }
|
|
76
|
+
|
|
32
77
|
const rules = [];
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return { ok: false, rules: [], error: `malformed rule: ${JSON.stringify(r).slice(0, 80)}` };
|
|
36
|
-
}
|
|
78
|
+
const skipped = [];
|
|
79
|
+
for (const f of names) {
|
|
37
80
|
try {
|
|
38
|
-
|
|
81
|
+
const { ok, item, errors } = parseEnvelope(readFileSync(join(dir, f), 'utf8'));
|
|
82
|
+
const rule = ok ? compileRule(item) : null;
|
|
83
|
+
if (rule) rules.push(rule);
|
|
84
|
+
else skipped.push({ file: f, error: ok ? 'malformed rule payload' : (errors[0] ?? 'parse error') });
|
|
39
85
|
} catch (e) {
|
|
40
|
-
|
|
86
|
+
skipped.push({ file: f, error: e.message });
|
|
41
87
|
}
|
|
42
88
|
}
|
|
43
|
-
|
|
89
|
+
const result = { ok: true, rules, skipped };
|
|
90
|
+
if (sig != null) cache.set(dir, { sig, result });
|
|
91
|
+
return result;
|
|
44
92
|
} catch (e) {
|
|
45
|
-
return { ok: false, rules: [], error: e.message };
|
|
93
|
+
return { ok: false, rules: [], skipped: [], error: e.message }; // engine trouble → fail open
|
|
46
94
|
}
|
|
47
95
|
}
|
|
48
96
|
|
|
@@ -1,30 +1,26 @@
|
|
|
1
1
|
// zuzuu/instructions/adapter.mjs
|
|
2
|
-
// The Instructions faculty adapter
|
|
2
|
+
// The Instructions faculty adapter. Wraps steering-amendment proposals
|
|
3
3
|
// behind the faculty-spine adapter contract — { name, ingest, validate, apply,
|
|
4
4
|
// render } — so `zuzuu review` can surface and approve them uniformly.
|
|
5
5
|
//
|
|
6
6
|
// An instructions proposal payload is a steering amendment:
|
|
7
|
-
// { text } — a line or paragraph
|
|
7
|
+
// { id?, text } — a line or paragraph of steering
|
|
8
8
|
//
|
|
9
|
-
// apply:
|
|
10
|
-
//
|
|
9
|
+
// apply: writes the amendment as a Faculty Standard envelope item under
|
|
10
|
+
// .zuzuu/instructions/items/<id>.md (kind: amendment; body = the text).
|
|
11
|
+
// The pinned steering itself lives at items/steering.md; future
|
|
12
|
+
// amendments are MORE items, never edits to steering. Idempotent: a
|
|
13
|
+
// text already present in any instructions item is not duplicated.
|
|
11
14
|
//
|
|
12
15
|
// Registers itself on import.
|
|
13
16
|
|
|
14
|
-
import { join } from 'node:path';
|
|
15
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
16
17
|
import * as registry from '../faculty/registry.mjs';
|
|
18
|
+
import { listFacultyItems, writeFacultyItem } from '../faculty/items.mjs';
|
|
19
|
+
import { deriveTitle } from '../faculty/envelope.mjs';
|
|
20
|
+
import { slugify } from '../knowledge/items.mjs';
|
|
17
21
|
|
|
18
22
|
const name = 'instructions';
|
|
19
23
|
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// helpers
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
function projectMdPath(agentDir) {
|
|
25
|
-
return join(agentDir, 'instructions', 'project.md');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
24
|
// ---------------------------------------------------------------------------
|
|
29
25
|
// adapter contract
|
|
30
26
|
// ---------------------------------------------------------------------------
|
|
@@ -50,29 +46,33 @@ function validate(_agentDir, payload) {
|
|
|
50
46
|
}
|
|
51
47
|
|
|
52
48
|
/**
|
|
53
|
-
* Apply an approved amendment:
|
|
54
|
-
* identical
|
|
49
|
+
* Apply an approved amendment: write an amendment item (idempotent on
|
|
50
|
+
* identical text — won't duplicate steering already present in any item).
|
|
55
51
|
* @returns {{ok:boolean, action:string, itemIds:string[]}}
|
|
56
52
|
*/
|
|
57
53
|
function apply(agentDir, proposal) {
|
|
58
|
-
const text = proposal?.payload?.text ?? '';
|
|
59
|
-
|
|
60
|
-
// Ensure the instructions dir exists
|
|
61
|
-
mkdirSync(join(agentDir, 'instructions'), { recursive: true });
|
|
54
|
+
const text = String(proposal?.payload?.text ?? '').trim();
|
|
62
55
|
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
// Idempotence: skip if the exact text is already present
|
|
67
|
-
if (existing.includes(text)) {
|
|
56
|
+
// Idempotence: skip if the exact text already lives in an instructions item
|
|
57
|
+
const { items } = listFacultyItems(agentDir, 'instructions');
|
|
58
|
+
if (items.some((i) => String(i.body ?? '').includes(text))) {
|
|
68
59
|
return { ok: true, action: 'amended instructions (already present)', itemIds: [] };
|
|
69
60
|
}
|
|
70
61
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
62
|
+
const id = proposal?.payload?.id || slugify(text, 50);
|
|
63
|
+
writeFacultyItem(agentDir, {
|
|
64
|
+
id,
|
|
65
|
+
faculty: name,
|
|
66
|
+
kind: 'amendment',
|
|
67
|
+
title: deriveTitle(text, id),
|
|
68
|
+
status: 'active',
|
|
69
|
+
created_at: new Date().toISOString().replace(/\.\d+Z$/, 'Z'),
|
|
70
|
+
provenance: Array.isArray(proposal?.provenance) ? proposal.provenance : [],
|
|
71
|
+
payload: { scope: 'project' },
|
|
72
|
+
body: text,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return { ok: true, action: 'amended instructions', itemIds: [id] };
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/**
|
|
@@ -1,31 +1,35 @@
|
|
|
1
|
-
// Knowledge items — files as truth
|
|
2
|
-
// .zuzuu/knowledge/items/<id>.md:
|
|
3
|
-
// writer and reader; grammar below) + a prose body (the fact in your voice).
|
|
1
|
+
// Knowledge items — files as truth, in the Faculty Standard envelope (W24).
|
|
2
|
+
// One item per markdown file under .zuzuu/knowledge/items/<id>.md:
|
|
4
3
|
//
|
|
5
4
|
// ---
|
|
6
5
|
// id: test-command
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// faculty: knowledge
|
|
7
|
+
// kind: command
|
|
8
|
+
// title: The test command
|
|
9
9
|
// status: active
|
|
10
|
-
//
|
|
11
|
-
// command: npm test
|
|
12
|
-
// relations:
|
|
13
|
-
// - type: relates-to
|
|
14
|
-
// target: ci-pipeline
|
|
15
|
-
// commentary: optional
|
|
10
|
+
// created_at: 2026-06-10T12:00:00Z
|
|
16
11
|
// provenance:
|
|
17
12
|
// - session: ses_abc
|
|
18
13
|
// ref: occurrences=12
|
|
14
|
+
// payload:
|
|
15
|
+
// type: command
|
|
16
|
+
// attributes:
|
|
17
|
+
// command: npm test
|
|
18
|
+
// relations:
|
|
19
|
+
// - type: relates-to
|
|
20
|
+
// target: ci-pipeline
|
|
19
21
|
// ---
|
|
20
22
|
// Body prose.
|
|
21
23
|
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
24
|
+
// This module is a thin wrapper over faculty/envelope.mjs: the ON-DISK format
|
|
25
|
+
// is the envelope; the IN-MEMORY item shape stays the historical knowledge one
|
|
26
|
+
// ({id, type, created_at, status, attributes, relations, provenance, body}) so
|
|
27
|
+
// the registry/ER/index/digest pipeline is untouched. Ids are unchanged by the
|
|
28
|
+
// standard — only frontmatter keys moved (type/attributes/relations → payload).
|
|
26
29
|
|
|
27
30
|
import { join } from 'node:path';
|
|
28
31
|
import { existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync } from 'node:fs';
|
|
32
|
+
import { parseEnvelope, serializeEnvelope, deriveTitle } from '../faculty/envelope.mjs';
|
|
29
33
|
|
|
30
34
|
export const itemsDir = (agentDir) => join(agentDir, 'knowledge', 'items');
|
|
31
35
|
|
|
@@ -38,88 +42,49 @@ export function slugify(text, max = 60) {
|
|
|
38
42
|
.replace(/-+$/, '') || 'item';
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const quoteIfNeeded = (s) => {
|
|
46
|
-
const t = String(s);
|
|
47
|
-
if (t.includes('\n')) throw new Error('item values must be single-line');
|
|
48
|
-
return /[:#'"\[\]{}]|^\s|\s$/.test(t) ? JSON.stringify(t) : t;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
/** Parse an item file's text → item object. Throws on grammar violations. */
|
|
45
|
+
/**
|
|
46
|
+
* Parse an item file's text → the in-memory knowledge item. Throws on grammar
|
|
47
|
+
* violations (callers catch — allItems collects, inbox falls back to prose).
|
|
48
|
+
*/
|
|
52
49
|
export function parseItem(text) {
|
|
53
|
-
const
|
|
54
|
-
if (!
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (['attributes', 'relations', 'provenance'].includes(key)) {
|
|
69
|
-
section = key;
|
|
70
|
-
if (val) throw new Error(`${key} must be a block`);
|
|
71
|
-
} else {
|
|
72
|
-
section = null;
|
|
73
|
-
item[key] = unquote(val);
|
|
74
|
-
}
|
|
75
|
-
} else if (section === 'attributes') {
|
|
76
|
-
const kv = line.match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
|
|
77
|
-
if (!kv) throw new Error(`bad attribute line: ${line}`);
|
|
78
|
-
item.attributes[kv[1]] = unquote(kv[2]);
|
|
79
|
-
} else if (section === 'relations' || section === 'provenance') {
|
|
80
|
-
if (line.startsWith('- ')) {
|
|
81
|
-
current = {};
|
|
82
|
-
item[section].push(current);
|
|
83
|
-
const kv = line.slice(2).match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
|
|
84
|
-
if (!kv) throw new Error(`bad ${section} entry: ${line}`);
|
|
85
|
-
current[kv[1]] = unquote(kv[2]);
|
|
86
|
-
} else {
|
|
87
|
-
if (!current) throw new Error(`${section} entry continuation without "-": ${line}`);
|
|
88
|
-
const kv = line.match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
|
|
89
|
-
if (!kv) throw new Error(`bad ${section} line: ${line}`);
|
|
90
|
-
current[kv[1]] = unquote(kv[2]);
|
|
91
|
-
}
|
|
92
|
-
} else {
|
|
93
|
-
throw new Error(`unexpected indented line: ${line}`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
if (!item.id) throw new Error('item missing id');
|
|
50
|
+
const { ok, item: env, errors } = parseEnvelope(text);
|
|
51
|
+
if (!ok) throw new Error(errors[0] ?? 'invalid envelope');
|
|
52
|
+
if (env.faculty !== 'knowledge') throw new Error(`not a knowledge item (faculty: ${env.faculty})`);
|
|
53
|
+
const p = env.payload ?? {};
|
|
54
|
+
const item = {
|
|
55
|
+
attributes: p.attributes ?? {},
|
|
56
|
+
relations: p.relations ?? [],
|
|
57
|
+
provenance: env.provenance ?? [],
|
|
58
|
+
body: env.body,
|
|
59
|
+
};
|
|
60
|
+
item.id = env.id;
|
|
61
|
+
item.type = p.type ?? env.kind;
|
|
62
|
+
if (env.created_at != null) item.created_at = env.created_at;
|
|
63
|
+
if (env.updated_at != null) item.updated_at = env.updated_at;
|
|
64
|
+
if (env.status != null) item.status = env.status;
|
|
97
65
|
if (!item.type) throw new Error('item missing type');
|
|
98
66
|
return item;
|
|
99
67
|
}
|
|
100
68
|
|
|
101
|
-
/** Serialize an item
|
|
69
|
+
/** Serialize an in-memory knowledge item → envelope file text. */
|
|
102
70
|
export function serializeItem(item) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
lines.push('---', '');
|
|
122
|
-
return lines.join('\n') + (item.body ? item.body.trim() + '\n' : '');
|
|
71
|
+
if (!item.id) throw new Error('item missing id');
|
|
72
|
+
if (!item.type) throw new Error('item missing type');
|
|
73
|
+
const payload = { type: item.type };
|
|
74
|
+
if (Object.keys(item.attributes ?? {}).length) payload.attributes = item.attributes;
|
|
75
|
+
if ((item.relations ?? []).length) payload.relations = item.relations;
|
|
76
|
+
return serializeEnvelope({
|
|
77
|
+
id: item.id,
|
|
78
|
+
faculty: 'knowledge',
|
|
79
|
+
kind: item.type,
|
|
80
|
+
title: item.title ?? deriveTitle(item.body, item.id),
|
|
81
|
+
status: item.status,
|
|
82
|
+
created_at: item.created_at,
|
|
83
|
+
updated_at: item.updated_at,
|
|
84
|
+
provenance: item.provenance ?? [],
|
|
85
|
+
payload,
|
|
86
|
+
body: item.body,
|
|
87
|
+
});
|
|
123
88
|
}
|
|
124
89
|
|
|
125
90
|
/** Write an item to its canonical file. Returns the path. */
|
package/zuzuu/live/install.mjs
CHANGED
|
@@ -13,7 +13,7 @@ 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 .zuzuu/guardrails/rules
|
|
16
|
+
// sit on the hot path: it evaluates the .zuzuu/guardrails/items/ rules 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'];
|
package/zuzuu/memory/adapter.mjs
CHANGED
|
@@ -1,58 +1,26 @@
|
|
|
1
1
|
// zuzuu/memory/adapter.mjs
|
|
2
|
-
// The Memory faculty adapter
|
|
2
|
+
// The Memory faculty adapter. Wraps episode proposals behind the
|
|
3
3
|
// faculty-spine adapter contract — { name, ingest, validate, apply, render } —
|
|
4
4
|
// so `zuzuu review` can surface and approve memory entries uniformly.
|
|
5
5
|
//
|
|
6
|
-
// A memory proposal payload is an episode record
|
|
7
|
-
// { id, date, title, provenance, body }
|
|
6
|
+
// A memory proposal payload is an episode record:
|
|
7
|
+
// { id, date, title, provenance: {sessions, hosts}, tags, body }
|
|
8
8
|
// id format: mem-<YYYY-MM-DD>-<slug>
|
|
9
9
|
//
|
|
10
|
-
// apply: writes .zuzuu/memory/entries/<id>.md
|
|
11
|
-
//
|
|
10
|
+
// apply: writes .zuzuu/memory/entries/<id>.md as a Faculty Standard envelope
|
|
11
|
+
// (kind: episode; payload = {sessions, hosts, tags}; body = the
|
|
12
|
+
// Attempted / Resulted / Remember-next-time sections).
|
|
12
13
|
//
|
|
13
14
|
// Registers itself on import.
|
|
14
15
|
|
|
15
|
-
import { join } from 'node:path';
|
|
16
|
-
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
17
16
|
import * as registry from '../faculty/registry.mjs';
|
|
17
|
+
import { writeFacultyItem } from '../faculty/items.mjs';
|
|
18
18
|
|
|
19
19
|
const name = 'memory';
|
|
20
20
|
|
|
21
21
|
// mem-<YYYY-MM-DD>-<slug>: the id must START with "mem-"
|
|
22
22
|
const MEM_ID_RE = /^mem-/;
|
|
23
23
|
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
// helpers
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
|
|
28
|
-
function entriesDir(agentDir) {
|
|
29
|
-
return join(agentDir, 'memory', 'entries');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function entryPath(agentDir, id) {
|
|
33
|
-
return join(entriesDir(agentDir), `${id}.md`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Render YAML frontmatter block from the payload fields. */
|
|
37
|
-
function renderFrontmatter(payload) {
|
|
38
|
-
const lines = ['---'];
|
|
39
|
-
lines.push(`id: ${payload.id}`);
|
|
40
|
-
if (payload.date) lines.push(`date: ${payload.date}`);
|
|
41
|
-
if (payload.title) lines.push(`title: ${payload.title}`);
|
|
42
|
-
if (payload.provenance) {
|
|
43
|
-
lines.push('provenance:');
|
|
44
|
-
const p = payload.provenance;
|
|
45
|
-
if (Array.isArray(p.sessions)) lines.push(` sessions: [${p.sessions.join(', ')}]`);
|
|
46
|
-
if (Array.isArray(p.hosts)) lines.push(` hosts: [${p.hosts.join(', ')}]`);
|
|
47
|
-
}
|
|
48
|
-
if (Array.isArray(payload.tags) && payload.tags.length) {
|
|
49
|
-
lines.push(`tags: [${payload.tags.join(', ')}]`);
|
|
50
|
-
}
|
|
51
|
-
lines.push('status: curated');
|
|
52
|
-
lines.push('---');
|
|
53
|
-
return lines.join('\n');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
24
|
// ---------------------------------------------------------------------------
|
|
57
25
|
// adapter contract
|
|
58
26
|
// ---------------------------------------------------------------------------
|
|
@@ -83,22 +51,29 @@ function validate(_agentDir, payload) {
|
|
|
83
51
|
}
|
|
84
52
|
|
|
85
53
|
/**
|
|
86
|
-
* Apply an approved episode proposal: write the entry
|
|
54
|
+
* Apply an approved episode proposal: write the envelope entry file.
|
|
87
55
|
* @returns {{ok:boolean, action:string, itemIds:string[]}}
|
|
88
56
|
*/
|
|
89
57
|
function apply(agentDir, proposal) {
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
58
|
+
const p = proposal?.payload ?? {};
|
|
59
|
+
const envPayload = {};
|
|
60
|
+
if (Array.isArray(p.provenance?.sessions) && p.provenance.sessions.length) envPayload.sessions = p.provenance.sessions.map(String);
|
|
61
|
+
if (Array.isArray(p.provenance?.hosts) && p.provenance.hosts.length) envPayload.hosts = p.provenance.hosts.map(String);
|
|
62
|
+
if (Array.isArray(p.tags) && p.tags.length) envPayload.tags = p.tags.map(String);
|
|
63
|
+
|
|
64
|
+
writeFacultyItem(agentDir, {
|
|
65
|
+
id: p.id,
|
|
66
|
+
faculty: name,
|
|
67
|
+
kind: 'episode',
|
|
68
|
+
title: p.title,
|
|
69
|
+
status: 'active',
|
|
70
|
+
created_at: p.date || new Date().toISOString().replace(/\.\d+Z$/, 'Z'),
|
|
71
|
+
provenance: Array.isArray(proposal?.provenance) ? proposal.provenance : [],
|
|
72
|
+
payload: envPayload,
|
|
73
|
+
body: p.body ?? '',
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return { ok: true, action: `wrote memory ${p.id}`, itemIds: [p.id] };
|
|
102
77
|
}
|
|
103
78
|
|
|
104
79
|
/**
|
package/zuzuu/miners/actions.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { join } from 'node:path';
|
|
|
9
9
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
10
10
|
import { slugify } from '../knowledge/items.mjs';
|
|
11
11
|
import { isSafeSlug, actionsDir, inboxDir } from '../actions/manifest.mjs';
|
|
12
|
+
import { serializeEnvelope } from '../faculty/envelope.mjs';
|
|
12
13
|
import { register } from './registry.mjs';
|
|
13
14
|
|
|
14
15
|
// Must match the constant in knowledge/distill.mjs (adjacent Bash separator).
|
|
@@ -86,27 +87,20 @@ export function propose(agentDir, aggregated) {
|
|
|
86
87
|
|
|
87
88
|
mkdirSync(inboxSlug, { recursive: true });
|
|
88
89
|
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
slug,
|
|
92
|
-
title,
|
|
93
|
-
description: `Recurring command sequence detected from session traces: ${steps.join(' → ')}.`,
|
|
94
|
-
promptSnippet,
|
|
95
|
-
};
|
|
96
|
-
writeFileSync(join(inboxSlug, 'action.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
97
|
-
|
|
98
|
-
// SKILL.md — numbered runbook steps.
|
|
90
|
+
// ACTION.md — a runbook envelope (no run.mjs; the body IS the procedure).
|
|
91
|
+
// The body's first line is the digest one-liner (promptSnippet).
|
|
99
92
|
const stepsBlock = steps.map((cmd, i) => `${i + 1}. \`${cmd}\``).join('\n');
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
93
|
+
writeFileSync(join(inboxSlug, 'ACTION.md'), serializeEnvelope({
|
|
94
|
+
id: slug,
|
|
95
|
+
faculty: 'actions',
|
|
96
|
+
kind: 'runbook',
|
|
97
|
+
title,
|
|
98
|
+
status: 'active',
|
|
99
|
+
created_at: new Date().toISOString().replace(/\.\d+Z$/, 'Z'),
|
|
100
|
+
provenance: [],
|
|
101
|
+
payload: {},
|
|
102
|
+
body: `${promptSnippet}\n\nRecurring command sequence detected from session traces.\n\n## Steps\n\n${stepsBlock}`,
|
|
103
|
+
}));
|
|
110
104
|
|
|
111
105
|
count++;
|
|
112
106
|
}
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
// Self-registers on import.
|
|
17
17
|
|
|
18
18
|
import { join } from 'node:path';
|
|
19
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
20
19
|
import { slugify } from '../knowledge/items.mjs';
|
|
21
20
|
import { makeProposal, writeProposal, listProposals, isArchivedResolved } from '../faculty/proposal.mjs';
|
|
21
|
+
import { loadRules as loadRuleItems } from '../guardrails.mjs';
|
|
22
22
|
import { register } from './registry.mjs';
|
|
23
23
|
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
@@ -48,14 +48,12 @@ function guardId(cmd) {
|
|
|
48
48
|
return 'guard-' + slugify(cmd, 50);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/**
|
|
52
|
-
function
|
|
53
|
-
const path = join(agentDir, 'guardrails', 'rules.json');
|
|
54
|
-
if (!existsSync(path)) return { version: 1, rules: [] };
|
|
51
|
+
/** Ids of live rule items (guardrails/items/*.md); absent/unreadable → none. */
|
|
52
|
+
function liveRuleIds(agentDir) {
|
|
55
53
|
try {
|
|
56
|
-
return
|
|
54
|
+
return new Set(loadRuleItems(join(agentDir, 'guardrails')).rules.map((r) => r.id));
|
|
57
55
|
} catch {
|
|
58
|
-
return
|
|
56
|
+
return new Set();
|
|
59
57
|
}
|
|
60
58
|
}
|
|
61
59
|
|
|
@@ -126,7 +124,7 @@ export function aggregate(sessions, { minFailures = 3, minSessions = 2 } = {}) {
|
|
|
126
124
|
* Write a guardrails proposal into .zuzuu/guardrails/proposals/ for each candidate.
|
|
127
125
|
* Idempotent:
|
|
128
126
|
* - skips if a guardrails proposal with the same payload.id already exists
|
|
129
|
-
* - skips if
|
|
127
|
+
* - skips if a live rule item (guardrails/items/<id>.md) already exists
|
|
130
128
|
* - skips if the id is already resolved in proposals/archive/ — a rejection
|
|
131
129
|
* is remembered; re-distilling never resurrects it
|
|
132
130
|
*
|
|
@@ -141,9 +139,8 @@ export function propose(agentDir, aggregated) {
|
|
|
141
139
|
const existing = listProposals(agentDir, 'guardrails');
|
|
142
140
|
const existingIds = new Set(existing.map((p) => p.payload?.id).filter(Boolean));
|
|
143
141
|
|
|
144
|
-
//
|
|
145
|
-
const
|
|
146
|
-
const rulesIds = new Set((rulesData.rules ?? []).map((r) => r.id).filter(Boolean));
|
|
142
|
+
// Live rule items (ids already applied).
|
|
143
|
+
const rulesIds = liveRuleIds(agentDir);
|
|
147
144
|
|
|
148
145
|
let count = 0;
|
|
149
146
|
for (const c of aggregated) {
|