@zuzuucodes/cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -0
  3. package/bin/zuzuu.mjs +133 -0
  4. package/experiments/experiment-1-trace-capture/adapters/claude-code.mjs +220 -0
  5. package/experiments/experiment-1-trace-capture/adapters/codex.mjs +201 -0
  6. package/experiments/experiment-1-trace-capture/adapters/gemini-cli.mjs +113 -0
  7. package/experiments/experiment-1-trace-capture/adapters/host-adapter.mjs +43 -0
  8. package/experiments/experiment-1-trace-capture/adapters/opencode.mjs +205 -0
  9. package/experiments/experiment-1-trace-capture/adapters/pi.mjs +218 -0
  10. package/experiments/experiment-1-trace-capture/adapters/registry.mjs +20 -0
  11. package/experiments/experiment-1-trace-capture/adapters/signals.mjs +44 -0
  12. package/experiments/experiment-1-trace-capture/core/event.mjs +58 -0
  13. package/experiments/experiment-1-trace-capture/core/ids.mjs +32 -0
  14. package/experiments/experiment-1-trace-capture/core/otlp.mjs +54 -0
  15. package/experiments/experiment-1-trace-capture/core/render.mjs +63 -0
  16. package/experiments/experiment-1-trace-capture/core/spans.mjs +43 -0
  17. package/package.json +56 -0
  18. package/zuzuu/actions/adapter.mjs +130 -0
  19. package/zuzuu/actions/convert.mjs +27 -0
  20. package/zuzuu/actions/dispatch.mjs +87 -0
  21. package/zuzuu/actions/inbox.mjs +56 -0
  22. package/zuzuu/actions/manifest.mjs +72 -0
  23. package/zuzuu/actions/marker.mjs +4 -0
  24. package/zuzuu/actions/runner.mjs +37 -0
  25. package/zuzuu/actions/schema.mjs +73 -0
  26. package/zuzuu/actions/trail.mjs +22 -0
  27. package/zuzuu/capture-core.mjs +49 -0
  28. package/zuzuu/commands/act-author.mjs +72 -0
  29. package/zuzuu/commands/act.mjs +101 -0
  30. package/zuzuu/commands/capture.mjs +32 -0
  31. package/zuzuu/commands/code.mjs +84 -0
  32. package/zuzuu/commands/digest.mjs +23 -0
  33. package/zuzuu/commands/distill.mjs +46 -0
  34. package/zuzuu/commands/doctor.mjs +197 -0
  35. package/zuzuu/commands/enable.mjs +195 -0
  36. package/zuzuu/commands/eval.mjs +101 -0
  37. package/zuzuu/commands/explain.mjs +119 -0
  38. package/zuzuu/commands/generation.mjs +107 -0
  39. package/zuzuu/commands/hook.mjs +209 -0
  40. package/zuzuu/commands/inbox.mjs +73 -0
  41. package/zuzuu/commands/init.mjs +89 -0
  42. package/zuzuu/commands/knowledge.mjs +152 -0
  43. package/zuzuu/commands/migrate.mjs +125 -0
  44. package/zuzuu/commands/review.mjs +299 -0
  45. package/zuzuu/commands/status.mjs +82 -0
  46. package/zuzuu/commands/trace.mjs +19 -0
  47. package/zuzuu/digest.mjs +149 -0
  48. package/zuzuu/eval/rank.mjs +31 -0
  49. package/zuzuu/eval/score.mjs +85 -0
  50. package/zuzuu/eval/signals.mjs +57 -0
  51. package/zuzuu/faculty/contract.mjs +19 -0
  52. package/zuzuu/faculty/gate.mjs +65 -0
  53. package/zuzuu/faculty/generation.mjs +392 -0
  54. package/zuzuu/faculty/proposal.mjs +166 -0
  55. package/zuzuu/faculty/provenance.mjs +35 -0
  56. package/zuzuu/faculty/registry.mjs +33 -0
  57. package/zuzuu/faculty/trail.mjs +27 -0
  58. package/zuzuu/guardrails/adapter.mjs +134 -0
  59. package/zuzuu/guardrails.mjs +89 -0
  60. package/zuzuu/inject.mjs +46 -0
  61. package/zuzuu/instructions/adapter.mjs +93 -0
  62. package/zuzuu/knowledge/adapter.mjs +99 -0
  63. package/zuzuu/knowledge/distill.mjs +237 -0
  64. package/zuzuu/knowledge/embed.mjs +52 -0
  65. package/zuzuu/knowledge/er.mjs +98 -0
  66. package/zuzuu/knowledge/inbox.mjs +43 -0
  67. package/zuzuu/knowledge/index.mjs +194 -0
  68. package/zuzuu/knowledge/items.mjs +154 -0
  69. package/zuzuu/knowledge/proposals.mjs +196 -0
  70. package/zuzuu/knowledge/registry.mjs +115 -0
  71. package/zuzuu/live/install.mjs +76 -0
  72. package/zuzuu/live/live-store.mjs +78 -0
  73. package/zuzuu/live/probe.mjs +55 -0
  74. package/zuzuu/live/reconcile.mjs +33 -0
  75. package/zuzuu/memory/adapter.mjs +121 -0
  76. package/zuzuu/miners/actions.mjs +118 -0
  77. package/zuzuu/miners/guardrails.mjs +174 -0
  78. package/zuzuu/miners/instructions.mjs +152 -0
  79. package/zuzuu/miners/knowledge.mjs +22 -0
  80. package/zuzuu/miners/memory.mjs +27 -0
  81. package/zuzuu/miners/registry.mjs +31 -0
  82. package/zuzuu/scaffold.mjs +213 -0
  83. package/zuzuu/session.mjs +72 -0
  84. package/zuzuu/store.mjs +104 -0
