@inetafrica/open-claudia 2.6.39 → 2.6.41
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/core/dream.js +78 -19
- package/core/pack-review.js +26 -25
- package/core/packs.js +30 -1
- package/package.json +4 -3
- package/test-pack-nesting.js +115 -0
package/core/dream.js
CHANGED
|
@@ -41,6 +41,16 @@ const DREAM_MODEL = pickDreamModel();
|
|
|
41
41
|
// reasoning effort by default. Overridable via DREAM_EFFORT (low|medium|high|xhigh|max).
|
|
42
42
|
const DREAM_EFFORT = process.env.DREAM_EFFORT || "max";
|
|
43
43
|
const DREAM_CRON = process.env.DREAM_CRON || "0 4 * * *";
|
|
44
|
+
// Consolidation model timeout. The model weighs the WHOLE corpus in one pass,
|
|
45
|
+
// so a fixed budget that was fine at 30 packs starves at 130 (the timeout that
|
|
46
|
+
// motivated this). Floor 20m, +6s per pack, capped at 40m; DREAM_TIMEOUT_MS
|
|
47
|
+
// (ms) overrides entirely. Even if it still times out, the deterministic phases
|
|
48
|
+
// (prefix nesting, graph tend) run regardless — the model pass is best-effort.
|
|
49
|
+
function dreamTimeoutMs(packCount = 0) {
|
|
50
|
+
const override = Number(process.env.DREAM_TIMEOUT_MS);
|
|
51
|
+
if (override > 0) return override;
|
|
52
|
+
return Math.min(20 * 60 * 1000 + packCount * 6000, 40 * 60 * 1000);
|
|
53
|
+
}
|
|
44
54
|
const MAX_PACK_CHARS = 2500;
|
|
45
55
|
const MAX_ENTITY_CHARS = 900;
|
|
46
56
|
const LIMITS = {
|
|
@@ -149,7 +159,7 @@ Your job — decide what consolidation, if any, is warranted:
|
|
|
149
159
|
|
|
150
160
|
1. merges: packs that are clearly the SAME topic under different names get merged into one. Supply the merged Stance/Procedure/State (synthesised, not concatenated; null leaves the target's section alone) and a one-sentence journal note. Be conservative: when in doubt, do not merge. A Stance marked USER-AUTHORED is off-limits — leave its stance null; the system preserves it verbatim regardless. Packs marked [ABILITY] are reusable cross-project how-tos, NOT project trackers: only ever merge an [ABILITY] into another [ABILITY] (the merged result must stay an ability), and never merge a project pack into an [ABILITY] or an [ABILITY] into a project pack — that would lose the reusable how-to or pollute it with one project's specifics.
|
|
151
161
|
2. umbrellas: when 3+ packs are siblings under one theme, create an umbrella pack whose State is a 3-6 line map of the family ("for X see pack Y"), and list its children. The umbrella is a router, not a duplicate.
|
|
152
|
-
3. parents: assign an existing pack as parent of another (sub-topic relationship) without creating anything.
|
|
162
|
+
3. parents: assign an existing pack as parent of another (sub-topic relationship) without creating anything. NOTE: a separate deterministic pass already auto-files packs whose slug is a prefix-child of another (e.g. foo-bar under foo), so DON'T spend a parents op on those — reserve parents (and umbrellas) for semantic groupings the slug prefix can't catch (differently-named siblings of one theme).
|
|
153
163
|
4. retag: tighten descriptions and tags. The router FTS-matches incoming messages against name/description/tags, so generic words there cause false matches. Descriptions should be one specific line; tags specific nouns.
|
|
154
164
|
5. entity_merges: the same real-world entity recorded twice gets merged (the better slug wins).
|
|
155
165
|
6. entity_notes: rewrite an entity's Notes to be current and cross-linked — mention related packs as [[pack-dir]] and related entities by name.
|
|
@@ -277,7 +287,7 @@ function applyDream(decision, backupRoot) {
|
|
|
277
287
|
const lines = [];
|
|
278
288
|
const gone = new Set(); // dirs/slugs removed this run
|
|
279
289
|
|
|
280
|
-
for (const m of decision.merges) {
|
|
290
|
+
for (const m of decision.merges || []) {
|
|
281
291
|
try {
|
|
282
292
|
const into = m?.into && packs.readPack(m.into);
|
|
283
293
|
const from = [].concat(m?.from || []).filter((d) => d && d !== m.into && !gone.has(d) && packs.readPack(d));
|
|
@@ -312,7 +322,7 @@ function applyDream(decision, backupRoot) {
|
|
|
312
322
|
} catch (e) { console.warn(`[dream] merge failed: ${e.message}`); }
|
|
313
323
|
}
|
|
314
324
|
|
|
315
|
-
for (const u of decision.umbrellas) {
|
|
325
|
+
for (const u of decision.umbrellas || []) {
|
|
316
326
|
try {
|
|
317
327
|
const dir = packs.slugify(u?.dir || u?.name);
|
|
318
328
|
if (!dir || gone.has(dir)) continue;
|
|
@@ -345,7 +355,7 @@ function applyDream(decision, backupRoot) {
|
|
|
345
355
|
} catch (e) { console.warn(`[dream] umbrella failed: ${e.message}`); }
|
|
346
356
|
}
|
|
347
357
|
|
|
348
|
-
for (const p of decision.parents) {
|
|
358
|
+
for (const p of decision.parents || []) {
|
|
349
359
|
try {
|
|
350
360
|
const child = p?.pack && !gone.has(p.pack) && packs.readPack(p.pack);
|
|
351
361
|
if (!child || !p.parent || gone.has(p.parent) || !packs.readPack(p.parent)) continue;
|
|
@@ -358,7 +368,7 @@ function applyDream(decision, backupRoot) {
|
|
|
358
368
|
} catch (e) { console.warn(`[dream] parent failed: ${e.message}`); }
|
|
359
369
|
}
|
|
360
370
|
|
|
361
|
-
for (const r of decision.retag) {
|
|
371
|
+
for (const r of decision.retag || []) {
|
|
362
372
|
try {
|
|
363
373
|
if (!r?.pack || gone.has(r.pack) || !packs.readPack(r.pack)) continue;
|
|
364
374
|
const desc = typeof r.description === "string" ? r.description.trim() : "";
|
|
@@ -374,7 +384,7 @@ function applyDream(decision, backupRoot) {
|
|
|
374
384
|
} catch (e) { console.warn(`[dream] retag failed: ${e.message}`); }
|
|
375
385
|
}
|
|
376
386
|
|
|
377
|
-
for (const em of decision.entity_merges) {
|
|
387
|
+
for (const em of decision.entity_merges || []) {
|
|
378
388
|
try {
|
|
379
389
|
const into = em?.into && entities.readEntity(em.into);
|
|
380
390
|
const from = [].concat(em?.from || []).filter((s) => s && s !== em.into && !gone.has(s) && entities.readEntity(s));
|
|
@@ -397,7 +407,7 @@ function applyDream(decision, backupRoot) {
|
|
|
397
407
|
} catch (e) { console.warn(`[dream] entity merge failed: ${e.message}`); }
|
|
398
408
|
}
|
|
399
409
|
|
|
400
|
-
for (const en of decision.entity_notes) {
|
|
410
|
+
for (const en of decision.entity_notes || []) {
|
|
401
411
|
try {
|
|
402
412
|
const ent = en?.entity && !gone.has(en.entity) && entities.readEntity(en.entity);
|
|
403
413
|
if (!ent || typeof en.notes !== "string" || !en.notes.trim()) continue;
|
|
@@ -700,6 +710,33 @@ function staleTaskReport() {
|
|
|
700
710
|
} catch (e) { return ""; }
|
|
701
711
|
}
|
|
702
712
|
|
|
713
|
+
// Deterministic hierarchy backfill. Files every orphan context pack under the
|
|
714
|
+
// LONGEST existing context pack whose dir is a slug-prefix of it (kazee-people-x
|
|
715
|
+
// → kazee-people). Runs every dream AFTER the model's own parents/umbrellas
|
|
716
|
+
// decision, so it only mops up the obvious prefix families the model left flat.
|
|
717
|
+
// Prefix parents are always shorter, so this can't cycle; never overrides a
|
|
718
|
+
// parent already set (by user, reviewer, or this run's model). Backs up each
|
|
719
|
+
// pack it moves. Returns per-parent summary lines + a count.
|
|
720
|
+
function nestPrefixFamilies(backupRoot) {
|
|
721
|
+
const all = packs.listPacks();
|
|
722
|
+
const contextDirs = all.filter((p) => p.kind !== "ability").map((p) => p.dir);
|
|
723
|
+
const byParent = new Map();
|
|
724
|
+
for (const p of all) {
|
|
725
|
+
if (p.kind === "ability" || p.parent) continue;
|
|
726
|
+
const parent = packs.parentByPrefix(p.dir, contextDirs);
|
|
727
|
+
if (!parent || !packs.readPack(parent) || wouldCycle(p.dir, parent)) continue;
|
|
728
|
+
try {
|
|
729
|
+
if (backupRoot) backupPack(p.dir, backupRoot);
|
|
730
|
+
packs.setParent(p.dir, parent);
|
|
731
|
+
byParent.set(parent, (byParent.get(parent) || 0) + 1);
|
|
732
|
+
} catch (e) { console.warn(`[dream] nest failed for ${p.dir}: ${e.message}`); }
|
|
733
|
+
}
|
|
734
|
+
const lines = [...byParent.entries()].map(([parent, n]) =>
|
|
735
|
+
`🌳 Filed ${n} sub-pack${n === 1 ? "" : "s"} under ${parent}`);
|
|
736
|
+
const filed = [...byParent.values()].reduce((a, b) => a + b, 0);
|
|
737
|
+
return { lines, filed };
|
|
738
|
+
}
|
|
739
|
+
|
|
703
740
|
async function runDream({ trigger = "manual" } = {}) {
|
|
704
741
|
if (!enabled()) return { skipped: "dream is disabled (DREAM=off)" };
|
|
705
742
|
if (_dreaming) return { skipped: "a dream is already in progress" };
|
|
@@ -709,18 +746,40 @@ async function runDream({ trigger = "manual" } = {}) {
|
|
|
709
746
|
|
|
710
747
|
_dreaming = true;
|
|
711
748
|
try {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
749
|
+
// Best-effort consolidation: on a large corpus this call can time out or
|
|
750
|
+
// return unreadable JSON. That must NOT abort the dream — the deterministic
|
|
751
|
+
// phases below (prefix nesting, graph tend) need no model and are the
|
|
752
|
+
// load-bearing cleanup. Catch any model failure, fall back to an empty
|
|
753
|
+
// decision (applyDream no-ops), note it for the report, and carry on.
|
|
754
|
+
let decision = null;
|
|
755
|
+
let modelFailNote = "";
|
|
756
|
+
try {
|
|
757
|
+
const { text } = await spawnSubagent(buildDreamPrompt(), {
|
|
758
|
+
model: DREAM_MODEL,
|
|
759
|
+
effort: DREAM_EFFORT,
|
|
760
|
+
timeoutMs: dreamTimeoutMs(packCount),
|
|
761
|
+
systemPrompt: "You are a background memory consolidation process. Reply with ONLY the requested JSON object. No prose, no markdown, no tool use.",
|
|
762
|
+
});
|
|
763
|
+
decision = parseDream(text);
|
|
764
|
+
if (!decision) modelFailNote = "the AI consolidation step returned unreadable output";
|
|
765
|
+
} catch (e) {
|
|
766
|
+
modelFailNote = `the AI consolidation step didn't finish (${e.message})`;
|
|
767
|
+
}
|
|
768
|
+
if (!decision) decision = { report: "" };
|
|
720
769
|
|
|
721
770
|
const backupRoot = makeBackupRoot();
|
|
722
771
|
const applied = applyDream(decision, backupRoot);
|
|
723
|
-
|
|
772
|
+
// Deterministic hierarchy backfill (kazee-people-* → kazee-people), after the
|
|
773
|
+
// model's parents/umbrellas decision so it only fills the obvious prefix
|
|
774
|
+
// families left flat. Backed up + announced like everything else.
|
|
775
|
+
let nestLines = [];
|
|
776
|
+
try { nestLines = nestPrefixFamilies(backupRoot).lines; }
|
|
777
|
+
catch (e) { console.warn(`[dream] nesting pass failed: ${e.message}`); }
|
|
778
|
+
const consolidationLines = applied.concat(nestLines);
|
|
779
|
+
let report = decision.report || (consolidationLines.length > 0 ? "Tidied up my memory overnight." : "");
|
|
780
|
+
if (modelFailNote) {
|
|
781
|
+
report = `Heads up — ${modelFailNote}, so I skipped the AI-led merges this round but still ran the automatic filing + graph upkeep.${report ? " " + report : ""}`;
|
|
782
|
+
}
|
|
724
783
|
|
|
725
784
|
// Phase 2: self-improvement introspection. Reviews the day's seeds, reads
|
|
726
785
|
// its own code/memory READ-ONLY (allowedTools whitelist), promotes lessons
|
|
@@ -767,11 +826,11 @@ async function runDream({ trigger = "manual" } = {}) {
|
|
|
767
826
|
} catch (e) { /* graph is best-effort */ }
|
|
768
827
|
|
|
769
828
|
const staleNote = staleTaskReport();
|
|
770
|
-
const dreamLines =
|
|
829
|
+
const dreamLines = consolidationLines.concat(introApplied);
|
|
771
830
|
|
|
772
831
|
const reportPath = writeDreamReport({
|
|
773
832
|
model: DREAM_MODEL, effort: DREAM_EFFORT, trigger,
|
|
774
|
-
consolidation: { report: decision.report, lines:
|
|
833
|
+
consolidation: { report: decision.report, lines: consolidationLines },
|
|
775
834
|
introspection: { report: introReport, lines: introApplied, proposed: introProposed },
|
|
776
835
|
graphNote, staleNote, backupRoot,
|
|
777
836
|
});
|
|
@@ -826,7 +885,7 @@ function initDream(adapters) {
|
|
|
826
885
|
}
|
|
827
886
|
|
|
828
887
|
module.exports = {
|
|
829
|
-
runDream, initDream, buildDreamPrompt, parseDream, applyDream, manageAbilityTiers,
|
|
888
|
+
runDream, initDream, buildDreamPrompt, parseDream, applyDream, manageAbilityTiers, nestPrefixFamilies, dreamTimeoutMs,
|
|
830
889
|
buildIntrospectionPrompt, parseIntrospection, applyIntrospection, writeDreamReport,
|
|
831
890
|
enabled, summaryEnabled, introspectEnabled, selfApplyEnabled,
|
|
832
891
|
DREAM_CRON, DREAM_MODEL, DREAM_EFFORT, PROMOTE_MIN_PROJECTS,
|
package/core/pack-review.js
CHANGED
|
@@ -167,31 +167,24 @@ function guardCheck(a) {
|
|
|
167
167
|
return hit;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
// Deterministic dedupe backstop for the create path.
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
// pack
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
// own
|
|
170
|
+
// Deterministic dedupe backstop for the create path. A cheap model tends to
|
|
171
|
+
// spawn a fresh pack for each VERSION/RELEASE of a project an existing pack
|
|
172
|
+
// already owns ("<project> v2.6.39"); this folds such a proposal into the
|
|
173
|
+
// project pack instead of spawning a sibling. Scoped tightly: only context
|
|
174
|
+
// packs, only a *versioned* proposal whose name/description strongly matches an
|
|
175
|
+
// existing pack. NOTE: a non-versioned prefix-child (kazee-people-leave under
|
|
176
|
+
// kazee-people) is NOT folded here — it is filed as a sub-pack under its parent
|
|
177
|
+
// at creation (see birth-time parenting in applyAction), which keeps sub-area
|
|
178
|
+
// detail as its own node instead of collapsing it into the parent's State.
|
|
179
179
|
function findDuplicatePack({ dir, name, description, tags }) {
|
|
180
|
-
const slug = packs.slugify(dir || name || "");
|
|
181
|
-
if (!slug) return null;
|
|
182
|
-
const contextDirs = packs.listPacks().filter((p) => p.kind !== "ability").map((p) => p.dir);
|
|
183
|
-
for (const d of contextDirs) {
|
|
184
|
-
if (d === slug) continue;
|
|
185
|
-
if (slug.startsWith(d + "-") || d.startsWith(slug + "-")) return d;
|
|
186
|
-
}
|
|
187
180
|
const tagStr = Array.isArray(tags) ? tags.join(" ") : "";
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
181
|
+
const text = `${name || ""} ${description || ""} ${tagStr}`.trim();
|
|
182
|
+
const versioned = /\bv?\d+\.\d+(?:\.\d+)?\b|\brelease\b|\bv\d+\b/i.test(text);
|
|
183
|
+
if (!versioned) return null;
|
|
184
|
+
const slug = packs.slugify(dir || name || "");
|
|
185
|
+
const ctx = new Set(packs.listPacks().filter((p) => p.kind !== "ability").map((p) => p.dir));
|
|
186
|
+
for (const m of packs.matchPacks(text, { limit: 3, threshold: 4 })) {
|
|
187
|
+
if (m.dir !== slug && ctx.has(m.dir)) return m.dir;
|
|
195
188
|
}
|
|
196
189
|
return null;
|
|
197
190
|
}
|
|
@@ -243,11 +236,19 @@ function applyAction(a) {
|
|
|
243
236
|
const ex = packs.readPack(dupeDir);
|
|
244
237
|
return { kind: "update", dir: dupeDir, name: (ex && ex.name) || dupeDir, note: a.journal || "folded a new-pack proposal into the existing pack" };
|
|
245
238
|
}
|
|
239
|
+
// Birth-time hierarchy: a new context pack whose slug is a prefix-child of an
|
|
240
|
+
// existing pack (kazee-people-leave → kazee-people) is filed under it at
|
|
241
|
+
// creation, so families never accumulate flat at the top level. Abilities are
|
|
242
|
+
// cross-project and never prefix-nested.
|
|
243
|
+
const parentDir = kind === "context"
|
|
244
|
+
? packs.parentByPrefix(dir, packs.listPacks().filter((p) => p.kind !== "ability").map((p) => p.dir))
|
|
245
|
+
: null;
|
|
246
246
|
const pack = packs.createPack({
|
|
247
247
|
dir,
|
|
248
248
|
name: a.name,
|
|
249
249
|
description: a.description,
|
|
250
250
|
tags: Array.isArray(a.tags) ? a.tags.slice(0, 6) : [],
|
|
251
|
+
parent: parentDir && packs.readPack(parentDir) ? parentDir : null,
|
|
251
252
|
stance: a.stance || "",
|
|
252
253
|
procedure: a.procedure || "",
|
|
253
254
|
state: a.state || "",
|
|
@@ -256,7 +257,7 @@ function applyAction(a) {
|
|
|
256
257
|
learned_on: kind === "ability" ? learned_on : "",
|
|
257
258
|
applied_on: kind === "ability" ? projects : [],
|
|
258
259
|
}, "reviewer");
|
|
259
|
-
return { kind: "create", dir: pack.dir, name: pack.name, note: a.description || "", ability: kind === "ability", learned_on: pack.learned_on };
|
|
260
|
+
return { kind: "create", dir: pack.dir, name: pack.name, note: a.description || "", ability: kind === "ability", learned_on: pack.learned_on, parent: pack.parent };
|
|
260
261
|
}
|
|
261
262
|
return null;
|
|
262
263
|
}
|
|
@@ -319,7 +320,7 @@ function reviewTurn({ userText, assistantText, channelId, announce }) {
|
|
|
319
320
|
} else if (r && r.kind === "create" && r.ability) {
|
|
320
321
|
lines.push(`🧩 New ability: ${r.name}${r.learned_on ? ` (learned on ${r.learned_on})` : ""}\n${clipWords(r.note, 180)}\n↳ open-claudia pack show ${r.dir}`);
|
|
321
322
|
} else if (r && r.kind === "create") {
|
|
322
|
-
lines.push(`📦 New pack: ${r.name}\n${clipWords(r.note, 180)}\n↳ open-claudia pack show ${r.dir}`);
|
|
323
|
+
lines.push(`📦 New pack: ${r.name}${r.parent ? ` (filed under ${r.parent})` : ""}\n${clipWords(r.note, 180)}\n↳ open-claudia pack show ${r.dir}`);
|
|
323
324
|
} else if (r) {
|
|
324
325
|
lines.push(`✏️ ${r.name} — ${clipWords(r.note, 180)}${r.appliedTo ? ` 🧩 (now also applies to ${r.appliedTo})` : ""}\n↳ open-claudia pack show ${r.dir}`);
|
|
325
326
|
}
|
package/core/packs.js
CHANGED
|
@@ -171,6 +171,35 @@ function setKind(nameOrDir, kind) {
|
|
|
171
171
|
return pack;
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
// The natural parent of a pack within a slug family: the LONGEST existing dir
|
|
175
|
+
// that is a hyphen-boundary prefix of `dir` (kazee-people-mobile-x →
|
|
176
|
+
// kazee-people if it exists, else kazee). Pure — the caller supplies candidate
|
|
177
|
+
// dirs (so it can scope to context packs / exclude self). A strict prefix is
|
|
178
|
+
// always shorter than the slug, so a prefix parent can never be a descendant:
|
|
179
|
+
// nesting by prefix cannot create a cycle.
|
|
180
|
+
function parentByPrefix(dir, candidateDirs) {
|
|
181
|
+
const slug = slugify(dir);
|
|
182
|
+
if (!slug) return null;
|
|
183
|
+
let best = null;
|
|
184
|
+
for (const d of candidateDirs || []) {
|
|
185
|
+
if (!d || d === slug) continue;
|
|
186
|
+
if (slug.startsWith(d + "-") && (!best || d.length > best.length)) best = d;
|
|
187
|
+
}
|
|
188
|
+
return best;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Structural setter for a pack's parent (hierarchy). Separate from updatePack:
|
|
192
|
+
// parent is frontmatter metadata, not an authored content section, so it carries
|
|
193
|
+
// no provenance and never touches Stance/State/etc. Pass null/"" to un-parent.
|
|
194
|
+
function setParent(nameOrDir, parentDir) {
|
|
195
|
+
const pack = findPack(nameOrDir);
|
|
196
|
+
if (!pack) return null;
|
|
197
|
+
pack.parent = parentDir ? slugify(parentDir) : null;
|
|
198
|
+
pack.updated = new Date().toISOString();
|
|
199
|
+
writePack(pack);
|
|
200
|
+
return pack;
|
|
201
|
+
}
|
|
202
|
+
|
|
174
203
|
// Record that an ability was applied on a project (provenance of reuse): appends
|
|
175
204
|
// to applied_on (deduped) and sets learned_on to the first project if unset.
|
|
176
205
|
function recordApplied(nameOrDir, project) {
|
|
@@ -562,7 +591,7 @@ function matchPacks(text, { limit = 3, threshold = null } = {}) {
|
|
|
562
591
|
module.exports = {
|
|
563
592
|
PACKS_DIR, SECTIONS, slugify,
|
|
564
593
|
listPacks, findPack, readPack, writePack, createPack, updatePack, removePack,
|
|
565
|
-
listSkillPacks, setSkill, listAbilities, setKind, recordApplied, recordCoUse,
|
|
594
|
+
listSkillPacks, setSkill, listAbilities, setKind, parentByPrefix, setParent, recordApplied, recordCoUse,
|
|
566
595
|
touchUsed, packNameFromPath, matchPacks, reindex, markIndexDirty,
|
|
567
596
|
archivePack, restorePack, listArchived,
|
|
568
597
|
readProvenance, provenanceOf, setProvenance, recordForegroundWrite,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inetafrica/open-claudia",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.41",
|
|
4
4
|
"description": "Your always-on AI coding assistant — Claude Code, Cursor Agent, and OpenAI Codex via Telegram or Kazee Chat",
|
|
5
5
|
"main": "bot.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"setup": "node setup.js",
|
|
11
11
|
"start": "node bot.js",
|
|
12
|
-
"test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-learning-e2e.js"
|
|
12
|
+
"test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-pack-nesting.js"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"bot.js",
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
"test-ability-transfer.js",
|
|
42
42
|
"test-ability-tiers.js",
|
|
43
43
|
"test-ability-merge-guard.js",
|
|
44
|
-
"test-learning-e2e.js"
|
|
44
|
+
"test-learning-e2e.js",
|
|
45
|
+
"test-pack-nesting.js"
|
|
45
46
|
],
|
|
46
47
|
"keywords": [
|
|
47
48
|
"claude",
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Hierarchy nesting: the deterministic prefix-family pass that keeps pack
|
|
2
|
+
// families (kazee-people-*) filed under their parent instead of sprawling flat
|
|
3
|
+
// at the top level. Covers the pure selector (parentByPrefix), the structural
|
|
4
|
+
// setter (setParent), reviewer birth-time parenting (applyAction create), the
|
|
5
|
+
// dream backfill (nestPrefixFamilies), and the versioned-only fold guard.
|
|
6
|
+
const assert = require("assert");
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const os = require("os");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
|
|
11
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "pack-nesting-"));
|
|
12
|
+
process.env.PACKS_DIR = path.join(tmp, "packs");
|
|
13
|
+
|
|
14
|
+
const packs = require("./core/packs");
|
|
15
|
+
const review = require("./core/pack-review");
|
|
16
|
+
const dream = require("./core/dream");
|
|
17
|
+
|
|
18
|
+
// ── parentByPrefix: pure longest-prefix selection ──
|
|
19
|
+
assert.strictEqual(
|
|
20
|
+
packs.parentByPrefix("kazee-people-leave-display", ["kazee", "kazee-people", "other"]),
|
|
21
|
+
"kazee-people",
|
|
22
|
+
"longest matching prefix wins"
|
|
23
|
+
);
|
|
24
|
+
assert.strictEqual(packs.parentByPrefix("kazee", ["kazee"]), null, "a pack is never its own parent");
|
|
25
|
+
assert.strictEqual(packs.parentByPrefix("kazeebot", ["kazee"]), null, "prefix must end on a dash boundary");
|
|
26
|
+
assert.strictEqual(packs.parentByPrefix("orphan", ["kazee", "kazee-people"]), null, "no prefix → no parent");
|
|
27
|
+
assert.strictEqual(packs.parentByPrefix("anything", []), null, "no candidates → null");
|
|
28
|
+
|
|
29
|
+
// ── setParent: structural setter writes + clears frontmatter ──
|
|
30
|
+
packs.createPack({ dir: "np-root", name: "NP Root", description: "a root" });
|
|
31
|
+
packs.createPack({ dir: "np-solo", name: "NP Solo", description: "a leaf" });
|
|
32
|
+
packs.setParent("np-solo", "np-root");
|
|
33
|
+
assert.strictEqual(packs.readPack("np-solo").parent, "np-root", "setParent persists parent");
|
|
34
|
+
packs.setParent("np-solo", null);
|
|
35
|
+
assert.strictEqual(packs.readPack("np-solo").parent, null, "setParent(null) un-parents");
|
|
36
|
+
|
|
37
|
+
// ── reviewer birth-time parenting: a new prefix-child files under its parent ──
|
|
38
|
+
packs.createPack({ dir: "kazee", name: "Kazee", description: "the platform" });
|
|
39
|
+
packs.createPack({ dir: "kazee-people", name: "Kazee People", description: "the HR module" });
|
|
40
|
+
const born = review.applyAction({
|
|
41
|
+
action: "create", dir: "kazee-people-payroll", name: "Kazee People Payroll",
|
|
42
|
+
description: "payroll inside the HR module", state: "started",
|
|
43
|
+
});
|
|
44
|
+
assert.strictEqual(born.kind, "create", "a fresh prefix-child is created, not folded");
|
|
45
|
+
assert.strictEqual(born.parent, "kazee-people", "birth-time parent is the LONGEST prefix (kazee-people, not kazee)");
|
|
46
|
+
assert.strictEqual(packs.readPack("kazee-people-payroll").parent, "kazee-people", "parent persisted to disk");
|
|
47
|
+
|
|
48
|
+
const orphanBorn = review.applyAction({
|
|
49
|
+
action: "create", dir: "standalone-thing", name: "Standalone Thing",
|
|
50
|
+
description: "belongs to no family", state: "started",
|
|
51
|
+
});
|
|
52
|
+
assert.strictEqual(orphanBorn.parent, null, "a pack with no prefix-parent stays top-level");
|
|
53
|
+
|
|
54
|
+
// abilities are cross-project and must never be prefix-nested
|
|
55
|
+
const abilityBorn = review.applyAction({
|
|
56
|
+
action: "create", dir: "kazee-people-export", name: "Kazee People Export",
|
|
57
|
+
description: "reusable export how-to", kind: "ability", learned_on: "kazee",
|
|
58
|
+
});
|
|
59
|
+
assert.strictEqual(abilityBorn.parent, null, "an ability is never prefix-nested");
|
|
60
|
+
|
|
61
|
+
// ── dream.nestPrefixFamilies: backfills existing flat families ──
|
|
62
|
+
// Three flat siblings + the root, plus an ability that must be left alone.
|
|
63
|
+
packs.createPack({ dir: "billing", name: "Billing", description: "billing system" });
|
|
64
|
+
packs.createPack({ dir: "billing-invoices", name: "Billing Invoices", description: "invoice generation" });
|
|
65
|
+
packs.createPack({ dir: "billing-refunds", name: "Billing Refunds", description: "refund flow" });
|
|
66
|
+
packs.createPack({ dir: "billing-cli", name: "Billing CLI", description: "reusable billing CLI", kind: "ability", learned_on: "billing" });
|
|
67
|
+
|
|
68
|
+
const res = dream.nestPrefixFamilies(null); // null backupRoot → skip backups in test
|
|
69
|
+
assert.strictEqual(packs.readPack("billing-invoices").parent, "billing", "flat sibling filed under root");
|
|
70
|
+
assert.strictEqual(packs.readPack("billing-refunds").parent, "billing", "second flat sibling filed under root");
|
|
71
|
+
assert.strictEqual(packs.readPack("billing").parent, null, "the family root stays top-level");
|
|
72
|
+
assert.strictEqual(packs.readPack("billing-cli").parent, null, "the billing ability is NOT nested");
|
|
73
|
+
assert.ok(res.filed >= 2, "nest pass reports what it filed");
|
|
74
|
+
assert.ok(res.lines.some((l) => l.includes("billing")), "nest pass announces the family it filed");
|
|
75
|
+
|
|
76
|
+
// running it again is a no-op (idempotent — everything already parented)
|
|
77
|
+
const again = dream.nestPrefixFamilies(null);
|
|
78
|
+
assert.strictEqual(again.filed, 0, "second pass files nothing new");
|
|
79
|
+
|
|
80
|
+
// ── dream resilience: the deterministic cleanup must not depend on the model ──
|
|
81
|
+
// applyDream tolerates an empty decision (the fallback used when the AI
|
|
82
|
+
// consolidation step times out / returns garbage) instead of throwing.
|
|
83
|
+
assert.deepStrictEqual(dream.applyDream({}, null), [], "empty decision applies nothing, throws nothing");
|
|
84
|
+
assert.deepStrictEqual(dream.applyDream({ report: "" }, null), [], "report-only fallback decision is safe");
|
|
85
|
+
|
|
86
|
+
// timeout scales with corpus: floor 20m, grows per pack, capped at 40m, env override.
|
|
87
|
+
assert.strictEqual(dream.dreamTimeoutMs(0), 20 * 60 * 1000, "floor is 20 minutes");
|
|
88
|
+
assert.ok(dream.dreamTimeoutMs(130) > dream.dreamTimeoutMs(30), "more packs → longer budget");
|
|
89
|
+
assert.strictEqual(dream.dreamTimeoutMs(100000), 40 * 60 * 1000, "capped at 40 minutes");
|
|
90
|
+
const prev = process.env.DREAM_TIMEOUT_MS;
|
|
91
|
+
process.env.DREAM_TIMEOUT_MS = "777000";
|
|
92
|
+
assert.strictEqual(dream.dreamTimeoutMs(50), 777000, "DREAM_TIMEOUT_MS overrides entirely");
|
|
93
|
+
if (prev === undefined) delete process.env.DREAM_TIMEOUT_MS; else process.env.DREAM_TIMEOUT_MS = prev;
|
|
94
|
+
|
|
95
|
+
// ── versioned-only fold (FTS-gated): a versioned proposal folds into its pack,
|
|
96
|
+
// a non-versioned near-duplicate does NOT. Skipped when node:sqlite is absent. ──
|
|
97
|
+
if (packs.reindex()) {
|
|
98
|
+
packs.createPack({ dir: "payroll-tax-filing", name: "Payroll Tax Filing", description: "quarterly payroll tax filing" });
|
|
99
|
+
packs.reindex();
|
|
100
|
+
const folded = review.applyAction({
|
|
101
|
+
action: "create", dir: "payroll-tax-filing-v2", name: "Payroll Tax Filing v2 release",
|
|
102
|
+
description: "payroll tax filing notes", state: "v2 shipped",
|
|
103
|
+
});
|
|
104
|
+
assert.strictEqual(folded.kind, "update", "a versioned near-dup folds into the existing pack");
|
|
105
|
+
assert.strictEqual(folded.dir, "payroll-tax-filing", "fold targets the existing pack");
|
|
106
|
+
|
|
107
|
+
const notFolded = review.applyAction({
|
|
108
|
+
action: "create", dir: "payroll-tax-summary", name: "Payroll Tax Summary",
|
|
109
|
+
description: "payroll tax filing summary view", state: "started",
|
|
110
|
+
});
|
|
111
|
+
assert.strictEqual(notFolded.kind, "create", "a non-versioned near-dup is a real new pack, not a fold");
|
|
112
|
+
console.log("pack nesting OK (incl. versioned-fold)");
|
|
113
|
+
} else {
|
|
114
|
+
console.log("pack nesting OK (versioned-fold skipped — no node:sqlite)");
|
|
115
|
+
}
|