openvibers/openvibe.codes Commits
Load the small memory index into every prompt (#408)
Changes
@@@ -12,7 +12,8 @@* engine.runerror what a failed run really said* engine.adapters every harness Codes can run, with the portal's argv and environment* engine.jobrunner one attempt of one harness, the way the portal runs one-- * engine.prompt the prompt every job gets (the rules, the lessons, the digest, the brief)++ * engine.prompt the prompt every job gets (the rules, the lessons, the memory, the digest, the brief)++ * engine.memory the small memory index: MEMORY.md always, topics by search* engine.repomap the repo digest a job gets instead of a directory tree* engine.joblines the lines a task's page shows while it runs* engine.logins what this machine has: each CLI, signed in or not, and its limit windows@@@ -37,6 +38,7 @@ const runerror = require('./runerror');const adapters = require('./adapters');const jobrunner = require('./jobrunner');const prompt = require('./prompt');++const memory = require('./memory');const repomap = require('./repomap');const joblines = require('./joblines');const logins = require('./logins');@@@ -60,6 +62,7 @@ module.exports = {adapters,jobrunner,prompt,++ memory,repomap,joblines,logins,nnew file mode 100644@@@ -0,0 +1,101 @@++'use strict';++++/**++ * The small memory index: what the job remembers, from files beside the work.++ *++ * A repository keeps `MEMORY.md` (the index, always loaded) and `memory/<topic>.md` (one file per topic, loaded by++ * search). The index is cheap and stable, so every prompt carries it; a topic costs tokens, so only the topics the++ * brief's words point at ride along, most overlapping first. Everything is capped: an index is a pointer, not a dump.++ *++ * A folder with neither file gets nothing, never an error: memory is optional, and a job must not fail for lack of++ * remembering. The nightly dream pass (dedupe and correction against transcripts) and the receipt's cache-hit share++ * live on the Know side; this module is what Codes loads and what the dream pass reads back.++ *++ * indexOf(dir) the index text, capped, or ''++ * topics(dir) every topic file: [{ topic, text }]++ * search(dir, query) topics by word overlap, best first, capped++ * block({ dir, brief }) { text, indexChars, topics } — the prompt block++ * INDEX_CHARS, MAX_TOPICS, TOPIC_CHARS, INDEX_FILE, TOPICS_DIR++ */++++const fs = require('fs');++const path = require('path');++++const INDEX_FILE = 'MEMORY.md';++const TOPICS_DIR = 'memory';++const INDEX_CHARS = 4000;++const MAX_TOPICS = 2;++const TOPIC_CHARS = 2000;++++/** The file's text, capped, or '' when it is not there or not readable. */++function readCapped(file, max) {++ try {++ const text = fs.readFileSync(file, 'utf8');++ if (!text || !text.trim()) return '';++ return text.length > max ? text.slice(0, max) : text;++ } catch { return ''; }++}++++/** The index: the repository's MEMORY.md, always loaded, capped. */++function indexOf(dir) {++ if (!dir) return '';++ return readCapped(path.join(String(dir), INDEX_FILE), INDEX_CHARS);++}++++/** Every topic file under the repository's memory folder. */++function topics(dir) {++ if (!dir) return [];++ let names;++ try { names = fs.readdirSync(path.join(String(dir), TOPICS_DIR)); } catch { return []; }++ const out = [];++ for (const name of names.sort()) {++ if (!name.endsWith('.md') || name.startsWith('.')) continue;++ const text = readCapped(path.join(String(dir), TOPICS_DIR, name), TOPIC_CHARS);++ if (text) out.push({ topic: name.replace(/\.md$/, ''), text });++ }++ return out;++}++++/** The brief's words worth searching for: lowercased, short and common ones dropped. */++function wordsOf(query) {++ const stop = new Set(('the,a,an,and,or,to,of,in,on,for,with,from,that,this,what,when,how,are,was,were,is,it,as,at,be,by,do,does,did,you,your,into,over,under,than,then,than,not,but,all,any,can,will,just,make,fix,ship,use,add,new,old,way,our,its,their,them,they,we,my,me,i').split(','));++ return [...new Set(String(query || '').toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2 && !stop.has(w)))];++}++++/**++ * The topics the query points at: word overlap between the brief and the topic, most overlapping first, capped at++ * MAX_TOPICS. A topic no brief word reaches is not loaded, however short; ties break by topic name, so the choice++ * is stable for the same brief.++ */++function search(dir, query) {++ const words = wordsOf(query);++ if (!words.length) return [];++ const scored = [];++ for (const t of topics(dir)) {++ const hay = (t.topic + '\n' + t.text).toLowerCase();++ let score = 0;++ for (const w of words) if (hay.includes(w)) score++;++ if (score > 0) scored.push({ ...t, score });++ }++ scored.sort((a, b) => b.score - a.score || (a.topic < b.topic ? -1 : 1));++ return scored.slice(0, MAX_TOPICS);++}++++/** The prompt block: the index always, then the topics the brief points at. Empty when there is no memory. */++function block({ dir, brief } = {}) {++ const index = indexOf(dir);++ const found = search(dir, brief);++ const parts = [];++ if (index) parts.push('# Memory (index, always loaded)\n' + index);++ for (const t of found) parts.push(`# Memory: ${t.topic}\n` + t.text);++ return {++ text: parts.join('\n\n'),++ indexChars: index.length,++ topics: found.map((t) => ({ topic: t.topic, score: t.score, chars: t.text.length })),++ };++}++++module.exports = {++ INDEX_FILE, TOPICS_DIR, INDEX_CHARS, MAX_TOPICS, TOPIC_CHARS,++ indexOf, topics, search, block,++};@@@ -13,6 +13,7 @@const fs = require('fs');const path = require('path');const { repomap } = require('./repomap');++const { block: memoryBlock } = require('./memory');const HERE = __dirname;const FRAGMENTS = path.join(HERE, 'fragments');@@@ -63,12 +64,15 @@ function assemble({ brief, dir, env = process.env, know } = {}) {const houseRules = fragment('house-rules.md');const repo = repomap(dir);const learned = lessons({ file: know, repo: repo.repo, brief, env });++ // The small memory index: MEMORY.md always, the topics the brief points at (engine/memory.js).++ const memory = memoryBlock({ dir, brief });// Every prompt section is inspectable as a versioned layer. Stable layers stay first;// the request is always last so changing the task leaves the cached prefix byte-identical.const parts = [{ name: 'git-rule', layer: 'platform', source: 'engine/fragments/git-rule.md', version: 1, text: gitRule },{ name: 'house-rules', layer: 'product', source: 'engine/fragments/house-rules.md', version: 1, text: houseRules },{ name: 'lessons', layer: 'role', source: 'Know recall', version: 1, text: learned },++ { name: 'memory', layer: 'role', source: 'engine/memory.js', version: 1, text: memory.text },{ name: 'repo-digest', layer: 'role', source: 'engine/repomap.js', version: 1, text: repo.text },{ name: 'brief', layer: 'task', source: 'launch request', version: 1, text: String(brief || '') },].filter((p) => p.text && p.text.trim());nnew file mode 100644@@@ -0,0 +1,85 @@++'use strict';++++/**++ * Release 2 item 8 (Codes slice): the small memory index.++ *++ * MEMORY.md is always loaded; memory/<topic>.md files are loaded by search — the brief's words pick them, most++ * overlap first, at most MAX_TOPICS. Everything is capped, and a folder with no memory gives nothing, never an++ * error. The assembled prompt carries the block as its memory part.++ */++const assert = require('assert');++const fs = require('fs');++const os = require('os');++const path = require('path');++const memory = require('../engine/memory');++const { assemble } = require('../engine/prompt');++const { check, done } = require('./helpers/boot');++++const mem = (dir) => {++ fs.writeFileSync(path.join(dir, 'MEMORY.md'), '# Index\nAlways remember the deploy freeze on Fridays.\n');++ fs.mkdirSync(path.join(dir, 'memory'));++ fs.writeFileSync(path.join(dir, 'memory', 'deploy.md'), '# Deploy\nThe deploy freeze runs Friday UTC. Release trains ship Thursdays.\n');++ fs.writeFileSync(path.join(dir, 'memory', 'tipping.md'), '# Tipping\nTips settle in Billing on Mondays.\n');++ fs.writeFileSync(path.join(dir, 'memory', 'unrelated.md'), '# Gardening\nRoses need pruning in March.\n');++};++++(async () => {++ try {++ await check('the index is always loaded and capped', () => {++ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mem-idx-'));++ mem(dir);++ const b = memory.block({ dir, brief: 'something else entirely' });++ assert.ok(b.text.includes('deploy freeze'), 'the index rides along whatever the brief');++ assert.strictEqual(b.topics.length, 0, 'no topic matches, none is loaded');++ fs.writeFileSync(path.join(dir, 'MEMORY.md'), 'x'.repeat(9000));++ assert.ok(memory.indexOf(dir).length <= memory.INDEX_CHARS, 'the index is capped');++ fs.rmSync(dir, { recursive: true, force: true });++ });++++ await check('topics load by search, most overlap first, at most two', () => {++ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mem-top-'));++ mem(dir);++ const found = memory.search(dir, 'when is the deploy freeze and do release trains ship?');++ assert.deepStrictEqual(found.map((t) => t.topic), ['deploy'], 'only the matching topic loads');++ const both = memory.search(dir, 'deploy freeze tips settle billing monday');++ assert.deepStrictEqual(both.map((t) => t.topic), ['tipping', 'deploy'], 'most overlap first');++ assert.deepStrictEqual(memory.search(dir, 'roses pruning')[0].topic, 'unrelated');++ assert.deepStrictEqual(memory.search(dir, 'quantum entanglement'), [], 'nothing matching, nothing loaded');++ fs.rmSync(dir, { recursive: true, force: true });++ });++++ await check('no memory is nothing, never an error', () => {++ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mem-none-'));++ assert.strictEqual(memory.indexOf(dir), '');++ assert.deepStrictEqual(memory.topics(dir), []);++ assert.deepStrictEqual(memory.search(dir, 'deploy'), []);++ assert.strictEqual(memory.block({ dir, brief: 'deploy' }).text, '');++ assert.strictEqual(memory.block({}).text, '');++ fs.rmSync(dir, { recursive: true, force: true });++ });++++ await check('the assembled prompt carries the memory between the lessons and the digest', () => {++ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mem-asm-'));++ require('node:child_process').execFileSync('git', ['init', '-q'], { cwd: dir });++ fs.writeFileSync(path.join(dir, 'README.md'), '# Demo\n');++ mem(dir);++ const built = assemble({ brief: 'when is the deploy freeze?', dir });++ const names = built.parts.map((p) => p.name);++ assert.deepStrictEqual(names, ['git-rule', 'house-rules', 'memory', 'repo-digest', 'brief']);++ const part = built.parts.find((p) => p.name === 'memory');++ assert.ok(part.text.includes('deploy freeze'), 'the index is in the prompt');++ assert.ok(part.text.includes('Release trains ship Thursdays'), 'the matching topic is in the prompt');++ assert.ok(!part.text.includes('Roses need pruning'), 'the unmatching topic stays out');++ assert.strictEqual(part.layer, 'role');++ const bare = assemble({ brief: 'x', dir: fs.mkdtempSync(path.join(os.tmpdir(), 'mem-bare-')) });++ assert.ok(!bare.parts.some((p) => p.name === 'memory'), 'no memory files, no part');++ fs.rmSync(dir, { recursive: true, force: true });++ });++++ console.log('memory: the index always, the topics the brief points at, capped');++ } catch (e) {++ console.error(e);++ process.exit(1);++ }++ done();++})();
@@ -12,7 +12,8 @@
@@ -12,7 +12,8 @@
* engine.runerror what a failed run really said
* engine.runerror what a failed run really said
* engine.adapters every harness Codes can run, with the portal's argv and environment
* engine.adapters every harness Codes can run, with the portal's argv and environment
* engine.jobrunner one attempt of one harness, the way the portal runs one
* engine.jobrunner one attempt of one harness, the way the portal runs one
- * engine.prompt the prompt every job gets (the rules, the lessons, the digest, the brief)
+ * engine.prompt the prompt every job gets (the rules, the lessons, the memory, the digest, the brief)
+ * engine.memory the small memory index: MEMORY.md always, topics by search
* engine.repomap the repo digest a job gets instead of a directory tree
* engine.repomap the repo digest a job gets instead of a directory tree
* engine.joblines the lines a task's page shows while it runs
* engine.joblines the lines a task's page shows while it runs
* engine.logins what this machine has: each CLI, signed in or not, and its limit windows
* engine.logins what this machine has: each CLI, signed in or not, and its limit windows
@@ -37,6 +38,7 @@ const runerror = require('./runerror');
@@ -37,6 +38,7 @@ const runerror = require('./runerror');
const adapters = require('./adapters');
const adapters = require('./adapters');
const jobrunner = require('./jobrunner');
const jobrunner = require('./jobrunner');
const prompt = require('./prompt');
const prompt = require('./prompt');
+const memory = require('./memory');
const repomap = require('./repomap');
const repomap = require('./repomap');
const joblines = require('./joblines');
const joblines = require('./joblines');
const logins = require('./logins');
const logins = require('./logins');
@@ -60,6 +62,7 @@ module.exports = {
@@ -60,6 +62,7 @@ module.exports = {
adapters,
adapters,
jobrunner,
jobrunner,
prompt,
prompt,
+ memory,
repomap,
repomap,
joblines,
joblines,
logins,
logins,
new file mode 100644
new file mode 100644
@@ -0,0 +1,101 @@
@@ -0,0 +1,101 @@
+'use strict';
+
+/**
+ * The small memory index: what the job remembers, from files beside the work.
+ *
+ * A repository keeps `MEMORY.md` (the index, always loaded) and `memory/<topic>.md` (one file per topic, loaded by
+ * search). The index is cheap and stable, so every prompt carries it; a topic costs tokens, so only the topics the
+ * brief's words point at ride along, most overlapping first. Everything is capped: an index is a pointer, not a dump.
+ *
+ * A folder with neither file gets nothing, never an error: memory is optional, and a job must not fail for lack of
+ * remembering. The nightly dream pass (dedupe and correction against transcripts) and the receipt's cache-hit share
+ * live on the Know side; this module is what Codes loads and what the dream pass reads back.
+ *
+ * indexOf(dir) the index text, capped, or ''
+ * topics(dir) every topic file: [{ topic, text }]
+ * search(dir, query) topics by word overlap, best first, capped
+ * block({ dir, brief }) { text, indexChars, topics } — the prompt block
+ * INDEX_CHARS, MAX_TOPICS, TOPIC_CHARS, INDEX_FILE, TOPICS_DIR
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+const INDEX_FILE = 'MEMORY.md';
+const TOPICS_DIR = 'memory';
+const INDEX_CHARS = 4000;
+const MAX_TOPICS = 2;
+const TOPIC_CHARS = 2000;
+
+/** The file's text, capped, or '' when it is not there or not readable. */
+function readCapped(file, max) {
+ try {
+ const text = fs.readFileSync(file, 'utf8');
+ if (!text || !text.trim()) return '';
+ return text.length > max ? text.slice(0, max) : text;
+ } catch { return ''; }
+}
+
+/** The index: the repository's MEMORY.md, always loaded, capped. */
+function indexOf(dir) {
+ if (!dir) return '';
+ return readCapped(path.join(String(dir), INDEX_FILE), INDEX_CHARS);
+}
+
+/** Every topic file under the repository's memory folder. */
+function topics(dir) {
+ if (!dir) return [];
+ let names;
+ try { names = fs.readdirSync(path.join(String(dir), TOPICS_DIR)); } catch { return []; }
+ const out = [];
+ for (const name of names.sort()) {
+ if (!name.endsWith('.md') || name.startsWith('.')) continue;
+ const text = readCapped(path.join(String(dir), TOPICS_DIR, name), TOPIC_CHARS);
+ if (text) out.push({ topic: name.replace(/\.md$/, ''), text });
+ }
+ return out;
+}
+
+/** The brief's words worth searching for: lowercased, short and common ones dropped. */
+function wordsOf(query) {
+ const stop = new Set(('the,a,an,and,or,to,of,in,on,for,with,from,that,this,what,when,how,are,was,were,is,it,as,at,be,by,do,does,did,you,your,into,over,under,than,then,than,not,but,all,any,can,will,just,make,fix,ship,use,add,new,old,way,our,its,their,them,they,we,my,me,i').split(','));
+ return [...new Set(String(query || '').toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2 && !stop.has(w)))];
+}
+
+/**
+ * The topics the query points at: word overlap between the brief and the topic, most overlapping first, capped at
+ * MAX_TOPICS. A topic no brief word reaches is not loaded, however short; ties break by topic name, so the choice
+ * is stable for the same brief.
+ */
+function search(dir, query) {
+ const words = wordsOf(query);
+ if (!words.length) return [];
+ const scored = [];
+ for (const t of topics(dir)) {
+ const hay = (t.topic + '\n' + t.text).toLowerCase();
+ let score = 0;
+ for (const w of words) if (hay.includes(w)) score++;
+ if (score > 0) scored.push({ ...t, score });
+ }
+ scored.sort((a, b) => b.score - a.score || (a.topic < b.topic ? -1 : 1));
+ return scored.slice(0, MAX_TOPICS);
+}
+
+/** The prompt block: the index always, then the topics the brief points at. Empty when there is no memory. */
+function block({ dir, brief } = {}) {
+ const index = indexOf(dir);
+ const found = search(dir, brief);
+ const parts = [];
+ if (index) parts.push('# Memory (index, always loaded)\n' + index);
+ for (const t of found) parts.push(`# Memory: ${t.topic}\n` + t.text);
+ return {
+ text: parts.join('\n\n'),
+ indexChars: index.length,
+ topics: found.map((t) => ({ topic: t.topic, score: t.score, chars: t.text.length })),
+ };
+}
+
+module.exports = {
+ INDEX_FILE, TOPICS_DIR, INDEX_CHARS, MAX_TOPICS, TOPIC_CHARS,
+ indexOf, topics, search, block,
+};
@@ -13,6 +13,7 @@
@@ -13,6 +13,7 @@
const fs = require('fs');
const fs = require('fs');
const path = require('path');
const path = require('path');
const { repomap } = require('./repomap');
const { repomap } = require('./repomap');
+const { block: memoryBlock } = require('./memory');
const HERE = __dirname;
const HERE = __dirname;
const FRAGMENTS = path.join(HERE, 'fragments');
const FRAGMENTS = path.join(HERE, 'fragments');
@@ -63,12 +64,15 @@ function assemble({ brief, dir, env = process.env, know } = {}) {
@@ -63,12 +64,15 @@ function assemble({ brief, dir, env = process.env, know } = {}) {
const houseRules = fragment('house-rules.md');
const houseRules = fragment('house-rules.md');
const repo = repomap(dir);
const repo = repomap(dir);
const learned = lessons({ file: know, repo: repo.repo, brief, env });
const learned = lessons({ file: know, repo: repo.repo, brief, env });
+ // The small memory index: MEMORY.md always, the topics the brief points at (engine/memory.js).
+ const memory = memoryBlock({ dir, brief });
// Every prompt section is inspectable as a versioned layer. Stable layers stay first;
// Every prompt section is inspectable as a versioned layer. Stable layers stay first;
// the request is always last so changing the task leaves the cached prefix byte-identical.
// the request is always last so changing the task leaves the cached prefix byte-identical.
const parts = [
const parts = [
{ name: 'git-rule', layer: 'platform', source: 'engine/fragments/git-rule.md', version: 1, text: gitRule },
{ name: 'git-rule', layer: 'platform', source: 'engine/fragments/git-rule.md', version: 1, text: gitRule },
{ name: 'house-rules', layer: 'product', source: 'engine/fragments/house-rules.md', version: 1, text: houseRules },
{ name: 'house-rules', layer: 'product', source: 'engine/fragments/house-rules.md', version: 1, text: houseRules },
{ name: 'lessons', layer: 'role', source: 'Know recall', version: 1, text: learned },
{ name: 'lessons', layer: 'role', source: 'Know recall', version: 1, text: learned },
+ { name: 'memory', layer: 'role', source: 'engine/memory.js', version: 1, text: memory.text },
{ name: 'repo-digest', layer: 'role', source: 'engine/repomap.js', version: 1, text: repo.text },
{ name: 'repo-digest', layer: 'role', source: 'engine/repomap.js', version: 1, text: repo.text },
{ name: 'brief', layer: 'task', source: 'launch request', version: 1, text: String(brief || '') },
{ name: 'brief', layer: 'task', source: 'launch request', version: 1, text: String(brief || '') },
].filter((p) => p.text && p.text.trim());
].filter((p) => p.text && p.text.trim());
new file mode 100644
new file mode 100644
@@ -0,0 +1,85 @@
@@ -0,0 +1,85 @@
+'use strict';
+
+/**
+ * Release 2 item 8 (Codes slice): the small memory index.
+ *
+ * MEMORY.md is always loaded; memory/<topic>.md files are loaded by search — the brief's words pick them, most
+ * overlap first, at most MAX_TOPICS. Everything is capped, and a folder with no memory gives nothing, never an
+ * error. The assembled prompt carries the block as its memory part.
+ */
+const assert = require('assert');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const memory = require('../engine/memory');
+const { assemble } = require('../engine/prompt');
+const { check, done } = require('./helpers/boot');
+
+const mem = (dir) => {
+ fs.writeFileSync(path.join(dir, 'MEMORY.md'), '# Index\nAlways remember the deploy freeze on Fridays.\n');
+ fs.mkdirSync(path.join(dir, 'memory'));
+ fs.writeFileSync(path.join(dir, 'memory', 'deploy.md'), '# Deploy\nThe deploy freeze runs Friday UTC. Release trains ship Thursdays.\n');
+ fs.writeFileSync(path.join(dir, 'memory', 'tipping.md'), '# Tipping\nTips settle in Billing on Mondays.\n');
+ fs.writeFileSync(path.join(dir, 'memory', 'unrelated.md'), '# Gardening\nRoses need pruning in March.\n');
+};
+
+(async () => {
+ try {
+ await check('the index is always loaded and capped', () => {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mem-idx-'));
+ mem(dir);
+ const b = memory.block({ dir, brief: 'something else entirely' });
+ assert.ok(b.text.includes('deploy freeze'), 'the index rides along whatever the brief');
+ assert.strictEqual(b.topics.length, 0, 'no topic matches, none is loaded');
+ fs.writeFileSync(path.join(dir, 'MEMORY.md'), 'x'.repeat(9000));
+ assert.ok(memory.indexOf(dir).length <= memory.INDEX_CHARS, 'the index is capped');
+ fs.rmSync(dir, { recursive: true, force: true });
+ });
+
+ await check('topics load by search, most overlap first, at most two', () => {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mem-top-'));
+ mem(dir);
+ const found = memory.search(dir, 'when is the deploy freeze and do release trains ship?');
+ assert.deepStrictEqual(found.map((t) => t.topic), ['deploy'], 'only the matching topic loads');
+ const both = memory.search(dir, 'deploy freeze tips settle billing monday');
+ assert.deepStrictEqual(both.map((t) => t.topic), ['tipping', 'deploy'], 'most overlap first');
+ assert.deepStrictEqual(memory.search(dir, 'roses pruning')[0].topic, 'unrelated');
+ assert.deepStrictEqual(memory.search(dir, 'quantum entanglement'), [], 'nothing matching, nothing loaded');
+ fs.rmSync(dir, { recursive: true, force: true });
+ });
+
+ await check('no memory is nothing, never an error', () => {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mem-none-'));
+ assert.strictEqual(memory.indexOf(dir), '');
+ assert.deepStrictEqual(memory.topics(dir), []);
+ assert.deepStrictEqual(memory.search(dir, 'deploy'), []);
+ assert.strictEqual(memory.block({ dir, brief: 'deploy' }).text, '');
+ assert.strictEqual(memory.block({}).text, '');
+ fs.rmSync(dir, { recursive: true, force: true });
+ });
+
+ await check('the assembled prompt carries the memory between the lessons and the digest', () => {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mem-asm-'));
+ require('node:child_process').execFileSync('git', ['init', '-q'], { cwd: dir });
+ fs.writeFileSync(path.join(dir, 'README.md'), '# Demo\n');
+ mem(dir);
+ const built = assemble({ brief: 'when is the deploy freeze?', dir });
+ const names = built.parts.map((p) => p.name);
+ assert.deepStrictEqual(names, ['git-rule', 'house-rules', 'memory', 'repo-digest', 'brief']);
+ const part = built.parts.find((p) => p.name === 'memory');
+ assert.ok(part.text.includes('deploy freeze'), 'the index is in the prompt');
+ assert.ok(part.text.includes('Release trains ship Thursdays'), 'the matching topic is in the prompt');
+ assert.ok(!part.text.includes('Roses need pruning'), 'the unmatching topic stays out');
+ assert.strictEqual(part.layer, 'role');
+ const bare = assemble({ brief: 'x', dir: fs.mkdtempSync(path.join(os.tmpdir(), 'mem-bare-')) });
+ assert.ok(!bare.parts.some((p) => p.name === 'memory'), 'no memory files, no part');
+ fs.rmSync(dir, { recursive: true, force: true });
+ });
+
+ console.log('memory: the index always, the topics the brief points at, capped');
+ } catch (e) {
+ console.error(e);
+ process.exit(1);
+ }
+ done();
+})();