@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/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
- // (services/memoryAuthorship.ts). Writes also send source: 'cli' in the body,
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
- // (services/memoryAuthorship.ts). Writes also send source: 'cli' in the body,
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',
@@ -110,37 +110,125 @@ function main() {
110
110
  process.exit(0);
111
111
  }
112
112
 
113
- var result = runMemoryQuery([
114
- 'memory', 'query',
115
- '--match', 'project context, conventions, recent decisions, user preferences',
116
- '--top-k', '8'
117
- ]);
118
-
119
- if (!result || result.status !== 0 || !result.stdout) {
120
- appendLog('memory query failed or returned no stdout (status=' + (result && result.status) + ', stderr=' + (result && result.stderr ? String(result.stderr).slice(0, 300) : '') + ')');
121
- process.exit(0);
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
- var results;
125
- try {
126
- results = JSON.parse(result.stdout);
127
- } catch (e) {
128
- appendLog('failed to parse memory query stdout as JSON: ' + e.message);
129
- process.exit(0);
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 (!Array.isArray(results) || results.length === 0) {
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
- var lines = results.slice(0, 8).map(function (r) {
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
- return '- [' + category + '] ' + summary;
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
- var result = runMemoryQuery([
263
- 'memory', 'query',
264
- '--match', 'project context, conventions, recent decisions, user preferences',
265
- '--top-k', '8'
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
- if (!result || result.status !== 0 || !result.stdout) {
269
- appendLog('memory query failed or returned no stdout (status=' + (result && result.status) + ', stderr=' + (result && result.stderr ? String(result.stderr).slice(0, 300) : '') + ')');
270
- emitContext('');
271
- process.exit(0);
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 results;
275
- try {
276
- results = JSON.parse(result.stdout);
277
- } catch (e) {
278
- appendLog('failed to parse memory query stdout as JSON: ' + e.message);
279
- emitContext('');
280
- process.exit(0);
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 (!Array.isArray(results) || results.length === 0) {
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
- var lines = results.slice(0, 8).map(function (r) {
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
- return '- [' + category + '] ' + summary;
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
- var result = runMemoryQuery([
404
- 'memory', 'query',
405
- '--match', 'project context, conventions, recent decisions, user preferences',
406
- '--top-k', '8'
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
- if (!result || result.status !== 0 || !result.stdout) {
410
- appendLog('memory query failed or returned no stdout (status=' + (result && result.status) + ', stderr=' + (result && result.stderr ? String(result.stderr).slice(0, 300) : '') + ')');
411
- console.log('{}');
412
- process.exit(0);
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 results;
416
- try {
417
- results = JSON.parse(result.stdout);
418
- } catch (e) {
419
- appendLog('failed to parse memory query stdout as JSON: ' + e.message);
420
- console.log('{}');
421
- process.exit(0);
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
- if (!Array.isArray(results) || results.length === 0) {
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 lines = results.slice(0, 8).map(function (r) {
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
- return '- [' + category + '] ' + summary;
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: 15 }]
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: 15 }]
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.