@empirical-gauzza/cli 1.0.33 → 1.0.34

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/doctor.js CHANGED
@@ -156,7 +156,7 @@ function claudeHookStatus() {
156
156
  && entry.hooks.some((h) => h && typeof h.command === 'string' && h.command.includes(hooksDir)));
157
157
  });
158
158
 
159
- return { missingScripts, missingRegistrations: hookEvents.filter((e) => !registeredEvents.includes(e)), manifestPath: settingsPath };
159
+ return { missingScripts, missingRegistrations: hookEvents.filter((e) => !registeredEvents.includes(e)), manifestPath: settingsPath, registeredEvents };
160
160
  }
161
161
 
162
162
  function codexHookStatus() {
@@ -174,24 +174,30 @@ function codexHookStatus() {
174
174
  && entry.hooks.some((h) => h && typeof h.command === 'string' && h.command.includes(hooksDir)));
175
175
  });
176
176
 
177
- return { missingScripts, missingRegistrations: hookEvents.filter((e) => !registeredEvents.includes(e)), manifestPath: hooksJsonPath };
177
+ return { missingScripts, missingRegistrations: hookEvents.filter((e) => !registeredEvents.includes(e)), manifestPath: hooksJsonPath, registeredEvents };
178
178
  }
179
179
 
180
+ // SessionStart only (2026-09-18): Copilot's agentStop reminder hook was removed entirely (recording
181
+ // stays instructions-only via copilot-instructions.md), and its UserPromptSubmit real-recall hook
182
+ // was ALSO removed after confirming live that Copilot renders modifiedPrompt back as the user's own
183
+ // visible message - not acceptable for silent recall. Real recall moved to sessionStart's
184
+ // additionalContext instead (re-confirmed against GitHub's current hooks reference that this event
185
+ // supports it for command hooks), so sessionStart is once again the only event expected/checked.
180
186
  function copilotHookStatus() {
181
187
  const hooksDir = getEmpiricalHooksDir();
182
- const scripts = ['copilot-session-start.js', 'copilot-user-prompt-submit.js', 'copilot-stop.js'];
188
+ const scripts = ['copilot-session-start.js'];
183
189
  const missingScripts = scripts.filter((name) => !readUtf8IfExists(path.join(hooksDir, name)));
184
190
 
185
191
  const hookFilePath = path.join(os.homedir(), '.copilot', 'hooks', 'empirical-hooks.json');
186
192
  const hookFile = readJsonIfExists(hookFilePath) || {};
187
- const hookEvents = ['sessionStart', 'userPromptSubmitted', 'agentStop'];
193
+ const hookEvents = ['sessionStart'];
188
194
  const registeredEvents = hookEvents.filter((eventName) => {
189
195
  const entries = hookFile.hooks && hookFile.hooks[eventName];
190
196
  if (!Array.isArray(entries)) return false;
191
197
  return entries.some((h) => h && typeof h.command === 'string' && h.command.includes(hooksDir));
192
198
  });
193
199
 
194
- return { missingScripts, missingRegistrations: hookEvents.filter((e) => !registeredEvents.includes(e)), manifestPath: hookFilePath };
200
+ return { missingScripts, missingRegistrations: hookEvents.filter((e) => !registeredEvents.includes(e)), manifestPath: hookFilePath, registeredEvents };
195
201
  }
196
202
 
