@empirical-gauzza/cli 1.0.29 → 1.0.31

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.
@@ -41,6 +41,31 @@ function writeIntegrationMode(integrationMode) {
41
41
  writeDeviceConfig(config);
42
42
  }
43
43
 
44
+ // Hosts (codex / copilot / claude) that Empirical has actually been installed for on this
45
+ // machine, recorded by `empirical install` and `empirical plugin install`. `doctor` reads this
46
+ // so a host that merely exists on the machine (a ~/.claude folder left by another tool, or a CLI
47
+ // the user deliberately skipped in the installer) is reported as skipped, not as broken.
48
+ // Returns null when nothing was ever recorded (installs that predate this field), so callers
49
+ // can tell "unknown" apart from "none".
50
+ const KNOWN_HOST_IDS = ['codex', 'copilot', 'claude'];
51
+
52
+ function readInstalledHosts() {
53
+ const config = readDeviceConfig();
54
+ if (!Array.isArray(config.installedHosts)) return null;
55
+ return config.installedHosts.filter((id) => KNOWN_HOST_IDS.includes(id));
56
+ }
57
+
58
+ function addInstalledHosts(hostIds) {
59
+ const config = readDeviceConfig();
60
+ const existing = Array.isArray(config.installedHosts) ? config.installedHosts : [];
61
+ const merged = new Set(existing.filter((id) => KNOWN_HOST_IDS.includes(id)));
62
+ for (const id of hostIds || []) {
63
+ if (KNOWN_HOST_IDS.includes(id)) merged.add(id);
64
+ }
65
+ config.installedHosts = KNOWN_HOST_IDS.filter((id) => merged.has(id));
66
+ writeDeviceConfig(config);
67
+ }
68
+
44
69
  function readPolicyCache() {
45
70
  const config = readDeviceConfig();
46
71
  return config.empiricalPolicy || null;
@@ -101,6 +126,8 @@ module.exports = {
101
126
  writeDeviceConfig,
102
127
  readIntegrationMode,
103
128
  writeIntegrationMode,
129
+ readInstalledHosts,
130
+ addInstalledHosts,
104
131
  readPolicyCache,
105
132
  writePolicyCache,
106
133
  ensureDeviceId,
package/lib/doctor.js CHANGED
@@ -13,6 +13,39 @@ const { detectInstallTargets } = require('./installers/detect');
13
13
  const { readInstalledVersions } = require('./pluginUpdateCheck');
14
14
  const { CORE_PLUGIN_NAME, OPTIONAL_PLUGIN_NAMES } = require('./installers/nativePlugin');
15
15
  const { getEmpiricalHooksDir, getCodexHooksScriptsDir, getCodexHomeDir } = require('./installers/hooks');
16
+ const { readIntegrationMode, readInstalledHosts } = require('./deviceConfig');
17
+
18
+ // ─── Which detected hosts doctor should grade ──────────────────────────────────
19
+ // Doctor output is read by agents, and a FAIL line with a "Run: ..." remedy is, to an agent, an
20
+ // instruction. So a host the user never set Empirical up for must not be graded as broken:
21
+ // - If the installer recorded which hosts it installed for, grade exactly those.
22
+ // - Otherwise (installs that predate the record), grade only hosts whose binary is on PATH; a
23
+ // bare dotfolder (e.g. ~/.claude left behind with no `claude` command) is nothing to check.
24
+ // Skipped hosts still get one informational line so the user can see why they were left out.
25
+ function partitionHostsToCheck(detectedTargets, installedHosts) {
26
+ const checked = [];
27
+ const skipped = [];
28
+ for (const target of detectedTargets) {
29
+ if (installedHosts !== null) {
30
+ if (installedHosts.includes(target.id)) {
31
+ checked.push(target);
32
+ } else {
33
+ const evidence = target.onPath
34
+ ? `${target.id} is on PATH`
35
+ : `only its ${target.configDir} folder exists; ${target.id} is not on PATH`;
36
+ skipped.push({ target, reason: `not set up for Empirical (${evidence}). To add it: empirical install.` });
37
+ }
38
+ } else if (target.onPath) {
39
+ checked.push(target);
40
+ } else {
41
+ skipped.push({
42
+ target,
43
+ reason: `only its ${target.configDir} folder exists; ${target.id} is not on PATH, so there is nothing to check. To set it up later: empirical install.`
44
+ });
45
+ }
46
+ }
47
+ return { checked, skipped };
48
+ }
16
49
 
17
50
  // ─── Host MCP config registration ─────────────────────────────────────────────
18
51
  // Each host stores "is the Empirical MCP server registered" in a different shape and location.
@@ -377,11 +410,28 @@ async function runDoctor(args) {
377
410
  }
378
411
 
379
412
  // ─── MCP wiring diagnostics (C-007) ─────────────────────────────────────
380
- const detectedTargets = detectInstallTargets();
381
- if (detectedTargets.length === 0) {
413
+ const allDetected = detectInstallTargets();
414
+ let installedHosts = null;
415
+ let integrationMode = 1;
416
+ try {
417
+ installedHosts = readInstalledHosts();
418
+ integrationMode = readIntegrationMode();
419
+ } catch (_error) {
420
+ installedHosts = null;
421
+ integrationMode = 1;
422
+ }
423
+ const { checked: detectedTargets, skipped: skippedTargets } = partitionHostsToCheck(allDetected, installedHosts);
424
+
425
+ if (allDetected.length === 0) {
382
426
  addCheck('mcp_hosts_detected', 'warn', 'No supported host CLI detected (codex/claude/copilot). Nothing to check for MCP wiring.');
427
+ } else if (detectedTargets.length === 0) {
428
+ addCheck('mcp_hosts_detected', 'warn', `Detected: ${allDetected.map((t) => t.label).join(', ')}, but none is set up for Empirical. Run: empirical install.`);
383
429
  } else {
384
- addCheck('mcp_hosts_detected', 'ok', `Detected: ${detectedTargets.map((t) => t.label).join(', ')}.`);
430
+ const modeLabel = integrationMode === 2 ? 'CLI-direct' : integrationMode === 3 ? 'MCP + CLI' : 'MCP';
431
+ addCheck('mcp_hosts_detected', 'ok', `Checking: ${detectedTargets.map((t) => t.label).join(', ')} (${modeLabel} mode).`);
432
+ }
433
+ for (const { target, reason } of skippedTargets) {
434
+ addCheck(`host_${target.id}`, 'skip', `${target.label}: ${reason}`);
385
435
  }
386
436
 
387
437
  for (const target of detectedTargets) {
@@ -393,7 +443,16 @@ async function runDoctor(args) {
393
443
  } catch (error) {
394
444
  result = { ok: false, message: `Could not inspect ${target.label} MCP config: ${error.message}` };
395
445
  }
396
- addCheck(`mcp_config_${target.id}`, result.ok ? 'ok' : 'fail', `${target.label}: ${result.message}`);
446
+ if (integrationMode === 2) {
447
+ // CLI-direct mode: agents call the `empirical` CLI, so an unregistered MCP server is the
448
+ // expected state, not a defect. Report whatever is there without failing on absence.
449
+ const message = result.ok
450
+ ? `${result.message} (CLI-direct mode; MCP registration is optional.)`
451
+ : 'CLI-direct mode: MCP server not registered, and none is expected. Agents use the empirical CLI.';
452
+ addCheck(`mcp_config_${target.id}`, 'ok', `${target.label}: ${message}`);
453
+ } else {
454
+ addCheck(`mcp_config_${target.id}`, result.ok ? 'ok' : 'fail', `${target.label}: ${result.message}`);
455
+ }
397
456
  }
398
457
 
399
458
  const mcpReachability = await checkMcpReachability(token);
@@ -464,6 +523,7 @@ async function runDoctor(args) {
464
523
  const statusLabel = (status) => {
465
524
  if (status === 'ok') return colorize(ANSI.green, '[OK] ');
466
525
  if (status === 'warn') return colorize(ANSI.yellow, '[WARN]');
526
+ if (status === 'skip') return colorize(ANSI.dim, '[SKIP]');
467
527
  return colorize(ANSI.red, '[FAIL]');
468
528
  };
469
529
  for (const check of checks) {
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',
@@ -23,8 +23,9 @@ const { detectInstallTargets } = require('./installers/detect');
23
23
  const { installClaudeHooks, installCodexHooks, installCopilotHooks } = require('./installers/hooks');
24
24
  const { runCodex } = require('./installers/codex');
25
25
  const { installNativePlugin, OPTIONAL_PLUGIN_NAMES } = require('./installers/nativePlugin');
26
+ const { writeCoreMemoryGuidance } = require('./installers/coreMemoryBlock');
26
27
  const { policyFromInstallerOptions, savePolicy: persistPolicy } = require('./policyCommands');
27
- const { writeIntegrationMode } = require('./deviceConfig');
28
+ const { writeIntegrationMode, addInstalledHosts } = require('./deviceConfig');
28
29
 
29
30
  function pluginNamesForInstallerOptions(options = {}) {
30
31
  const names = ['empirical-memory'];
@@ -234,10 +235,13 @@ async function runInstallFlow() {
234
235
  log(uiHeader(`Step 2/2: Installing for ${target.label}...`));
235
236
  try {
236
237
  if (integrationMode === 2) {
237
- // CLI-direct: skip MCP registration; the native plugin owns guidance.
238
+ // CLI-direct: skip MCP registration only. The skill, the hooks AND the global
239
+ // instructions block are all still required -- a real Copilot install that skipped the
240
+ // instructions file here left the agent ignoring Empirical for a whole session.
238
241
  const pluginNames = pluginNamesForInstallerOptions(promptOptions);
239
242
  installNativePlugin(target.id, pluginNames, integrationMode);
240
243
  log(` - ${target.label} native plugins installed: ${pluginNames.join(', ')}.`);
244
+ writeCoreMemoryGuidance(target.id, pluginNames);
241
245
  if (target.id === 'claude') {
242
246
  try {
243
247
  installClaudeHooks(pluginNames);
@@ -260,6 +264,7 @@ async function runInstallFlow() {
260
264
  }
261
265
  }
262
266
  log(uiSuccess(`Done: ${target.label} (CLI-direct mode)`));
267
+ addInstalledHosts([target.id]);
263
268
  } else {
264
269
  // Mode 1 or 3: run MCP install normally
265
270
  const pluginNames = pluginNamesForInstallerOptions(promptOptions);
@@ -269,6 +274,7 @@ async function runInstallFlow() {
269
274
  integrationMode
270
275
  });
271
276
  log(uiSuccess(`Done: ${target.label}`));
277
+ addInstalledHosts([target.id]);
272
278
  if (target.id === 'codex') {
273
279
  codexInstalled = true;
274
280
  codexNeedsPostLogin = Boolean(installResult?.needsPostLogin);
@@ -11,7 +11,7 @@ const {
11
11
  const { INSTALL_PROMPT_OPTIONS_DEFAULTS } = require('./promptBlocks');
12
12
  const { installClaudeHooks } = require('./hooks');
13
13
  const { installNativePlugin, CORE_PLUGIN_NAME } = require('./nativePlugin');
14
- const { buildCoreMemoryBlock, upsertCoreMemoryBlock } = require('./coreMemoryBlock');
14
+ const { writeCoreMemoryGuidance } = require('./coreMemoryBlock');
15
15
 
16
16
  function runClaude(args, options = {}) {
17
17
  return runResolvedCommand('claude', args, options);
@@ -30,14 +30,7 @@ function installClaude(headlessToken, promptOptions = INSTALL_PROMPT_OPTIONS_DEF
30
30
  // writes the same kind of identity-framed statement into the one file Claude reads every
31
31
  // session unconditionally, inside a clearly delimited block so any of the user's own content
32
32
  // in CLAUDE.md is left untouched.
33
- try {
34
- const claudeMdPath = path.join(os.homedir(), '.claude', 'CLAUDE.md');
35
- const block = buildCoreMemoryBlock({ pluginNames: installOptions.pluginNames || [CORE_PLUGIN_NAME] });
36
- upsertCoreMemoryBlock(claudeMdPath, block);
37
- log(` - Wrote Empirical core memory guidance to ${claudeMdPath}.`);
38
- } catch (error) {
39
- warn(` - Could not write Empirical core memory guidance to CLAUDE.md: ${error.message}`);
40
- }
33
+ writeCoreMemoryGuidance('claude', installOptions.pluginNames || [CORE_PLUGIN_NAME]);
41
34
 
42
35
  // installNativePlugin throws if the plugin install/update genuinely fails (see
43
36
  // assertPluginInstallSucceeded in nativePlugin.js) - hooks are only installed AFTER this
@@ -11,7 +11,7 @@ const {
11
11
  const { INSTALL_PROMPT_OPTIONS_DEFAULTS } = require('./promptBlocks');
12
12
  const { installCodexHooks } = require('./hooks');
13
13
  const { installNativePlugin, CORE_PLUGIN_NAME } = require('./nativePlugin');
14
- const { buildCoreMemoryBlock, upsertCoreMemoryBlock } = require('./coreMemoryBlock');
14
+ const { writeCoreMemoryGuidance } = require('./coreMemoryBlock');
15
15
 
16
16
  function runCodex(args, options = {}) {
17
17
  return runResolvedCommand('codex', args, options);
@@ -26,14 +26,7 @@ function installCodex(headlessToken, promptOptions = INSTALL_PROMPT_OPTIONS_DEFA
26
26
  // 2026-08-24: see claude.js's identical block for why this now writes AGENTS.md instead of
27
27
  // leaving it untouched -- hooks + an installable skill alone were not enough against a
28
28
  // competing identity-framed built-in memory feature in a real session.
29
- try {
30
- const agentsMdPath = path.join(os.homedir(), '.codex', 'AGENTS.md');
31
- const block = buildCoreMemoryBlock({ pluginNames: installOptions.pluginNames || [CORE_PLUGIN_NAME] });
32
- upsertCoreMemoryBlock(agentsMdPath, block);
33
- log(` - Wrote Empirical core memory guidance to ${agentsMdPath}.`);
34
- } catch (error) {
35
- warn(` - Could not write Empirical core memory guidance to AGENTS.md: ${error.message}`);
36
- }
29
+ writeCoreMemoryGuidance('codex', installOptions.pluginNames || [CORE_PLUGIN_NAME]);
37
30
 
38
31
  // installNativePlugin throws on genuine failure (see assertPluginInstallSucceeded in
39
32
  // nativePlugin.js) - hooks install only AFTER this succeeds, never before, so a failed/skipped
@@ -12,7 +12,7 @@ const {
12
12
  const { INSTALL_PROMPT_OPTIONS_DEFAULTS } = require('./promptBlocks');
13
13
  const { installCopilotHooks } = require('./hooks');
14
14
  const { installNativePlugin, CORE_PLUGIN_NAME } = require('./nativePlugin');
15
- const { buildCoreMemoryBlock, upsertCoreMemoryBlock } = require('./coreMemoryBlock');
15
+ const { writeCoreMemoryGuidance } = require('./coreMemoryBlock');
16
16
 
17
17
  function installCopilot(headlessToken, promptOptions = INSTALL_PROMPT_OPTIONS_DEFAULTS, installOptions = {}) {
18
18
  const home = os.homedir();
@@ -65,13 +65,7 @@ function installCopilot(headlessToken, promptOptions = INSTALL_PROMPT_OPTIONS_DE
65
65
  // 2026-08-24: see claude.js's identical block for why this now writes copilot-instructions.md
66
66
  // instead of leaving it untouched -- hooks + an installable skill alone were not enough
67
67
  // against a competing identity-framed built-in memory feature in a real session.
68
- try {
69
- const block = buildCoreMemoryBlock({ pluginNames: installOptions.pluginNames || [CORE_PLUGIN_NAME] });
70
- upsertCoreMemoryBlock(instructionsPath, block);
71
- log(` - Wrote Empirical core memory guidance to ${instructionsPath}.`);
72
- } catch (error) {
73
- warn(` - Could not write Empirical core memory guidance to copilot-instructions.md: ${error.message}`);
74
- }
68
+ writeCoreMemoryGuidance('copilot', installOptions.pluginNames || [CORE_PLUGIN_NAME]);
75
69
 
76
70
  // installNativePlugin throws on genuine failure (see assertPluginInstallSucceeded in
77
71
  // nativePlugin.js) - hooks install only AFTER this succeeds, never before, so a failed/skipped
@@ -1,6 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  const { readUtf8IfExists, writeFile, ensureDir } = require('../fsUtils');
4
+ const { log, warn } = require('../ui');
5
+ const os = require('os');
4
6
  const path = require('path');
5
7
  const { CORE_PLUGIN_NAME, OPTIONAL_PLUGIN_NAMES } = require('./nativePlugin');
6
8
 
@@ -88,9 +90,48 @@ function upsertCoreMemoryBlock(filePath, block) {
88
90
  writeFile(filePath, next);
89
91
  }
90
92
 
93
+ // The one instructions file each host reads unconditionally every session. Kept here, next to
94
+ // the block it receives, so every install path (full installer in any mode, `plugin install`)
95
+ // writes to the same place instead of each path re-deriving it.
96
+ const INSTRUCTIONS_FILE_BY_HOST = {
97
+ claude: ['.claude', 'CLAUDE.md'],
98
+ codex: ['.codex', 'AGENTS.md'],
99
+ copilot: ['.copilot', 'copilot-instructions.md']
100
+ };
101
+
102
+ function instructionsFilePathForHost(hostId) {
103
+ const segments = INSTRUCTIONS_FILE_BY_HOST[hostId];
104
+ if (!segments) throw new Error(`No global instructions file is known for host "${hostId}".`);
105
+ return path.join(os.homedir(), ...segments);
106
+ }
107
+
108
+ /**
109
+ * Writes (or refreshes) the core memory block in the host's global instructions file. Every
110
+ * install path must call this, whatever integration mode the user picked: a real CLI-direct
111
+ * Copilot install (2026-09-17) installed the skill and hooks but skipped this file, and the
112
+ * agent ignored Empirical for the whole session exactly as the 2026-08-24 incident predicted.
113
+ * Never throws; a failure here is logged and must not fail the install that already succeeded.
114
+ * Returns the path written, or null on failure.
115
+ */
116
+ function writeCoreMemoryGuidance(hostId, pluginNames = [CORE_PLUGIN_NAME]) {
117
+ let filePath = null;
118
+ try {
119
+ filePath = instructionsFilePathForHost(hostId);
120
+ const block = buildCoreMemoryBlock({ pluginNames: pluginNames && pluginNames.length ? pluginNames : [CORE_PLUGIN_NAME] });
121
+ upsertCoreMemoryBlock(filePath, block);
122
+ log(` - Wrote Empirical core memory guidance to ${filePath}.`);
123
+ return filePath;
124
+ } catch (error) {
125
+ warn(` - Could not write Empirical core memory guidance for ${hostId}${filePath ? ` (${filePath})` : ''}: ${error.message}`);
126
+ return null;
127
+ }
128
+ }
129
+
91
130
  module.exports = {
92
131
  BEGIN_MARKER,
93
132
  END_MARKER,
94
133
  buildCoreMemoryBlock,
95
- upsertCoreMemoryBlock
134
+ upsertCoreMemoryBlock,
135
+ instructionsFilePathForHost,
136
+ writeCoreMemoryGuidance
96
137
  };
@@ -8,33 +8,49 @@ const { installCodex } = require('./codex');
8
8
  const { installCopilot } = require('./copilot');
9
9
  const { installClaude } = require('./claude');
10
10
 
11
+ // A host counts as detected when its binary is on PATH OR its home dotfolder exists. The
12
+ // dotfolder alone is a weak signal (another tool, an uninstalled CLI, or a desktop app can leave
13
+ // one behind), so each target also carries `onPath` and `configDirExists` separately; doctor uses
14
+ // them to say "found only a folder, nothing to check" instead of failing the host.
11
15
  function detectInstallTargets() {
12
16
  const home = os.homedir();
13
17
  const targets = [];
14
18
 
15
- const codexDetected = commandExists('codex') || fs.existsSync(path.join(home, '.codex'));
16
- if (codexDetected) {
19
+ const codexOnPath = commandExists('codex');
20
+ const codexDirExists = fs.existsSync(path.join(home, '.codex'));
21
+ if (codexOnPath || codexDirExists) {
17
22
  targets.push({
18
23
  id: 'codex',
19
24
  label: 'Codex CLI',
25
+ onPath: codexOnPath,
26
+ configDirExists: codexDirExists,
27
+ configDir: path.join(home, '.codex'),
20
28
  install: installCodex
21
29
  });
22
30
  }
23
31
 
24
- const copilotDetected = commandExists('copilot') || fs.existsSync(path.join(home, '.copilot'));
25
- if (copilotDetected) {
32
+ const copilotOnPath = commandExists('copilot');
33
+ const copilotDirExists = fs.existsSync(path.join(home, '.copilot'));
34
+ if (copilotOnPath || copilotDirExists) {
26
35
  targets.push({
27
36
  id: 'copilot',
28
37
  label: 'GitHub Copilot CLI',
38
+ onPath: copilotOnPath,
39
+ configDirExists: copilotDirExists,
40
+ configDir: path.join(home, '.copilot'),
29
41
  install: installCopilot
30
42
  });
31
43
  }
32
44
 
33
- const claudeDetected = commandExists('claude') || fs.existsSync(path.join(home, '.claude'));
34
- if (claudeDetected) {
45
+ const claudeOnPath = commandExists('claude');
46
+ const claudeDirExists = fs.existsSync(path.join(home, '.claude'));
47
+ if (claudeOnPath || claudeDirExists) {
35
48
  targets.push({
36
49
  id: 'claude',
37
50
  label: 'Claude CLI',
51
+ onPath: claudeOnPath,
52
+ configDirExists: claudeDirExists,
53
+ configDir: path.join(home, '.claude'),
38
54
  install: installClaude
39
55
  });
40
56
  }
@@ -85,7 +85,9 @@ function runMemoryQuery(args) {
85
85
  appendLog('resolved direct entry point: ' + entryPoint);
86
86
  return spawnSync(process.execPath, [entryPoint].concat(args), {
87
87
  encoding: 'utf8',
88
- timeout: 10000,
88
+ // Must stay under the host manifest cap for this event (25s). Recall is chronically 2-7s
89
+ // and the old 10s here killed the query silently while the host still had 15s to spare.
90
+ timeout: 20000,
89
91
  shell: false
90
92
  });
91
93
  }
@@ -97,7 +99,7 @@ function runMemoryQuery(args) {
97
99
  appendLog('direct entry point not resolved, falling back to shell invocation (shell=' + (process.platform === 'win32') + ')');
98
100
  return spawnSync('empirical', args, {
99
101
  encoding: 'utf8',
100
- timeout: 10000,
102
+ timeout: 20000,
101
103
  shell: process.platform === 'win32'
102
104
  });
103
105
  }
@@ -310,7 +312,9 @@ function runMemoryQuery(args) {
310
312
  appendLog('resolved direct entry point: ' + entryPoint);
311
313
  return spawnSync(process.execPath, [entryPoint].concat(args), {
312
314
  encoding: 'utf8',
313
- timeout: 10000,
315
+ // Must stay under the host manifest cap for this event (25s). Recall is chronically 2-7s
316
+ // and the old 10s here killed the query silently while the host still had 15s to spare.
317
+ timeout: 20000,
314
318
  shell: false
315
319
  });
316
320
  }
@@ -318,7 +322,7 @@ function runMemoryQuery(args) {
318
322
  appendLog('direct entry point not resolved, falling back to shell invocation (shell=' + (process.platform === 'win32') + ')');
319
323
  return spawnSync('empirical', args, {
320
324
  encoding: 'utf8',
321
- timeout: 10000,
325
+ timeout: 20000,
322
326
  shell: process.platform === 'win32'
323
327
  });
324
328
  }
@@ -552,7 +556,9 @@ function runMemoryQuery(args) {
552
556
  appendLog('resolved direct entry point: ' + entryPoint);
553
557
  return spawnSync(process.execPath, [entryPoint].concat(args), {
554
558
  encoding: 'utf8',
555
- timeout: 10000,
559
+ // Must stay under the host manifest cap for this event (25s). Recall is chronically 2-7s
560
+ // and the old 10s here killed the query silently while the host still had 15s to spare.
561
+ timeout: 20000,
556
562
  shell: false
557
563
  });
558
564
  }
@@ -560,7 +566,7 @@ function runMemoryQuery(args) {
560
566
  appendLog('direct entry point not resolved, falling back to shell invocation (shell=' + (process.platform === 'win32') + ')');
561
567
  return spawnSync('empirical', args, {
562
568
  encoding: 'utf8',
563
- timeout: 10000,
569
+ timeout: 20000,
564
570
  shell: process.platform === 'win32'
565
571
  });
566
572
  }
@@ -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.