@zuzuucodes/cli 1.2.0 → 1.2.2
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 -6
- package/package.json +3 -5
- package/zuzuu/capture/bin/capture.mjs +99 -0
- package/zuzuu/capture/bin/inspect-trace.mjs +13 -0
- package/zuzuu/capture-core.mjs +3 -3
- package/zuzuu/commands/capture.mjs +1 -1
- package/zuzuu/commands/distill.mjs +4 -1
- package/zuzuu/commands/doctor.mjs +1 -1
- package/zuzuu/commands/hook.mjs +1 -1
- package/zuzuu/commands/init.mjs +1 -1
- package/zuzuu/commands/status.mjs +1 -1
- package/zuzuu/commands/trace.mjs +1 -1
- package/zuzuu/faculty/proposal.mjs +26 -0
- package/zuzuu/knowledge/distill.mjs +8 -4
- package/zuzuu/knowledge/proposals.mjs +10 -1
- package/zuzuu/live/reconcile.mjs +1 -1
- package/zuzuu/miners/guardrails.mjs +6 -1
- package/zuzuu/miners/instructions.mjs +6 -1
- package/zuzuu/miners/knowledge.mjs +6 -3
- package/zuzuu/store.mjs +1 -1
- /package/{experiments/experiment-1-trace-capture → zuzuu/capture}/adapters/claude-code.mjs +0 -0
- /package/{experiments/experiment-1-trace-capture → zuzuu/capture}/adapters/codex.mjs +0 -0
- /package/{experiments/experiment-1-trace-capture → zuzuu/capture}/adapters/gemini-cli.mjs +0 -0
- /package/{experiments/experiment-1-trace-capture → zuzuu/capture}/adapters/host-adapter.mjs +0 -0
- /package/{experiments/experiment-1-trace-capture → zuzuu/capture}/adapters/opencode.mjs +0 -0
- /package/{experiments/experiment-1-trace-capture → zuzuu/capture}/adapters/pi.mjs +0 -0
- /package/{experiments/experiment-1-trace-capture → zuzuu/capture}/adapters/registry.mjs +0 -0
- /package/{experiments/experiment-1-trace-capture → zuzuu/capture}/adapters/signals.mjs +0 -0
- /package/{experiments/experiment-1-trace-capture → zuzuu/capture}/core/event.mjs +0 -0
- /package/{experiments/experiment-1-trace-capture → zuzuu/capture}/core/ids.mjs +0 -0
- /package/{experiments/experiment-1-trace-capture → zuzuu/capture}/core/otlp.mjs +0 -0
- /package/{experiments/experiment-1-trace-capture → zuzuu/capture}/core/render.mjs +0 -0
- /package/{experiments/experiment-1-trace-capture → zuzuu/capture}/core/spans.mjs +0 -0
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ zuzuu doctor # health + lost-session reconciliation
|
|
|
40
40
|
|
|
41
41
|
¹ **Codex is interactive-only** — `codex exec` (headless) fires no hooks (verified, v0.138.0), so live capture + gate work when you run Codex interactively; headless Codex still gets post-hoc `zuzuu capture`.
|
|
42
42
|
|
|
43
|
-
All five verified against **real sessions** — never fixtures; every host's live capture + gate was wired from **real captured hook payloads** and dogfooded end-to-end ([`
|
|
43
|
+
All five verified against **real sessions** — never fixtures; every host's live capture + gate was wired from **real captured hook payloads** and dogfooded end-to-end ([`docs/LOG.md`](docs/LOG.md) exp-11 Gemini/Codex, exp-12 OpenCode/pi). Gate semantics are host-honest: deny hard-blocks everywhere; `ask` maps to a native prompt on Claude, defers to the host elsewhere.
|
|
44
44
|
|
|
45
45
|
**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`.)
|
|
46
46
|
|
|
@@ -77,15 +77,14 @@ All five verified against **real sessions** — never fixtures; every host's liv
|
|
|
77
77
|
|
|
78
78
|
| Path | What |
|
|
79
79
|
|---|---|
|
|
80
|
-
| [`zuzuu/`](zuzuu/) + `bin/zuzuu.mjs` | the CLI — capture, live lifecycle, faculty home (product surface) |
|
|
81
|
-
| [`
|
|
82
|
-
| [`app/`](app/) | the durable application skeleton (be / run / evolve) — proven code harvests here |
|
|
80
|
+
| [`zuzuu/`](zuzuu/) + `bin/zuzuu.mjs` | the CLI — capture pipeline (`zuzuu/capture/`), live lifecycle, faculty home (product surface) |
|
|
81
|
+
| [`web/`](web/) | the visual workbench — nested project (daemon + React SPA), ships bundled inside the npm package |
|
|
83
82
|
| [`tests/`](tests/) | hermetic unit + regression (`npm test`) + real-data smoke playgrounds (`npm run playground`) |
|
|
84
|
-
| [`docs/`](docs/) | [`DESIGN.md`](docs/DESIGN.md) (the canon) + [`inspiration/`](docs/inspiration/) (the research shelf
|
|
83
|
+
| [`docs/`](docs/) | [`DESIGN.md`](docs/DESIGN.md) (the canon) + [`LOG.md`](docs/LOG.md) (the build journal) + [`inspiration/`](docs/inspiration/) (the research shelf) |
|
|
85
84
|
|
|
86
85
|
## How this is built (the method)
|
|
87
86
|
|
|
88
|
-
**
|
|
87
|
+
**Prove on real data, record in the journal.** Every capability is verified against *real* sessions/wire data (never invented fixtures) before it counts; the record lives in [`docs/LOG.md`](docs/LOG.md) (append-only). Live experimentation happens in disposable project directories outside the repo, keeping the codebase clean. Built in public — day-by-day on X ([@h1902y](https://x.com/h1902y)).
|
|
89
88
|
|
|
90
89
|
## License & status
|
|
91
90
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zuzuucodes/cli",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -42,15 +42,13 @@
|
|
|
42
42
|
"bin/",
|
|
43
43
|
"zuzuu/",
|
|
44
44
|
"web-app/",
|
|
45
|
-
"experiments/experiment-1-trace-capture/core/",
|
|
46
|
-
"experiments/experiment-1-trace-capture/adapters/",
|
|
47
45
|
"LICENSE",
|
|
48
46
|
"README.md"
|
|
49
47
|
],
|
|
50
48
|
"scripts": {
|
|
51
49
|
"zuzuu": "node bin/zuzuu.mjs",
|
|
52
|
-
"capture": "node
|
|
53
|
-
"inspect": "node
|
|
50
|
+
"capture": "node zuzuu/capture/bin/capture.mjs",
|
|
51
|
+
"inspect": "node zuzuu/capture/bin/inspect-trace.mjs",
|
|
54
52
|
"test": "node --test 'tests/**/*.test.mjs'",
|
|
55
53
|
"playground": "node tests/playground/run.mjs",
|
|
56
54
|
"prepublishOnly": "npm test",
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// capture — read a host's session log, normalize it, write an OTLP/JSON trace.
|
|
3
|
+
//
|
|
4
|
+
// node bin/capture.mjs [--host NAME] [--project STR] [--session ID] [--file PATH] [--out PATH]
|
|
5
|
+
// node bin/capture.mjs --list # show detected hosts + sessions
|
|
6
|
+
//
|
|
7
|
+
// With no --host, picks the first detected host (transcript-present). Pure file
|
|
8
|
+
// parsing — no host needs to be running.
|
|
9
|
+
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { dirname, join } from 'node:path';
|
|
12
|
+
import { ADAPTERS, byName, detected } from '../adapters/registry.mjs';
|
|
13
|
+
import { eventsToSpans } from '../core/spans.mjs';
|
|
14
|
+
import { toExportRequest, writeNdjson } from '../core/otlp.mjs';
|
|
15
|
+
import { EventKind } from '../core/event.mjs';
|
|
16
|
+
|
|
17
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const DEFAULT_OUT_DIR = join(HERE, '..', 'out');
|
|
19
|
+
|
|
20
|
+
function parseArgs(argv) {
|
|
21
|
+
const a = { _: [] };
|
|
22
|
+
for (let i = 0; i < argv.length; i++) {
|
|
23
|
+
const t = argv[i];
|
|
24
|
+
if (t === '--list') a.list = true;
|
|
25
|
+
else if (t === '-h' || t === '--help') a.help = true;
|
|
26
|
+
else if (t.startsWith('--')) a[t.slice(2)] = argv[++i];
|
|
27
|
+
else a._.push(t);
|
|
28
|
+
}
|
|
29
|
+
return a;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function printList() {
|
|
33
|
+
const hosts = detected();
|
|
34
|
+
if (!hosts.length) {
|
|
35
|
+
console.log('No hosts detected on this machine.');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
for (const adapter of hosts) {
|
|
39
|
+
const sessions = adapter.listSessions({ cwd: process.cwd() });
|
|
40
|
+
console.log(`\n● ${adapter.name} — ${sessions.length} session(s)`);
|
|
41
|
+
for (const s of sessions.slice(0, 8)) {
|
|
42
|
+
console.log(` ${s.sessionId} ${s.label ?? ''}`);
|
|
43
|
+
}
|
|
44
|
+
if (sessions.length > 8) console.log(` … and ${sessions.length - 8} more`);
|
|
45
|
+
}
|
|
46
|
+
console.log();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function chooseRef(adapter, args) {
|
|
50
|
+
if (args.file) return args.file; // explicit transcript path (claude-code)
|
|
51
|
+
const sessions = adapter.listSessions({ cwd: process.cwd(), project: args.project });
|
|
52
|
+
let pool = sessions;
|
|
53
|
+
if (args.project) pool = pool.filter((s) => (s.label ?? '').includes(args.project));
|
|
54
|
+
if (args.session) pool = pool.filter((s) => s.sessionId === args.session || s.sessionId.includes(args.session));
|
|
55
|
+
if (!pool.length) throw new Error(`no matching session for ${adapter.name} (sessions found: ${sessions.length})`);
|
|
56
|
+
return pool[0].ref;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function summarize(trace) {
|
|
60
|
+
const counts = {};
|
|
61
|
+
for (const e of trace.events) counts[e.kind] = (counts[e.kind] || 0) + 1;
|
|
62
|
+
return counts;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function main() {
|
|
66
|
+
const args = parseArgs(process.argv.slice(2));
|
|
67
|
+
if (args.help) {
|
|
68
|
+
console.log('usage: capture.mjs [--host NAME] [--project STR] [--session ID] [--file PATH] [--out PATH] | --list');
|
|
69
|
+
console.log('hosts:', ADAPTERS.map((a) => a.name).join(', '));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (args.list) return printList();
|
|
73
|
+
|
|
74
|
+
const adapter = args.host ? byName(args.host) : detected()[0];
|
|
75
|
+
if (!adapter) {
|
|
76
|
+
console.error(args.host ? `unknown host: ${args.host}` : 'no host detected (try --list)');
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const ref = chooseRef(adapter, args);
|
|
81
|
+
const trace = adapter.parse(ref);
|
|
82
|
+
const { traceId, spans } = eventsToSpans(trace);
|
|
83
|
+
const request = toExportRequest({ traceId, spans }, { host: trace.host, sessionId: trace.sessionId });
|
|
84
|
+
|
|
85
|
+
const outPath = args.out || join(DEFAULT_OUT_DIR, `${trace.host}-${trace.sessionId}.otlp.jsonl`);
|
|
86
|
+
writeNdjson(outPath, [request]);
|
|
87
|
+
|
|
88
|
+
const counts = summarize(trace);
|
|
89
|
+
const root = trace.events.find((e) => e.kind === EventKind.SESSION) || trace.events[0];
|
|
90
|
+
const durSec = root ? ((root.endMs - root.startMs) / 1000).toFixed(1) : '?';
|
|
91
|
+
console.log(`captured ${trace.host} session ${trace.sessionId}`);
|
|
92
|
+
console.log(` trace_id : ${traceId}`);
|
|
93
|
+
console.log(` events : ${spans.length} (${Object.entries(counts).map(([k, v]) => `${k}:${v}`).join(', ')})`);
|
|
94
|
+
console.log(` duration : ${durSec}s`);
|
|
95
|
+
console.log(` written : ${outPath}`);
|
|
96
|
+
console.log(` inspect : node ${join('zuzuu', 'capture', 'bin', 'inspect-trace.mjs')} "${outPath}"`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
main();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// inspect-trace — pretty-print the span tree of an OTLP/JSON NDJSON trace file.
|
|
3
|
+
//
|
|
4
|
+
// node bin/inspect-trace.mjs out/claude-code-<id>.otlp.jsonl
|
|
5
|
+
|
|
6
|
+
import { loadSpans, renderTree } from '../core/render.mjs';
|
|
7
|
+
|
|
8
|
+
const file = process.argv[2];
|
|
9
|
+
if (!file) {
|
|
10
|
+
console.error('usage: inspect-trace.mjs <trace.otlp.jsonl>');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
console.log(renderTree(loadSpans(file)));
|
package/zuzuu/capture-core.mjs
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
// (`hook`/`reconcile`, statuses active/completed/abandoned). One proven path —
|
|
4
4
|
// Design B: the hook never builds spans, it re-runs THIS.
|
|
5
5
|
|
|
6
|
-
import { eventsToSpans } from '
|
|
7
|
-
import { toExportRequest } from '
|
|
8
|
-
import { EventKind, Status } from '
|
|
6
|
+
import { eventsToSpans } from './capture/core/spans.mjs';
|
|
7
|
+
import { toExportRequest } from './capture/core/otlp.mjs';
|
|
8
|
+
import { EventKind, Status } from './capture/core/event.mjs';
|
|
9
9
|
import { makeSession, SessionState } from './session.mjs';
|
|
10
10
|
import { writeTrace, upsertSession, gitInfo } from './store.mjs';
|
|
11
11
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// `zuzuu capture` — post-hoc: parse a host transcript into a git-native trace + record.
|
|
2
2
|
|
|
3
|
-
import { ADAPTERS, byName, detected } from '
|
|
3
|
+
import { ADAPTERS, byName, detected } from '../capture/adapters/registry.mjs';
|
|
4
4
|
import { captureTrace } from '../capture-core.mjs';
|
|
5
5
|
import { paths } from '../store.mjs';
|
|
6
6
|
|
|
@@ -40,7 +40,10 @@ export function distill(args) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
const r = distillSessions(agentDir, pairs);
|
|
43
|
-
|
|
43
|
+
const skips = r.archivedSkips ?? [];
|
|
44
|
+
console.log(`distilled ${r.sessionsMined} session(s) → ${r.proposals.length} proposal(s)${r.registryProposals.length ? ` (+${r.registryProposals.length} registry)` : ''}${skips.length ? ` (${skips.length} archived-skip)` : ''}`);
|
|
44
45
|
for (const p of r.proposals) console.log(` ${p.er.verdict.padEnd(9)} ${p.id}`);
|
|
46
|
+
// already resolved (rejected/approved) in proposals/archive/ — not re-filed
|
|
47
|
+
for (const p of skips) console.log(` archived-skip ${p.id} (${p.archived})`);
|
|
45
48
|
if (r.proposals.length) console.log('next: zuzuu review');
|
|
46
49
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// real problems (warnings don't fail). Phase 2 will also reconcile lost sessions.
|
|
3
3
|
|
|
4
4
|
import { mkdirSync, accessSync, constants } from 'node:fs';
|
|
5
|
-
import { detected } from '
|
|
5
|
+
import { detected } from '../capture/adapters/registry.mjs';
|
|
6
6
|
import { paths, gitInfo } from '../store.mjs';
|
|
7
7
|
import { listLive } from '../live/live-store.mjs';
|
|
8
8
|
import { reconcile } from '../live/reconcile.mjs';
|
package/zuzuu/commands/hook.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { readFileSync, writeFileSync, appendFileSync, mkdirSync } from 'node:fs';
|
|
15
15
|
import { join, dirname } from 'node:path';
|
|
16
|
-
import { byName } from '
|
|
16
|
+
import { byName } from '../capture/adapters/registry.mjs';
|
|
17
17
|
import { captureTrace } from '../capture-core.mjs';
|
|
18
18
|
import { SessionState } from '../session.mjs';
|
|
19
19
|
import { openLive, touchLive, closeLive } from '../live/live-store.mjs';
|
package/zuzuu/commands/init.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import { join, basename } from 'node:path';
|
|
|
14
14
|
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
15
|
import { applyScaffold, ensureGitignore, homeExists } from '../scaffold.mjs';
|
|
16
16
|
import { injectBlock, facultiesBlock, hasBlock, BLOCK_VERSION } from '../inject.mjs';
|
|
17
|
-
import { detected } from '
|
|
17
|
+
import { detected } from '../capture/adapters/registry.mjs';
|
|
18
18
|
import { repoRoot } from '../store.mjs';
|
|
19
19
|
import { migrateHome } from './migrate.mjs';
|
|
20
20
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// `zuzuu status` — detected hosts + recorded sessions (the git-native index).
|
|
2
2
|
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
|
-
import { detected } from '
|
|
4
|
+
import { detected } from '../capture/adapters/registry.mjs';
|
|
5
5
|
import { readIndex, paths } from '../store.mjs';
|
|
6
6
|
import { FACULTIES } from '../faculty/contract.mjs';
|
|
7
7
|
import { listProposals } from '../faculty/proposal.mjs';
|
package/zuzuu/commands/trace.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// `zuzuu trace [--last | <file>]` — print the span tree of a captured trace.
|
|
2
2
|
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
|
-
import { loadSpans, renderTree } from '
|
|
4
|
+
import { loadSpans, renderTree } from '../capture/core/render.mjs';
|
|
5
5
|
import { lastTrace } from '../store.mjs';
|
|
6
6
|
|
|
7
7
|
export function trace(args) {
|
|
@@ -96,6 +96,32 @@ export function readProposal(agentDir, faculty, id) {
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Read and normalise a resolved proposal from the faculty's archive.
|
|
101
|
+
* Returns null if no archive record exists or it is unreadable (never throws).
|
|
102
|
+
*/
|
|
103
|
+
export function readArchived(agentDir, faculty, id) {
|
|
104
|
+
const path = join(archiveDir(agentDir, faculty), `${id}.json`);
|
|
105
|
+
if (!existsSync(path)) return null;
|
|
106
|
+
try {
|
|
107
|
+
return normalise(JSON.parse(readFileSync(path, 'utf8')), faculty);
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* True when an id is already resolved in the archive (rejected OR approved).
|
|
115
|
+
* Policy: a rejection is remembered — filing layers must skip these ids so a
|
|
116
|
+
* re-distill over the same sessions never resurrects a resolved proposal.
|
|
117
|
+
* (Approved ids are skipped too: the work is done; ER handles enrichment.)
|
|
118
|
+
* Gate at the CALLERS — writeProposal stays a dumb writer.
|
|
119
|
+
*/
|
|
120
|
+
export function isArchivedResolved(agentDir, faculty, id) {
|
|
121
|
+
const rec = readArchived(agentDir, faculty, id);
|
|
122
|
+
return !!rec && (rec.status === 'rejected' || rec.status === 'approved');
|
|
123
|
+
}
|
|
124
|
+
|
|
99
125
|
/**
|
|
100
126
|
* List all pending proposals for a faculty (files in proposals/ not in archive/).
|
|
101
127
|
* Normalises each record. Skips unreadable files (fail-soft).
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// failures — tools failing ≥3× → `fact` candidates (worth knowing!)
|
|
13
13
|
|
|
14
14
|
import { readFileSync } from 'node:fs';
|
|
15
|
-
import * as registry from '
|
|
15
|
+
import * as registry from '../capture/adapters/registry.mjs';
|
|
16
16
|
import { slugify } from './items.mjs';
|
|
17
17
|
import { createProposal, fileRegistryProposals } from './proposals.mjs';
|
|
18
18
|
|
|
@@ -204,13 +204,17 @@ export function mineHostSession({ host, ref, sessionId }) {
|
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
/** Run the full distill: mine sessions (all hosts) → candidates → ER → proposals.
|
|
207
|
+
/** Run the full distill: mine sessions (all hosts) → candidates → ER → proposals.
|
|
208
|
+
* Candidates whose id is already resolved in proposals/archive/ are NOT
|
|
209
|
+
* re-filed (a rejection is remembered) — they come back as `archivedSkips`. */
|
|
208
210
|
export function distillSessions(agentDir, pairs) {
|
|
209
211
|
const mined = pairs.map(mineHostSession).filter(Boolean);
|
|
210
212
|
const candidates = aggregate(mined);
|
|
211
|
-
const
|
|
213
|
+
const results = candidates.map((c) => createProposal(agentDir, { candidate: c.candidate, source: 'distill', evidence: c.evidence }));
|
|
214
|
+
const proposals = results.filter((p) => p.status !== 'archived-skip');
|
|
215
|
+
const archivedSkips = results.filter((p) => p.status === 'archived-skip');
|
|
212
216
|
const registryProposals = fileRegistryProposals(agentDir);
|
|
213
|
-
return { sessionsMined: mined.length, proposals, registryProposals };
|
|
217
|
+
return { sessionsMined: mined.length, proposals, registryProposals, archivedSkips };
|
|
214
218
|
}
|
|
215
219
|
|
|
216
220
|
/**
|
|
@@ -15,6 +15,7 @@ import { allItems, readItem, writeItem, slugify } from './items.mjs';
|
|
|
15
15
|
import { upsertItem } from './index.mjs';
|
|
16
16
|
import { resolve as erResolve, merge } from './er.mjs';
|
|
17
17
|
import { mechanicalScore } from '../eval/score.mjs';
|
|
18
|
+
import { readArchived } from '../faculty/proposal.mjs';
|
|
18
19
|
|
|
19
20
|
export const proposalsDir = (agentDir) => join(agentDir, 'knowledge', 'proposals');
|
|
20
21
|
const archiveDir = (agentDir) => join(proposalsDir(agentDir), 'archive');
|
|
@@ -27,12 +28,20 @@ function writeProposal(agentDir, p) {
|
|
|
27
28
|
return p;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
/** Run ER for a candidate and file a pending proposal (deduped per candidate).
|
|
31
|
+
/** Run ER for a candidate and file a pending proposal (deduped per candidate).
|
|
32
|
+
* A rejection is remembered: if the derived id is already RESOLVED in
|
|
33
|
+
* proposals/archive/ (rejected or approved), nothing is filed — the call
|
|
34
|
+
* returns `{ id, status: 'archived-skip', archived: <resolved status> }` so
|
|
35
|
+
* callers can count/report the skip instead of resurrecting the proposal. */
|
|
31
36
|
export function createProposal(agentDir, { candidate, source, evidence = {} }) {
|
|
32
37
|
const { items } = allItems(agentDir);
|
|
33
38
|
candidate.id = candidate.id || slugify(candidate.body);
|
|
34
39
|
const er = erResolve(candidate, items);
|
|
35
40
|
const id = `${candidate.id}-${shortHash(candidate.id + source)}`;
|
|
41
|
+
const archived = readArchived(agentDir, 'knowledge', id);
|
|
42
|
+
if (archived && (archived.status === 'rejected' || archived.status === 'approved')) {
|
|
43
|
+
return { id, status: 'archived-skip', archived: archived.status };
|
|
44
|
+
}
|
|
36
45
|
const existing = join(proposalsDir(agentDir), `${id}.json`);
|
|
37
46
|
if (existsSync(existing)) {
|
|
38
47
|
// refresh evidence on the pending proposal instead of duplicating it
|
package/zuzuu/live/reconcile.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// correct capture of the abandoned session before closing it. Nothing is lost.
|
|
5
5
|
|
|
6
6
|
import { listLive, isStale, closeLive } from './live-store.mjs';
|
|
7
|
-
import { byName } from '
|
|
7
|
+
import { byName } from '../capture/adapters/registry.mjs';
|
|
8
8
|
import { captureTrace } from '../capture-core.mjs';
|
|
9
9
|
import { SessionState } from '../session.mjs';
|
|
10
10
|
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { join } from 'node:path';
|
|
19
19
|
import { existsSync, readFileSync } from 'node:fs';
|
|
20
20
|
import { slugify } from '../knowledge/items.mjs';
|
|
21
|
-
import { makeProposal, writeProposal, listProposals } from '../faculty/proposal.mjs';
|
|
21
|
+
import { makeProposal, writeProposal, listProposals, isArchivedResolved } from '../faculty/proposal.mjs';
|
|
22
22
|
import { register } from './registry.mjs';
|
|
23
23
|
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
@@ -127,6 +127,8 @@ export function aggregate(sessions, { minFailures = 3, minSessions = 2 } = {}) {
|
|
|
127
127
|
* Idempotent:
|
|
128
128
|
* - skips if a guardrails proposal with the same payload.id already exists
|
|
129
129
|
* - skips if rules.json already has a rule with that id
|
|
130
|
+
* - skips if the id is already resolved in proposals/archive/ — a rejection
|
|
131
|
+
* is remembered; re-distilling never resurrects it
|
|
130
132
|
*
|
|
131
133
|
* The proposals flow through `zuzuu review` → guardrails adapter on approval.
|
|
132
134
|
*
|
|
@@ -159,6 +161,9 @@ export function propose(agentDir, aggregated) {
|
|
|
159
161
|
evidence,
|
|
160
162
|
});
|
|
161
163
|
|
|
164
|
+
// A rejection is remembered: never resurrect an archive-resolved id.
|
|
165
|
+
if (isArchivedResolved(agentDir, 'guardrails', proposal.id)) continue;
|
|
166
|
+
|
|
162
167
|
writeProposal(agentDir, proposal);
|
|
163
168
|
count++;
|
|
164
169
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { join } from 'node:path';
|
|
13
13
|
import { existsSync, readFileSync } from 'node:fs';
|
|
14
|
-
import { makeProposal, writeProposal, listProposals } from '../faculty/proposal.mjs';
|
|
14
|
+
import { makeProposal, writeProposal, listProposals, isArchivedResolved } from '../faculty/proposal.mjs';
|
|
15
15
|
import { register } from './registry.mjs';
|
|
16
16
|
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
@@ -103,6 +103,8 @@ export function aggregate(sessions, { minSessions = 2 } = {}) {
|
|
|
103
103
|
* Idempotent:
|
|
104
104
|
* - skips if an instructions proposal with the same derived id already exists
|
|
105
105
|
* - skips if the text is already present in project.md
|
|
106
|
+
* - skips if the id is already resolved in proposals/archive/ — a rejection
|
|
107
|
+
* is remembered; re-distilling never resurrects it
|
|
106
108
|
*
|
|
107
109
|
* @param {string} agentDir
|
|
108
110
|
* @param {ReturnType<typeof aggregate>} aggregated
|
|
@@ -137,6 +139,9 @@ export function propose(agentDir, aggregated) {
|
|
|
137
139
|
evidence,
|
|
138
140
|
});
|
|
139
141
|
|
|
142
|
+
// A rejection is remembered: never resurrect an archive-resolved id.
|
|
143
|
+
if (isArchivedResolved(agentDir, 'instructions', proposal.id)) continue;
|
|
144
|
+
|
|
140
145
|
writeProposal(agentDir, proposal);
|
|
141
146
|
count++;
|
|
142
147
|
}
|
|
@@ -7,12 +7,15 @@ import { aggregate } from '../knowledge/distill.mjs';
|
|
|
7
7
|
import { createProposal } from '../knowledge/proposals.mjs';
|
|
8
8
|
import { register } from './registry.mjs';
|
|
9
9
|
|
|
10
|
-
/** File one knowledge proposal per aggregated candidate; return the count
|
|
10
|
+
/** File one knowledge proposal per aggregated candidate; return the count of
|
|
11
|
+
* actually-filed proposals (archive-resolved ids are skipped, not re-filed). */
|
|
11
12
|
export function propose(agentDir, aggregated) {
|
|
13
|
+
let count = 0;
|
|
12
14
|
for (const c of aggregated) {
|
|
13
|
-
createProposal(agentDir, { candidate: c.candidate, source: 'distill', evidence: c.evidence });
|
|
15
|
+
const p = createProposal(agentDir, { candidate: c.candidate, source: 'distill', evidence: c.evidence });
|
|
16
|
+
if (p && p.status !== 'archived-skip') count++;
|
|
14
17
|
}
|
|
15
|
-
return
|
|
18
|
+
return count;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
export const miner = { faculty: 'knowledge', aggregate, propose };
|
package/zuzuu/store.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { join, relative, resolve, isAbsolute } from 'node:path';
|
|
12
12
|
import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, writeFileSync, renameSync } from 'node:fs';
|
|
13
13
|
import { spawnSync } from 'node:child_process';
|
|
14
|
-
import { writeNdjson } from '
|
|
14
|
+
import { writeNdjson } from './capture/core/otlp.mjs';
|
|
15
15
|
|
|
16
16
|
const INDEX_VERSION = 1;
|
|
17
17
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|