@empirical-gauzza/cli 1.0.28 → 1.0.29
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/bin/empirical.js +1 -1
- package/lib/installers/hooks.js +321 -61
- package/package.json +1 -1
package/bin/empirical.js
CHANGED
package/lib/installers/hooks.js
CHANGED
|
@@ -110,37 +110,125 @@ function main() {
|
|
|
110
110
|
process.exit(0);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
113
|
+
// Declared inside main() on purpose: function declarations hoist, and other hook templates in
|
|
114
|
+
// this file have their own main() without the memory-query plumbing.
|
|
115
|
+
|
|
116
|
+
// WHAT PROJECT IS THIS, in priority order. Guessing from the directory name is the last resort,
|
|
117
|
+
// not the contract: a repo folder is not always what the corpus tags memories with.
|
|
118
|
+
// 1. EMPIRICAL_PROJECT_TAG / EMPIRICAL_TOPICS environment variables
|
|
119
|
+
// 2. .empirical.json in the working directory or any ancestor, up to 6 levels:
|
|
120
|
+
// { "project": "empirical_v2", "topics": ["search ranking", "deploy"] }
|
|
121
|
+
// 3. the working directory name, lowercased
|
|
122
|
+
function readProjectConfig() {
|
|
123
|
+
var config = { project: '', topics: [] };
|
|
124
|
+
try {
|
|
125
|
+
var dir = process.cwd();
|
|
126
|
+
for (var depth = 0; depth < 6; depth += 1) {
|
|
127
|
+
var candidate = path.join(dir, '.empirical.json');
|
|
128
|
+
if (fs.existsSync(candidate)) {
|
|
129
|
+
var parsed = JSON.parse(fs.readFileSync(candidate, 'utf8'));
|
|
130
|
+
config.project = String(parsed && parsed.project ? parsed.project : '').trim().toLowerCase();
|
|
131
|
+
if (parsed && Array.isArray(parsed.topics)) {
|
|
132
|
+
config.topics = parsed.topics.map(function (t) { return String(t || '').trim(); }).filter(Boolean);
|
|
133
|
+
}
|
|
134
|
+
appendLog('loaded project config from ' + candidate);
|
|
135
|
+
return config;
|
|
136
|
+
}
|
|
137
|
+
var parent = path.dirname(dir);
|
|
138
|
+
if (!parent || parent === dir) { break; }
|
|
139
|
+
dir = parent;
|
|
140
|
+
}
|
|
141
|
+
} catch (e) {
|
|
142
|
+
appendLog('project config unreadable, continuing without it: ' + e.message);
|
|
143
|
+
}
|
|
144
|
+
return config;
|
|
122
145
|
}
|
|
123
146
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
147
|
+
// requireAll selects --tags-all (AND) over --tags (OR). This matters: --tags is an OR match, so
|
|
148
|
+
// asking for 'empirical_v2,todo' returns every project's todos, which put a wellnessonbalance
|
|
149
|
+
// item into an empirical_v2 session during testing on 2026-09-16.
|
|
150
|
+
function fetchMemories(matchText, tags, topK, requireAll) {
|
|
151
|
+
var args = ['memory', 'query', '--match', matchText, '--top-k', String(topK)];
|
|
152
|
+
if (tags) { args.push(requireAll ? '--tags-all' : '--tags', tags); }
|
|
153
|
+
var queryResult = runMemoryQuery(args);
|
|
154
|
+
if (!queryResult || queryResult.status !== 0 || !queryResult.stdout) {
|
|
155
|
+
appendLog('memory query failed (tags=' + (tags || 'none') + ', status=' + (queryResult && queryResult.status) + ', stderr=' + (queryResult && queryResult.stderr ? String(queryResult.stderr).slice(0, 200) : '') + ')');
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
var parsed = JSON.parse(queryResult.stdout);
|
|
160
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
161
|
+
} catch (e) {
|
|
162
|
+
appendLog('failed to parse memory query stdout as JSON: ' + e.message);
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
var fileConfig = readProjectConfig();
|
|
168
|
+
var envTag = String(process.env.EMPIRICAL_PROJECT_TAG || '').trim().toLowerCase();
|
|
169
|
+
var envTopics = String(process.env.EMPIRICAL_TOPICS || '').split(',').map(function (t) { return t.trim(); }).filter(Boolean);
|
|
170
|
+
|
|
171
|
+
var tag = envTag || fileConfig.project;
|
|
172
|
+
var tagSource = envTag ? 'env' : (fileConfig.project ? 'config' : 'cwd');
|
|
173
|
+
if (!tag) {
|
|
174
|
+
try {
|
|
175
|
+
tag = String(path.basename(process.cwd() || '') || '').trim().toLowerCase();
|
|
176
|
+
} catch (e) {
|
|
177
|
+
tag = '';
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
var topics = envTopics.length ? envTopics : fileConfig.topics;
|
|
182
|
+
var contextMatch = topics.length
|
|
183
|
+
? topics.join(', ')
|
|
184
|
+
: 'project context, conventions, recent decisions, user preferences';
|
|
185
|
+
|
|
186
|
+
appendLog('project tag=' + (tag || '(none)') + ' (source=' + tagSource + '), topics=' + (topics.length ? topics.join('|') : '(default)'));
|
|
187
|
+
|
|
188
|
+
// Scope every query to the project. Measured 2026-09-16: with no tag filter all 8 slots went to
|
|
189
|
+
// one unrelated project during a full session in a different repo, so the only automatic context
|
|
190
|
+
// rail in the product was spending every slot on the wrong thing. A tag filter also disables the
|
|
191
|
+
// server's agent-process-note exclusion, which is what makes a project's own scar tissue and
|
|
192
|
+
// work history reachable here at all.
|
|
193
|
+
//
|
|
194
|
+
// Open items get their OWN query instead of competing for rank. Tested 2026-09-16: a due-dated
|
|
195
|
+
// memory did not surface in the generic top-8 with or without a project filter, because dueness
|
|
196
|
+
// is not a semantic property. Anything that must be seen needs a reserved slot.
|
|
197
|
+
// AND, not OR: this must be THIS project's todos. No --status filter on purpose, because todos
|
|
198
|
+
// written without a data.status would silently vanish from the one place they get surfaced;
|
|
199
|
+
// the label tells the reader to confirm instead.
|
|
200
|
+
var todos = tag ? fetchMemories('open todo, next action, follow-up, due', tag + ',todo', 3, true) : [];
|
|
201
|
+
var context = tag ? fetchMemories(contextMatch, tag, 6) : [];
|
|
202
|
+
|
|
203
|
+
// A corpus whose tags do not match the resolved project must never end up worse off than before
|
|
204
|
+
// this change, so fall back to the original unscoped query.
|
|
205
|
+
if (context.length === 0) {
|
|
206
|
+
appendLog('project-scoped query returned nothing, falling back to unscoped');
|
|
207
|
+
context = fetchMemories(contextMatch, '', 8);
|
|
130
208
|
}
|
|
131
209
|
|
|
132
|
-
if (
|
|
210
|
+
if (todos.length === 0 && context.length === 0) {
|
|
133
211
|
appendLog('memory query returned zero results');
|
|
134
212
|
process.exit(0);
|
|
135
213
|
}
|
|
136
214
|
|
|
215
|
+
var openCount = todos.length;
|
|
216
|
+
var results = todos.concat(context);
|
|
217
|
+
|
|
137
218
|
appendLog('memory query returned ' + results.length + ' result(s), injecting into session context');
|
|
138
219
|
|
|
139
|
-
|
|
220
|
+
// Summaries in a mature corpus run to thousands of characters. Injecting eight in full is a wall
|
|
221
|
+
// of text that gets skimmed past, which fails the same way as never injecting them at all:
|
|
222
|
+
// presence in context is not attention. Truncate hard; the agent can query for the rest.
|
|
223
|
+
var lines = results.slice(0, 9).map(function (r, i) {
|
|
140
224
|
var category = r && r.category ? r.category : 'memory';
|
|
141
|
-
var summary = r && r.summary ? r.summary : '';
|
|
142
|
-
|
|
225
|
+
var summary = String(r && r.summary ? r.summary : '');
|
|
226
|
+
summary = summary.split('\\n').join(' ').split('\\r').join(' ').split('\\t').join(' ').trim();
|
|
227
|
+
if (summary.length > 240) { summary = summary.slice(0, 237) + '...'; }
|
|
228
|
+
var prefix = i < openCount ? '- [OPEN ITEM, confirm status] ' : '- [' + category + '] ';
|
|
229
|
+
return prefix + summary;
|
|
143
230
|
});
|
|
231
|
+
lines.push('- Before risky or repeated work, ask first: empirical memory query --match "<what you are about to do>" --tags scar-tissue');
|
|
144
232
|
|
|
145
233
|
console.log('Empirical memory context for this session:\\n' + lines.join('\\n'));
|
|
146
234
|
process.exit(0);
|
|
@@ -259,40 +347,126 @@ function main() {
|
|
|
259
347
|
process.exit(0);
|
|
260
348
|
}
|
|
261
349
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
350
|
+
// Declared inside main() on purpose: function declarations hoist, and other hook templates in
|
|
351
|
+
// this file have their own main() without the memory-query plumbing.
|
|
352
|
+
|
|
353
|
+
// WHAT PROJECT IS THIS, in priority order. Guessing from the directory name is the last resort,
|
|
354
|
+
// not the contract: a repo folder is not always what the corpus tags memories with.
|
|
355
|
+
// 1. EMPIRICAL_PROJECT_TAG / EMPIRICAL_TOPICS environment variables
|
|
356
|
+
// 2. .empirical.json in the working directory or any ancestor, up to 6 levels:
|
|
357
|
+
// { "project": "empirical_v2", "topics": ["search ranking", "deploy"] }
|
|
358
|
+
// 3. the working directory name, lowercased
|
|
359
|
+
function readProjectConfig() {
|
|
360
|
+
var config = { project: '', topics: [] };
|
|
361
|
+
try {
|
|
362
|
+
var dir = process.cwd();
|
|
363
|
+
for (var depth = 0; depth < 6; depth += 1) {
|
|
364
|
+
var candidate = path.join(dir, '.empirical.json');
|
|
365
|
+
if (fs.existsSync(candidate)) {
|
|
366
|
+
var parsed = JSON.parse(fs.readFileSync(candidate, 'utf8'));
|
|
367
|
+
config.project = String(parsed && parsed.project ? parsed.project : '').trim().toLowerCase();
|
|
368
|
+
if (parsed && Array.isArray(parsed.topics)) {
|
|
369
|
+
config.topics = parsed.topics.map(function (t) { return String(t || '').trim(); }).filter(Boolean);
|
|
370
|
+
}
|
|
371
|
+
appendLog('loaded project config from ' + candidate);
|
|
372
|
+
return config;
|
|
373
|
+
}
|
|
374
|
+
var parent = path.dirname(dir);
|
|
375
|
+
if (!parent || parent === dir) { break; }
|
|
376
|
+
dir = parent;
|
|
377
|
+
}
|
|
378
|
+
} catch (e) {
|
|
379
|
+
appendLog('project config unreadable, continuing without it: ' + e.message);
|
|
380
|
+
}
|
|
381
|
+
return config;
|
|
382
|
+
}
|
|
267
383
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
384
|
+
// requireAll selects --tags-all (AND) over --tags (OR). This matters: --tags is an OR match, so
|
|
385
|
+
// asking for 'empirical_v2,todo' returns every project's todos, which put a wellnessonbalance
|
|
386
|
+
// item into an empirical_v2 session during testing on 2026-09-16.
|
|
387
|
+
function fetchMemories(matchText, tags, topK, requireAll) {
|
|
388
|
+
var args = ['memory', 'query', '--match', matchText, '--top-k', String(topK)];
|
|
389
|
+
if (tags) { args.push(requireAll ? '--tags-all' : '--tags', tags); }
|
|
390
|
+
var queryResult = runMemoryQuery(args);
|
|
391
|
+
if (!queryResult || queryResult.status !== 0 || !queryResult.stdout) {
|
|
392
|
+
appendLog('memory query failed (tags=' + (tags || 'none') + ', status=' + (queryResult && queryResult.status) + ', stderr=' + (queryResult && queryResult.stderr ? String(queryResult.stderr).slice(0, 200) : '') + ')');
|
|
393
|
+
return [];
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
var parsed = JSON.parse(queryResult.stdout);
|
|
397
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
398
|
+
} catch (e) {
|
|
399
|
+
appendLog('failed to parse memory query stdout as JSON: ' + e.message);
|
|
400
|
+
return [];
|
|
401
|
+
}
|
|
272
402
|
}
|
|
273
403
|
|
|
274
|
-
var
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
404
|
+
var fileConfig = readProjectConfig();
|
|
405
|
+
var envTag = String(process.env.EMPIRICAL_PROJECT_TAG || '').trim().toLowerCase();
|
|
406
|
+
var envTopics = String(process.env.EMPIRICAL_TOPICS || '').split(',').map(function (t) { return t.trim(); }).filter(Boolean);
|
|
407
|
+
|
|
408
|
+
var tag = envTag || fileConfig.project;
|
|
409
|
+
var tagSource = envTag ? 'env' : (fileConfig.project ? 'config' : 'cwd');
|
|
410
|
+
if (!tag) {
|
|
411
|
+
try {
|
|
412
|
+
tag = String(path.basename(process.cwd() || '') || '').trim().toLowerCase();
|
|
413
|
+
} catch (e) {
|
|
414
|
+
tag = '';
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
var topics = envTopics.length ? envTopics : fileConfig.topics;
|
|
419
|
+
var contextMatch = topics.length
|
|
420
|
+
? topics.join(', ')
|
|
421
|
+
: 'project context, conventions, recent decisions, user preferences';
|
|
422
|
+
|
|
423
|
+
appendLog('project tag=' + (tag || '(none)') + ' (source=' + tagSource + '), topics=' + (topics.length ? topics.join('|') : '(default)'));
|
|
424
|
+
|
|
425
|
+
// Scope every query to the project. Measured 2026-09-16: with no tag filter all 8 slots went to
|
|
426
|
+
// one unrelated project during a full session in a different repo, so the only automatic context
|
|
427
|
+
// rail in the product was spending every slot on the wrong thing. A tag filter also disables the
|
|
428
|
+
// server's agent-process-note exclusion, which is what makes a project's own scar tissue and
|
|
429
|
+
// work history reachable here at all.
|
|
430
|
+
//
|
|
431
|
+
// Open items get their OWN query instead of competing for rank. Tested 2026-09-16: a due-dated
|
|
432
|
+
// memory did not surface in the generic top-8 with or without a project filter, because dueness
|
|
433
|
+
// is not a semantic property. Anything that must be seen needs a reserved slot.
|
|
434
|
+
// AND, not OR: this must be THIS project's todos. No --status filter on purpose, because todos
|
|
435
|
+
// written without a data.status would silently vanish from the one place they get surfaced;
|
|
436
|
+
// the label tells the reader to confirm instead.
|
|
437
|
+
var todos = tag ? fetchMemories('open todo, next action, follow-up, due', tag + ',todo', 3, true) : [];
|
|
438
|
+
var context = tag ? fetchMemories(contextMatch, tag, 6) : [];
|
|
439
|
+
|
|
440
|
+
// A corpus whose tags do not match the resolved project must never end up worse off than before
|
|
441
|
+
// this change, so fall back to the original unscoped query.
|
|
442
|
+
if (context.length === 0) {
|
|
443
|
+
appendLog('project-scoped query returned nothing, falling back to unscoped');
|
|
444
|
+
context = fetchMemories(contextMatch, '', 8);
|
|
281
445
|
}
|
|
282
446
|
|
|
283
|
-
if (
|
|
447
|
+
if (todos.length === 0 && context.length === 0) {
|
|
284
448
|
appendLog('memory query returned zero results');
|
|
285
449
|
emitContext('');
|
|
286
450
|
process.exit(0);
|
|
287
451
|
}
|
|
288
452
|
|
|
453
|
+
var openCount = todos.length;
|
|
454
|
+
var results = todos.concat(context);
|
|
455
|
+
|
|
289
456
|
appendLog('memory query returned ' + results.length + ' result(s), injecting into session context');
|
|
290
457
|
|
|
291
|
-
|
|
458
|
+
// Summaries in a mature corpus run to thousands of characters. Injecting eight in full is a wall
|
|
459
|
+
// of text that gets skimmed past, which fails the same way as never injecting them at all:
|
|
460
|
+
// presence in context is not attention. Truncate hard; the agent can query for the rest.
|
|
461
|
+
var lines = results.slice(0, 9).map(function (r, i) {
|
|
292
462
|
var category = r && r.category ? r.category : 'memory';
|
|
293
|
-
var summary = r && r.summary ? r.summary : '';
|
|
294
|
-
|
|
463
|
+
var summary = String(r && r.summary ? r.summary : '');
|
|
464
|
+
summary = summary.split('\\n').join(' ').split('\\r').join(' ').split('\\t').join(' ').trim();
|
|
465
|
+
if (summary.length > 240) { summary = summary.slice(0, 237) + '...'; }
|
|
466
|
+
var prefix = i < openCount ? '- [OPEN ITEM, confirm status] ' : '- [' + category + '] ';
|
|
467
|
+
return prefix + summary;
|
|
295
468
|
});
|
|
469
|
+
lines.push('- Before risky or repeated work, ask first: empirical memory query --match "<what you are about to do>" --tags scar-tissue');
|
|
296
470
|
|
|
297
471
|
console.log(JSON.stringify({
|
|
298
472
|
hookSpecificOutput: {
|
|
@@ -400,38 +574,124 @@ function main() {
|
|
|
400
574
|
process.exit(0);
|
|
401
575
|
}
|
|
402
576
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
577
|
+
// Declared inside main() on purpose: function declarations hoist, and other hook templates in
|
|
578
|
+
// this file have their own main() without the memory-query plumbing.
|
|
579
|
+
|
|
580
|
+
// WHAT PROJECT IS THIS, in priority order. Guessing from the directory name is the last resort,
|
|
581
|
+
// not the contract: a repo folder is not always what the corpus tags memories with.
|
|
582
|
+
// 1. EMPIRICAL_PROJECT_TAG / EMPIRICAL_TOPICS environment variables
|
|
583
|
+
// 2. .empirical.json in the working directory or any ancestor, up to 6 levels:
|
|
584
|
+
// { "project": "empirical_v2", "topics": ["search ranking", "deploy"] }
|
|
585
|
+
// 3. the working directory name, lowercased
|
|
586
|
+
function readProjectConfig() {
|
|
587
|
+
var config = { project: '', topics: [] };
|
|
588
|
+
try {
|
|
589
|
+
var dir = process.cwd();
|
|
590
|
+
for (var depth = 0; depth < 6; depth += 1) {
|
|
591
|
+
var candidate = path.join(dir, '.empirical.json');
|
|
592
|
+
if (fs.existsSync(candidate)) {
|
|
593
|
+
var parsed = JSON.parse(fs.readFileSync(candidate, 'utf8'));
|
|
594
|
+
config.project = String(parsed && parsed.project ? parsed.project : '').trim().toLowerCase();
|
|
595
|
+
if (parsed && Array.isArray(parsed.topics)) {
|
|
596
|
+
config.topics = parsed.topics.map(function (t) { return String(t || '').trim(); }).filter(Boolean);
|
|
597
|
+
}
|
|
598
|
+
appendLog('loaded project config from ' + candidate);
|
|
599
|
+
return config;
|
|
600
|
+
}
|
|
601
|
+
var parent = path.dirname(dir);
|
|
602
|
+
if (!parent || parent === dir) { break; }
|
|
603
|
+
dir = parent;
|
|
604
|
+
}
|
|
605
|
+
} catch (e) {
|
|
606
|
+
appendLog('project config unreadable, continuing without it: ' + e.message);
|
|
607
|
+
}
|
|
608
|
+
return config;
|
|
609
|
+
}
|
|
408
610
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
611
|
+
// requireAll selects --tags-all (AND) over --tags (OR). This matters: --tags is an OR match, so
|
|
612
|
+
// asking for 'empirical_v2,todo' returns every project's todos, which put a wellnessonbalance
|
|
613
|
+
// item into an empirical_v2 session during testing on 2026-09-16.
|
|
614
|
+
function fetchMemories(matchText, tags, topK, requireAll) {
|
|
615
|
+
var args = ['memory', 'query', '--match', matchText, '--top-k', String(topK)];
|
|
616
|
+
if (tags) { args.push(requireAll ? '--tags-all' : '--tags', tags); }
|
|
617
|
+
var queryResult = runMemoryQuery(args);
|
|
618
|
+
if (!queryResult || queryResult.status !== 0 || !queryResult.stdout) {
|
|
619
|
+
appendLog('memory query failed (tags=' + (tags || 'none') + ', status=' + (queryResult && queryResult.status) + ', stderr=' + (queryResult && queryResult.stderr ? String(queryResult.stderr).slice(0, 200) : '') + ')');
|
|
620
|
+
return [];
|
|
621
|
+
}
|
|
622
|
+
try {
|
|
623
|
+
var parsed = JSON.parse(queryResult.stdout);
|
|
624
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
625
|
+
} catch (e) {
|
|
626
|
+
appendLog('failed to parse memory query stdout as JSON: ' + e.message);
|
|
627
|
+
return [];
|
|
628
|
+
}
|
|
413
629
|
}
|
|
414
630
|
|
|
415
|
-
var
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
631
|
+
var fileConfig = readProjectConfig();
|
|
632
|
+
var envTag = String(process.env.EMPIRICAL_PROJECT_TAG || '').trim().toLowerCase();
|
|
633
|
+
var envTopics = String(process.env.EMPIRICAL_TOPICS || '').split(',').map(function (t) { return t.trim(); }).filter(Boolean);
|
|
634
|
+
|
|
635
|
+
var tag = envTag || fileConfig.project;
|
|
636
|
+
var tagSource = envTag ? 'env' : (fileConfig.project ? 'config' : 'cwd');
|
|
637
|
+
if (!tag) {
|
|
638
|
+
try {
|
|
639
|
+
tag = String(path.basename(process.cwd() || '') || '').trim().toLowerCase();
|
|
640
|
+
} catch (e) {
|
|
641
|
+
tag = '';
|
|
642
|
+
}
|
|
422
643
|
}
|
|
423
644
|
|
|
424
|
-
|
|
645
|
+
var topics = envTopics.length ? envTopics : fileConfig.topics;
|
|
646
|
+
var contextMatch = topics.length
|
|
647
|
+
? topics.join(', ')
|
|
648
|
+
: 'project context, conventions, recent decisions, user preferences';
|
|
649
|
+
|
|
650
|
+
appendLog('project tag=' + (tag || '(none)') + ' (source=' + tagSource + '), topics=' + (topics.length ? topics.join('|') : '(default)'));
|
|
651
|
+
|
|
652
|
+
// Scope every query to the project. Measured 2026-09-16: with no tag filter all 8 slots went to
|
|
653
|
+
// one unrelated project during a full session in a different repo, so the only automatic context
|
|
654
|
+
// rail in the product was spending every slot on the wrong thing. A tag filter also disables the
|
|
655
|
+
// server's agent-process-note exclusion, which is what makes a project's own scar tissue and
|
|
656
|
+
// work history reachable here at all.
|
|
657
|
+
//
|
|
658
|
+
// Open items get their OWN query instead of competing for rank. Tested 2026-09-16: a due-dated
|
|
659
|
+
// memory did not surface in the generic top-8 with or without a project filter, because dueness
|
|
660
|
+
// is not a semantic property. Anything that must be seen needs a reserved slot.
|
|
661
|
+
// AND, not OR: this must be THIS project's todos. No --status filter on purpose, because todos
|
|
662
|
+
// written without a data.status would silently vanish from the one place they get surfaced;
|
|
663
|
+
// the label tells the reader to confirm instead.
|
|
664
|
+
var todos = tag ? fetchMemories('open todo, next action, follow-up, due', tag + ',todo', 3, true) : [];
|
|
665
|
+
var context = tag ? fetchMemories(contextMatch, tag, 6) : [];
|
|
666
|
+
|
|
667
|
+
// A corpus whose tags do not match the resolved project must never end up worse off than before
|
|
668
|
+
// this change, so fall back to the original unscoped query.
|
|
669
|
+
if (context.length === 0) {
|
|
670
|
+
appendLog('project-scoped query returned nothing, falling back to unscoped');
|
|
671
|
+
context = fetchMemories(contextMatch, '', 8);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (todos.length === 0 && context.length === 0) {
|
|
425
675
|
appendLog('memory query returned zero results');
|
|
426
676
|
console.log('{}');
|
|
427
677
|
process.exit(0);
|
|
428
678
|
}
|
|
429
679
|
|
|
430
|
-
var
|
|
680
|
+
var openCount = todos.length;
|
|
681
|
+
var results = todos.concat(context);
|
|
682
|
+
|
|
683
|
+
// Summaries in a mature corpus run to thousands of characters. Injecting eight in full is a wall
|
|
684
|
+
// of text that gets skimmed past, which fails the same way as never injecting them at all:
|
|
685
|
+
// presence in context is not attention. Truncate hard; the agent can query for the rest.
|
|
686
|
+
var lines = results.slice(0, 9).map(function (r, i) {
|
|
431
687
|
var category = r && r.category ? r.category : 'memory';
|
|
432
|
-
var summary = r && r.summary ? r.summary : '';
|
|
433
|
-
|
|
688
|
+
var summary = String(r && r.summary ? r.summary : '');
|
|
689
|
+
summary = summary.split('\\n').join(' ').split('\\r').join(' ').split('\\t').join(' ').trim();
|
|
690
|
+
if (summary.length > 240) { summary = summary.slice(0, 237) + '...'; }
|
|
691
|
+
var prefix = i < openCount ? '- [OPEN ITEM, confirm status] ' : '- [' + category + '] ';
|
|
692
|
+
return prefix + summary;
|
|
434
693
|
});
|
|
694
|
+
lines.push('- Before risky or repeated work, ask first: empirical memory query --match "<what you are about to do>" --tags scar-tissue');
|
|
435
695
|
|
|
436
696
|
// Logged only - Copilot's sessionStart event has no field that surfaces this to the model.
|
|
437
697
|
appendLog('memory query returned ' + results.length + ' result(s) (session-start has no output channel to inject them, logged only): ' + lines.join(' | '));
|
|
@@ -840,7 +1100,7 @@ function installClaudeHooks(pluginNames) {
|
|
|
840
1100
|
for (const matcher of ['startup', 'resume', 'compact']) {
|
|
841
1101
|
settings.hooks.SessionStart.push({
|
|
842
1102
|
matcher,
|
|
843
|
-
hooks: [{ type: 'command', command: buildHookCommand(sessionStartScriptPath), timeout:
|
|
1103
|
+
hooks: [{ type: 'command', command: buildHookCommand(sessionStartScriptPath), timeout: 25 }]
|
|
844
1104
|
});
|
|
845
1105
|
}
|
|
846
1106
|
|
|
@@ -969,7 +1229,7 @@ function installCodexHooks(pluginNames) {
|
|
|
969
1229
|
for (const matcher of ['startup', 'resume']) {
|
|
970
1230
|
hooksFile.hooks.SessionStart.push({
|
|
971
1231
|
matcher,
|
|
972
|
-
hooks: [{ type: 'command', command: sessionStartCommand, commandWindows: sessionStartCommandWindows, timeout:
|
|
1232
|
+
hooks: [{ type: 'command', command: sessionStartCommand, commandWindows: sessionStartCommandWindows, timeout: 25 }]
|
|
973
1233
|
});
|
|
974
1234
|
}
|
|
975
1235
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@empirical-gauzza/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.29",
|
|
4
4
|
"description": "Persistent cross-tool memory for AI agents. Give Claude Code, Codex, Copilot, Cursor, and ChatGPT one shared memory: save project context once, recall it everywhere via CLI or MCP.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"memory",
|