@inetafrica/open-claudia 2.6.36 → 2.6.38
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/CHANGELOG.md +3 -0
- package/README.md +2 -1
- package/bin/cli.js +18 -0
- package/bin/ideas.js +69 -0
- package/bin/keyring.js +64 -0
- package/bin/lessons.js +72 -0
- package/bin/pack.js +45 -2
- package/bot.js +8 -0
- package/core/actions.js +10 -2
- package/core/config.js +10 -1
- package/core/day-seeds.js +98 -0
- package/core/dream.js +413 -18
- package/core/handlers.js +153 -9
- package/core/ideas.js +114 -0
- package/core/keyring.js +79 -0
- package/core/lessons.js +276 -0
- package/core/pack-review.js +95 -14
- package/core/packs.js +95 -2
- package/core/recall/discoverer.js +5 -2
- package/core/recall/graph.js +17 -0
- package/core/recall/index.js +12 -5
- package/core/redact.js +25 -3
- package/core/runner.js +44 -2
- package/core/subagent.js +20 -4
- package/core/system-prompt.js +51 -4
- package/package.json +11 -3
- package/test-abilities.js +53 -0
- package/test-ability-couse.js +68 -0
- package/test-ability-extraction.js +109 -0
- package/test-ability-merge-guard.js +42 -0
- package/test-ability-tiers.js +57 -0
- package/test-ability-transfer.js +70 -0
- package/test-learning-e2e.js +98 -0
- package/test-project-transcripts-smoke.js +50 -0
- package/test-recall-discoverer.js +3 -0
- package/test-recall-engine.js +7 -5
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// PROOF (dream merge safety): the dream must never merge across the
|
|
2
|
+
// ability/context boundary — that would either lose a reusable how-to or
|
|
3
|
+
// pollute it with one project's specifics. Abilities merge only into abilities.
|
|
4
|
+
// Exercises the real applyDream merge path.
|
|
5
|
+
const assert = require("assert");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
|
|
10
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "ability-merge-"));
|
|
11
|
+
process.env.PACKS_DIR = path.join(tmp, "packs");
|
|
12
|
+
const backup = path.join(tmp, "backup");
|
|
13
|
+
|
|
14
|
+
const packs = require("./core/packs");
|
|
15
|
+
const dream = require("./core/dream");
|
|
16
|
+
|
|
17
|
+
// reach 1 + not-skill so the deterministic tier pass leaves them alone.
|
|
18
|
+
packs.createPack({ dir: "deploy-flow", name: "Deploy Flow", description: "reusable deploy", kind: "ability", applied_on: ["x"] });
|
|
19
|
+
packs.createPack({ dir: "spaces", name: "Spaces", description: "the spaces project" });
|
|
20
|
+
|
|
21
|
+
// ── Cross-boundary merge (ability → context) must be REFUSED.
|
|
22
|
+
let decision = dream.parseDream(JSON.stringify({ merges: [{ into: "spaces", from: ["deploy-flow"], journal: "merge" }] }));
|
|
23
|
+
let lines = dream.applyDream(decision, backup);
|
|
24
|
+
assert.ok(packs.readPack("deploy-flow"), "ability was NOT removed by a cross-boundary merge");
|
|
25
|
+
assert.strictEqual(packs.readPack("deploy-flow").kind, "ability", "ability stays an ability");
|
|
26
|
+
assert.strictEqual(packs.readPack("spaces").kind, "context", "project pack stays context");
|
|
27
|
+
assert.ok(lines.some((l) => /Skipped merge into spaces/.test(l)), "cross-boundary merge announced as skipped");
|
|
28
|
+
|
|
29
|
+
// ── Context → ability is equally refused.
|
|
30
|
+
decision = dream.parseDream(JSON.stringify({ merges: [{ into: "deploy-flow", from: ["spaces"], journal: "merge" }] }));
|
|
31
|
+
lines = dream.applyDream(decision, backup);
|
|
32
|
+
assert.ok(packs.readPack("spaces"), "context pack was NOT absorbed into an ability");
|
|
33
|
+
|
|
34
|
+
// ── Ability → ability IS allowed (the merged result stays an ability).
|
|
35
|
+
packs.createPack({ dir: "deploy-old", name: "Deploy Old", description: "older deploy notes", kind: "ability", applied_on: ["y"] });
|
|
36
|
+
decision = dream.parseDream(JSON.stringify({ merges: [{ into: "deploy-flow", from: ["deploy-old"], journal: "consolidated deploy notes" }] }));
|
|
37
|
+
lines = dream.applyDream(decision, backup);
|
|
38
|
+
assert.ok(!packs.readPack("deploy-old"), "duplicate ability was absorbed");
|
|
39
|
+
assert.strictEqual(packs.readPack("deploy-flow").kind, "ability", "merged result stays an ability");
|
|
40
|
+
assert.ok(lines.some((l) => /Merged deploy-old into deploy-flow/.test(l)), "ability→ability merge applied");
|
|
41
|
+
|
|
42
|
+
console.log("ability merge guard OK — abilities never merge with project packs; ability→ability merges still work");
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// PROOF (dream tier management): the nightly dream PROMOTES an ability that has
|
|
2
|
+
// transferred across enough projects to the always-on skill index, and eases a
|
|
3
|
+
// cold, never-spread always-on ability back to the match-gated pool — both
|
|
4
|
+
// deterministic from reuse breadth, both reversible. Uses the real packs store.
|
|
5
|
+
const assert = require("assert");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
|
|
10
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "ability-tiers-"));
|
|
11
|
+
process.env.PACKS_DIR = path.join(tmp, "packs");
|
|
12
|
+
// Make "cold" instant so the demote path is exercisable without backdating files.
|
|
13
|
+
process.env.DREAM_ARCHIVE_IDLE_DAYS = "0";
|
|
14
|
+
|
|
15
|
+
const packs = require("./core/packs");
|
|
16
|
+
const dream = require("./core/dream");
|
|
17
|
+
assert.strictEqual(dream.PROMOTE_MIN_PROJECTS, 3, "default promote threshold is 3 projects");
|
|
18
|
+
|
|
19
|
+
// Broadly-reused ability (3 projects) — should PROMOTE to always-on.
|
|
20
|
+
packs.createPack({
|
|
21
|
+
dir: "broad-ability", name: "Broad Ability", description: "transfers everywhere",
|
|
22
|
+
kind: "ability", learned_on: "a", applied_on: ["a", "b", "c"],
|
|
23
|
+
});
|
|
24
|
+
// Narrowly-used ability (2 projects) — stays match-gated, no promotion yet.
|
|
25
|
+
packs.createPack({
|
|
26
|
+
dir: "narrow-ability", name: "Narrow Ability", description: "two projects so far",
|
|
27
|
+
kind: "ability", learned_on: "a", applied_on: ["a", "b"],
|
|
28
|
+
});
|
|
29
|
+
// A once-promoted always-on ability that never spread (reach 0) and is cold —
|
|
30
|
+
// should DEMOTE back to match-gated (still an ability).
|
|
31
|
+
packs.createPack({ dir: "cold-skill", name: "Cold Skill", description: "no longer earning its slot", kind: "ability" });
|
|
32
|
+
packs.setSkill("cold-skill", true);
|
|
33
|
+
assert.strictEqual(packs.readPack("cold-skill").skill, true, "cold-skill starts always-on");
|
|
34
|
+
|
|
35
|
+
const lines = dream.manageAbilityTiers();
|
|
36
|
+
|
|
37
|
+
// Promotion happened.
|
|
38
|
+
assert.strictEqual(packs.readPack("broad-ability").skill, true, "broad ability promoted to always-on");
|
|
39
|
+
assert.strictEqual(packs.readPack("broad-ability").kind, "ability", "promotion keeps it an ability");
|
|
40
|
+
assert.ok(lines.some((l) => /Promoted .*Broad Ability/.test(l)), "promotion announced");
|
|
41
|
+
|
|
42
|
+
// No premature promotion.
|
|
43
|
+
assert.strictEqual(packs.readPack("narrow-ability").skill, false, "narrow ability NOT promoted (only 2 projects)");
|
|
44
|
+
|
|
45
|
+
// Demotion happened, but it stays an ability (match-gated), not archived.
|
|
46
|
+
assert.strictEqual(packs.readPack("cold-skill").skill, false, "cold skill eased out of always-on");
|
|
47
|
+
assert.strictEqual(packs.readPack("cold-skill").kind, "ability", "demotion keeps it an ability (still findable)");
|
|
48
|
+
assert.ok(packs.listAbilities().some((a) => a.dir === "cold-skill"), "demoted ability remains in the ability pool");
|
|
49
|
+
assert.ok(lines.some((l) => /Eased .*Cold Skill/.test(l)), "demotion announced");
|
|
50
|
+
|
|
51
|
+
// Idempotent: a second pass makes no further changes (broad now skill+reach3 so
|
|
52
|
+
// not eligible for demote; cold now not-skill so not eligible for either).
|
|
53
|
+
const second = dream.manageAbilityTiers();
|
|
54
|
+
assert.strictEqual(second.length, 0, "second pass is a no-op");
|
|
55
|
+
|
|
56
|
+
console.log("ability tiers OK — broad reuse promotes to always-on; cold/never-spread eases out; both reversible, idempotent");
|
|
57
|
+
console.log(" " + lines.join("\n "));
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// PROOF: an ability learned while working on one project surfaces when working
|
|
2
|
+
// on a DIFFERENT project — the cross-session / cross-project reuse the learning
|
|
3
|
+
// system is built on. Runs against the REAL recall graph (temp db, stub corpus),
|
|
4
|
+
// using the real default tunables. No reimplementation, no rigged thresholds.
|
|
5
|
+
const assert = require("assert");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
|
|
10
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "ability-transfer-"));
|
|
11
|
+
process.env.RECALL_GRAPH_DB = path.join(tmp, "graph.db");
|
|
12
|
+
|
|
13
|
+
const graph = require("./core/recall/graph");
|
|
14
|
+
if (!graph.available()) { console.log("ability transfer OK (skipped — no node:sqlite)"); process.exit(0); }
|
|
15
|
+
|
|
16
|
+
// Corpus:
|
|
17
|
+
// - chat-mobile, spaces : two projects that both ship a mobile app.
|
|
18
|
+
// - billing : a project that does NOT (negative control).
|
|
19
|
+
// - mobile-app-deploy : a reusable ABILITY (kind:"ability") extracted from
|
|
20
|
+
// the first deploy; records where it was learned.
|
|
21
|
+
// Projects point at the ability via [[mobile-app-deploy]] in their body.
|
|
22
|
+
const ABILITY_STANCE =
|
|
23
|
+
"Reusable how-to. Ship a mobile app: bump versionCode, build the APK, push the in-app updater. learned_on: chat-mobile (2026-06).";
|
|
24
|
+
const packsStub = {
|
|
25
|
+
listPacks: () => [
|
|
26
|
+
{ dir: "chat-mobile", name: "Chat Mobile", tags: [], parent: null,
|
|
27
|
+
sections: { Stance: "Spaces chat client. Ship via [[mobile-app-deploy]].", Procedure: "", State: "", Journal: "" } },
|
|
28
|
+
{ dir: "spaces", name: "Spaces", tags: [], parent: null,
|
|
29
|
+
sections: { Stance: "Spaces app. Releases follow [[mobile-app-deploy]].", Procedure: "", State: "", Journal: "" } },
|
|
30
|
+
{ dir: "billing", name: "Billing", tags: [], parent: null,
|
|
31
|
+
sections: { Stance: "Invoices and payments. No mobile build.", Procedure: "", State: "", Journal: "" } },
|
|
32
|
+
{ dir: "mobile-app-deploy", name: "Mobile App Deploy", tags: [], parent: null, kind: "ability",
|
|
33
|
+
sections: { Stance: ABILITY_STANCE, Procedure: "1. bump versionCode 2. build APK 3. in-app updater", State: "", Journal: "" } },
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
const entStub = { listEntities: () => [] };
|
|
37
|
+
|
|
38
|
+
// Structural sync derives governed-by edges from [[links]] → shared concern.
|
|
39
|
+
graph.syncFromCorpus(packsStub, entStub);
|
|
40
|
+
const edges = graph.allEdges();
|
|
41
|
+
assert.ok(edges.some((e) => e.src === "pack:chat-mobile" && e.dst === "pack:mobile-app-deploy" && e.type === "governed-by"),
|
|
42
|
+
"chat-mobile is governed-by the ability");
|
|
43
|
+
assert.ok(edges.some((e) => e.src === "pack:spaces" && e.dst === "pack:mobile-app-deploy" && e.type === "governed-by"),
|
|
44
|
+
"spaces is governed-by the ability");
|
|
45
|
+
|
|
46
|
+
// THE PROOF: working on `spaces` — a project the ability was NOT learned on —
|
|
47
|
+
// surfaces the ability via spreading activation (default tunables, no rigging).
|
|
48
|
+
const fromSpaces = graph.expand([{ id: "pack:spaces", score: 4 }], {});
|
|
49
|
+
assert.ok(fromSpaces.has("pack:mobile-app-deploy"),
|
|
50
|
+
"ability surfaces when working on spaces (cross-project reuse)");
|
|
51
|
+
|
|
52
|
+
// It also surfaces from its origin project, and even for a weak seed.
|
|
53
|
+
const fromChat = graph.expand([{ id: "pack:chat-mobile", score: 4 }], {});
|
|
54
|
+
assert.ok(fromChat.has("pack:mobile-app-deploy"), "ability surfaces from its origin project");
|
|
55
|
+
const weakSeed = graph.expand([{ id: "pack:spaces", score: 1 }], {});
|
|
56
|
+
assert.ok(weakSeed.has("pack:mobile-app-deploy"), "surfaces even for a weak seed (threshold scales with seed)");
|
|
57
|
+
|
|
58
|
+
// Negative control: an unrelated project does NOT pull the ability in.
|
|
59
|
+
const fromBilling = graph.expand([{ id: "pack:billing", score: 4 }], {});
|
|
60
|
+
assert.ok(!fromBilling.has("pack:mobile-app-deploy"),
|
|
61
|
+
"unlinked project does NOT surface the ability (it's the link doing the work, not noise)");
|
|
62
|
+
|
|
63
|
+
// Provenance lives on the ability node itself, ready to show the user.
|
|
64
|
+
assert.ok(/learned_on:\s*chat-mobile/.test(ABILITY_STANCE), "ability records where it was learned");
|
|
65
|
+
|
|
66
|
+
console.log("ability transfer OK — a learned-once pattern surfaces across projects");
|
|
67
|
+
console.log(" working on spaces → graph pulls in:", [...fromSpaces.keys()].join(", ") || "(none)");
|
|
68
|
+
console.log(" working on chat → graph pulls in:", [...fromChat.keys()].join(", ") || "(none)");
|
|
69
|
+
console.log(" working on billing → graph pulls in:", [...fromBilling.keys()].join(", ") || "(none)");
|
|
70
|
+
console.log(" provenance on ability:", ABILITY_STANCE.match(/learned_on:[^.]*/)[0]);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// CAPSTONE PROOF of the goal: "feel like it remembers and reuses ideas/patterns/
|
|
2
|
+
// abilities across sessions and projects, like it is learning and improving."
|
|
3
|
+
// One narrative walks the WHOLE lifecycle against real code — reviewer extraction,
|
|
4
|
+
// FTS router, recall graph, the co-use loop, and the dream — no reimplementation.
|
|
5
|
+
//
|
|
6
|
+
// Stage 1 capture — a reusable how-to done on project A is extracted into an ability
|
|
7
|
+
// Stage 2 recall@home — the ability is linked to A and surfaces while working on A
|
|
8
|
+
// Stage 3 cold-start — on a BRAND-NEW project B it surfaces by ACTIVITY (FTS), no edge yet
|
|
9
|
+
// Stage 4 transfer — using it on B records reuse, the edge forms, B now surfaces it via the graph
|
|
10
|
+
// Stage 5 graduation — once reused widely, the dream promotes it to always-on
|
|
11
|
+
// Stage 6 provenance — it can always say where it was learned and where it has been applied
|
|
12
|
+
const assert = require("assert");
|
|
13
|
+
const fs = require("fs");
|
|
14
|
+
const os = require("os");
|
|
15
|
+
const path = require("path");
|
|
16
|
+
|
|
17
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "learning-e2e-"));
|
|
18
|
+
process.env.PACKS_DIR = path.join(tmp, "packs");
|
|
19
|
+
process.env.RECALL_GRAPH_DB = path.join(tmp, "graph.db");
|
|
20
|
+
|
|
21
|
+
const packs = require("./core/packs");
|
|
22
|
+
const graph = require("./core/recall/graph");
|
|
23
|
+
const review = require("./core/pack-review");
|
|
24
|
+
const dream = require("./core/dream");
|
|
25
|
+
const entStub = { listEntities: () => [] };
|
|
26
|
+
const haveGraph = graph.available();
|
|
27
|
+
|
|
28
|
+
// Three real projects exist as context packs over time.
|
|
29
|
+
packs.createPack({ dir: "spaces", name: "Spaces", description: "the Spaces web+mobile app" });
|
|
30
|
+
packs.createPack({ dir: "chat-mobile", name: "Chat Mobile", description: "the Spaces chat client" });
|
|
31
|
+
packs.createPack({ dir: "field-app", name: "Field App", description: "the field-ops companion app" });
|
|
32
|
+
|
|
33
|
+
// reindex() returns false when node:sqlite is unavailable — use it as the FTS gate.
|
|
34
|
+
const haveFts = packs.reindex();
|
|
35
|
+
|
|
36
|
+
// ── Stage 1: CAPTURE. A turn ships a mobile build on `spaces`; the reviewer
|
|
37
|
+
// extracts the reusable how-to as an ability with activity-oriented metadata.
|
|
38
|
+
const cap = review.applyAction({
|
|
39
|
+
action: "create", dir: "mobile-app-deploy", name: "Mobile App Deploy",
|
|
40
|
+
description: "Ship a mobile app: bump versionCode, build the APK, push the in-app updater",
|
|
41
|
+
tags: ["mobile", "deploy", "apk", "versioncode", "release"],
|
|
42
|
+
kind: "ability", learned_on: "spaces", applied_on: ["spaces"],
|
|
43
|
+
procedure: "1. bump versionCode 2. build APK 3. push in-app updater",
|
|
44
|
+
journal: "Shipped the spaces mobile build.",
|
|
45
|
+
});
|
|
46
|
+
assert.ok(cap.ability && cap.learned_on === "spaces", "Stage 1: ability captured, learned on spaces");
|
|
47
|
+
|
|
48
|
+
if (haveFts) packs.reindex();
|
|
49
|
+
if (haveGraph) graph.syncFromCorpus(packs, entStub);
|
|
50
|
+
|
|
51
|
+
// ── Stage 2: RECALL AT HOME. Link formed; working on spaces surfaces it.
|
|
52
|
+
if (haveGraph) {
|
|
53
|
+
const fromSpaces = graph.expand([{ id: "pack:spaces", score: 4 }], {});
|
|
54
|
+
assert.ok(fromSpaces.has("pack:mobile-app-deploy"), "Stage 2: ability surfaces on its origin project");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Stage 3: COLD-START on a NEW project. No graph edge to chat-mobile yet, but
|
|
58
|
+
// the ability is discoverable by the WORK being done (activity-term FTS) — the
|
|
59
|
+
// crucial "I've done something like this before" moment in a new context.
|
|
60
|
+
if (haveGraph) {
|
|
61
|
+
const fromChatNoEdge = graph.expand([{ id: "pack:chat-mobile", score: 4 }], {});
|
|
62
|
+
assert.ok(!fromChatNoEdge.has("pack:mobile-app-deploy"), "Stage 3: no graph edge to the new project yet");
|
|
63
|
+
}
|
|
64
|
+
if (haveFts) {
|
|
65
|
+
const hits = packs.matchPacks("ship the chat mobile app: bump versionCode and build the apk", { limit: 5 });
|
|
66
|
+
assert.ok(hits.some((h) => h.dir === "mobile-app-deploy"),
|
|
67
|
+
"Stage 3: ability surfaces in a brand-new project via activity match, before any edge");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Stage 4: TRANSFER ON USE. The agent opens the ability + chat-mobile together
|
|
71
|
+
// (the co-use signal). recordCoUse grows applied_on, the edge forms on sync,
|
|
72
|
+
// and chat-mobile now surfaces it through the graph too.
|
|
73
|
+
const transferred = packs.recordCoUse(["pack:mobile-app-deploy", "pack:chat-mobile"]);
|
|
74
|
+
assert.deepStrictEqual(transferred.map((t) => t.project), ["chat-mobile"], "Stage 4: reuse recorded from real use");
|
|
75
|
+
assert.deepStrictEqual(packs.readPack("mobile-app-deploy").applied_on, ["spaces", "chat-mobile"], "Stage 4: applied_on grew");
|
|
76
|
+
if (haveGraph) {
|
|
77
|
+
graph.syncFromCorpus(packs, entStub);
|
|
78
|
+
const fromChat = graph.expand([{ id: "pack:chat-mobile", score: 4 }], {});
|
|
79
|
+
assert.ok(fromChat.has("pack:mobile-app-deploy"), "Stage 4: new project now surfaces it via the graph");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Stage 5: GRADUATION. Reused on a third project, the dream promotes it to the
|
|
83
|
+
// always-on tier so it is offered everywhere, not just on matched projects.
|
|
84
|
+
packs.recordCoUse(["pack:mobile-app-deploy", "pack:field-app"]);
|
|
85
|
+
assert.strictEqual(packs.readPack("mobile-app-deploy").applied_on.length, 3, "Stage 5: applied across three projects");
|
|
86
|
+
const tierLines = dream.manageAbilityTiers();
|
|
87
|
+
assert.strictEqual(packs.readPack("mobile-app-deploy").skill, true, "Stage 5: dream promoted it to always-on");
|
|
88
|
+
assert.ok(tierLines.some((l) => /Promoted .*Mobile App Deploy/.test(l)), "Stage 5: promotion announced");
|
|
89
|
+
assert.ok(packs.listSkillPacks().some((p) => p.dir === "mobile-app-deploy"), "Stage 5: now in the always-on skill index");
|
|
90
|
+
|
|
91
|
+
// ── Stage 6: PROVENANCE. It can always say where it was learned and applied.
|
|
92
|
+
const ab = packs.readPack("mobile-app-deploy");
|
|
93
|
+
assert.strictEqual(ab.learned_on, "spaces", "Stage 6: remembers where it was learned");
|
|
94
|
+
assert.deepStrictEqual(ab.applied_on, ["spaces", "chat-mobile", "field-app"], "Stage 6: remembers everywhere it transferred");
|
|
95
|
+
|
|
96
|
+
const skips = [!haveGraph && "graph", !haveFts && "fts"].filter(Boolean);
|
|
97
|
+
console.log(`learning E2E OK — capture → recall → cold-start → transfer → graduation → provenance${skips.length ? ` (skipped: ${skips.join(", ")} — no node:sqlite)` : ""}`);
|
|
98
|
+
console.log(" learned on:", ab.learned_on, "| applied on:", ab.applied_on.join(", "), "| always-on:", ab.skill);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const os = require("os");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const assert = require("assert");
|
|
5
|
+
const { ProjectTranscripts, normalizeProjectPath, projectHash } = require("./project-transcripts");
|
|
6
|
+
|
|
7
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "open-claudia-transcripts-"));
|
|
8
|
+
const project = path.join(tmp, "repo");
|
|
9
|
+
fs.mkdirSync(project);
|
|
10
|
+
|
|
11
|
+
const transcripts = new ProjectTranscripts({
|
|
12
|
+
configDir: tmp,
|
|
13
|
+
maxEntryChars: 100,
|
|
14
|
+
redact: (value) => String(value).replace(/SECRET/g, "[REDACTED]"),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const normalized = normalizeProjectPath(project);
|
|
18
|
+
const hash = projectHash(project);
|
|
19
|
+
assert.strictEqual(hash, projectHash(normalized));
|
|
20
|
+
|
|
21
|
+
const result = transcripts.append({
|
|
22
|
+
role: "user",
|
|
23
|
+
text: "hello SECRET " + "x".repeat(100),
|
|
24
|
+
userId: "telegram:1",
|
|
25
|
+
chat: { transport: "telegram", id: "1" },
|
|
26
|
+
projectName: "repo",
|
|
27
|
+
projectPath: project,
|
|
28
|
+
backend: "codex",
|
|
29
|
+
sessionId: "sess-1",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
assert.ok(result.transcriptPath.startsWith(path.join(tmp, "transcripts")));
|
|
33
|
+
assert.ok(fs.existsSync(result.transcriptPath));
|
|
34
|
+
const line = fs.readFileSync(result.transcriptPath, "utf8").trim();
|
|
35
|
+
const entry = JSON.parse(line);
|
|
36
|
+
assert.strictEqual(entry.project.path, normalized);
|
|
37
|
+
// The stored hash is per-user (projectHash seeds with userId), so verify it
|
|
38
|
+
// against the same user the entry was written for — not the path-only hash above.
|
|
39
|
+
assert.strictEqual(entry.project.hash, projectHash(normalized, "telegram:1"));
|
|
40
|
+
assert.strictEqual(entry.backend, "codex");
|
|
41
|
+
assert.ok(entry.truncated);
|
|
42
|
+
assert.ok(!entry.text.includes("SECRET"));
|
|
43
|
+
assert.ok(entry.text.includes("[REDACTED]"));
|
|
44
|
+
|
|
45
|
+
const note = transcripts.buildPointerNote(project, "repo", "telegram:1");
|
|
46
|
+
assert.ok(note.includes(result.transcriptPath));
|
|
47
|
+
assert.ok(note.includes("Do not read the whole file"));
|
|
48
|
+
|
|
49
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
50
|
+
console.log("project-transcripts smoke OK");
|
|
@@ -59,6 +59,7 @@ const helpers = { packsLib, entitiesLib, mergeMatches, buildPackBlock, buildEnti
|
|
|
59
59
|
});
|
|
60
60
|
assert.strictEqual(gated.packBlock, "");
|
|
61
61
|
assert.strictEqual(gated.packMatches.length, 0);
|
|
62
|
+
assert.strictEqual(gated.gated, true, "gated turn flags gated:true");
|
|
62
63
|
assert.strictEqual(builtPacks, null, "gated turn never builds blocks");
|
|
63
64
|
|
|
64
65
|
// seeded turn: FTS hits kazee-mobile → fail-open keeps it, block rendered
|
|
@@ -69,6 +70,8 @@ const helpers = { packsLib, entitiesLib, mergeMatches, buildPackBlock, buildEnti
|
|
|
69
70
|
assert.strictEqual(out.packBlock, "PACKBLOCK");
|
|
70
71
|
assert.strictEqual(out.packMatches.length, 1);
|
|
71
72
|
assert.strictEqual(out.packMatches[0].dir, "kazee-mobile");
|
|
73
|
+
assert.strictEqual(out.gated, false, "seeded turn is not gated");
|
|
74
|
+
assert.strictEqual(typeof out.why, "object", "seeded turn returns a why map");
|
|
72
75
|
assert.ok(builtPacks && builtPacks.some((m) => m.dir === "kazee-mobile"), "seed reached the builder");
|
|
73
76
|
|
|
74
77
|
// resilient: a throwing matcher must not blow up the engine
|
package/test-recall-engine.js
CHANGED
|
@@ -3,18 +3,20 @@ const assert = require("assert");
|
|
|
3
3
|
const recall = require("./core/recall");
|
|
4
4
|
|
|
5
5
|
// --- selector ---
|
|
6
|
-
assert.strictEqual(recall.
|
|
7
|
-
assert.strictEqual(recall.activeEngineName({
|
|
8
|
-
assert.strictEqual(recall.activeEngineName({ recallEngine:
|
|
6
|
+
assert.strictEqual(recall.DEFAULT_ENGINE, "discoverer", "discoverer is the product default");
|
|
7
|
+
assert.strictEqual(recall.activeEngineName({}), "discoverer", "no setting → default (discoverer)");
|
|
8
|
+
assert.strictEqual(recall.activeEngineName({ recallEngine: null }), "discoverer", "null setting → default");
|
|
9
|
+
assert.strictEqual(recall.activeEngineName({ recallEngine: "classic" }), "classic", "classic still selectable as opt-out");
|
|
10
|
+
assert.strictEqual(recall.activeEngineName({ recallEngine: "nope" }), "discoverer", "unknown → default");
|
|
9
11
|
assert.strictEqual(recall.activeEngineName({ recallEngine: "CLASSIC" }), "classic", "case-insensitive");
|
|
10
12
|
assert.ok(recall.listEngines().includes("classic"));
|
|
11
13
|
assert.strictEqual(typeof recall.getEngine("classic").run, "function");
|
|
12
|
-
assert.strictEqual(recall.getEngine("bogus").name, "classic", "getEngine
|
|
14
|
+
assert.strictEqual(recall.getEngine("bogus").name, "classic", "getEngine hard-floor stays classic (crash-proof)");
|
|
13
15
|
|
|
14
16
|
// env override when no setting
|
|
15
17
|
const prev = process.env.RECALL_ENGINE;
|
|
16
18
|
process.env.RECALL_ENGINE = "classic";
|
|
17
|
-
assert.strictEqual(recall.activeEngineName(null), "classic");
|
|
19
|
+
assert.strictEqual(recall.activeEngineName(null), "classic", "env var overrides the default when no per-channel setting");
|
|
18
20
|
if (prev === undefined) delete process.env.RECALL_ENGINE; else process.env.RECALL_ENGINE = prev;
|
|
19
21
|
|
|
20
22
|
// --- classic engine orchestration: calls helpers and returns their blocks ---
|