@@ -0,0 +1,72 @@
1
+ // zuzuu/actions/manifest.mjs
2
+ // Reads the Actions faculty off disk: one action per dir under agent/actions/.
3
+ // Two kinds — `script` (has run.mjs + action.json) and `runbook` (SKILL.md prose).
4
+ // Pure-ish: filesystem reads only, no logging, no process control.
5
+
6
+ import { join } from 'node:path';
7
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
8
+
9
+ // Action slugs: letters/digits start, then letters/digits/-/_. No dots or slashes
10
+ // → cannot escape agent/actions/ via path traversal.
11
+ export const SAFE_SLUG = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
12
+ export function isSafeSlug(slug) {
13
+ return typeof slug === 'string' && SAFE_SLUG.test(slug);
14
+ }
15
+
16
+ export const actionsDir = (agentDir) => join(agentDir, 'actions');
17
+ export const inboxDir = (agentDir) => join(actionsDir(agentDir), 'inbox');
18
+ const actionDir = (agentDir, slug) => join(actionsDir(agentDir), slug);
19
+
20
+ /** Read action.json for a slug → object, or null if absent/unparseable. */
21
+ export function loadManifest(agentDir, slug) {
22
+ const path = join(actionDir(agentDir, slug), 'action.json');
23
+ try {
24
+ return JSON.parse(readFileSync(path, 'utf8'));
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ /** Pull `name` / `description` from a SKILL.md YAML-ish frontmatter (best-effort). */
31
+ function skillFrontmatter(text) {
32
+ const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
33
+ const fm = {};
34
+ if (m) {
35
+ for (const line of m[1].split('\n')) {
36
+ const kv = line.match(/^(\w+):\s*(.*)$/);
37
+ if (kv) fm[kv[1]] = kv[2].trim();
38
+ }
39
+ }
40
+ return fm;
41
+ }
42
+
43
+ /**
44
+ * List actions in a base dir as {slug, kind, title, promptSnippet}.
45
+ * `script` = dir has run.mjs; `runbook` = dir has SKILL.md; other entries skipped.
46
+ * Reads the manifest directly from each entry dir (works for any baseDir, e.g. the inbox).
47
+ */
48
+ export function listActions(baseDir) {
49
+ if (!existsSync(baseDir)) return [];
50
+ const out = [];
51
+ for (const name of readdirSync(baseDir)) {
52
+ const d = join(baseDir, name);
53
+ let isDir = false;
54
+ try { isDir = statSync(d).isDirectory(); } catch { /* skip */ }
55
+ if (!isDir) continue; // ignores README.md and any stray files
56
+ if (existsSync(join(d, 'run.mjs'))) {
57
+ let man = {};
58
+ try { man = JSON.parse(readFileSync(join(d, 'action.json'), 'utf8')); } catch { /* slug fallback */ }
59
+ out.push({ slug: name, kind: 'script', title: man.title ?? name, promptSnippet: man.promptSnippet ?? man.description ?? name });
60
+ } else if (existsSync(join(d, 'SKILL.md'))) {
61
+ let fm = {};
62
+ try { fm = skillFrontmatter(readFileSync(join(d, 'SKILL.md'), 'utf8')); } catch { /* slug fallback */ }
63
+ out.push({ slug: name, kind: 'runbook', title: fm.name ?? name, promptSnippet: fm.description ?? name });
64
+ }
65
+ }
66
+ return out;
67
+ }
68
+
69
+ /** Active actions under agent/actions/ (the inbox subdir is excluded). */
70
+ export function allActions(agentDir) {
71
+ return listActions(actionsDir(agentDir)).filter((a) => a.slug !== 'inbox');
72
+ }
@@ -0,0 +1,4 @@
1
+ // zuzuu/actions/marker.mjs
2
+ // The result-marker sentinel, in its own module so importing it has NO side
3
+ // effects (runner.mjs runs harness logic at top-level and must never be imported).
4
+ export const MARKER = '__ZUZUU_ACT_RESULT__';
@@ -0,0 +1,37 @@
1
+ // zuzuu/actions/runner.mjs
2
+ // The child harness spawned by dispatch.runAction. NOT imported by anything —
3
+ // it's executed: `node runner.mjs <payloadJson>`. It runs the action in its own
4
+ // process (isolating process.exit/throw), then prints exactly one result marker.
5
+ //
6
+ // payload = { runPath, inputs, outputs, default_args, args }
7
+ // stdout: the action's own logs, then a final line `__ZUZUU_ACT_RESULT__<json>`.
8
+
9
+ import { pathToFileURL } from 'node:url';
10
+ import { validateInputs, validateOutputs } from './schema.mjs';
11
+ import { MARKER } from './marker.mjs';
12
+
13
+ function emit(obj) {
14
+ process.stdout.write('\n' + MARKER + JSON.stringify(obj) + '\n');
15
+ }
16
+
17
+ const payload = JSON.parse(process.argv[2] || '{}');
18
+ try {
19
+ const mod = await import(pathToFileURL(payload.runPath).href);
20
+ if (typeof mod.main !== 'function') {
21
+ emit({ ok: false, error: 'not_runnable', detail: 'run.mjs must export an async main(args)' });
22
+ } else {
23
+ let merged = { ...(payload.default_args ?? {}), ...(payload.args ?? {}) };
24
+ if (typeof mod.prepareArguments === 'function') merged = mod.prepareArguments(merged);
25
+ const vi = validateInputs(payload.inputs, {}, merged);
26
+ if (!vi.ok) {
27
+ emit({ ok: false, error: 'invalid_input', detail: vi.error });
28
+ } else {
29
+ const result = await mod.main(vi.args);
30
+ const vo = validateOutputs(payload.outputs, result);
31
+ if (!vo.ok) emit({ ok: false, error: 'invalid_output', detail: vo.error });
32
+ else emit({ ok: true, value: vo.value });
33
+ }
34
+ }
35
+ } catch (e) {
36
+ emit({ ok: false, error: 'script_error', detail: String((e && e.message) || e) });
37
+ }
@@ -0,0 +1,73 @@
1
+ // zuzuu/actions/schema.mjs
2
+ // A hand-rolled JSON-Schema *subset* validator — zero-dep (no Ajv), matching the
3
+ // project's node-builtins-only policy. Supports: object (properties, required),
4
+ // array (items), string/number/integer/boolean scalars, enum, and basic length/
5
+ // range constraints. Returns an array of error strings ([] = valid). No coercion:
6
+ // values are expected to already carry real JSON types.
7
+
8
+ function isPlainObject(v) {
9
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
10
+ }
11
+
12
+ /** @returns {string[]} error messages; empty array = valid */
13
+ export function validate(schema, value, path = '$') {
14
+ const errors = [];
15
+ if (!schema || typeof schema !== 'object') return errors; // no schema → accept
16
+ const type = schema.type;
17
+
18
+ if (type === 'object') {
19
+ if (!isPlainObject(value)) return [`${path}: expected object`];
20
+ for (const req of schema.required ?? []) {
21
+ if (!(req in value)) errors.push(`${path}.${req}: required`);
22
+ }
23
+ for (const [k, sub] of Object.entries(schema.properties ?? {})) {
24
+ if (k in value) errors.push(...validate(sub, value[k], `${path}.${k}`));
25
+ }
26
+ return errors;
27
+ }
28
+
29
+ if (type === 'array') {
30
+ if (!Array.isArray(value)) return [`${path}: expected array`];
31
+ if (schema.items) value.forEach((v, i) => errors.push(...validate(schema.items, v, `${path}[${i}]`)));
32
+ return errors;
33
+ }
34
+
35
+ if (type === 'string' && typeof value !== 'string') errors.push(`${path}: expected string`);
36
+ else if (type === 'number' && (typeof value !== 'number' || Number.isNaN(value))) errors.push(`${path}: expected number`);
37
+ else if (type === 'integer' && !Number.isInteger(value)) errors.push(`${path}: expected integer`);
38
+ else if (type === 'boolean' && typeof value !== 'boolean') errors.push(`${path}: expected boolean`);
39
+
40
+ if (schema.enum && errors.length === 0 && !schema.enum.includes(value)) errors.push(`${path}: must be one of ${schema.enum.join(', ')}`);
41
+ if (typeof value === 'string') {
42
+ if (schema.minLength != null && value.length < schema.minLength) errors.push(`${path}: shorter than minLength ${schema.minLength}`);
43
+ if (schema.maxLength != null && value.length > schema.maxLength) errors.push(`${path}: longer than maxLength ${schema.maxLength}`);
44
+ }
45
+ if (typeof value === 'number') {
46
+ if (schema.minimum != null && value < schema.minimum) errors.push(`${path}: below minimum ${schema.minimum}`);
47
+ if (schema.maximum != null && value > schema.maximum) errors.push(`${path}: above maximum ${schema.maximum}`);
48
+ }
49
+
50
+ return errors;
51
+ }
52
+
53
+ /**
54
+ * Validate caller inputs against the manifest `inputs` schema.
55
+ * Merges default_args then caller args (caller wins).
56
+ * @returns {{ok:true,args:object} | {ok:false,error:string,errors:string[]}}
57
+ */
58
+ export function validateInputs(schema, defaults = {}, caller = {}) {
59
+ const args = { ...(defaults ?? {}), ...(caller ?? {}) };
60
+ const errors = validate(schema ?? { type: 'object' }, args);
61
+ return errors.length ? { ok: false, error: errors[0], errors } : { ok: true, args };
62
+ }
63
+
64
+ /**
65
+ * Validate an action's return value against the manifest `outputs` schema.
66
+ * Enforces the main(args) → object contract first.
67
+ * @returns {{ok:true,value:object} | {ok:false,error:string,errors?:string[]}}
68
+ */
69
+ export function validateOutputs(schema, value) {
70
+ if (!isPlainObject(value)) return { ok: false, error: 'action output must be a JSON object' };
71
+ const errors = validate(schema ?? { type: 'object' }, value);
72
+ return errors.length ? { ok: false, error: errors[0], errors } : { ok: true, value };
73
+ }
@@ -0,0 +1,22 @@
1
+ // zuzuu/actions/trail.mjs
2
+ // The actions observability trail (A7): every `zuzuu act` run appends an outcome
3
+ // record to agent/.live/actions.jsonl. This is the "details" side of the result —
4
+ // the agent sees the marker value; the trace keeps the metadata. Fail-soft: a
5
+ // logging failure must never affect the action (mirrors the guardrails decision log).
6
+
7
+ import { join } from 'node:path';
8
+ import { mkdirSync, appendFileSync } from 'node:fs';
9
+ import { liveDir } from '../store.mjs';
10
+
11
+ /** Append a fail-soft outcome record. Never throws. */
12
+ export function recordOutcome(agentDir, { slug, ok, error } = {}) {
13
+ try {
14
+ const dir = liveDir(agentDir);
15
+ mkdirSync(dir, { recursive: true });
16
+ const rec = { at: new Date().toISOString(), slug, ok: !!ok };
17
+ if (error) rec.error = error;
18
+ appendFileSync(join(dir, 'actions.jsonl'), JSON.stringify(rec) + '\n');
19
+ } catch {
20
+ /* logging must never affect the action */
21
+ }
22
+ }
@@ -0,0 +1,49 @@
1
+ // Shared capture: transcript -> OTLP trace blob + git-native session record.
2
+ // Used by `zuzuu capture` (post-hoc, status `captured`) and the live lifecycle
3
+ // (`hook`/`reconcile`, statuses active/completed/abandoned). One proven path —
4
+ // Design B: the hook never builds spans, it re-runs THIS.
5
+
6
+ import { eventsToSpans } from '../experiments/experiment-1-trace-capture/core/spans.mjs';
7
+ import { toExportRequest } from '../experiments/experiment-1-trace-capture/core/otlp.mjs';
8
+ import { EventKind, Status } from '../experiments/experiment-1-trace-capture/core/event.mjs';
9
+ import { makeSession, SessionState } from './session.mjs';
10
+ import { writeTrace, upsertSession, gitInfo } from './store.mjs';
11
+
12
+ export function countsOf(trace) {
13
+ return {
14
+ turns: trace.events.filter((e) => e.kind === EventKind.TURN).length,
15
+ tools: trace.events.filter((e) => e.kind === EventKind.TOOL_CALL).length,
16
+ errors: trace.events.filter((e) => e.kind === EventKind.TOOL_CALL && e.status === Status.ERROR).length,
17
+ };
18
+ }
19
+
20
+ /**
21
+ * Parse a transcript via `adapter`, write the OTLP blob, upsert the index record.
22
+ * Idempotent (deterministic ids) — safe to re-run on every lifecycle signal.
23
+ * @param {string|null} [generation] active generation id — threads from the OPEN
24
+ * hook so every session carries its Run linkage (WS3-T3). Null-safe.
25
+ * @returns {{trace, traceId, spans, traceRef, record, counts}}
26
+ */
27
+ export function captureTrace({ adapter, ref, status = SessionState.CAPTURED, cwd = process.cwd(), generation = null }) {
28
+ const trace = adapter.parse(ref);
29
+ const { traceId, spans } = eventsToSpans(trace);
30
+ const request = toExportRequest({ traceId, spans }, { host: trace.host, sessionId: trace.sessionId });
31
+ const traceRef = writeTrace(trace.host, trace.sessionId, [request], cwd);
32
+
33
+ const root = trace.events.find((e) => e.kind === EventKind.SESSION) || trace.events[0];
34
+ const counts = countsOf(trace);
35
+ const record = makeSession({
36
+ id: trace.sessionId,
37
+ host: trace.host,
38
+ status,
39
+ startedAt: new Date(root.startMs).toISOString(),
40
+ endedAt: new Date(root.endMs).toISOString(),
41
+ traceId,
42
+ traceRef,
43
+ git: gitInfo(cwd),
44
+ counts,
45
+ generation,
46
+ });
47
+ upsertSession(record, cwd);
48
+ return { trace, traceId, spans, traceRef, record, counts };
49
+ }
@@ -0,0 +1,72 @@
1
+ // zuzuu/commands/act-author.mjs
2
+ // `zuzuu act new <slug>` — scaffold a script action (idempotent, no-clobber).
3
+ // `zuzuu act schema <slug> [--mcp|--openai|--anthropic]` — convert the manifest.
4
+
5
+ import { mkdirSync, existsSync, writeFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { actionsDir, inboxDir, loadManifest, isSafeSlug } from '../actions/manifest.mjs';
8
+ import { toMcpTool, toOpenAITool, toAnthropicTool } from '../actions/convert.mjs';
9
+
10
+ function manifestStub(slug) {
11
+ return JSON.stringify({
12
+ slug,
13
+ title: slug,
14
+ description: 'what this action does',
15
+ promptSnippet: `one line the digest shows for ${slug}`,
16
+ inputs: { type: 'object', properties: {}, required: [] },
17
+ outputs: { type: 'object', properties: {} },
18
+ default_args: {},
19
+ requires: [],
20
+ }, null, 2) + '\n';
21
+ }
22
+
23
+ const RUN_TEMPLATE = `// run.mjs — implement the action. Export async main(args) → a JSON object.
24
+ // Optional: export prepareArguments(args) to fold legacy args before validation.
25
+
26
+ // export function prepareArguments(args) { return args; }
27
+
28
+ export async function main(args) {
29
+ // args is validated against action.json "inputs"; return must match "outputs".
30
+ return { ok: true };
31
+ }
32
+ `;
33
+
34
+ /** Scaffold an action dir under `baseDir/<slug>/`. No-clobber. */
35
+ function scaffoldInto(baseDir, slug) {
36
+ if (!isSafeSlug(slug)) throw new Error(`invalid slug '${slug}' — letters, digits, - and _ only`);
37
+ const dir = join(baseDir, slug);
38
+ mkdirSync(dir, { recursive: true });
39
+ const created = [];
40
+ const write = (name, body) => {
41
+ const p = join(dir, name);
42
+ if (!existsSync(p)) { writeFileSync(p, body); created.push(name); }
43
+ };
44
+ write('action.json', manifestStub(slug));
45
+ write('run.mjs', RUN_TEMPLATE);
46
+ return { created };
47
+ }
48
+
49
+ /** Scaffold a live action (agent/actions/<slug>/). Humans author here directly. */
50
+ export function scaffoldAction(agentDir, slug) {
51
+ return scaffoldInto(actionsDir(agentDir), slug);
52
+ }
53
+
54
+ /** Scaffold a PROPOSED action (agent/actions/inbox/<slug>/) — agents propose here. */
55
+ export function proposeAction(agentDir, slug) {
56
+ return scaffoldInto(inboxDir(agentDir), slug);
57
+ }
58
+
59
+ export function newAction(agentDir, slug) {
60
+ if (!slug) { console.error('usage: zuzuu act new <slug>'); process.exit(1); }
61
+ const { created } = scaffoldAction(agentDir, slug);
62
+ if (created.length) console.log(`scaffolded action '${slug}' → ${created.join(', ')} in agent/actions/${slug}/`);
63
+ else console.log(`action '${slug}' already complete — nothing to do`);
64
+ }
65
+
66
+ export function schema(agentDir, slug, args = {}) {
67
+ if (!slug) { console.error('usage: zuzuu act schema <slug> [--mcp|--openai|--anthropic]'); process.exit(1); }
68
+ const man = loadManifest(agentDir, slug);
69
+ if (!man) { console.error(`no action '${slug}' (missing action.json)`); process.exit(1); }
70
+ const def = args.openai ? toOpenAITool(man) : args.anthropic ? toAnthropicTool(man) : toMcpTool(man);
71
+ console.log(JSON.stringify(def, null, 2));
72
+ }
@@ -0,0 +1,101 @@
1
+ // zuzuu/commands/act.mjs
2
+ // `zuzuu act` — the Actions faculty CLI. The host's Bash invokes this, so each run
3
+ // is an observable span already covered by the guardrails gate. Subcommands:
4
+ // zuzuu act [list] the index (slug · kind · snippet)
5
+ // zuzuu act show <slug> full manifest (script) or SKILL.md (runbook)
6
+ // zuzuu act <slug> [--args J] run a script action
7
+ // zuzuu act new <slug> scaffold (Task 8)
8
+ // zuzuu act schema <slug> convert to a tool definition (Task 8)
9
+
10
+ import { readFileSync, existsSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { paths } from '../store.mjs';
13
+ import { allActions, loadManifest, actionsDir, isSafeSlug } from '../actions/manifest.mjs';
14
+ import { runAction } from '../actions/dispatch.mjs';
15
+ import { MARKER } from '../actions/marker.mjs';
16
+ import { newAction, schema as schemaCmd, proposeAction } from './act-author.mjs';
17
+ import { listProposedActions, activateAction, rejectAction } from '../actions/inbox.mjs';
18
+ import { recordOutcome } from '../actions/trail.mjs';
19
+
20
+ const RESERVED = new Set(['list', 'show', 'new', 'schema', 'propose', 'inbox', 'approve', 'reject']);
21
+
22
+ function requireSlug(slug, usage) {
23
+ if (!slug) { console.error(usage); process.exit(1); }
24
+ if (!isSafeSlug(slug)) { console.error(`invalid slug '${slug}' — letters, digits, - and _ only`); process.exit(1); }
25
+ return slug;
26
+ }
27
+
28
+ function list(agentDir) {
29
+ const actions = allActions(agentDir);
30
+ if (!actions.length) return console.log('no actions yet — scaffold one with `zuzuu act new <slug>`');
31
+ for (const a of actions.sort((x, y) => x.slug.localeCompare(y.slug))) {
32
+ console.log(` ${a.slug} [${a.kind}] ${a.promptSnippet}`);
33
+ }
34
+ }
35
+
36
+ function show(agentDir, slug) {
37
+ if (!slug) { console.error('usage: zuzuu act show <slug>'); process.exit(1); }
38
+ const man = loadManifest(agentDir, slug);
39
+ if (man) return console.log(JSON.stringify(man, null, 2));
40
+ const skill = join(actionsDir(agentDir), slug, 'SKILL.md');
41
+ if (existsSync(skill)) return process.stdout.write(readFileSync(skill, 'utf8'));
42
+ console.error(`no action '${slug}'`);
43
+ process.exit(1);
44
+ }
45
+
46
+ function run(agentDir, slug, args) {
47
+ let callerArgs = {};
48
+ if (args.args) {
49
+ try { callerArgs = JSON.parse(args.args); }
50
+ catch { console.error('--args must be valid JSON'); process.exit(1); }
51
+ }
52
+ const r = runAction(agentDir, slug, callerArgs);
53
+ recordOutcome(agentDir, { slug, ok: r.ok, error: r.ok ? undefined : r.error });
54
+ if (r.logs) process.stdout.write(r.logs + '\n');
55
+ console.log(MARKER + JSON.stringify(r.ok ? { ok: true, value: r.value } : { ok: false, error: r.error, detail: r.detail }));
56
+ if (r.ok) console.log(`✓ ${slug} ok`);
57
+ else console.error(`✗ ${slug}: ${r.error}${r.detail ? ` — ${r.detail}` : ''}`);
58
+ process.exit(r.ok ? 0 : 1);
59
+ }
60
+
61
+ function propose(agentDir, slug) {
62
+ const { created } = proposeAction(agentDir, slug);
63
+ if (created.length) console.log(`proposed action '${slug}' → ${created.join(', ')} in agent/actions/inbox/${slug}/ (review with \`zuzuu review\`)`);
64
+ else console.log(`proposed action '${slug}' already complete — nothing to do`);
65
+ }
66
+
67
+ function inbox(agentDir) {
68
+ const pending = listProposedActions(agentDir);
69
+ if (!pending.length) return console.log('no proposed actions — inbox empty');
70
+ for (const a of pending.sort((x, y) => x.slug.localeCompare(y.slug))) {
71
+ console.log(` ${a.slug} [${a.kind}] ${a.promptSnippet}`);
72
+ }
73
+ }
74
+
75
+ function approve(agentDir, slug) {
76
+ const r = activateAction(agentDir, slug);
77
+ console.log(r.ok ? `✓ activated '${slug}'` : `✗ ${r.error}`);
78
+ process.exit(r.ok ? 0 : 1);
79
+ }
80
+
81
+ function reject(agentDir, slug) {
82
+ const r = rejectAction(agentDir, slug);
83
+ console.log(r.ok ? `✓ rejected '${slug}'` : `✗ ${r.error}`);
84
+ process.exit(r.ok ? 0 : 1);
85
+ }
86
+
87
+ export function act(args) {
88
+ const agentDir = paths().dir;
89
+ const sub = args._[0];
90
+ if (!sub || sub === 'list') return list(agentDir);
91
+ if (sub === 'show') return show(agentDir, requireSlug(args._[1], 'usage: zuzuu act show <slug>'));
92
+ if (sub === 'new') return newAction(agentDir, requireSlug(args._[1], 'usage: zuzuu act new <slug>'));
93
+ if (sub === 'schema') return schemaCmd(agentDir, requireSlug(args._[1], 'usage: zuzuu act schema <slug> [--openai|--anthropic]'), args);
94
+ 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>'));
98
+ // future-reserved guard: extend RESERVED + add a handler above in tandem
99
+ if (RESERVED.has(sub)) { console.error(`unknown: zuzuu act ${sub}`); process.exit(1); }
100
+ return run(agentDir, requireSlug(sub, 'usage: zuzuu act <slug> [--args JSON]'), args);
101
+ }
@@ -0,0 +1,32 @@
1
+ // `zuzuu capture` — post-hoc: parse a host transcript into a git-native trace + record.
2
+
3
+ import { ADAPTERS, byName, detected } from '../../experiments/experiment-1-trace-capture/adapters/registry.mjs';
4
+ import { captureTrace } from '../capture-core.mjs';
5
+ import { paths } from '../store.mjs';
6
+
7
+ function chooseRef(adapter, args) {
8
+ if (args.file) return args.file;
9
+ const sessions = adapter.listSessions({ cwd: process.cwd(), project: args.project });
10
+ let pool = sessions;
11
+ if (args.session) pool = pool.filter((s) => s.sessionId === args.session || s.sessionId.includes(args.session));
12
+ if (!pool.length) throw new Error(`no matching session for ${adapter.name} (found ${sessions.length})`);
13
+ return pool[0].ref;
14
+ }
15
+
16
+ export function capture(args) {
17
+ const adapter = args.host ? byName(args.host) : detected()[0];
18
+ if (!adapter) {
19
+ console.error(args.host ? `unknown host: ${args.host} (known: ${ADAPTERS.map((a) => a.name).join(', ')})` : 'no host detected — run `zuzuu status`');
20
+ process.exit(1);
21
+ }
22
+
23
+ const { record, spans, traceRef, counts } = captureTrace({ adapter, ref: chooseRef(adapter, args) });
24
+ const { index } = paths();
25
+ console.log(`captured ${record.host} session ${record.id}`);
26
+ console.log(` status : ${record.status}`);
27
+ console.log(` spans : ${spans.length} (turns:${counts.turns}, tools:${counts.tools}, errors:${counts.errors})`);
28
+ console.log(` git : ${record.git.commit ? record.git.commit.slice(0, 8) : '(no repo)'} ${record.git.branch || ''}`);
29
+ console.log(` trace : ${traceRef} (git-ignored)`);
30
+ console.log(` indexed : ${index} (tracked)`);
31
+ console.log(` inspect : zuzuu trace --last`);
32
+ }
@@ -0,0 +1,84 @@
1
+ // `zuzuu code` — launch OpenCode as the bundled default host, pre-wired with the
2
+ // faculty home + the zuzuu plugin (capture + gate + digest). We CONFIGURE + LAUNCH
3
+ // the real `opencode` binary; we never fork it and never drive it headlessly
4
+ // (the observe model; interactive-first). Stage 2 of the product sequence.
5
+ //
6
+ // Zero-dep: OpenCode is a runtime PEER — detected, and installed on demand if
7
+ // missing — never an npm dependency.
8
+
9
+ import { existsSync, readSync } from 'node:fs';
10
+ import { resolve } from 'node:path';
11
+ import { spawnSync } from 'node:child_process';
12
+ import { init } from './init.mjs';
13
+ import { enable } from './enable.mjs';
14
+ import { repoRoot } from '../store.mjs';
15
+ import { homeExists } from '../scaffold.mjs';
16
+
17
+ // Run a command inside `dir` without permanently changing the process cwd —
18
+ // init/enable resolve their target from process.cwd(), so we chdir around them.
19
+ function inDir(dir, fn) {
20
+ const prev = process.cwd();
21
+ try { process.chdir(dir); return fn(); } finally { process.chdir(prev); }
22
+ }
23
+
24
+ // --- default (real) deps; tests inject fakes for everything external ---
25
+ const realDetect = () => { try { return spawnSync('opencode', ['--version'], { stdio: 'ignore' }).status === 0; } catch { return false; } };
26
+ const realInstall = () => { try { return spawnSync('npm', ['install', '-g', 'opencode-ai'], { stdio: 'inherit' }).status === 0; } catch { return false; } };
27
+ const realLaunch = ({ cwd, model, passthrough }) => {
28
+ const a = [...(model ? ['-m', model] : []), ...(passthrough || [])];
29
+ const r = spawnSync('opencode', a, { cwd, stdio: 'inherit' });
30
+ return r.status ?? 0;
31
+ };
32
+ // Synchronous y/n. Only reached when opencode is missing AND not --yes; the deps
33
+ // seam means tests never call this. Default to 'y' if stdin can't be read.
34
+ function realPrompt(q) {
35
+ process.stdout.write(`${q} `);
36
+ try { const b = Buffer.alloc(8); const n = readSync(0, b, 0, 8, null); return b.toString('utf8', 0, n).trim().toLowerCase().startsWith('n') ? 'n' : 'y'; }
37
+ catch { return 'y'; }
38
+ }
39
+
40
+ /**
41
+ * `zuzuu code [dir] [--model M] [--yes] [-- …opencode args]`
42
+ * @returns process exit code (number) — bin calls process.exit(code(args)).
43
+ */
44
+ export function code(args = {}, deps = {}) {
45
+ const d = {
46
+ detect: realDetect,
47
+ install: realInstall,
48
+ launch: realLaunch,
49
+ prompt: realPrompt,
50
+ runInit: (dir) => inDir(dir, () => init({ _: [] })),
51
+ runEnable: (dir) => inDir(dir, () => enable({ host: 'opencode', quiet: true })),
52
+ log: (...m) => console.log(...m),
53
+ ...deps,
54
+ };
55
+
56
+ // 1. resolve the project dir
57
+ const dir = args._?.[0] ? resolve(String(args._[0])) : process.cwd();
58
+ if (!existsSync(dir)) { d.log(`zuzuu code: no such directory: ${dir}`); return 1; }
59
+
60
+ // 2. ensure the faculty home (only when absent — keeps output clean; init is idempotent)
61
+ if (!homeExists(repoRoot(dir))) d.runInit(dir);
62
+
63
+ // 3. ensure OpenCode (detect + install-on-demand)
64
+ if (!d.detect()) {
65
+ d.log("OpenCode isn't installed.");
66
+ const consent = args.yes || args.y || d.prompt('Install it now? (npm i -g opencode-ai) [Y/n]') !== 'n';
67
+ if (!consent || !d.install() || !d.detect()) {
68
+ d.log('Install it with: npm i -g opencode-ai');
69
+ return 1;
70
+ }
71
+ }
72
+
73
+ // 4. ensure the zuzuu plugin (capture + gate + digest) — FAIL-OPEN: never block the launch
74
+ let wired = true;
75
+ try { d.runEnable(dir); } catch (e) { wired = false; d.log(`zuzuu code: could not wire the zuzuu plugin (${e?.message || e}) — launching unwired.`); }
76
+
77
+ // a clean one-screen summary of what the newcomer just got (vs. the verbose enable output)
78
+ d.log('zuzuu code → OpenCode, faculty-equipped');
79
+ d.log(` ✓ faculty home (agent/) ${wired ? '✓ capture + guardrails gate ✓ session grounding' : '⚠ plugin not wired (degraded)'}`);
80
+ d.log(` → launching OpenCode in ${dir} …`);
81
+
82
+ // 5. launch the real OpenCode (configure + launch, never drive)
83
+ return d.launch({ cwd: dir, model: args.model || null, passthrough: args['--'] || [] });
84
+ }
@@ -0,0 +1,23 @@
1
+ // zuzuu/commands/digest.mjs
2
+ // `zuzuu digest [--json] [--budget N]` — print the grounding brief a session
3
+ // start would inject. Lets a human (or a hookless host) see exactly what the
4
+ // agent sees.
5
+
6
+ import { paths } from '../store.mjs';
7
+ import { computeDigest } from '../digest.mjs';
8
+
9
+ /** Pure: the digest payload — the zuzuu-web /digest source (the daemon also reads agent/.live/digest.md directly). */
10
+ export function digestData(agentDir, opts = {}) {
11
+ const d = computeDigest(agentDir, opts);
12
+ return { text: d.text ?? '' };
13
+ }
14
+
15
+ export function digest(args) {
16
+ const agentDir = paths().dir;
17
+ const opts = {};
18
+ // guard `--budget` with no value (parseArgs → true → Number(true)===1 → near-empty digest)
19
+ if (args.budget && args.budget !== true) opts.budget = Number(args.budget);
20
+ const d = computeDigest(agentDir, opts);
21
+ if (args.json) console.log(JSON.stringify(d, null, 2));
22
+ else process.stdout.write(d.text);
23
+ }
@@ -0,0 +1,46 @@
1
+ // `zuzuu distill` — mine real sessions into proposals (source A).
2
+ //
3
+ // Default: knowledge only (back-compat, via distillSessions). With
4
+ // `--all-faculties`: mine each transcript ONCE into a superset, then run every
5
+ // registered faculty miner (knowledge today; actions/guardrails/instructions/
6
+ // memory land in later WS5 tasks) over the shared sessions array.
7
+
8
+ import { paths } from '../store.mjs';
9
+ import { distillSessions, transcriptsFor, mineHostSession } from '../knowledge/distill.mjs';
10
+ import * as registry from '../miners/registry.mjs';
11
+ // Import miner modules so they self-register.
12
+ import '../miners/knowledge.mjs';
13
+ import '../miners/actions.mjs';
14
+ import '../miners/guardrails.mjs';
15
+ import '../miners/instructions.mjs';
16
+ import '../miners/memory.mjs';
17
+
18
+ export function distill(args) {
19
+ const scope = args.all ? 'all' : args.session ? null : 'last';
20
+ const pairs = transcriptsFor({ scope: scope ?? 'all', session: args.session || null, cwd: process.cwd() });
21
+ if (!pairs.length) {
22
+ console.error('no sessions found to distill (no detected-host transcripts for this project)');
23
+ process.exit(2);
24
+ }
25
+ const agentDir = paths().dir;
26
+
27
+ if (args['all-faculties'] || args.allFaculties) {
28
+ const sessions = pairs.map(mineHostSession).filter(Boolean);
29
+ const hosts = new Set(sessions.map((s) => s.host));
30
+ console.log(`distilled ${sessions.length} session(s) across ${hosts.size} host(s) and ${registry.all().length} faculty miner(s):`);
31
+ let total = 0;
32
+ for (const miner of registry.all()) {
33
+ const cand = miner.aggregate(sessions, {});
34
+ const n = miner.propose(agentDir, cand);
35
+ total += n;
36
+ console.log(` ${miner.faculty.padEnd(12)} ${n} proposal(s)`);
37
+ }
38
+ if (total) console.log('next: zuzuu review');
39
+ return;
40
+ }
41
+
42
+ const r = distillSessions(agentDir, pairs);
43
+ console.log(`distilled ${r.sessionsMined} session(s) → ${r.proposals.length} proposal(s)${r.registryProposals.length ? ` (+${r.registryProposals.length} registry)` : ''}`);
44
+ for (const p of r.proposals) console.log(` ${p.er.verdict.padEnd(9)} ${p.id}`);
45
+ if (r.proposals.length) console.log('next: zuzuu review');
46
+ }