metame-cli 1.5.22 → 1.5.24
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/index.js +18 -1
- package/package.json +3 -2
- package/scripts/core/team-session-route.js +164 -0
- package/scripts/daemon-agent-commands.js +41 -43
- package/scripts/daemon-bridges.js +50 -9
- package/scripts/daemon-claude-engine.js +13 -1
- package/scripts/daemon-command-router.js +24 -6
- package/scripts/daemon-command-session-route.js +13 -38
- package/scripts/daemon-reactive-lifecycle.js +6 -6
- package/scripts/daemon-session-commands.js +49 -45
- package/scripts/daemon-session-store.js +109 -2
- package/scripts/daemon-warm-pool.js +65 -14
- package/scripts/daemon.js +7 -0
- package/scripts/ops-mission-queue.js +24 -1
- package/scripts/ops-reactive-bootstrap.js +46 -2
- package/scripts/resolve-yaml.js +3 -0
- package/scripts/runtime-bootstrap.js +77 -0
- package/scripts/core/handoff.test.js +0 -1074
- package/scripts/core/memory-model.test.js +0 -486
- package/scripts/core/reactive-paths.test.js +0 -35
- package/scripts/core/reactive-prompt.test.js +0 -88
- package/scripts/core/reactive-signal.test.js +0 -88
- package/scripts/core/thread-chat-id.test.js +0 -113
- package/scripts/sync-readme.js +0 -64
|
@@ -1,486 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { describe, it } = require('node:test');
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
|
|
6
|
-
const {
|
|
7
|
-
matchScope,
|
|
8
|
-
scoreMemoryItem,
|
|
9
|
-
rankMemoryItems,
|
|
10
|
-
allocateBudget,
|
|
11
|
-
judgeMerge,
|
|
12
|
-
assemblePromptBlocks,
|
|
13
|
-
shouldPromote,
|
|
14
|
-
shouldArchive,
|
|
15
|
-
KIND_WEIGHTS,
|
|
16
|
-
DEFAULT_BUDGET,
|
|
17
|
-
RECENCY_HALF_LIFE_DAYS,
|
|
18
|
-
} = require('./memory-model');
|
|
19
|
-
|
|
20
|
-
function makeItem(overrides = {}) {
|
|
21
|
-
return {
|
|
22
|
-
id: 'mem_test',
|
|
23
|
-
kind: 'insight',
|
|
24
|
-
state: 'active',
|
|
25
|
-
title: 'Test item',
|
|
26
|
-
content: 'Test content',
|
|
27
|
-
confidence: 0.7,
|
|
28
|
-
project: 'metame',
|
|
29
|
-
scope: null,
|
|
30
|
-
agent_key: null,
|
|
31
|
-
task_key: null,
|
|
32
|
-
session_id: null,
|
|
33
|
-
source_type: 'extract',
|
|
34
|
-
tags: '[]',
|
|
35
|
-
fts_rank: 0.5,
|
|
36
|
-
search_count: 0,
|
|
37
|
-
last_searched_at: null,
|
|
38
|
-
created_at: new Date().toISOString(),
|
|
39
|
-
updated_at: new Date().toISOString(),
|
|
40
|
-
...overrides,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function daysAgo(n) {
|
|
45
|
-
const d = new Date();
|
|
46
|
-
d.setDate(d.getDate() - n);
|
|
47
|
-
return d.toISOString();
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
// matchScope
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
|
|
54
|
-
describe('matchScope', () => {
|
|
55
|
-
it('exact match (same project + scope + agent) → 1.0', () => {
|
|
56
|
-
const itemScope = { project: 'metame', scope: 'core', agent: 'jarvis' };
|
|
57
|
-
const queryScope = { project: 'metame', scope: 'core', agent: 'jarvis' };
|
|
58
|
-
assert.strictEqual(matchScope(itemScope, queryScope), 1.0);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('same project, different scope → 0.6', () => {
|
|
62
|
-
const itemScope = { project: 'metame', scope: 'daemon', agent: null };
|
|
63
|
-
const queryScope = { project: 'metame', scope: 'core', agent: null };
|
|
64
|
-
assert.strictEqual(matchScope(itemScope, queryScope), 0.6);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('wildcard project * → 0.3', () => {
|
|
68
|
-
const itemScope = { project: '*', scope: null, agent: null };
|
|
69
|
-
const queryScope = { project: 'metame', scope: 'core', agent: null };
|
|
70
|
-
assert.strictEqual(matchScope(itemScope, queryScope), 0.3);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('no match at all → 0', () => {
|
|
74
|
-
const itemScope = { project: 'other', scope: 'x', agent: 'a' };
|
|
75
|
-
const queryScope = { project: 'metame', scope: 'core', agent: 'jarvis' };
|
|
76
|
-
assert.strictEqual(matchScope(itemScope, queryScope), 0);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('handles missing/null fields gracefully', () => {
|
|
80
|
-
// null/undefined → 0
|
|
81
|
-
assert.strictEqual(matchScope(null, null), 0);
|
|
82
|
-
assert.strictEqual(matchScope(null, { project: 'metame' }), 0);
|
|
83
|
-
assert.strictEqual(matchScope({ project: 'metame' }, null), 0);
|
|
84
|
-
// both empty → both default to '*' → 0.3 (wildcard rule)
|
|
85
|
-
assert.strictEqual(matchScope({}, {}), 0.3);
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
// ---------------------------------------------------------------------------
|
|
90
|
-
// scoreMemoryItem
|
|
91
|
-
// ---------------------------------------------------------------------------
|
|
92
|
-
|
|
93
|
-
describe('scoreMemoryItem', () => {
|
|
94
|
-
const scopeCtx = { project: 'metame', scope: 'core', agent: 'jarvis' };
|
|
95
|
-
|
|
96
|
-
it('convention item with high scope match scores highest', () => {
|
|
97
|
-
const item = makeItem({
|
|
98
|
-
kind: 'convention',
|
|
99
|
-
project: 'metame',
|
|
100
|
-
scope: 'core',
|
|
101
|
-
agent_key: 'jarvis',
|
|
102
|
-
confidence: 1.0,
|
|
103
|
-
fts_rank: 1.0,
|
|
104
|
-
});
|
|
105
|
-
const query = { text: 'test', scope: scopeCtx };
|
|
106
|
-
const score = scoreMemoryItem(item, query, scopeCtx);
|
|
107
|
-
assert.ok(score > 0.5, `expected high score, got ${score}`);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('episode item with low scope match scores lower than convention', () => {
|
|
111
|
-
const convention = makeItem({
|
|
112
|
-
kind: 'convention',
|
|
113
|
-
project: 'metame',
|
|
114
|
-
scope: 'core',
|
|
115
|
-
agent_key: 'jarvis',
|
|
116
|
-
confidence: 1.0,
|
|
117
|
-
fts_rank: 1.0,
|
|
118
|
-
});
|
|
119
|
-
const episode = makeItem({
|
|
120
|
-
kind: 'episode',
|
|
121
|
-
project: 'other',
|
|
122
|
-
confidence: 0.3,
|
|
123
|
-
fts_rank: 0.1,
|
|
124
|
-
});
|
|
125
|
-
const convScore = scoreMemoryItem(convention, 'test', scopeCtx);
|
|
126
|
-
const epiScore = scoreMemoryItem(episode, 'test', scopeCtx);
|
|
127
|
-
assert.ok(convScore > epiScore, `convention ${convScore} should beat episode ${epiScore}`);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('recency decay: item from today vs 60 days ago', () => {
|
|
131
|
-
const recent = makeItem({ created_at: new Date().toISOString() });
|
|
132
|
-
const old = makeItem({ created_at: daysAgo(60) });
|
|
133
|
-
const query = { text: 'test', scope: scopeCtx };
|
|
134
|
-
const recentScore = scoreMemoryItem(recent, query, scopeCtx);
|
|
135
|
-
const oldScore = scoreMemoryItem(old, query, scopeCtx);
|
|
136
|
-
assert.ok(recentScore > oldScore, `recent ${recentScore} should beat old ${oldScore}`);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('missing fts_rank defaults to 0', () => {
|
|
140
|
-
const item = makeItem({ fts_rank: undefined });
|
|
141
|
-
const query = { text: 'test', scope: scopeCtx };
|
|
142
|
-
const score = scoreMemoryItem(item, query, scopeCtx);
|
|
143
|
-
assert.strictEqual(typeof score, 'number');
|
|
144
|
-
assert.ok(!Number.isNaN(score));
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('confidence 0 vs 1 difference', () => {
|
|
148
|
-
const low = makeItem({ confidence: 0 });
|
|
149
|
-
const high = makeItem({ confidence: 1 });
|
|
150
|
-
const query = { text: 'test', scope: scopeCtx };
|
|
151
|
-
const lowScore = scoreMemoryItem(low, query, scopeCtx);
|
|
152
|
-
const highScore = scoreMemoryItem(high, query, scopeCtx);
|
|
153
|
-
assert.ok(highScore > lowScore, `high conf ${highScore} should beat low ${lowScore}`);
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// ---------------------------------------------------------------------------
|
|
158
|
-
// rankMemoryItems
|
|
159
|
-
// ---------------------------------------------------------------------------
|
|
160
|
-
|
|
161
|
-
describe('rankMemoryItems', () => {
|
|
162
|
-
const scopeCtx = { project: 'metame', scope: 'core', agent: 'jarvis' };
|
|
163
|
-
const query = { text: 'test', scope: scopeCtx };
|
|
164
|
-
|
|
165
|
-
it('returns items sorted by score descending', () => {
|
|
166
|
-
const items = [
|
|
167
|
-
makeItem({ id: 'low', confidence: 0.1, fts_rank: 0.1 }),
|
|
168
|
-
makeItem({ id: 'high', confidence: 1.0, fts_rank: 1.0, kind: 'convention', project: 'metame', scope: 'core', agent_key: 'jarvis' }),
|
|
169
|
-
];
|
|
170
|
-
const ranked = rankMemoryItems(items, query, scopeCtx);
|
|
171
|
-
assert.strictEqual(ranked[0].id, 'high');
|
|
172
|
-
assert.strictEqual(ranked[1].id, 'low');
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('each item gets .score property added', () => {
|
|
176
|
-
const items = [makeItem()];
|
|
177
|
-
const ranked = rankMemoryItems(items, query, scopeCtx);
|
|
178
|
-
assert.strictEqual(typeof ranked[0].score, 'number');
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it('empty array returns empty array', () => {
|
|
182
|
-
const ranked = rankMemoryItems([], query, scopeCtx);
|
|
183
|
-
assert.deepStrictEqual(ranked, []);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it('all same score: stable order', () => {
|
|
187
|
-
const items = [
|
|
188
|
-
makeItem({ id: 'a' }),
|
|
189
|
-
makeItem({ id: 'b' }),
|
|
190
|
-
makeItem({ id: 'c' }),
|
|
191
|
-
];
|
|
192
|
-
const ranked = rankMemoryItems(items, query, scopeCtx);
|
|
193
|
-
// Same score → original insertion order preserved
|
|
194
|
-
const ids = ranked.map((r) => r.id);
|
|
195
|
-
assert.deepStrictEqual(ids, ['a', 'b', 'c']);
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
// ---------------------------------------------------------------------------
|
|
200
|
-
// allocateBudget
|
|
201
|
-
// ---------------------------------------------------------------------------
|
|
202
|
-
|
|
203
|
-
describe('allocateBudget', () => {
|
|
204
|
-
it('respects per-kind limits (e.g., 8 conventions max)', () => {
|
|
205
|
-
const items = Array.from({ length: 20 }, (_, i) =>
|
|
206
|
-
makeItem({ id: `c${i}`, kind: 'convention', state: 'active', score: 1 - i * 0.01 })
|
|
207
|
-
);
|
|
208
|
-
const result = allocateBudget(items, DEFAULT_BUDGET);
|
|
209
|
-
assert.ok(result.convention.length <= (DEFAULT_BUDGET.convention || 8));
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it('only includes state=active items', () => {
|
|
213
|
-
const items = [
|
|
214
|
-
makeItem({ id: 'a1', state: 'active', kind: 'insight', score: 0.9 }),
|
|
215
|
-
makeItem({ id: 'a2', state: 'candidate', kind: 'insight', score: 0.95 }),
|
|
216
|
-
];
|
|
217
|
-
const result = allocateBudget(items, DEFAULT_BUDGET);
|
|
218
|
-
const allIds = [...result.convention, ...result.insight, ...result.profile, ...result.episode].map((i) => i.id);
|
|
219
|
-
assert.ok(!allIds.includes('a2'), 'candidate item should be filtered out');
|
|
220
|
-
assert.ok(allIds.includes('a1'), 'active item should be included');
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it('filters out candidate/archived items', () => {
|
|
224
|
-
const items = [
|
|
225
|
-
makeItem({ id: 'cand', state: 'candidate', kind: 'insight', score: 0.9 }),
|
|
226
|
-
makeItem({ id: 'arch', state: 'archived', kind: 'insight', score: 0.8 }),
|
|
227
|
-
makeItem({ id: 'act', state: 'active', kind: 'insight', score: 0.7 }),
|
|
228
|
-
];
|
|
229
|
-
const result = allocateBudget(items, DEFAULT_BUDGET);
|
|
230
|
-
const allIds = [...result.convention, ...result.insight, ...result.profile, ...result.episode].map((i) => i.id);
|
|
231
|
-
assert.ok(!allIds.includes('cand'));
|
|
232
|
-
assert.ok(!allIds.includes('arch'));
|
|
233
|
-
assert.ok(allIds.includes('act'));
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it('empty input returns empty buckets', () => {
|
|
237
|
-
const result = allocateBudget([], DEFAULT_BUDGET);
|
|
238
|
-
assert.deepStrictEqual(result.convention, []);
|
|
239
|
-
assert.deepStrictEqual(result.insight, []);
|
|
240
|
-
assert.deepStrictEqual(result.profile, []);
|
|
241
|
-
assert.deepStrictEqual(result.episode, []);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it('budget overflow: 20 conventions truncated to limit', () => {
|
|
245
|
-
const items = Array.from({ length: 20 }, (_, i) =>
|
|
246
|
-
makeItem({ id: `c${i}`, kind: 'convention', state: 'active', score: 1 - i * 0.01 })
|
|
247
|
-
);
|
|
248
|
-
const budget = { convention: 5, insight: 5, profile: 3, episode: 3 };
|
|
249
|
-
const result = allocateBudget(items, budget);
|
|
250
|
-
assert.strictEqual(result.convention.length, 5);
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('custom budget config overrides defaults', () => {
|
|
254
|
-
const items = [
|
|
255
|
-
makeItem({ id: 'i1', kind: 'insight', state: 'active', score: 0.9 }),
|
|
256
|
-
makeItem({ id: 'i2', kind: 'insight', state: 'active', score: 0.8 }),
|
|
257
|
-
makeItem({ id: 'i3', kind: 'insight', state: 'active', score: 0.7 }),
|
|
258
|
-
];
|
|
259
|
-
const budget = { convention: 8, insight: 1, profile: 3, episode: 3 };
|
|
260
|
-
const result = allocateBudget(items, budget);
|
|
261
|
-
assert.strictEqual(result.insight.length, 1);
|
|
262
|
-
});
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
// ---------------------------------------------------------------------------
|
|
266
|
-
// judgeMerge
|
|
267
|
-
// ---------------------------------------------------------------------------
|
|
268
|
-
|
|
269
|
-
describe('judgeMerge', () => {
|
|
270
|
-
it('exact content match → noop', () => {
|
|
271
|
-
const candidate = makeItem({ title: 'Rule A', content: 'Do X always' });
|
|
272
|
-
const existing = [makeItem({ id: 'e1', title: 'Rule A', content: 'Do X always' })];
|
|
273
|
-
const result = judgeMerge(candidate, existing);
|
|
274
|
-
assert.strictEqual(result.action, 'noop');
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it('same title, different content → supersede with targetId', () => {
|
|
278
|
-
const candidate = makeItem({ title: 'Rule A', content: 'Do X v2' });
|
|
279
|
-
const existing = [makeItem({ id: 'e1', title: 'Rule A', content: 'Do X v1' })];
|
|
280
|
-
const result = judgeMerge(candidate, existing);
|
|
281
|
-
assert.strictEqual(result.action, 'supersede');
|
|
282
|
-
assert.strictEqual(result.targetId, 'e1');
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
it('genuinely new → promote', () => {
|
|
286
|
-
const candidate = makeItem({ title: 'Brand new rule', content: 'Something unique' });
|
|
287
|
-
const existing = [makeItem({ id: 'e1', title: 'Old rule', content: 'Old content' })];
|
|
288
|
-
const result = judgeMerge(candidate, existing);
|
|
289
|
-
assert.strictEqual(result.action, 'promote');
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it('protected item (source_type=manual) → reject', () => {
|
|
293
|
-
const candidate = makeItem({ title: 'Rule A', content: 'Do X v2' });
|
|
294
|
-
const existing = [makeItem({ id: 'e1', title: 'Rule A', content: 'Do X v1', source_type: 'manual' })];
|
|
295
|
-
const result = judgeMerge(candidate, existing);
|
|
296
|
-
assert.strictEqual(result.action, 'reject');
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it('protected item (tags=[protected]) → reject', () => {
|
|
300
|
-
const candidate = makeItem({ title: 'Rule A', content: 'Do X v2' });
|
|
301
|
-
const existing = [makeItem({ id: 'e1', title: 'Rule A', content: 'Do X v1', tags: '["protected"]' })];
|
|
302
|
-
const result = judgeMerge(candidate, existing);
|
|
303
|
-
assert.strictEqual(result.action, 'reject');
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
it('protected item (kind=profile, confidence=0.95) → reject', () => {
|
|
307
|
-
const candidate = makeItem({ title: 'User pref', content: 'Updated pref' });
|
|
308
|
-
const existing = [makeItem({ id: 'e1', title: 'User pref', content: 'Original pref', kind: 'profile', confidence: 0.95 })];
|
|
309
|
-
const result = judgeMerge(candidate, existing);
|
|
310
|
-
assert.strictEqual(result.action, 'reject');
|
|
311
|
-
});
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
// ---------------------------------------------------------------------------
|
|
315
|
-
// assemblePromptBlocks
|
|
316
|
-
// ---------------------------------------------------------------------------
|
|
317
|
-
|
|
318
|
-
describe('assemblePromptBlocks', () => {
|
|
319
|
-
it('formats items as "- [title]: content"', () => {
|
|
320
|
-
const allocated = {
|
|
321
|
-
convention: [makeItem({ title: 'Rule 1', content: 'Always do X' })],
|
|
322
|
-
insight: [],
|
|
323
|
-
profile: [],
|
|
324
|
-
episode: [],
|
|
325
|
-
};
|
|
326
|
-
const blocks = assemblePromptBlocks(allocated);
|
|
327
|
-
assert.ok(blocks.conventions.includes('- [Rule 1]: Always do X'));
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
it('long content truncated to 200 chars', () => {
|
|
331
|
-
const longContent = 'A'.repeat(300);
|
|
332
|
-
const allocated = {
|
|
333
|
-
convention: [makeItem({ title: 'Long', content: longContent })],
|
|
334
|
-
insight: [],
|
|
335
|
-
profile: [],
|
|
336
|
-
episode: [],
|
|
337
|
-
};
|
|
338
|
-
const blocks = assemblePromptBlocks(allocated);
|
|
339
|
-
// Truncated content should be <= 200 chars (plus possible ellipsis)
|
|
340
|
-
const line = blocks.conventions.split('\n').find((l) => l.includes('[Long]'));
|
|
341
|
-
assert.ok(line, 'should contain the item');
|
|
342
|
-
assert.ok(line.length < 250, `line too long: ${line.length}`);
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
it('empty buckets produce empty strings', () => {
|
|
346
|
-
const allocated = { convention: [], insight: [], profile: [], episode: [] };
|
|
347
|
-
const blocks = assemblePromptBlocks(allocated);
|
|
348
|
-
assert.strictEqual(blocks.conventions, '');
|
|
349
|
-
assert.strictEqual(blocks.insights, '');
|
|
350
|
-
assert.strictEqual(blocks.profile, '');
|
|
351
|
-
assert.strictEqual(blocks.episodes, '');
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
it('missing title handled gracefully', () => {
|
|
355
|
-
const allocated = {
|
|
356
|
-
convention: [makeItem({ title: null, content: 'No title content' })],
|
|
357
|
-
insight: [],
|
|
358
|
-
profile: [],
|
|
359
|
-
episode: [],
|
|
360
|
-
};
|
|
361
|
-
const blocks = assemblePromptBlocks(allocated);
|
|
362
|
-
assert.ok(blocks.conventions.includes('No title content'));
|
|
363
|
-
});
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
// ---------------------------------------------------------------------------
|
|
367
|
-
// shouldPromote
|
|
368
|
-
// ---------------------------------------------------------------------------
|
|
369
|
-
|
|
370
|
-
describe('shouldPromote', () => {
|
|
371
|
-
it('search_count=3, last_searched_at=today → true', () => {
|
|
372
|
-
const item = makeItem({ search_count: 3, last_searched_at: new Date().toISOString(), state: 'candidate' });
|
|
373
|
-
assert.strictEqual(shouldPromote(item), true);
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
it('search_count=2 → false', () => {
|
|
377
|
-
const item = makeItem({ search_count: 2, last_searched_at: new Date().toISOString(), state: 'candidate' });
|
|
378
|
-
assert.strictEqual(shouldPromote(item), false);
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
it('search_count=5, last_searched_at=8 days ago → false', () => {
|
|
382
|
-
const item = makeItem({ search_count: 5, last_searched_at: daysAgo(8), state: 'candidate' });
|
|
383
|
-
assert.strictEqual(shouldPromote(item), false);
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
it('search_count=0 → false', () => {
|
|
387
|
-
const item = makeItem({ search_count: 0, state: 'candidate' });
|
|
388
|
-
assert.strictEqual(shouldPromote(item), false);
|
|
389
|
-
});
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
// ---------------------------------------------------------------------------
|
|
393
|
-
// shouldArchive
|
|
394
|
-
// ---------------------------------------------------------------------------
|
|
395
|
-
|
|
396
|
-
describe('shouldArchive', () => {
|
|
397
|
-
it('candidate, 31 days old, never searched → true', () => {
|
|
398
|
-
const item = makeItem({ state: 'candidate', created_at: daysAgo(31), search_count: 0 });
|
|
399
|
-
assert.strictEqual(shouldArchive(item), true);
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
it('candidate, 29 days old → false', () => {
|
|
403
|
-
const item = makeItem({ state: 'candidate', created_at: daysAgo(29), search_count: 0 });
|
|
404
|
-
assert.strictEqual(shouldArchive(item), false);
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
it('active, 91 days old, confidence=0.5, not searched 31 days → true', () => {
|
|
408
|
-
const item = makeItem({
|
|
409
|
-
state: 'active',
|
|
410
|
-
created_at: daysAgo(91),
|
|
411
|
-
confidence: 0.5,
|
|
412
|
-
last_searched_at: daysAgo(31),
|
|
413
|
-
search_count: 1,
|
|
414
|
-
});
|
|
415
|
-
assert.strictEqual(shouldArchive(item), true);
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
it('active, 91 days old, confidence=0.7 → false (confidence too high)', () => {
|
|
419
|
-
const item = makeItem({
|
|
420
|
-
state: 'active',
|
|
421
|
-
created_at: daysAgo(91),
|
|
422
|
-
confidence: 0.7,
|
|
423
|
-
last_searched_at: daysAgo(31),
|
|
424
|
-
search_count: 1,
|
|
425
|
-
});
|
|
426
|
-
assert.strictEqual(shouldArchive(item), false);
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
it('source_type=manual → NEVER archive', () => {
|
|
430
|
-
const item = makeItem({
|
|
431
|
-
state: 'candidate',
|
|
432
|
-
created_at: daysAgo(100),
|
|
433
|
-
source_type: 'manual',
|
|
434
|
-
search_count: 0,
|
|
435
|
-
});
|
|
436
|
-
assert.strictEqual(shouldArchive(item), false);
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
it('tags=[protected] → NEVER archive', () => {
|
|
440
|
-
const item = makeItem({
|
|
441
|
-
state: 'candidate',
|
|
442
|
-
created_at: daysAgo(100),
|
|
443
|
-
tags: '["protected"]',
|
|
444
|
-
search_count: 0,
|
|
445
|
-
});
|
|
446
|
-
assert.strictEqual(shouldArchive(item), false);
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
it('kind=convention + source_type=manual → NEVER archive', () => {
|
|
450
|
-
const item = makeItem({
|
|
451
|
-
kind: 'convention',
|
|
452
|
-
state: 'active',
|
|
453
|
-
created_at: daysAgo(200),
|
|
454
|
-
confidence: 0.3,
|
|
455
|
-
source_type: 'manual',
|
|
456
|
-
last_searched_at: daysAgo(60),
|
|
457
|
-
search_count: 0,
|
|
458
|
-
});
|
|
459
|
-
assert.strictEqual(shouldArchive(item), false);
|
|
460
|
-
});
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
// ---------------------------------------------------------------------------
|
|
464
|
-
// Constants sanity checks
|
|
465
|
-
// ---------------------------------------------------------------------------
|
|
466
|
-
|
|
467
|
-
describe('constants', () => {
|
|
468
|
-
it('KIND_WEIGHTS is an object with expected keys', () => {
|
|
469
|
-
assert.ok(typeof KIND_WEIGHTS === 'object');
|
|
470
|
-
assert.ok('convention' in KIND_WEIGHTS);
|
|
471
|
-
assert.ok('insight' in KIND_WEIGHTS);
|
|
472
|
-
assert.ok('profile' in KIND_WEIGHTS);
|
|
473
|
-
assert.ok('episode' in KIND_WEIGHTS);
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
it('DEFAULT_BUDGET has per-kind limits', () => {
|
|
477
|
-
assert.ok(typeof DEFAULT_BUDGET === 'object');
|
|
478
|
-
assert.ok(typeof DEFAULT_BUDGET.convention === 'number');
|
|
479
|
-
assert.ok(typeof DEFAULT_BUDGET.insight === 'number');
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
it('RECENCY_HALF_LIFE_DAYS is a positive number', () => {
|
|
483
|
-
assert.ok(typeof RECENCY_HALF_LIFE_DAYS === 'number');
|
|
484
|
-
assert.ok(RECENCY_HALF_LIFE_DAYS > 0);
|
|
485
|
-
});
|
|
486
|
-
});
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { describe, it } = require('node:test');
|
|
4
|
-
const assert = require('node:assert');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const { resolveReactivePaths, resolveLegacyPaths } = require('./reactive-paths');
|
|
7
|
-
|
|
8
|
-
describe('resolveReactivePaths', () => {
|
|
9
|
-
it('returns correct directory structure', () => {
|
|
10
|
-
const p = resolveReactivePaths('scientist', '/home/user/.metame');
|
|
11
|
-
assert.equal(p.dir, path.join('/home/user/.metame', 'reactive', 'scientist'));
|
|
12
|
-
assert.equal(p.memory, path.join(p.dir, 'memory.md'));
|
|
13
|
-
assert.equal(p.l2cache, path.join(p.dir, 'l2cache.md'));
|
|
14
|
-
assert.equal(p.state, path.join(p.dir, 'state.md'));
|
|
15
|
-
assert.equal(p.events, path.join(p.dir, 'events.jsonl'));
|
|
16
|
-
assert.equal(p.latest, path.join(p.dir, 'latest.md'));
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('works with different keys', () => {
|
|
20
|
-
const p = resolveReactivePaths('my_project', '/tmp/meta');
|
|
21
|
-
assert.equal(p.dir, path.join('/tmp/meta', 'reactive', 'my_project'));
|
|
22
|
-
assert.equal(p.events, path.join('/tmp/meta', 'reactive', 'my_project', 'events.jsonl'));
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
describe('resolveLegacyPaths', () => {
|
|
27
|
-
it('returns flat legacy paths', () => {
|
|
28
|
-
const p = resolveLegacyPaths('scientist', '/home/user/.metame');
|
|
29
|
-
assert.equal(p.memory, path.join('/home/user/.metame', 'memory', 'now', 'scientist_memory.md'));
|
|
30
|
-
assert.equal(p.l2cache, path.join('/home/user/.metame', 'memory', 'now', 'scientist_l2cache.md'));
|
|
31
|
-
assert.equal(p.state, path.join('/home/user/.metame', 'memory', 'now', 'scientist.md'));
|
|
32
|
-
assert.equal(p.events, path.join('/home/user/.metame', 'events', 'scientist.jsonl'));
|
|
33
|
-
assert.equal(p.latest, path.join('/home/user/.metame', 'memory', 'agents', 'scientist_latest.md'));
|
|
34
|
-
});
|
|
35
|
-
});
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { describe, it } = require('node:test');
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
const { buildReactivePrompt } = require('./reactive-prompt');
|
|
6
|
-
|
|
7
|
-
describe('buildReactivePrompt', () => {
|
|
8
|
-
it('wraps prompt with reactive mode header (no memory, no retry)', () => {
|
|
9
|
-
const result = buildReactivePrompt('Do the task', {
|
|
10
|
-
depth: 3,
|
|
11
|
-
maxDepth: 50,
|
|
12
|
-
completionSignal: 'MISSION_COMPLETE',
|
|
13
|
-
});
|
|
14
|
-
assert.ok(result.includes('[REACTIVE MODE] depth 3/50'));
|
|
15
|
-
assert.ok(result.includes('NEXT_DISPATCH'));
|
|
16
|
-
assert.ok(result.includes('MISSION_COMPLETE'));
|
|
17
|
-
assert.ok(result.includes('Do the task'));
|
|
18
|
-
assert.ok(!result.includes('[Working Memory]'));
|
|
19
|
-
assert.ok(!result.includes('Warning:'));
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('injects working memory when provided', () => {
|
|
23
|
-
const result = buildReactivePrompt('Do the task', {
|
|
24
|
-
depth: 1,
|
|
25
|
-
maxDepth: 10,
|
|
26
|
-
completionSignal: 'DONE',
|
|
27
|
-
workingMemory: '## Recent Decisions\n- chose option A',
|
|
28
|
-
});
|
|
29
|
-
assert.ok(result.includes('[Working Memory]'));
|
|
30
|
-
assert.ok(result.includes('## Recent Decisions'));
|
|
31
|
-
assert.ok(result.includes('chose option A'));
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('includes retry warning when isRetry is true', () => {
|
|
35
|
-
const result = buildReactivePrompt('Check progress', {
|
|
36
|
-
depth: 5,
|
|
37
|
-
maxDepth: 50,
|
|
38
|
-
completionSignal: 'MISSION_COMPLETE',
|
|
39
|
-
isRetry: true,
|
|
40
|
-
});
|
|
41
|
-
assert.ok(result.includes('Warning:'));
|
|
42
|
-
assert.ok(result.includes('previous round'));
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('does not inject Working Memory block when workingMemory is empty string', () => {
|
|
46
|
-
const result = buildReactivePrompt('Do stuff', {
|
|
47
|
-
depth: 2,
|
|
48
|
-
maxDepth: 20,
|
|
49
|
-
completionSignal: 'MISSION_COMPLETE',
|
|
50
|
-
workingMemory: '',
|
|
51
|
-
});
|
|
52
|
-
assert.ok(!result.includes('[Working Memory]'));
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('does not inject Working Memory block when workingMemory is whitespace only', () => {
|
|
56
|
-
const result = buildReactivePrompt('Do stuff', {
|
|
57
|
-
depth: 2,
|
|
58
|
-
maxDepth: 20,
|
|
59
|
-
completionSignal: 'MISSION_COMPLETE',
|
|
60
|
-
workingMemory: ' \n ',
|
|
61
|
-
});
|
|
62
|
-
assert.ok(!result.includes('[Working Memory]'));
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('does not inject Working Memory block when workingMemory is undefined', () => {
|
|
66
|
-
const result = buildReactivePrompt('Do stuff', {
|
|
67
|
-
depth: 2,
|
|
68
|
-
maxDepth: 20,
|
|
69
|
-
completionSignal: 'MISSION_COMPLETE',
|
|
70
|
-
workingMemory: undefined,
|
|
71
|
-
});
|
|
72
|
-
assert.ok(!result.includes('[Working Memory]'));
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('includes both retry warning and working memory when both present', () => {
|
|
76
|
-
const result = buildReactivePrompt('Continue', {
|
|
77
|
-
depth: 4,
|
|
78
|
-
maxDepth: 50,
|
|
79
|
-
completionSignal: 'MISSION_COMPLETE',
|
|
80
|
-
workingMemory: 'Some context',
|
|
81
|
-
isRetry: true,
|
|
82
|
-
});
|
|
83
|
-
assert.ok(result.includes('Warning:'));
|
|
84
|
-
assert.ok(result.includes('[Working Memory]'));
|
|
85
|
-
assert.ok(result.includes('Some context'));
|
|
86
|
-
assert.ok(result.includes('Continue'));
|
|
87
|
-
});
|
|
88
|
-
});
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { describe, it } = require('node:test');
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
const { calculateNextAction } = require('./reactive-signal');
|
|
6
|
-
|
|
7
|
-
describe('calculateNextAction', () => {
|
|
8
|
-
it('returns proceed with count=0 when isComplete', () => {
|
|
9
|
-
const result = calculateNextAction({
|
|
10
|
-
hasSignals: false,
|
|
11
|
-
isComplete: true,
|
|
12
|
-
noSignalCount: 2,
|
|
13
|
-
maxRetries: 3,
|
|
14
|
-
});
|
|
15
|
-
assert.equal(result.action, 'proceed');
|
|
16
|
-
assert.equal(result.nextNoSignalCount, 0);
|
|
17
|
-
assert.equal(result.pauseReason, undefined);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('returns proceed with count=0 when hasSignals (not complete)', () => {
|
|
21
|
-
const result = calculateNextAction({
|
|
22
|
-
hasSignals: true,
|
|
23
|
-
isComplete: false,
|
|
24
|
-
noSignalCount: 1,
|
|
25
|
-
maxRetries: 3,
|
|
26
|
-
});
|
|
27
|
-
assert.equal(result.action, 'proceed');
|
|
28
|
-
assert.equal(result.nextNoSignalCount, 0);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('returns retry when no signals and count+1 < maxRetries', () => {
|
|
32
|
-
const result = calculateNextAction({
|
|
33
|
-
hasSignals: false,
|
|
34
|
-
isComplete: false,
|
|
35
|
-
noSignalCount: 0,
|
|
36
|
-
maxRetries: 3,
|
|
37
|
-
});
|
|
38
|
-
assert.equal(result.action, 'retry');
|
|
39
|
-
assert.equal(result.nextNoSignalCount, 1);
|
|
40
|
-
assert.equal(result.pauseReason, undefined);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('returns retry when count+1 is still less than maxRetries', () => {
|
|
44
|
-
const result = calculateNextAction({
|
|
45
|
-
hasSignals: false,
|
|
46
|
-
isComplete: false,
|
|
47
|
-
noSignalCount: 1,
|
|
48
|
-
maxRetries: 3,
|
|
49
|
-
});
|
|
50
|
-
assert.equal(result.action, 'retry');
|
|
51
|
-
assert.equal(result.nextNoSignalCount, 2);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('returns pause when count+1 >= maxRetries', () => {
|
|
55
|
-
const result = calculateNextAction({
|
|
56
|
-
hasSignals: false,
|
|
57
|
-
isComplete: false,
|
|
58
|
-
noSignalCount: 2,
|
|
59
|
-
maxRetries: 3,
|
|
60
|
-
});
|
|
61
|
-
assert.equal(result.action, 'pause');
|
|
62
|
-
assert.equal(result.nextNoSignalCount, 3);
|
|
63
|
-
assert.equal(result.pauseReason, 'no_signal_repeated');
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('returns pause when count already exceeds maxRetries', () => {
|
|
67
|
-
const result = calculateNextAction({
|
|
68
|
-
hasSignals: false,
|
|
69
|
-
isComplete: false,
|
|
70
|
-
noSignalCount: 5,
|
|
71
|
-
maxRetries: 3,
|
|
72
|
-
});
|
|
73
|
-
assert.equal(result.action, 'pause');
|
|
74
|
-
assert.equal(result.nextNoSignalCount, 6);
|
|
75
|
-
assert.equal(result.pauseReason, 'no_signal_repeated');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('isComplete takes priority over hasSignals', () => {
|
|
79
|
-
const result = calculateNextAction({
|
|
80
|
-
hasSignals: true,
|
|
81
|
-
isComplete: true,
|
|
82
|
-
noSignalCount: 2,
|
|
83
|
-
maxRetries: 3,
|
|
84
|
-
});
|
|
85
|
-
assert.equal(result.action, 'proceed');
|
|
86
|
-
assert.equal(result.nextNoSignalCount, 0);
|
|
87
|
-
});
|
|
88
|
-
});
|