@empirical-gauzza/cli 1.0.32 → 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/alertBanner.js +11 -1
- package/lib/doctor.js +17 -6
- package/lib/installers/coreMemoryBlock.js +15 -3
- package/lib/installers/hooks.js +102 -151
- package/package.json +1 -1
package/lib/alertBanner.js
CHANGED
|
@@ -23,6 +23,16 @@ function colorize(code, text) {
|
|
|
23
23
|
return `${code}${text}${ANSI.reset}`;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// Due times are shown in this terminal's own time zone, never the server's UTC sentence.
|
|
27
|
+
function localDueLine(alert) {
|
|
28
|
+
if (!alert || !alert.dueAt) return alert ? alert.body : '';
|
|
29
|
+
const d = new Date(alert.dueAt);
|
|
30
|
+
if (Number.isNaN(d.getTime())) return alert.body;
|
|
31
|
+
const when = d.toLocaleString(undefined, { weekday: 'short', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', timeZoneName: 'short' });
|
|
32
|
+
const rest = String(alert.body || '').replace(/^Due [^\n]*? UTC\.\s*/, '');
|
|
33
|
+
return `Due ${when}.${rest ? ` ${rest}` : ''}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
function reminderMemoryId(alert) {
|
|
27
37
|
const key = String(alert && alert.alertKey || '');
|
|
28
38
|
return key.startsWith('reminder_') ? key.slice('reminder_'.length) : null;
|
|
@@ -37,7 +47,7 @@ function renderOneAlert(alert) {
|
|
|
37
47
|
// A reminder the user set. Whoever reads this, person or agent, should surface it first;
|
|
38
48
|
// the agent case was observed being skipped when the banner read like a footnote.
|
|
39
49
|
lines.push(colorize(`${ANSI.bold}${color}`, `REMINDER DUE - tell the user now: ${alert.title.replace(/^Reminder:\s*/, '')}`));
|
|
40
|
-
lines.push(` ${alert
|
|
50
|
+
lines.push(` ${localDueLine(alert)}`);
|
|
41
51
|
for (const action of alert.actions || []) {
|
|
42
52
|
lines.push(` → ${action.label}: ${action.href}`);
|
|
43
53
|
}
|
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'
|
|
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'
|
|
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
|
-
|
|
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;
|
package/lib/installers/hooks.js
CHANGED
|
@@ -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
|
-
//
|
|
703
|
-
|
|
704
|
-
|
|
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)
|
|
721
|
-
//
|
|
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
|
|
780
|
-
//
|
|
781
|
-
//
|
|
782
|
-
//
|
|
783
|
-
//
|
|
784
|
-
//
|
|
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)
|
|
853
|
-
//
|
|
854
|
-
//
|
|
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
|
-
// - '
|
|
964
|
-
//
|
|
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
|
-
|
|
978
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
1272
|
-
//
|
|
1273
|
-
//
|
|
1274
|
-
|
|
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
|
-
|
|
1283
|
-
|
|
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
|
|
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.
|
|
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",
|