@zuzuucodes/cli 1.2.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zuzuucodes/cli",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -40,7 +40,10 @@ export function distill(args) {
40
40
  }
41
41
 
42
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)` : ''}`);
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
  }
@@ -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).
@@ -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 proposals = candidates.map((c) => createProposal(agentDir, { candidate: c.candidate, source: 'distill', evidence: c.evidence }));
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
@@ -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 aggregated.length;
18
+ return count;
16
19
  }
17
20
 
18
21
  export const miner = { faculty: 'knowledge', aggregate, propose };