@empirical-gauzza/cli 1.0.28 → 1.0.30
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/alertBanner.js +17 -0
- package/lib/constants.js +657 -640
- package/lib/http.js +2 -2
- package/lib/installers/hooks.js +321 -61
- package/lib/installers/skill-variants/SKILL.both.md +21 -0
- package/lib/installers/skill-variants/SKILL.cli.md +21 -0
- package/lib/installers/skill-variants/SKILL.mcp.md +21 -0
- package/lib/memoryCommands.js +88 -4
- package/package.json +1 -1
package/lib/http.js
CHANGED
|
@@ -84,7 +84,7 @@ function httpJson(urlString, method, payload, headers = {}, timeoutMs = 0) {
|
|
|
84
84
|
headers: {
|
|
85
85
|
Accept: 'application/json',
|
|
86
86
|
// Identifies the client so the server can tell a CLI write from a portal write
|
|
87
|
-
// (
|
|
87
|
+
// (@empirical/core's memoryAuthorship). Writes also send source: 'cli' in the body,
|
|
88
88
|
// which is authoritative; this covers every other endpoint.
|
|
89
89
|
'User-Agent': 'empirical-cli/1.0',
|
|
90
90
|
...(body ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } : {}),
|
|
@@ -144,7 +144,7 @@ function httpForm(urlString, method, params, headers = {}) {
|
|
|
144
144
|
headers: {
|
|
145
145
|
Accept: 'application/json',
|
|
146
146
|
// Identifies the client so the server can tell a CLI write from a portal write
|
|
147
|
-
// (
|
|
147
|
+
// (@empirical/core's memoryAuthorship). Writes also send source: 'cli' in the body,
|
|
148
148
|
// which is authoritative; this covers every other endpoint.
|
|
149
149
|
'User-Agent': 'empirical-cli/1.0',
|
|
150
150
|
'Content-Type': 'application/x-www-form-urlencoded',
|
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
|
|
|
@@ -195,6 +195,27 @@ Use these when connected: `get_empirical_policy`, `query_memories`, `list_memori
|
|
|
195
195
|
graph work, use `get_memory_neighbors` and `update_memory_relationships` when available instead of
|
|
196
196
|
reconstructing relationships through many unrelated calls.
|
|
197
197
|
|
|
198
|
+
## Reminders — set them, relay them, close them
|
|
199
|
+
|
|
200
|
+
Any memory can carry a due date. Empirical then brings it back: an email with a calendar entry
|
|
201
|
+
when the reminder is set, a nudge when it is due, and a notice attached to the next Empirical
|
|
202
|
+
response each of the user's agents receives.
|
|
203
|
+
|
|
204
|
+
- **Set one** when the user says "remind me", names a deadline, or a todo has a real due date.
|
|
205
|
+
Use the `dueAt` parameter on `record_graph_memory` / `update_memory_by_query` (ISO 8601), or
|
|
206
|
+
`empirical memory record ... --due 2026-10-14T09:00:00Z` on the CLI. `dueAt` is a real field the
|
|
207
|
+
server filters and fires on; a date written inside `data` is text and nothing will ever fire.
|
|
208
|
+
- **Relay it.** If any Empirical result starts with `REMINDER DUE`, or a CLI command prints a
|
|
209
|
+
banner that starts with `REMINDER DUE`, tell the user about it in your own words BEFORE the task
|
|
210
|
+
you were doing, then ask whether to mark it done. Each agent is shown a reminder once; if you do
|
|
211
|
+
not relay it, the user does not hear it from you.
|
|
212
|
+
- **Close it** when the user says it is handled: `acknowledge_reminder` with the `memoryId` from
|
|
213
|
+
the notice (or `state: "cancelled"` to drop it), or `empirical memory acknowledge --memory-id <id>`
|
|
214
|
+
(`--cancel` to drop). Closing frees the slot; plans cap concurrent open reminders.
|
|
215
|
+
- **What is due?** `query_memories` with `dueBefore` set to now and `remindState: ["pending"]`, or
|
|
216
|
+
`empirical memory query --match "what is due" --due-before <now> --remind-state pending`.
|
|
217
|
+
Dueness is a filter; semantic search will not rank a memory higher because it is due.
|
|
218
|
+
|
|
198
219
|
## Authentication and failures
|
|
199
220
|
|
|
200
221
|
- If a memory call fails, retry the same call once.
|
|
@@ -179,6 +179,27 @@ token error, run `empirical oauth bootstrap headless --write-env`. When in doubt
|
|
|
179
179
|
subcommand's flags, run `empirical doc memory`, `empirical doc kit`, or `empirical doc skill`
|
|
180
180
|
rather than guessing.
|
|
181
181
|
|
|
182
|
+
## Reminders — set them, relay them, close them
|
|
183
|
+
|
|
184
|
+
Any memory can carry a due date. Empirical then brings it back: an email with a calendar entry
|
|
185
|
+
when the reminder is set, a nudge when it is due, and a notice attached to the next Empirical
|
|
186
|
+
response each of the user's agents receives.
|
|
187
|
+
|
|
188
|
+
- **Set one** when the user says "remind me", names a deadline, or a todo has a real due date.
|
|
189
|
+
Use the `dueAt` parameter on `record_graph_memory` / `update_memory_by_query` (ISO 8601), or
|
|
190
|
+
`empirical memory record ... --due 2026-10-14T09:00:00Z` on the CLI. `dueAt` is a real field the
|
|
191
|
+
server filters and fires on; a date written inside `data` is text and nothing will ever fire.
|
|
192
|
+
- **Relay it.** If any Empirical result starts with `REMINDER DUE`, or a CLI command prints a
|
|
193
|
+
banner that starts with `REMINDER DUE`, tell the user about it in your own words BEFORE the task
|
|
194
|
+
you were doing, then ask whether to mark it done. Each agent is shown a reminder once; if you do
|
|
195
|
+
not relay it, the user does not hear it from you.
|
|
196
|
+
- **Close it** when the user says it is handled: `acknowledge_reminder` with the `memoryId` from
|
|
197
|
+
the notice (or `state: "cancelled"` to drop it), or `empirical memory acknowledge --memory-id <id>`
|
|
198
|
+
(`--cancel` to drop). Closing frees the slot; plans cap concurrent open reminders.
|
|
199
|
+
- **What is due?** `query_memories` with `dueBefore` set to now and `remindState: ["pending"]`, or
|
|
200
|
+
`empirical memory query --match "what is due" --due-before <now> --remind-state pending`.
|
|
201
|
+
Dueness is a filter; semantic search will not rank a memory higher because it is due.
|
|
202
|
+
|
|
182
203
|
## Authentication and failures
|
|
183
204
|
|
|
184
205
|
- If a memory call fails, retry the same call once.
|
|
@@ -167,6 +167,27 @@ An `empirical` CLI may also be installed on this machine for diagnostics (`empir
|
|
|
167
167
|
manual account setup, but it is not the point of this variant — do not lead with CLI commands for
|
|
168
168
|
query or record workflows here.
|
|
169
169
|
|
|
170
|
+
## Reminders — set them, relay them, close them
|
|
171
|
+
|
|
172
|
+
Any memory can carry a due date. Empirical then brings it back: an email with a calendar entry
|
|
173
|
+
when the reminder is set, a nudge when it is due, and a notice attached to the next Empirical
|
|
174
|
+
response each of the user's agents receives.
|
|
175
|
+
|
|
176
|
+
- **Set one** when the user says "remind me", names a deadline, or a todo has a real due date.
|
|
177
|
+
Use the `dueAt` parameter on `record_graph_memory` / `update_memory_by_query` (ISO 8601), or
|
|
178
|
+
`empirical memory record ... --due 2026-10-14T09:00:00Z` on the CLI. `dueAt` is a real field the
|
|
179
|
+
server filters and fires on; a date written inside `data` is text and nothing will ever fire.
|
|
180
|
+
- **Relay it.** If any Empirical result starts with `REMINDER DUE`, or a CLI command prints a
|
|
181
|
+
banner that starts with `REMINDER DUE`, tell the user about it in your own words BEFORE the task
|
|
182
|
+
you were doing, then ask whether to mark it done. Each agent is shown a reminder once; if you do
|
|
183
|
+
not relay it, the user does not hear it from you.
|
|
184
|
+
- **Close it** when the user says it is handled: `acknowledge_reminder` with the `memoryId` from
|
|
185
|
+
the notice (or `state: "cancelled"` to drop it), or `empirical memory acknowledge --memory-id <id>`
|
|
186
|
+
(`--cancel` to drop). Closing frees the slot; plans cap concurrent open reminders.
|
|
187
|
+
- **What is due?** `query_memories` with `dueBefore` set to now and `remindState: ["pending"]`, or
|
|
188
|
+
`empirical memory query --match "what is due" --due-before <now> --remind-state pending`.
|
|
189
|
+
Dueness is a filter; semantic search will not rank a memory higher because it is due.
|
|
190
|
+
|
|
170
191
|
## Authentication and failures
|
|
171
192
|
|
|
172
193
|
- If an MCP tool call fails, retry the same call once.
|