197
203
  const HOOK_STATUS_CHECKERS = {
@@ -495,7 +501,12 @@ async function runDoctor(args) {
495
501
  continue;
496
502
  }
497
503
  if (status.missingScripts.length === 0 && status.missingRegistrations.length === 0) {
498
- addCheck(`hooks_${target.id}`, 'ok', `${target.label}: SessionStart/UserPromptSubmit/Stop hook scripts present and registered in ${status.manifestPath}.`);
504
+ // status.registeredEvents (not a hardcoded event list) so this stays accurate per host -
505
+ // Copilot only registers sessionStart now (2026-09-18: userPromptSubmitted/agentStop
506
+ // reminder hooks were removed, see copilotHookStatus's comment), while Claude/Codex still
507
+ // register all three.
508
+ const eventList = (status.registeredEvents || []).join('/') || 'hook';
509
+ addCheck(`hooks_${target.id}`, 'ok', `${target.label}: ${eventList} hook script(s) present and registered in ${status.manifestPath}.`);
499
510
  } else {
500
511
  const parts = [];
501
512
  if (status.missingScripts.length) parts.push(`missing script file(s): ${status.missingScripts.join(', ')}`);
@@ -35,10 +35,22 @@ function describePlugin(name) {
35
35
  * run actually installed, not a static "here's everything that could exist" list, so it stays
36
36
  * accurate as skills are added or removed on a later `empirical install`/`empirical plugin` run.
37
37
  */
38
- function buildCoreMemoryBlock({ pluginNames = [CORE_PLUGIN_NAME] } = {}) {
38
+ function buildCoreMemoryBlock({ pluginNames = [CORE_PLUGIN_NAME], hostId } = {}) {
39
39
  const names = Array.isArray(pluginNames) && pluginNames.length ? pluginNames : [CORE_PLUGIN_NAME];
40
40
  const skillLines = names.map((name) => `- \`${name}\` - ${describePlugin(name)}`).join('\n');
41
41
 
42
+ // Copilot-only (2026-09-18): Copilot's hook events can't carry a passive reminder the way
43
+ // Claude/Codex's can (agentStop only supports decision:"block"|"allow" - see COPILOT_HOOK_STOP_SOURCE's
44
+ // removal in hooks.js), and Copilot has no reminder hooks installed at all as a result. This is
45
+ // the ONLY mechanism left nudging Copilot toward Empirical, so it has to be explicit and
46
+ // directive rather than assume a hook will catch what this misses.
47
+ const copilotAddendum = hostId === 'copilot'
48
+ ? `\n\nThis host has no reminder hook wired up for Empirical (a platform limitation, not an
49
+ oversight) - this instructions file is the only thing that will ever prompt you to use it. Query
50
+ and record proactively, on your own initiative, without waiting to be asked or reminded. Do this
51
+ before ending a turn where you completed meaningful work, not just when explicitly told to.`
52
+ : '';
53
+
42
54
  return `${BEGIN_MARKER}
43
55
  ## Persistent memory: Empirical
44
56
 
@@ -61,7 +73,7 @@ session notes into answers about the user's own life.
61
73
  Installed Empirical skills in this environment:
62
74
  ${skillLines}
63
75
 
64
- Run \`empirical doc\` for the full command reference, or \`empirical doc memory\` for exact query/record syntax.
76
+ Run \`empirical doc\` for the full command reference, or \`empirical doc memory\` for exact query/record syntax.${copilotAddendum}
65
77
  ${END_MARKER}`;
66
78
  }
67
79
 
@@ -117,7 +129,7 @@ function writeCoreMemoryGuidance(hostId, pluginNames = [CORE_PLUGIN_NAME]) {
117
129
  let filePath = null;
118
130
  try {
119
131
  filePath = instructionsFilePathForHost(hostId);
120
- const block = buildCoreMemoryBlock({ pluginNames: pluginNames && pluginNames.length ? pluginNames : [CORE_PLUGIN_NAME] });
132
+ const block = buildCoreMemoryBlock({ pluginNames: pluginNames && pluginNames.length ? pluginNames : [CORE_PLUGIN_NAME], hostId });
121
133
  upsertCoreMemoryBlock(filePath, block);
122
134
  log(` - Wrote Empirical core memory guidance to ${filePath}.`);
123
135
  return filePath;
@@ -571,8 +571,25 @@ function runMemoryQuery(args) {
571
571
  });
572
572
  }
573
573
 
574
+ // 2026-09-18: GitHub's own hooks reference (re-checked - the August 2026 check this file used to
575
+ // cite was stale) confirms sessionStart CAN inject additionalContext for command-type hooks, same
576
+ // flat { additionalContext: "..." } shape postToolUse etc. use. This event's input payload also
577
+ // carries an optional initialPrompt - using it as the query (like the removed UserPromptSubmit
578
+ // hook did for its own prompt) makes the one real query relevant to what's actually being asked,
579
+ // not just a generic default.
580
+ function readStdinPayload() {
581
+ try {
582
+ var raw = fs.readFileSync(0, 'utf8');
583
+ return raw ? JSON.parse(raw) : {};
584
+ } catch (e) {
585
+ return {};
586
+ }
587
+ }
588
+
574
589
  function main() {
575
590
  appendLog('hook fired (pid=' + process.pid + ', platform=' + process.platform + ')');
591
+ var payload = readStdinPayload();
592
+ var initialPrompt = String((payload && (payload.initialPrompt || payload.initial_prompt)) || '').trim();
576
593
 
577
594
  if (!commandExists('empirical')) {
578
595
  appendLog('empirical CLI not found on PATH, exiting');
@@ -651,9 +668,9 @@ function main() {
651
668
  var topics = envTopics.length ? envTopics : fileConfig.topics;
652
669
  var contextMatch = topics.length
653
670
  ? topics.join(', ')
654
- : 'project context, conventions, recent decisions, user preferences';
671
+ : (initialPrompt || 'project context, conventions, recent decisions, user preferences');
655
672
 
656
- appendLog('project tag=' + (tag || '(none)') + ' (source=' + tagSource + '), topics=' + (topics.length ? topics.join('|') : '(default)'));
673
+ appendLog('project tag=' + (tag || '(none)') + ' (source=' + tagSource + '), topics=' + (topics.length ? topics.join('|') : '(default)') + ', usingInitialPrompt=' + Boolean(initialPrompt && !topics.length));
657
674
 
658
675
  // Scope every query to the project. Measured 2026-09-16: with no tag filter all 8 slots went to
659
676
  // one unrelated project during a full session in a different repo, so the only automatic context
@@ -699,9 +716,13 @@ function main() {
699
716
  });
700
717
  lines.push('- Before risky or repeated work, ask first: empirical memory query --match "<what you are about to do>" --tags scar-tissue');
701
718
 
702
- // Logged only - Copilot's sessionStart event has no field that surfaces this to the model.
703
- appendLog('memory query returned ' + results.length + ' result(s) (session-start has no output channel to inject them, logged only): ' + lines.join(' | '));
704
- console.log('{}');
719
+ // additionalContext is honored for command-type sessionStart hooks (re-confirmed 2026-09-18
720
+ // against GitHub's current hooks reference - the prior "no output channel" comment here was
721
+ // stale). Delivered invisibly: this never touches the visible prompt, unlike the UserPromptSubmit
722
+ // approach it replaces (removed - see installCopilotHooks's comment).
723
+ var block = 'Empirical memory context for this session:\\n' + lines.join('\\n');
724
+ appendLog('memory query returned ' + results.length + ' result(s), injecting via additionalContext');
725
+ console.log(JSON.stringify({ additionalContext: block }));
705
726
  process.exit(0);
706
727
  }
707
728
 
@@ -714,11 +735,21 @@ try {
714
735
  }
715
736
  `;
716
737
 
738
+ // Copilot has no UserPromptSubmit real-recall hook (removed 2026-09-18, superseded by
739
+ // additionalContext delivery through COPILOT_HOOK_SESSION_START_SOURCE above). That approach was
740
+ // abandoned because Copilot renders the ENTIRE modifiedPrompt back in the terminal as the user's
741
+ // own visible message - confirmed live, not documented, and not what "real hook" was supposed to
742
+ // mean. sessionStart's additionalContext (re-confirmed against current GitHub docs - the prior
743
+ // "no output channel" comment on that hook was stale) delivers the same real query invisibly
744
+ // instead, at the cost of firing once per real session rather than being keyed to the very first
745
+ // prompt's exact text (it uses the sessionStart payload's own initialPrompt field instead, which
746
+ // is present per GitHub's schema but not verified live as of this comment).
747
+
717
748
  // Codex-only variant of the Stop reminder. Confirmed live (2026-08-08, via a real `codex
718
749
  // exec` run) that Codex's Stop hook specifically requires a { systemMessage: "..." } JSON shape
719
750
  // on stdout - plain text makes Codex report the hook as failed. Distinct from Claude's own JSON
720
- // shape (hookSpecificOutput.additionalContext, see CLAUDE_HOOK_STOP_SOURCE below) and Copilot's
721
- // (decision/reason, see COPILOT_HOOK_STOP_SOURCE) - each host has its own schema here.
751
+ // shape (hookSpecificOutput.additionalContext, see CLAUDE_HOOK_STOP_SOURCE below). Copilot has no
752
+ // Stop hook at all (removed 2026-09-18, see the comment above CLAUDE_HOOK_STOP_SOURCE).
722
753
  const CODEX_HOOK_STOP_SOURCE = `#!/usr/bin/env node
723
754
  'use strict';
724
755
 
@@ -776,82 +807,20 @@ try {
776
807
  }
777
808
  `;
778
809
 
779
- // Copilot-only variant of the Stop reminder above. Per GitHub's own hooks reference (fetched
780
- // 2026-08-08): agentStop's output schema is { decision: "block"|"allow", reason, modifiedResponse }
781
- // - there is no plain message field, so surfacing a reminder means returning decision:"block"
782
- // with reason as the forced-continuation prompt text. Confirmed this has the same loop-prevention
783
- // contract Claude Code uses: the input payload includes stop_hook_active ("true when this turn
784
- // was already forced to continue by a prior 'block' decision" - same semantics, same field name
785
- // as Claude's), and Copilot has its own hard backstop besides ("After 8 consecutive block
786
- // continuations, the CLI overrides the hook and ends the turn anyway"). Checking stop_hook_active
787
- // here means this hook only ever forces one continuation, never chains into that backstop.
788
- const COPILOT_HOOK_STOP_SOURCE = `#!/usr/bin/env node
789
- 'use strict';
790
-
791
- var fs = require('fs');
792
- var os = require('os');
793
- var path = require('path');
794
-
795
- var LOG_PATH = path.join(os.homedir(), '.empirical', 'hooks', 'stop.log');
796
- var MAX_LOG_BYTES = 200 * 1024;
797
-
798
- function appendLog(line) {
799
- try {
800
- fs.mkdirSync(path.dirname(LOG_PATH), { recursive: true });
801
- fs.appendFileSync(LOG_PATH, '[' + new Date().toISOString() + '] ' + line + '\\n');
802
- var stat = fs.statSync(LOG_PATH);
803
- if (stat.size > MAX_LOG_BYTES) {
804
- var tail = fs.readFileSync(LOG_PATH, 'utf8').slice(-MAX_LOG_BYTES / 2);
805
- fs.writeFileSync(LOG_PATH, tail);
806
- }
807
- } catch (e) {
808
- // Diagnostics must never break the hook.
809
- }
810
- }
811
-
812
- function readStdinPayload() {
813
- try {
814
- var raw = fs.readFileSync(0, 'utf8');
815
- return raw ? JSON.parse(raw) : {};
816
- } catch (e) {
817
- return {};
818
- }
819
- }
820
-
821
- function main() {
822
- var payload = readStdinPayload();
823
-
824
- if (payload && payload.stop_hook_active === true) {
825
- appendLog('stop_hook_active=true, skipping reminder (already forced one continuation this turn)');
826
- console.log('{}');
827
- process.exit(0);
828
- }
829
-
830
- appendLog('stop hook fired (pid=' + process.pid + ', platform=' + process.platform + '), forcing one continuation to surface reminder');
831
-
832
- console.log(JSON.stringify({
833
- decision: 'block',
834
- reason: '[Empirical reminder] Before finishing: if this turn included a completed decision, a corrected mistake, or meaningful work worth remembering, record it now, one memory per finding (a fix, a root cause, a decision, a lesson), via the empirical-memory, empirical-work-history, or empirical-scar-tissue skill or \`empirical memory record\`. Local notes are not recorded. Do not wait to be asked.'
835
- }));
836
- process.exit(0);
837
- }
838
-
839
- try {
840
- main();
841
- } catch (e) {
842
- try { appendLog('uncaught error in main(): ' + (e && e.message)); } catch (_e) { /* best effort */ }
843
- console.log('{}');
844
- process.exit(0);
845
- }
846
- `;
810
+ // Copilot's Stop reminder (agentStop) was removed entirely 2026-09-18 - see installCopilotHooks's
811
+ // comment in this file for why (agentStop only supports decision:"block"|"allow" with no passive
812
+ // field, so any reminder there forces a disruptive extra visible turn; no per-session cap on that
813
+ // avoids the tradeoff between nagging every turn and going silent right before something worth
814
+ // recording happens - confirmed live 2026-09-17/18 causing both failure modes in turn). Copilot
815
+ // relies entirely on copilot-instructions.md instead (see coreMemoryBlock.js's Copilot addendum).
847
816
 
848
817
  // Claude-only variant of the Stop reminder above. hookSpecificOutput.additionalContext (stdout,
849
818
  // exit 0) is Claude Code's non-blocking Stop-hook feedback channel: the text is fed into context
850
819
  // and the conversation continues, same outcome the earlier exit-2 approach achieved, but the
851
820
  // transcript UI renders it as a plain system reminder instead of a red "Stop hook error" line.
852
- // Codex needs its own systemMessage shape (CODEX_HOOK_STOP_SOURCE) and Copilot its own
853
- // decision/reason shape (COPILOT_HOOK_STOP_SOURCE) - plain console.log output (what this hook
854
- // used before the additionalContext fix) never reaches the model on any of the three hosts.
821
+ // Codex needs its own systemMessage shape (CODEX_HOOK_STOP_SOURCE) - plain console.log output
822
+ // (what this hook used before the additionalContext fix) never reaches the model. Copilot has no
823
+ // Stop hook at all (removed 2026-09-18, see the comment just above this one).
855
824
  const CLAUDE_HOOK_STOP_SOURCE = `#!/usr/bin/env node
856
825
  'use strict';
857
826
 
@@ -929,17 +898,19 @@ try {
929
898
  // The message itself is generated per-install from the actual plugin set being installed, not one
930
899
  // fixed string for everyone - e.g. someone who declined empirical-scar-tissue at install time
931
900
  // never sees a scar-tissue reminder, since that capability isn't actually available to them.
901
+ //
902
+ // Claude and Codex only - Copilot has no UserPromptSubmit reminder at all (removed 2026-09-18,
903
+ // see installCopilotHooks's comment in this file for why: its only output field is a full prompt
904
+ // replacement, not a side channel, so any injected reminder here reads as fake, model-authored
905
+ // text inside what looks like the user's own message).
932
906
  function buildPromptReminderMessage(pluginNames, platform) {
933
907
  const names = normalizePluginNames(pluginNames);
908
+
934
909
  const lines = [
935
910
  'Before responding: if this task needs prior context (research, debugging, a spec or status update, a handoff), query Empirical memory first via the empirical-memory skill or empirical memory query - do not assume you already have enough context.'
936
911
  ];
937
912
  if (names.includes('empirical-work-history')) {
938
913
  lines.push('If you are picking up previously completed work, check for a recent work-history entry before assuming there is none.');
939
- // Codex Stop runs after its final response has already been emitted, so it can only be an
940
- // advisory reminder. Put the completion gate into the pre-response context instead, where
941
- // Codex can still act on it. Keep this Codex-specific: Claude and Copilot have their own
942
- // stop-hook control flows and should not receive a changed prompt contract here.
943
914
  if (platform === 'codex') {
944
915
  lines.push('If this turn results in meaningful completed work, BEFORE sending your final response you MUST use the empirical-work-history skill or record it via Empirical. The entry must carry tag work-history and mass 1; verify the create receipt. CLI fallback must use --json-file with data.session and data.git; do not use the shorthand record command for coding work. Do not wait for the Stop hook.');
945
916
  }
@@ -960,52 +931,19 @@ function buildPromptReminderMessage(pluginNames, platform) {
960
931
  // `platform`:
961
932
  // - 'codex': Codex's required hookSpecificOutput.additionalContext JSON shape (confirmed live
962
933
  // 2026-08-08 via a real `codex exec` run - plain text made Codex report the hook as failed).
963
- // - 'copilot': Copilot's userPromptSubmitted event has NO additionalContext-equivalent field at
964
- // all (confirmed via GitHub's own hooks reference, fetched 2026-08-08) - the only output field
965
- // is modifiedPrompt, which REPLACES the prompt for the rest of the turn. So this reads the
966
- // real prompt text from stdin's `prompt` field and returns it with the reminder appended -
967
- // more invasive than Claude/Codex's side-channel injection (it's now part of the visible
968
- // prompt), but it's the only mechanism this event actually has. Falls back to {} (leave
969
- // unchanged) if stdin can't be read/parsed or has no prompt field, so a parsing edge case can
970
- // never corrupt or blank out the user's actual prompt.
971
- // - default (Claude/Copilot's plain console.log was the old assumption for both - WRONG for
972
- // Copilot, which requires valid JSON on stdout for every event, confirmed via the same fetch
973
- // above): plain console.log, confirmed correct for Claude only.
934
+ // - default ('claude'): plain console.log.
935
+ // Copilot has no variant here at all (removed 2026-09-18) - see installCopilotHooks's comment.
974
936
  function buildUserPromptSubmitHookSource(pluginNames, platform) {
975
937
  const messageLiteral = JSON.stringify(buildPromptReminderMessage(pluginNames, platform));
976
938
 
977
- let stdinHelper = '';
978
- let outputStatement;
979
- if (platform === 'codex') {
980
- outputStatement = `console.log(JSON.stringify({
939
+ const outputStatement = platform === 'codex'
940
+ ? `console.log(JSON.stringify({
981
941
  hookSpecificOutput: {
982
942
  hookEventName: 'UserPromptSubmit',
983
943
  additionalContext: ${messageLiteral}
984
944
  }
985
- }));`;
986
- } else if (platform === 'copilot') {
987
- stdinHelper = `
988
- function readStdinPayload() {
989
- try {
990
- var raw = fs.readFileSync(0, 'utf8');
991
- return raw ? JSON.parse(raw) : {};
992
- } catch (e) {
993
- return {};
994
- }
995
- }
996
- `;
997
- outputStatement = `var payload = readStdinPayload();
998
- if (payload && typeof payload.prompt === 'string' && payload.prompt.length > 0) {
999
- console.log(JSON.stringify({
1000
- modifiedPrompt: payload.prompt + '\\n\\n' + ${messageLiteral}
1001
- }));
1002
- } else {
1003
- appendLog('no usable prompt field on stdin payload, leaving prompt unchanged');
1004
- console.log('{}');
1005
- }`;
1006
- } else {
1007
- outputStatement = `console.log(${messageLiteral});`;
1008
- }
945
+ }));`
946
+ : `console.log(${messageLiteral});`;
1009
947
 
1010
948
  return `#!/usr/bin/env node
1011
949
  'use strict';
@@ -1030,7 +968,7 @@ function appendLog(line) {
1030
968
  // Diagnostics must never break the hook.
1031
969
  }
1032
970
  }
1033
- ${stdinHelper}
971
+
1034
972
  // A crash here must degrade to a silent no-op, never a visible "hook failed" error in the host CLI.
1035
973
  try {
1036
974
  appendLog('user-prompt-submit hook fired (pid=' + process.pid + ', platform=' + process.platform + ')');
@@ -1268,19 +1206,45 @@ function installCodexHooks(pluginNames) {
1268
1206
  // GitHub Copilot CLI's hook config is one JSON file per hook under ~/.copilot/hooks/ (any
1269
1207
  // filename — confirmed via GitHub's own docs at docs.github.com/en/copilot/reference/hooks-
1270
1208
  // reference), so unlike Claude/Codex this never needs to merge into a shared file: Empirical
1271
- // owns this file outright and can just overwrite it. Event key for turn-end is "agentStop"
1272
- // ("the main agent finishes a turn" per the same docs) — not "stop"/"Stop" like Claude/Codex.
1273
- // Schema: { version: 1, hooks: { sessionStart: [...], agentStop: [...] } }.
1274
- function installCopilotHooks(pluginNames) {
1209
+ // owns this file outright and can just overwrite it.
1210
+ //
1211
+ // SessionStart only (2026-09-18): userPromptSubmitted and agentStop reminder hooks were removed
1212
+ // for Copilot entirely, not just capped. Both were fundamentally fake compared to Claude/Codex's
1213
+ // versions: userPromptSubmitted's only output field is modifiedPrompt (a full prompt replacement,
1214
+ // not a side-channel), so its "reminder" was really text injected into what looks like the user's
1215
+ // own message; agentStop only supports decision:"block"|"allow" with no passive/silent field, so
1216
+ // any reminder there forces a disruptive extra visible turn, and no per-session cap on that can
1217
+ // avoid the tradeoff between nagging every turn and going silent right before something worth
1218
+ // recording happens (confirmed live 2026-09-17/18: both caused real, reproducible bugs - a
1219
+ // prompt-blanking regression and an unstoppable "record your work" loop). Copilot instead relies
1220
+ // entirely on the identity-framed instructions block coreMemoryBlock.js writes into
1221
+ // copilot-instructions.md, same mechanism Claude/Codex have as a backstop under their working
1222
+ // hooks - see that file's Copilot-only addendum for the stronger, more directive wording this
1223
+ // puts on Copilot specifically to compensate for having no hook nudge at all.
1224
+ function installCopilotHooks(_pluginNames) {
1275
1225
  const hooksDir = getEmpiricalHooksDir();
1276
1226
  ensureDir(hooksDir);
1277
1227
 
1278
1228
  const sessionStartScriptPath = path.join(hooksDir, 'copilot-session-start.js');
1279
- const stopScriptPath = path.join(hooksDir, 'copilot-stop.js');
1280
- const userPromptScriptPath = path.join(hooksDir, 'copilot-user-prompt-submit.js');
1281
1229
  writeFile(sessionStartScriptPath, COPILOT_HOOK_SESSION_START_SOURCE);
1282
- writeFile(stopScriptPath, COPILOT_HOOK_STOP_SOURCE);
1283
- writeFile(userPromptScriptPath, buildUserPromptSubmitHookSource(pluginNames, 'copilot'));
1230
+
1231
+ // Clean up scripts/state files from every removed Copilot hook (agentStop's Stop reminder, the
1232
+ // visible-modifiedPrompt UserPromptSubmit real-recall hook it was briefly replaced with) so an
1233
+ // upgrade never leaves orphaned, unregistered files behind.
1234
+ const fs = require('fs');
1235
+ const staleFiles = [
1236
+ path.join(hooksDir, 'copilot-stop.js'),
1237
+ path.join(hooksDir, 'stop-state.json'),
1238
+ path.join(hooksDir, 'copilot-user-prompt-submit.js'),
1239
+ path.join(hooksDir, 'prompt-state.json')
1240
+ ];
1241
+ for (const stalePath of staleFiles) {
1242
+ try {
1243
+ if (fs.existsSync(stalePath)) fs.unlinkSync(stalePath);
1244
+ } catch (_error) {
1245
+ // best-effort cleanup only
1246
+ }
1247
+ }
1284
1248
 
1285
1249
  const copilotHooksDir = path.join(os.homedir(), '.copilot', 'hooks');
1286
1250
  ensureDir(copilotHooksDir);
@@ -1290,7 +1254,6 @@ function installCopilotHooks(pluginNames) {
1290
1254
  // never lingers alongside the new one.
1291
1255
  const legacyHookFilePath = path.join(copilotHooksDir, 'empirical-session-start.json');
1292
1256
  try {
1293
- const fs = require('fs');
1294
1257
  if (fs.existsSync(legacyHookFilePath)) fs.unlinkSync(legacyHookFilePath);
1295
1258
  } catch (_error) {
1296
1259
  // best-effort cleanup only
@@ -1301,6 +1264,13 @@ function installCopilotHooks(pluginNames) {
1301
1264
  // PowerShell requires - see buildPowershellHookCommand) are always set: per GitHub's hooks
1302
1265
  // reference, an explicit `powershell` entry takes precedence over `command` on Windows, so this
1303
1266
  // is correct on both platforms without needing to detect the install-time OS here.
1267
+ //
1268
+ // sessionStart only (2026-09-18): no agentStop (recording stays instructions-only, see
1269
+ // coreMemoryBlock.js's Copilot addendum) and no userPromptSubmitted (its only output field,
1270
+ // modifiedPrompt, is rendered back as the user's own visible message - confirmed live, not
1271
+ // acceptable for silent recall). sessionStart's additionalContext delivers the same real query
1272
+ // invisibly instead, once per real session boundary (not an artificial timer), using that
1273
+ // event's own initialPrompt field as the query when present.
1304
1274
  const hookFileContent = {
1305
1275
  version: 1,
1306
1276
  hooks: {
@@ -1311,30 +1281,12 @@ function installCopilotHooks(pluginNames) {
1311
1281
  powershell: buildPowershellHookCommand(sessionStartScriptPath),
1312
1282
  timeoutSec: 15
1313
1283
  }
1314
- ],
1315
- // "userPromptSubmitted" per GitHub's hooks reference ("The user submits a prompt") - not
1316
- // "userPromptSubmit" (that camelCase spelling is Claude/Codex's convention, not Copilot's).
1317
- userPromptSubmitted: [
1318
- {
1319
- type: 'command',
1320
- command: buildHookCommand(userPromptScriptPath),
1321
- powershell: buildPowershellHookCommand(userPromptScriptPath),
1322
- timeoutSec: 10
1323
- }
1324
- ],
1325
- agentStop: [
1326
- {
1327
- type: 'command',
1328
- command: buildHookCommand(stopScriptPath),
1329
- powershell: buildPowershellHookCommand(stopScriptPath),
1330
- timeoutSec: 10
1331
- }
1332
1284
  ]
1333
1285
  }
1334
1286
  };
1335
1287
 
1336
1288
  writeFile(hookFilePath, `${JSON.stringify(hookFileContent, null, 2)}\n`);
1337
- log(' - Copilot SessionStart + userPromptSubmitted + agentStop hooks installed for deterministic memory lookups and reminders.');
1289
+ log(' - Copilot SessionStart hook installed (real recall via additionalContext). No UserPromptSubmit/agentStop reminders - see copilot-instructions.md instead.');
1338
1290
  }
1339
1291
 
1340
1292
  module.exports = {
@@ -1342,7 +1294,6 @@ module.exports = {
1342
1294
  CODEX_HOOK_SESSION_START_SOURCE,
1343
1295
  COPILOT_HOOK_SESSION_START_SOURCE,
1344
1296
  CODEX_HOOK_STOP_SOURCE,
1345
- COPILOT_HOOK_STOP_SOURCE,
1346
1297
  CLAUDE_HOOK_STOP_SOURCE,
1347
1298
  buildPromptReminderMessage,
1348
1299
  buildUserPromptSubmitHookSource,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empirical-gauzza/cli",
3
- "version": "1.0.33",
3
+ "version": "1.0.34",
4
4
  "description": "Persistent cross-tool memory for AI agents. Give Claude Code, Codex, Copilot, Cursor, and ChatGPT one shared memory: save project context once, recall it everywhere via CLI or MCP.",
5
5
  "keywords": [
6
6
  "memory",