@inetafrica/open-claudia 2.6.37 → 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.
@@ -30,6 +30,42 @@ function buildPersonaBlock() {
30
30
  }
31
31
  }
32
32
 
33
+ // Always-injected lessons: cross-cutting rules learned from past mistakes.
34
+ // Unlike packs/entities these are NOT topic-gated — they load every turn
35
+ // because the whole point is they apply when the topic ISN'T matched. They
36
+ // are binding defaults that override generic assumptions; the (src: pack)
37
+ // pointer is where the full context lives.
38
+ function buildLessonsBlock() {
39
+ try {
40
+ const { loadLessonsBlock, LESSONS_FILE } = require("./lessons");
41
+ const block = loadLessonsBlock();
42
+ if (!block) return "";
43
+ return `\n## Lessons learned\nHard-won rules distilled from past mistakes — things you got wrong before and were corrected on. These are always loaded (unlike topic-matched packs/notes) precisely because they apply when the topic ISN'T matched. Treat them as binding defaults that override generic assumptions; before acting on one, verify against its "(src: <pack>)" pointer, which holds the full context. Edit via /lessons or ${LESSONS_FILE}.\n\n${block}\n`;
44
+ } catch (e) {
45
+ return "";
46
+ }
47
+ }
48
+
49
+ // Always-on skill index (Hermes Tier-1): names + descriptions of packs you've
50
+ // flagged as reusable how-tos, so the agent knows each skill exists every turn
51
+ // even when the topic isn't matched. The full Procedure is NOT injected here —
52
+ // it loads on demand via `open-claudia pack show <dir>` (Tier-3 progressive
53
+ // disclosure). Only skill-flagged packs appear; the 80+ project-tracker packs
54
+ // stay topic-gated so this stays a short, high-signal list.
55
+ function buildSkillIndexBlock() {
56
+ try {
57
+ const packs = require("./packs").listSkillPacks();
58
+ if (!packs.length) return "";
59
+ const lines = packs
60
+ .slice(0, 30)
61
+ .map((p) => `- ${p.name} (\`${p.dir}\`) — ${p.description || "(no description)"}`)
62
+ .join("\n");
63
+ return `\n### Skills you've learned (always available)\nVerified how-tos you've been taught and can re-run. This is the index only — before doing one, load its full steps with \`open-claudia pack show <dir>\` and follow them rather than improvising (the Procedure encodes prerequisites and pitfalls you hit before):\n\n${lines}\n`;
64
+ } catch (e) {
65
+ return "";
66
+ }
67
+ }
68
+
33
69
  function buildSystemPrompt() {
34
70
  const state = currentState();
35
71
  const soul = loadSoul();
@@ -112,6 +148,7 @@ Keep replies clean and mobile-readable. Use short paragraphs and bullets. Avoid
112
148
  return `
113
149
  ${soul}
114
150
  ${buildPersonaBlock()}
151
+ ${buildLessonsBlock()}
115
152
  ## Runtime Context
116
153
  - Interface: ${channelLabel} chat through Open Claudia.
117
154
  - Active project path: ${state.currentSession ? state.currentSession.dir : "none"}
@@ -122,11 +159,13 @@ ${buildPersonaBlock()}
122
159
  Open Claudia learned skills are stored as context packs under ${path.join(CONFIG_DIR, "packs")}. Older \`~/.claude/skills/<name>/SKILL.md\` skills may have been migrated into packs; their reusable instructions live in the pack's Procedure section.
123
160
 
124
161
  If the user asks for a skill by name, do not rely only on the backend harness's native "Available skills" list. First use any Active context pack injected into the current request as the requested Open Claudia skill. If no matching pack was injected, inspect with \`open-claudia pack list\` / \`open-claudia pack show <dir>\` and legacy \`/skills\` paths before saying the skill does not exist.
162
+ ${buildSkillIndexBlock()}
125
163
 
126
164
  ## Stable Local Paths
127
165
  - Bot code: ${path.join(BOT_DIR, "bot.js")}
128
166
  - Soul file (identity + hard rules): ${SOUL_FILE}
129
167
  - Persona file (voice, evolved by dream): ${require("./persona").PERSONA_FILE}
168
+ - Lessons file (always-loaded learned rules; /lessons): ${require("./lessons").LESSONS_FILE}
130
169
  - Cron config: ${CRONS_FILE}
131
170
  - Vault file: ${VAULT_FILE}
132
171
  - Bot environment: ${path.join(BOT_DIR, ".env")} (sensitive; never expose values)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "2.6.37",
3
+ "version": "2.6.38",
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"
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"
13
13
  },
14
14
  "files": [
15
15
  "bot.js",
@@ -33,7 +33,15 @@
33
33
  "test-usage-accounting.js",
34
34
  "test-recall-engine.js",
35
35
  "test-recall-graph.js",
36
- "test-recall-discoverer.js"
36
+ "test-recall-discoverer.js",
37
+ "test-project-transcripts-smoke.js",
38
+ "test-abilities.js",
39
+ "test-ability-extraction.js",
40
+ "test-ability-couse.js",
41
+ "test-ability-transfer.js",
42
+ "test-ability-tiers.js",
43
+ "test-ability-merge-guard.js",
44
+ "test-learning-e2e.js"
37
45
  ],
38
46
  "keywords": [
39
47
  "claude",
@@ -0,0 +1,53 @@
1
+ // Abilities are first-class packs (kind:"ability") with reuse provenance
2
+ // (learned_on / applied_on). Verify the frontmatter round-trips, context packs
3
+ // stay churn-free, and the helpers behave.
4
+ const assert = require("assert");
5
+ const fs = require("fs");
6
+ const os = require("os");
7
+ const path = require("path");
8
+
9
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "abilities-"));
10
+ process.env.PACKS_DIR = path.join(tmp, "packs");
11
+
12
+ const packs = require("./core/packs");
13
+
14
+ // An ability with reuse provenance.
15
+ packs.createPack({
16
+ dir: "mobile-app-deploy", name: "Mobile App Deploy",
17
+ description: "Ship a mobile app: versionCode bump, APK, in-app updater",
18
+ stance: "Reusable how-to.", procedure: "1. bump 2. build 3. update",
19
+ kind: "ability", learned_on: "chat-mobile", applied_on: ["chat-mobile"],
20
+ });
21
+ let p = packs.readPack("mobile-app-deploy");
22
+ assert.strictEqual(p.kind, "ability", "kind persists");
23
+ assert.strictEqual(p.learned_on, "chat-mobile", "learned_on persists");
24
+ assert.deepStrictEqual(p.applied_on, ["chat-mobile"], "applied_on persists");
25
+
26
+ // A default pack is context and emits NO kind line (no churn on 121 existing packs).
27
+ packs.createPack({ dir: "billing", name: "Billing", description: "invoices" });
28
+ assert.strictEqual(packs.readPack("billing").kind, "context", "default kind is context");
29
+ const raw = fs.readFileSync(path.join(process.env.PACKS_DIR, "billing", "PACK.md"), "utf-8");
30
+ assert.ok(!/^kind:/m.test(raw), "context packs emit no kind line");
31
+ assert.ok(!/^learned_on:/m.test(raw), "context packs emit no learned_on line");
32
+
33
+ // listAbilities returns only abilities.
34
+ assert.deepStrictEqual(packs.listAbilities().map((a) => a.dir), ["mobile-app-deploy"], "listAbilities filters to abilities");
35
+
36
+ // recordApplied appends + dedupes, sets learned_on if unset.
37
+ packs.recordApplied("mobile-app-deploy", "spaces");
38
+ packs.recordApplied("mobile-app-deploy", "spaces");
39
+ p = packs.readPack("mobile-app-deploy");
40
+ assert.deepStrictEqual(p.applied_on, ["chat-mobile", "spaces"], "applied_on appends + dedupes");
41
+
42
+ // setSkill promotes to always-on AND implies ability (promoted things are abilities).
43
+ packs.createPack({ dir: "ci-verify", name: "CI Verify", description: "verify ci" });
44
+ packs.setSkill("ci-verify", true);
45
+ const sk = packs.readPack("ci-verify");
46
+ assert.strictEqual(sk.skill, true, "setSkill sets skill flag");
47
+ assert.strictEqual(sk.kind, "ability", "setSkill implies kind:ability");
48
+
49
+ // setKind can demote/relabel.
50
+ packs.setKind("ci-verify", "context");
51
+ assert.strictEqual(packs.readPack("ci-verify").kind, "context", "setKind relabels");
52
+
53
+ console.log("abilities OK — kind/learned_on/applied_on round-trip; helpers behave; context packs churn-free");
@@ -0,0 +1,68 @@
1
+ // PROOF (closed loop / automatic transfer): when the agent actually OPENS an
2
+ // ability pack alongside a project pack in one turn (the 📖 co-use signal),
3
+ // recordCoUse records the application, the governed-by edge forms on the next
4
+ // sync, and the ability becomes graph-discoverable from that project — WITHOUT
5
+ // the reviewer inferring anything. This is the automatic feedback loop.
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(), "ability-couse-"));
12
+ process.env.PACKS_DIR = path.join(tmp, "packs");
13
+ process.env.RECALL_GRAPH_DB = path.join(tmp, "graph.db");
14
+
15
+ const packs = require("./core/packs");
16
+ const graph = require("./core/recall/graph");
17
+ const entStub = { listEntities: () => [] };
18
+
19
+ // An ability learned on spaces, plus two other project context packs it has NOT
20
+ // been applied on yet.
21
+ packs.createPack({
22
+ dir: "mobile-app-deploy", name: "Mobile App Deploy",
23
+ description: "Ship a mobile app: bump versionCode, build APK, push in-app updater",
24
+ tags: ["mobile", "deploy", "apk"], kind: "ability",
25
+ learned_on: "spaces", applied_on: ["spaces"], procedure: "1. bump 2. build 3. update",
26
+ });
27
+ packs.createPack({ dir: "spaces", name: "Spaces", description: "Spaces app" });
28
+ packs.createPack({ dir: "chat-mobile", name: "Chat Mobile", description: "Spaces chat client" });
29
+ packs.createPack({ dir: "billing", name: "Billing", description: "invoices" });
30
+
31
+ // ── 1. CO-USE: the agent opens the ability and the chat-mobile project pack in
32
+ // the same turn. recordCoUse records the transfer and reports it as new.
33
+ const transferred = packs.recordCoUse(["pack:mobile-app-deploy", "pack:chat-mobile"]);
34
+ assert.strictEqual(transferred.length, 1, "one new transfer recorded");
35
+ assert.strictEqual(transferred[0].ability, "mobile-app-deploy");
36
+ assert.strictEqual(transferred[0].project, "chat-mobile");
37
+ assert.deepStrictEqual(packs.readPack("mobile-app-deploy").applied_on, ["spaces", "chat-mobile"], "applied_on grew from real use");
38
+
39
+ // Idempotent: opening them together again is not a NEW transfer (no re-announce).
40
+ assert.strictEqual(packs.recordCoUse(["pack:mobile-app-deploy", "pack:chat-mobile"]).length, 0, "repeat co-use is not new");
41
+
42
+ // Guard rails: an ability opened alone, or two context packs together, or an
43
+ // ability + entity, record nothing.
44
+ assert.strictEqual(packs.recordCoUse(["pack:mobile-app-deploy"]).length, 0, "ability alone records nothing");
45
+ assert.strictEqual(packs.recordCoUse(["pack:spaces", "pack:billing"]).length, 0, "two context packs record nothing");
46
+ assert.strictEqual(packs.recordCoUse(["pack:mobile-app-deploy", "entity:someone"]).length, 0, "ability + entity records nothing");
47
+ assert.deepStrictEqual(packs.readPack("billing").applied_on, [], "billing never gained applied_on");
48
+
49
+ if (!graph.available()) { console.log("ability co-use OK (graph asserts skipped — no node:sqlite)"); process.exit(0); }
50
+
51
+ // ── 2. The edge forms from the use-grown provenance, and the ability is now
52
+ // discoverable from chat-mobile via the graph.
53
+ graph.syncFromCorpus(packs, entStub);
54
+ const edges = graph.allEdges();
55
+ assert.ok(
56
+ edges.some((e) => e.src === "pack:chat-mobile" && e.dst === "pack:mobile-app-deploy" && e.type === "governed-by"),
57
+ "governed-by edge formed for chat-mobile from co-use"
58
+ );
59
+ const fromChat = graph.expand([{ id: "pack:chat-mobile", score: 4 }], {});
60
+ assert.ok(fromChat.has("pack:mobile-app-deploy"), "ability surfaces from chat-mobile after co-use");
61
+
62
+ // Negative control: billing was never co-used, so it does not surface it.
63
+ const fromBilling = graph.expand([{ id: "pack:billing", score: 4 }], {});
64
+ assert.ok(!fromBilling.has("pack:mobile-app-deploy"), "ability does NOT surface from an unrelated project");
65
+
66
+ console.log("ability co-use OK — actual use auto-records reuse, forms the edge, transfers the ability cross-project");
67
+ console.log(" applied_on after co-use:", packs.readPack("mobile-app-deploy").applied_on.join(", "));
68
+ console.log(" working on chat-mobile → graph pulls in:", [...fromChat.keys()].join(", ") || "(none)");
@@ -0,0 +1,109 @@
1
+ // PROOF (capture-time extraction): a reusable how-to demonstrated on ONE turn is
2
+ // extracted by the REVIEWER into an ability pack, the project→ability link forms
3
+ // after that FIRST occurrence (no [[link]] hand-authoring), and once the ability
4
+ // is re-applied to a SECOND project it becomes graph-discoverable from there too.
5
+ // Runs the real reviewer applyAction, real packs.js store, and real recall graph.
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(), "ability-extract-"));
12
+ process.env.PACKS_DIR = path.join(tmp, "packs");
13
+ process.env.RECALL_GRAPH_DB = path.join(tmp, "graph.db");
14
+
15
+ const packs = require("./core/packs");
16
+ const graph = require("./core/recall/graph");
17
+ const review = require("./core/pack-review");
18
+ const entStub = { listEntities: () => [] };
19
+
20
+ // Two real project context packs (so learned_on/applied_on can resolve to nodes),
21
+ // plus a negative-control project.
22
+ packs.createPack({ dir: "spaces", name: "Spaces", description: "Spaces app", stance: "Spaces project." });
23
+ packs.createPack({ dir: "chat-mobile", name: "Chat Mobile", description: "Spaces chat client", stance: "Chat project." });
24
+ packs.createPack({ dir: "billing", name: "Billing", description: "invoices and payments", stance: "Billing project." });
25
+
26
+ // ── 1. CAPTURE: the reviewer extracts an ability from a turn that shipped a
27
+ // mobile build on `spaces`. This is exactly the JSON the model is now told to
28
+ // emit (kind:"ability" + activity name/desc/tags + learned_on).
29
+ const created = review.applyAction({
30
+ action: "create",
31
+ dir: "mobile-app-deploy",
32
+ name: "Mobile App Deploy",
33
+ description: "Ship a mobile app: bump versionCode, build the APK, push the in-app updater",
34
+ tags: ["mobile", "deploy", "apk", "versioncode", "release"],
35
+ kind: "ability",
36
+ learned_on: "spaces",
37
+ applied_on: ["spaces"],
38
+ procedure: "1. bump versionCode 2. build APK 3. push in-app updater",
39
+ journal: "Shipped the spaces mobile build via versionCode bump + APK + in-app updater.",
40
+ });
41
+ assert.strictEqual(created.kind, "create", "ability is created");
42
+ assert.strictEqual(created.ability, true, "create result is flagged as an ability");
43
+ assert.strictEqual(created.learned_on, "spaces", "learned_on surfaces for the chat announcement");
44
+
45
+ const ability = packs.readPack("mobile-app-deploy");
46
+ assert.strictEqual(ability.kind, "ability", "persisted as kind:ability");
47
+ assert.strictEqual(ability.learned_on, "spaces", "learned_on persisted");
48
+ assert.deepStrictEqual(ability.applied_on, ["spaces"], "applied_on persisted");
49
+
50
+ // Activity-oriented metadata so the FIRST cross-project hit can match via FTS
51
+ // before any edge exists (the cold-start path).
52
+ assert.ok(/deploy|apk|versioncode/i.test(ability.description), "description is activity-oriented");
53
+ assert.ok(ability.tags.includes("apk"), "tags carry activity terms");
54
+
55
+ // The project pack was NOT mutated — no [[link]] injected, no provenance fight.
56
+ const spacesBody = Object.values(packs.readPack("spaces").sections).join("\n");
57
+ assert.ok(!/\[\[mobile-app-deploy\]\]/.test(spacesBody), "project pack left untouched (link is metadata-derived)");
58
+
59
+ if (!graph.available()) { console.log("ability extraction OK (graph asserts skipped — no node:sqlite)"); process.exit(0); }
60
+
61
+ // ── 2. LINK AFTER FIRST OCCURRENCE: syncFromCorpus derives governed-by from the
62
+ // ability's own provenance.
63
+ graph.syncFromCorpus(packs, entStub);
64
+ let edges = graph.allEdges();
65
+ assert.ok(
66
+ edges.some((e) => e.src === "pack:spaces" && e.dst === "pack:mobile-app-deploy" && e.type === "governed-by"),
67
+ "governed-by edge formed from metadata after the first occurrence"
68
+ );
69
+
70
+ // ── 3. REUSE ON ORIGIN: working on spaces surfaces the ability.
71
+ const fromSpaces = graph.expand([{ id: "pack:spaces", score: 4 }], {});
72
+ assert.ok(fromSpaces.has("pack:mobile-app-deploy"), "ability surfaces from its origin project");
73
+
74
+ // Negative control BEFORE reuse: an unrelated project does not pull it in.
75
+ const billingBefore = graph.expand([{ id: "pack:billing", score: 4 }], {});
76
+ assert.ok(!billingBefore.has("pack:mobile-app-deploy"), "unrelated project does not surface the ability");
77
+
78
+ // ── 4. CROSS-PROJECT TRANSFER: a later turn re-applies the SAME ability on
79
+ // chat-mobile. The reviewer records it via an update with applied_on (the
80
+ // inferred-from-content path; the co-use signal is the automatic one).
81
+ const reused = review.applyAction({
82
+ action: "update",
83
+ pack: "mobile-app-deploy",
84
+ applied_on: ["chat-mobile"],
85
+ journal: "Reused the mobile deploy flow to ship chat-mobile.",
86
+ });
87
+ assert.strictEqual(reused.appliedTo, "chat-mobile", "update records the cross-project reuse");
88
+ assert.deepStrictEqual(packs.readPack("mobile-app-deploy").applied_on, ["spaces", "chat-mobile"], "applied_on grew");
89
+
90
+ graph.syncFromCorpus(packs, entStub);
91
+ edges = graph.allEdges();
92
+ assert.ok(
93
+ edges.some((e) => e.src === "pack:chat-mobile" && e.dst === "pack:mobile-app-deploy" && e.type === "governed-by"),
94
+ "governed-by edge formed for the second project after reuse"
95
+ );
96
+ const fromChat = graph.expand([{ id: "pack:chat-mobile", score: 4 }], {});
97
+ assert.ok(fromChat.has("pack:mobile-app-deploy"), "ability now surfaces from the second project too");
98
+
99
+ // applied_on must NOT leak onto context packs (churn guard): re-applying to a
100
+ // context pack is ignored.
101
+ const ctxUpdate = review.applyAction({ action: "update", pack: "billing", applied_on: ["spaces"], journal: "noted" });
102
+ assert.strictEqual(ctxUpdate.appliedTo, null, "applied_on ignored on context packs");
103
+ assert.strictEqual(packs.readPack("billing").kind, "context", "billing stays context");
104
+ assert.deepStrictEqual(packs.readPack("billing").applied_on, [], "context pack gains no applied_on");
105
+
106
+ console.log("ability extraction OK — reviewer extracts an ability, link forms after first occurrence, transfers cross-project on reuse");
107
+ console.log(" working on spaces → graph pulls in:", [...fromSpaces.keys()].join(", ") || "(none)");
108
+ console.log(" working on chat-mobile → graph pulls in:", [...fromChat.keys()].join(", ") || "(none)");
109
+ console.log(" provenance on ability → learned on", ability.learned_on, "| applied on", packs.readPack("mobile-app-deploy").applied_on.join(", "));
@@ -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);