greprag 5.49.6 → 5.49.9
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/dist/commands/collision-reminder.d.ts +14 -0
- package/dist/commands/collision-reminder.js +40 -0
- package/dist/commands/collision-reminder.js.map +1 -0
- package/dist/commands/coordinate-gate.d.ts +12 -6
- package/dist/commands/coordinate-gate.js +24 -10
- package/dist/commands/coordinate-gate.js.map +1 -1
- package/dist/commands/corpus/client.d.ts +5 -1
- package/dist/commands/corpus/client.js +10 -3
- package/dist/commands/corpus/client.js.map +1 -1
- package/dist/commands/corpus/index.js +21 -1
- package/dist/commands/corpus/index.js.map +1 -1
- package/dist/commands/corpus/manage.js +31 -5
- package/dist/commands/corpus/manage.js.map +1 -1
- package/dist/commands/corpus/search.js +13 -6
- package/dist/commands/corpus/search.js.map +1 -1
- package/dist/commands/corpus/status.js +2 -1
- package/dist/commands/corpus/status.js.map +1 -1
- package/dist/commands/corpus/tags.d.ts +17 -0
- package/dist/commands/corpus/tags.js +91 -0
- package/dist/commands/corpus/tags.js.map +1 -0
- package/dist/commands/corpus-reminder.d.ts +15 -0
- package/dist/commands/corpus-reminder.js +34 -0
- package/dist/commands/corpus-reminder.js.map +1 -0
- package/dist/commands/friction-reminder.d.ts +22 -44
- package/dist/commands/friction-reminder.js +45 -63
- package/dist/commands/friction-reminder.js.map +1 -1
- package/dist/commands/init.js +7 -14
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/load-primer-reminder.d.ts +28 -0
- package/dist/commands/load-primer-reminder.js +51 -0
- package/dist/commands/load-primer-reminder.js.map +1 -0
- package/dist/commands/load.d.ts +15 -0
- package/dist/commands/load.js +112 -0
- package/dist/commands/load.js.map +1 -0
- package/dist/commands/memory-reflex.d.ts +17 -112
- package/dist/commands/memory-reflex.js +24 -233
- package/dist/commands/memory-reflex.js.map +1 -1
- package/dist/commands/reminder-registry.d.ts +12 -1
- package/dist/commands/reminder-registry.js +48 -4
- package/dist/commands/reminder-registry.js.map +1 -1
- package/dist/commands/reminder-types.d.ts +33 -0
- package/dist/commands/version-reminder.d.ts +15 -0
- package/dist/commands/version-reminder.js +34 -0
- package/dist/commands/version-reminder.js.map +1 -0
- package/dist/hook.js +134 -274
- package/dist/hook.js.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skill/templates/chip-leader.md +188 -0
- package/skill/templates/chip-spawn.md +4 -4
package/dist/hook.js
CHANGED
|
@@ -78,7 +78,6 @@ const guard_1 = require("./guard");
|
|
|
78
78
|
const reminder_registry_1 = require("./commands/reminder-registry");
|
|
79
79
|
const inbox_primer_reminder_1 = require("./commands/inbox-primer-reminder");
|
|
80
80
|
const friction_reminder_1 = require("./commands/friction-reminder");
|
|
81
|
-
const memory_reflex_1 = require("./commands/memory-reflex");
|
|
82
81
|
const worktree_state_1 = require("./worktree-state");
|
|
83
82
|
// Ingress-trigger bridge, Adapter A (LOCAL/state half) — the Stop hook computes
|
|
84
83
|
// the deterministic state values (stress / turnCount / keyword-seen) from the
|
|
@@ -786,37 +785,7 @@ async function store(input, source = 'claude-code') {
|
|
|
786
785
|
if (source === 'codex') {
|
|
787
786
|
turn.toolCalls.push(...(0, codex_hook_events_1.readCodexSubagentToolCalls)(input));
|
|
788
787
|
}
|
|
789
|
-
// Memory-reflex
|
|
790
|
-
// this turn: (1) score the FREE overlap proxy locally → bump the `used` numerator, and (2)
|
|
791
|
-
// carry the injection onto the turn POST so the server-side Flash-Lite judge (Chip A) can
|
|
792
|
-
// score it accurately. Consume-once: the stash is always cleared, so a turn with no injection
|
|
793
|
-
// never reuses a stale sample. adr: adr/memory-reflex.md
|
|
794
|
-
let memoryInjectionForStore = null;
|
|
795
|
-
try {
|
|
796
|
-
const effShort = (0, session_id_1.truncateSessionId)(input.session_id);
|
|
797
|
-
if (effShort) {
|
|
798
|
-
const pending = consumeMemoryInjection(effShort);
|
|
799
|
-
if (pending) {
|
|
800
|
-
memoryInjectionForStore = pending.hitText;
|
|
801
|
-
if ((0, memory_reflex_1.injectionWasUsed)(pending.hitText, turn.userPrompt, turn.agentResponse)) {
|
|
802
|
-
recordMemoryUsed(pending.projectId);
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
catch { /* efficacy capture is best-effort — never block the turn */ }
|
|
808
|
-
// Self-report scan (P1b, adr/memory-reflex.md). If the agent emitted a [[recall: X]] marker
|
|
809
|
-
// (taught by the memory announce) it's flagging its OWN context gap → stash the topic so the
|
|
810
|
-
// NEXT turn's notify pulls + injects it (the self-report trigger). Best-effort.
|
|
811
|
-
try {
|
|
812
|
-
const srShort = (0, session_id_1.truncateSessionId)(input.session_id);
|
|
813
|
-
if (srShort && anchor.projectId) {
|
|
814
|
-
const topic = (0, memory_reflex_1.extractRecallMarker)(turn.agentResponse);
|
|
815
|
-
if (topic)
|
|
816
|
-
stashSelfReport(srShort, anchor.projectId, topic);
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
catch { /* best-effort — never block the turn */ }
|
|
788
|
+
// (Memory-reflex efficacy capture removed 2026-06-22 with the auto-inject it scored.)
|
|
820
789
|
// Empty turn — nothing to capture.
|
|
821
790
|
if (!turn.userPrompt && !turn.agentResponse && turn.toolCalls.length === 0)
|
|
822
791
|
return;
|
|
@@ -853,12 +822,6 @@ async function store(input, source = 'claude-code') {
|
|
|
853
822
|
status: turn.status,
|
|
854
823
|
userPrompt,
|
|
855
824
|
agentResponse,
|
|
856
|
-
// The memory injection surfaced this turn (when any) — the SEAM the Flash-Lite efficacy
|
|
857
|
-
// judge (Chip A) consumes server-side at ingest to score used÷injected. Scrubbed + capped
|
|
858
|
-
// like the other text fields. Omitted entirely on the common no-injection turn.
|
|
859
|
-
...(memoryInjectionForStore
|
|
860
|
-
? { memoryInjection: capField((0, secret_scrubber_1.scrubString)(memoryInjectionForStore, redaction)) }
|
|
861
|
-
: {}),
|
|
862
825
|
toolCalls,
|
|
863
826
|
filesTouched: turn.filesTouched,
|
|
864
827
|
artifacts: artifactRefs,
|
|
@@ -938,6 +901,24 @@ async function fetchFrontDeskUnread(apiUrl, apiKey) {
|
|
|
938
901
|
return 0;
|
|
939
902
|
}
|
|
940
903
|
}
|
|
904
|
+
/** Fetch the names of api-docs-tagged corpus stores (the corpus-announce signal).
|
|
905
|
+
* Lists `/v1/stores?tag=api-docs` and returns the store names so the announce can
|
|
906
|
+
* name the on-tap reference banks. Best-effort: [] on any error → the announce
|
|
907
|
+
* stays silent. adr: adr/corpus-tags.md */
|
|
908
|
+
async function fetchCorpusApiDocs(apiUrl, apiKey) {
|
|
909
|
+
try {
|
|
910
|
+
const res = await fetch(`${apiUrl}/v1/stores?tag=api-docs`, {
|
|
911
|
+
headers: { 'Authorization': `Bearer ${apiKey}` },
|
|
912
|
+
});
|
|
913
|
+
if (!res.ok)
|
|
914
|
+
return [];
|
|
915
|
+
const data = await res.json();
|
|
916
|
+
return (data.stores || []).map(s => s.name || '').filter(Boolean);
|
|
917
|
+
}
|
|
918
|
+
catch {
|
|
919
|
+
return [];
|
|
920
|
+
}
|
|
921
|
+
}
|
|
941
922
|
/** Fetch this session's OWN unread count — messages addressed to `to_session_id =
|
|
942
923
|
* <short>` (mail a peer or the operator sent to THIS session). Safe + non-eavesdrop:
|
|
943
924
|
* reads only this session's mail, NOT the project-wide count the v5.6.1 removal retired
|
|
@@ -957,44 +938,6 @@ async function fetchSessionUnread(apiUrl, apiKey, short) {
|
|
|
957
938
|
return 0;
|
|
958
939
|
}
|
|
959
940
|
}
|
|
960
|
-
/** Fetch top episodic-memory hits for a query (the memory-reflex inline path), project-
|
|
961
|
-
* scoped, with a HARD timeout so a slow search never blocks the turn. POSTs the same
|
|
962
|
-
* `/v1/memory/query` the CLI `memory search` uses, with the de-pollution `session-turn`
|
|
963
|
-
* provenance filter. Returns [] on any error/abort → the reflex stays silent. The
|
|
964
|
-
* confidence gate + framing live in buildMemoryInjection (commands/memory-reflex.ts).
|
|
965
|
-
* adr: adr/memory-reflex.md */
|
|
966
|
-
async function fetchMemoryHits(apiUrl, apiKey, projectId, query, limit = 3, timeoutMs = 2000) {
|
|
967
|
-
const ctl = new AbortController();
|
|
968
|
-
const timer = setTimeout(() => ctl.abort(), timeoutMs);
|
|
969
|
-
try {
|
|
970
|
-
const res = await fetch(`${apiUrl}/v1/memory/query`, {
|
|
971
|
-
method: 'POST',
|
|
972
|
-
headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
|
|
973
|
-
body: JSON.stringify({ query, limit, projectId, filters: { provenance: 'session-turn' } }),
|
|
974
|
-
signal: ctl.signal,
|
|
975
|
-
});
|
|
976
|
-
if (!res.ok)
|
|
977
|
-
return { hits: [], quiet: false };
|
|
978
|
-
const data = await res.json();
|
|
979
|
-
// The server's Flash-Lite efficacy verdict rides the same response (adr/memory-reflex.md).
|
|
980
|
-
const quiet = data.memoryReflexQuiet === true;
|
|
981
|
-
if (!data.ok || !Array.isArray(data.nodes))
|
|
982
|
-
return { hits: [], quiet };
|
|
983
|
-
return {
|
|
984
|
-
hits: data.nodes.map(n => ({
|
|
985
|
-
content: n.content, score: n.score, confidence: n.confidence ?? null,
|
|
986
|
-
shape: n.shape ?? null, createdAt: n.createdAt ?? null, projectName: n.projectName ?? null,
|
|
987
|
-
})),
|
|
988
|
-
quiet,
|
|
989
|
-
};
|
|
990
|
-
}
|
|
991
|
-
catch {
|
|
992
|
-
return { hits: [], quiet: false };
|
|
993
|
-
}
|
|
994
|
-
finally {
|
|
995
|
-
clearTimeout(timer);
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
941
|
/** Identity-drift warning rate limiter.
|
|
999
942
|
*
|
|
1000
943
|
* The drift warning ("stored ID ≠ git-derived ID, run `greprag doctor`") used
|
|
@@ -1069,6 +1012,78 @@ function writeRecapOutput(text, mode) {
|
|
|
1069
1012
|
}
|
|
1070
1013
|
process.stdout.write(text);
|
|
1071
1014
|
}
|
|
1015
|
+
/** This CLI's installed version (from the bundled package.json). Null if unreadable. */
|
|
1016
|
+
function installedVersion() {
|
|
1017
|
+
try {
|
|
1018
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
1019
|
+
return typeof pkg.version === 'string' ? pkg.version : null;
|
|
1020
|
+
}
|
|
1021
|
+
catch {
|
|
1022
|
+
return null;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
/** Is semver `b` strictly greater than `a`? Plain numeric major.minor.patch compare
|
|
1026
|
+
* (greprag never ships pre-release tags). Non-numeric segments collapse to 0. */
|
|
1027
|
+
function isNewer(a, b) {
|
|
1028
|
+
const pa = a.split('.').map(n => parseInt(n, 10) || 0);
|
|
1029
|
+
const pb = b.split('.').map(n => parseInt(n, 10) || 0);
|
|
1030
|
+
for (let i = 0; i < 3; i++) {
|
|
1031
|
+
if ((pb[i] || 0) > (pa[i] || 0))
|
|
1032
|
+
return true;
|
|
1033
|
+
if ((pb[i] || 0) < (pa[i] || 0))
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
return false;
|
|
1037
|
+
}
|
|
1038
|
+
const VERSION_CHECK_TTL_MS = 24 * 60 * 60 * 1000; // once a day
|
|
1039
|
+
function versionCheckPath() {
|
|
1040
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
1041
|
+
return path.join(home, '.greprag', 'state', 'version-check.json');
|
|
1042
|
+
}
|
|
1043
|
+
/** The Deficiency-gated upgrade signal (version-reminder.ts). Returns {current, latest}
|
|
1044
|
+
* when the installed CLI is behind the latest published version, else null. The npm-registry
|
|
1045
|
+
* read is cached daily (a slow/offline check never blocks SessionStart — stale cache or no
|
|
1046
|
+
* cache → no announce, fail-quiet). */
|
|
1047
|
+
async function checkForUpdate() {
|
|
1048
|
+
const current = installedVersion();
|
|
1049
|
+
if (!current)
|
|
1050
|
+
return null;
|
|
1051
|
+
let latest = null;
|
|
1052
|
+
try {
|
|
1053
|
+
const raw = fs.readFileSync(versionCheckPath(), 'utf-8');
|
|
1054
|
+
const c = JSON.parse(raw);
|
|
1055
|
+
const age = c.checkedAt ? Date.now() - Date.parse(c.checkedAt) : Infinity;
|
|
1056
|
+
if (typeof c.latest === 'string' && Number.isFinite(age) && age < VERSION_CHECK_TTL_MS) {
|
|
1057
|
+
latest = c.latest; // fresh cache hit
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
catch { /* no/bad cache — refetch */ }
|
|
1061
|
+
if (!latest) {
|
|
1062
|
+
try {
|
|
1063
|
+
const ctl = new AbortController();
|
|
1064
|
+
const timer = setTimeout(() => ctl.abort(), 1500);
|
|
1065
|
+
const res = await fetch('https://registry.npmjs.org/greprag/latest', { signal: ctl.signal });
|
|
1066
|
+
clearTimeout(timer);
|
|
1067
|
+
if (res.ok) {
|
|
1068
|
+
const data = await res.json();
|
|
1069
|
+
if (typeof data.version === 'string') {
|
|
1070
|
+
latest = data.version;
|
|
1071
|
+
try {
|
|
1072
|
+
const file = versionCheckPath();
|
|
1073
|
+
if (!fs.existsSync(path.dirname(file)))
|
|
1074
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
1075
|
+
fs.writeFileSync(file, JSON.stringify({ latest, checkedAt: new Date().toISOString() }) + '\n');
|
|
1076
|
+
}
|
|
1077
|
+
catch { /* cache write best-effort */ }
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
catch { /* offline / slow → no announce this session */ }
|
|
1082
|
+
}
|
|
1083
|
+
if (latest && isNewer(current, latest))
|
|
1084
|
+
return { current, latest };
|
|
1085
|
+
return null;
|
|
1086
|
+
}
|
|
1072
1087
|
/** SessionStart — fetch recent episodic activity for this project and print
|
|
1073
1088
|
* to stdout. Claude Code injects the printed text as session context. Codex
|
|
1074
1089
|
* uses the same content via `hookSpecificOutput.additionalContext`. Fires
|
|
@@ -1177,12 +1192,19 @@ async function recap(input, mode = 'plain') {
|
|
|
1177
1192
|
// by the front-desk module's announce. NOT the v5.6.1 eavesdrop vector — see
|
|
1178
1193
|
// fetchFrontDeskUnread. adr: adr/address-grammar.md (2026-06-10), docs/front-desk-switchboard.md §5
|
|
1179
1194
|
const frontDeskUnread = await fetchFrontDeskUnread(cfg.apiUrl, cfg.apiKey);
|
|
1195
|
+
// Corpus-announce signal — the names of the api-docs-tagged reference banks on
|
|
1196
|
+
// tap, so the corpus module can tell the agent to search them before answering
|
|
1197
|
+
// from training data. Best-effort; empty → the announce stays silent.
|
|
1198
|
+
const corpusApiDocs = await fetchCorpusApiDocs(cfg.apiUrl, cfg.apiKey);
|
|
1180
1199
|
// Assistant doctrine auto-load — fires ONLY for the flagged assistant project
|
|
1181
1200
|
// (isAssistantProject), so a normal project sees zero change. The hook owns the
|
|
1182
1201
|
// doctrine-file read; the assistant-doctrine module routes the text. adr: adr/assistant-role.md
|
|
1183
1202
|
const assistantDoctrine = (0, project_anchor_1.isAssistantProject)(anchor)
|
|
1184
1203
|
? (0, assistant_doctrine_1.buildAssistantDoctrineContext)(cwd)
|
|
1185
1204
|
: null;
|
|
1205
|
+
// Deficiency-gated upgrade announce: daily-cached npm check → tell the user to
|
|
1206
|
+
// upgrade when behind. This is how a release actually propagates to everyone.
|
|
1207
|
+
const updateAvailable = await checkForUpdate();
|
|
1186
1208
|
// ── THE single SessionStart announce assembly (docs/reminder-interrupt.md §Registry
|
|
1187
1209
|
// spec) ──────────────────────────────────────────────────────────────────────────────
|
|
1188
1210
|
// Every agent-facing announce — setup-warning, front-desk, checkpoint, assistant-doctrine,
|
|
@@ -1206,6 +1228,8 @@ async function recap(input, mode = 'plain') {
|
|
|
1206
1228
|
frontDeskUnread,
|
|
1207
1229
|
openCheckpoints,
|
|
1208
1230
|
assistantDoctrine,
|
|
1231
|
+
corpusApiDocs,
|
|
1232
|
+
updateAvailable,
|
|
1209
1233
|
};
|
|
1210
1234
|
let announceReg = mechanicKilled() ? reminder_registry_1.REGISTRY.filter(m => m.id !== 'mechanic-friction') : reminder_registry_1.REGISTRY;
|
|
1211
1235
|
if (!announceShort)
|
|
@@ -1379,165 +1403,14 @@ function recordFrictionFire(projectId, tier, turnCount) {
|
|
|
1379
1403
|
}
|
|
1380
1404
|
catch { /* counter is best-effort — never block the turn */ }
|
|
1381
1405
|
}
|
|
1382
|
-
/** Local path for this project's memory-reflex fire-counter — the data the /mechanic
|
|
1383
|
-
* excessiveness monitor reads (fires · injected · silent · per-trigger). Beside the
|
|
1384
|
-
* friction counter under ~/.greprag/state. */
|
|
1385
|
-
function memoryReflexStatsPath(projectId) {
|
|
1386
|
-
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
1387
|
-
return path.join(home, '.greprag', 'state', `memory-reflex-${projectId}.json`);
|
|
1388
|
-
}
|
|
1389
|
-
/** Tally one memory-reflex fire (best-effort). `didInject` splits fire→injected vs silent
|
|
1390
|
-
* so the monitor can read the inject/fire ratio (firing without finding = noise). */
|
|
1391
|
-
function recordMemoryFire(projectId, trigger, didInject, turnCount) {
|
|
1392
|
-
try {
|
|
1393
|
-
const file = memoryReflexStatsPath(projectId);
|
|
1394
|
-
let prior = null;
|
|
1395
|
-
try {
|
|
1396
|
-
prior = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
1397
|
-
}
|
|
1398
|
-
catch { /* none yet */ }
|
|
1399
|
-
const next = (0, memory_reflex_1.tallyMemoryFire)(prior, trigger, didInject, turnCount, new Date().toISOString());
|
|
1400
|
-
const dir = path.dirname(file);
|
|
1401
|
-
if (!fs.existsSync(dir))
|
|
1402
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
1403
|
-
fs.writeFileSync(file, JSON.stringify(next, null, 2) + '\n');
|
|
1404
|
-
}
|
|
1405
|
-
catch { /* counter is best-effort — never block the turn */ }
|
|
1406
|
-
}
|
|
1407
|
-
/** Stash for the memory-reflex efficacy capture — the raw hit content injected THIS turn, so
|
|
1408
|
-
* the Stop hook can score the response's overlap against it (the `used` numerator). Keyed by
|
|
1409
|
-
* session (Stop has the short id); carries projectId so Stop needn't re-derive it. Written at
|
|
1410
|
-
* inject-time, consumed + deleted at Stop. adr: adr/memory-reflex.md */
|
|
1411
|
-
function memoryReflexPendingPath(short) {
|
|
1412
|
-
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
1413
|
-
return path.join(home, '.greprag', 'state', `memory-reflex-pending-${short}.json`);
|
|
1414
|
-
}
|
|
1415
|
-
function stashMemoryInjection(short, projectId, hitText, turnCount) {
|
|
1416
|
-
try {
|
|
1417
|
-
const file = memoryReflexPendingPath(short);
|
|
1418
|
-
const dir = path.dirname(file);
|
|
1419
|
-
if (!fs.existsSync(dir))
|
|
1420
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
1421
|
-
fs.writeFileSync(file, JSON.stringify({ projectId, hitText, turnCount }) + '\n');
|
|
1422
|
-
}
|
|
1423
|
-
catch { /* best-effort — a missed stash just means no efficacy sample this turn */ }
|
|
1424
|
-
}
|
|
1425
|
-
/** Read + DELETE the pending injection stash (consume-once, so a turn with no injection never
|
|
1426
|
-
* reuses a stale sample). Returns null on the common no-stash path. */
|
|
1427
|
-
function consumeMemoryInjection(short) {
|
|
1428
|
-
try {
|
|
1429
|
-
const file = memoryReflexPendingPath(short);
|
|
1430
|
-
const raw = fs.readFileSync(file, 'utf-8');
|
|
1431
|
-
try {
|
|
1432
|
-
fs.unlinkSync(file);
|
|
1433
|
-
}
|
|
1434
|
-
catch { /* already gone — fine */ }
|
|
1435
|
-
const p = JSON.parse(raw);
|
|
1436
|
-
if (!p || typeof p.projectId !== 'string' || typeof p.hitText !== 'string')
|
|
1437
|
-
return null;
|
|
1438
|
-
return { projectId: p.projectId, hitText: p.hitText };
|
|
1439
|
-
}
|
|
1440
|
-
catch {
|
|
1441
|
-
return null; // no stash this turn (the common case)
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
/** Bump the memory-reflex EFFICACY numerator (the response drew on the injection) — best-effort. */
|
|
1445
|
-
function recordMemoryUsed(projectId) {
|
|
1446
|
-
try {
|
|
1447
|
-
const file = memoryReflexStatsPath(projectId);
|
|
1448
|
-
let prior = null;
|
|
1449
|
-
try {
|
|
1450
|
-
prior = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
1451
|
-
}
|
|
1452
|
-
catch { /* none yet */ }
|
|
1453
|
-
const next = (0, memory_reflex_1.tallyMemoryUsed)(prior);
|
|
1454
|
-
const dir = path.dirname(file);
|
|
1455
|
-
if (!fs.existsSync(dir))
|
|
1456
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
1457
|
-
fs.writeFileSync(file, JSON.stringify(next, null, 2) + '\n');
|
|
1458
|
-
}
|
|
1459
|
-
catch { /* counter is best-effort */ }
|
|
1460
|
-
}
|
|
1461
|
-
/** Self-report stash (P1b, adr/memory-reflex.md) — the topic the agent flagged via a
|
|
1462
|
-
* `[[recall: X]]` marker LAST turn, so the NEXT turn's notify pulls + injects it. Separate
|
|
1463
|
-
* from the efficacy-injection stash. Keyed by session; carries projectId. Written at Stop,
|
|
1464
|
-
* consumed-once at the next notify. */
|
|
1465
|
-
function selfReportPendingPath(short) {
|
|
1466
|
-
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
1467
|
-
return path.join(home, '.greprag', 'state', `memory-reflex-selfreport-${short}.json`);
|
|
1468
|
-
}
|
|
1469
|
-
function stashSelfReport(short, projectId, topic) {
|
|
1470
|
-
try {
|
|
1471
|
-
const file = selfReportPendingPath(short);
|
|
1472
|
-
const dir = path.dirname(file);
|
|
1473
|
-
if (!fs.existsSync(dir))
|
|
1474
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
1475
|
-
fs.writeFileSync(file, JSON.stringify({ projectId, topic }) + '\n');
|
|
1476
|
-
}
|
|
1477
|
-
catch { /* best-effort — a missed self-report just means no pull this round */ }
|
|
1478
|
-
}
|
|
1479
|
-
/** Read + DELETE the self-report stash (consume-once). Null on the common no-marker path. */
|
|
1480
|
-
function consumeSelfReport(short) {
|
|
1481
|
-
try {
|
|
1482
|
-
const file = selfReportPendingPath(short);
|
|
1483
|
-
const raw = fs.readFileSync(file, 'utf-8');
|
|
1484
|
-
try {
|
|
1485
|
-
fs.unlinkSync(file);
|
|
1486
|
-
}
|
|
1487
|
-
catch { /* already gone — fine */ }
|
|
1488
|
-
const p = JSON.parse(raw);
|
|
1489
|
-
if (!p || typeof p.projectId !== 'string' || typeof p.topic !== 'string' || !p.topic)
|
|
1490
|
-
return null;
|
|
1491
|
-
return { projectId: p.projectId, topic: p.topic };
|
|
1492
|
-
}
|
|
1493
|
-
catch {
|
|
1494
|
-
return null;
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
/** Local cache of the SERVER's auto-quiet verdict for this project (the
|
|
1498
|
-
* mechanicKilled() analog for the memory-reflex). The Flash-Lite efficacy judge
|
|
1499
|
-
* decides the threshold server-side and ships the boolean on the /v1/memory/query
|
|
1500
|
-
* response; the hook caches it here and `memoryReflexQuieted` honors it with a
|
|
1501
|
-
* TTL (quietCacheSaysSilent). Beside the fire-counter under ~/.greprag/state. */
|
|
1502
|
-
function memoryReflexQuietPath(projectId) {
|
|
1503
|
-
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
1504
|
-
return path.join(home, '.greprag', 'state', `memory-reflex-quiet-${projectId}.json`);
|
|
1505
|
-
}
|
|
1506
|
-
/** Has the server's efficacy judge QUIETED this project's memory-reflex (low
|
|
1507
|
-
* used÷injected over a meaningful sample)? Reads the local cache; honors it only
|
|
1508
|
-
* while fresh (TTL) so a recovered reflex re-probes instead of staying silenced.
|
|
1509
|
-
* Fail-quiet: any error reads as NOT quieted (the reflex keeps working) —
|
|
1510
|
-
* mirrors mechanicKilled()'s fail-open posture. */
|
|
1511
|
-
function memoryReflexQuieted(projectId) {
|
|
1512
|
-
try {
|
|
1513
|
-
const raw = fs.readFileSync(memoryReflexQuietPath(projectId), 'utf-8');
|
|
1514
|
-
return (0, memory_reflex_1.quietCacheSaysSilent)(JSON.parse(raw), Date.now());
|
|
1515
|
-
}
|
|
1516
|
-
catch {
|
|
1517
|
-
return false;
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
/** Cache the server's auto-quiet verdict from a /v1/memory/query response — written
|
|
1521
|
-
* whenever the reflex actually probes (fired the search). Best-effort. */
|
|
1522
|
-
function cacheMemoryReflexQuiet(projectId, quiet) {
|
|
1523
|
-
try {
|
|
1524
|
-
const file = memoryReflexQuietPath(projectId);
|
|
1525
|
-
const dir = path.dirname(file);
|
|
1526
|
-
if (!fs.existsSync(dir))
|
|
1527
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
1528
|
-
const payload = { quiet, cachedAt: new Date().toISOString() };
|
|
1529
|
-
fs.writeFileSync(file, JSON.stringify(payload) + '\n');
|
|
1530
|
-
}
|
|
1531
|
-
catch { /* best-effort — a missed cache just means the reflex re-probes next turn */ }
|
|
1532
|
-
}
|
|
1533
1406
|
/** The reminder-interrupt registry CONTAINER call (docs/reminder-interrupt.md §Registry
|
|
1534
1407
|
* spec) — invoked from the proven-registered `notify` hook (claude-code path). Assembles
|
|
1535
1408
|
* the live env (the ONLY i/o: armed via isLocallyArmed, this session's own unread, the
|
|
1536
1409
|
* Stop-stashed stress/turnCount, the front-desk envelope notice) and fires every module
|
|
1537
1410
|
* whose detector reports deficient: watcher-arm (unarmed → nag if a peer messaged you, else
|
|
1538
|
-
* nudge; silent when armed), the stress-gated mechanic-friction, front-desk (a soft
|
|
1539
|
-
* "you've got mail" when a new envelope landed)
|
|
1540
|
-
*
|
|
1411
|
+
* nudge; silent when armed), the stress-gated mechanic-friction, and front-desk (a soft
|
|
1412
|
+
* "you've got mail" when a new envelope landed). The memory PRIMER announces at SessionStart;
|
|
1413
|
+
* the auto-inject was dropped (knowledge injection, not a reminder — docs/reminder-interrupt.md).
|
|
1541
1414
|
* The announce-only modules (setup-warning / checkpoint / assistant-doctrine) stay silent per
|
|
1542
1415
|
* turn — their env signals are SessionStart-only. Stacking; emitted once. Fail-open — any miss
|
|
1543
1416
|
* → silent. The Mechanic kill switch drops ONLY the mechanic module; the rest keep working. */
|
|
@@ -1564,48 +1437,22 @@ async function injectReminders(input, cfg, short) {
|
|
|
1564
1437
|
// null when nothing new. This is the network call the retired `mail` hook used to make;
|
|
1565
1438
|
// it moves here so the whole announce/reminder surface rides ONE detector loop.
|
|
1566
1439
|
const frontDeskNotice = await (0, front_desk_mail_1.buildFrontDeskNotice)(cfg.apiUrl, cfg.apiKey);
|
|
1567
|
-
//
|
|
1568
|
-
//
|
|
1569
|
-
//
|
|
1570
|
-
//
|
|
1571
|
-
//
|
|
1572
|
-
//
|
|
1573
|
-
let memoryHit = null;
|
|
1574
|
-
const prompt = typeof input.prompt === 'string' ? input.prompt : '';
|
|
1575
|
-
// Pick the memory query + trigger (the hybrid): a self-report marker the agent emitted LAST
|
|
1576
|
-
// turn ([[recall: X]], stashed at Stop) takes precedence over recall-intent in THIS prompt —
|
|
1577
|
-
// the agent explicitly flagged its own gap. One injection per turn.
|
|
1578
|
-
let memQuery = null;
|
|
1579
|
-
let memTrigger = 'keyword';
|
|
1580
|
-
const selfReport = anchor.projectId ? consumeSelfReport(short) : null;
|
|
1581
|
-
if (selfReport) {
|
|
1582
|
-
memQuery = selfReport.topic;
|
|
1583
|
-
memTrigger = 'selfReport';
|
|
1584
|
-
}
|
|
1585
|
-
else if ((0, memory_reflex_1.hasRecallIntent)(prompt)) {
|
|
1586
|
-
memQuery = prompt.slice(0, 500);
|
|
1587
|
-
memTrigger = 'keyword';
|
|
1588
|
-
}
|
|
1589
|
-
// Auto-quiet gate (adr/memory-reflex.md): if the server's Flash-Lite efficacy judge has
|
|
1590
|
-
// QUIETED this project's reflex (low used÷injected over a meaningful sample), skip the search
|
|
1591
|
-
// entirely — the cached verdict expires on a TTL so a recovered reflex re-probes. mechanicKilled() analog.
|
|
1592
|
-
if (anchor.projectId && memQuery && !memoryReflexQuieted(anchor.projectId)) {
|
|
1593
|
-
const { hits, quiet } = await fetchMemoryHits(cfg.apiUrl, cfg.apiKey, anchor.projectId, memQuery);
|
|
1594
|
-
cacheMemoryReflexQuiet(anchor.projectId, quiet); // refresh the verdict from this probe
|
|
1595
|
-
memoryHit = (0, memory_reflex_1.buildMemoryInjection)(hits);
|
|
1596
|
-
recordMemoryFire(anchor.projectId, memTrigger, !!memoryHit, turnCount);
|
|
1597
|
-
// Stash what was injected so the Stop hook can score whether the response used it
|
|
1598
|
-
// (the efficacy numerator). Only when something was actually surfaced.
|
|
1599
|
-
if (memoryHit)
|
|
1600
|
-
stashMemoryInjection(short, anchor.projectId, (0, memory_reflex_1.gatedHitText)(hits), turnCount);
|
|
1601
|
-
}
|
|
1440
|
+
// NOTE: the per-turn memory AUTO-INJECT (recall-intent → search → surface a hit) was
|
|
1441
|
+
// DROPPED 2026-06-22. It was knowledge-injection, NOT a behavioral reminder — a
|
|
1442
|
+
// different (undeveloped) system that was jammed into this registry for lack of a slot.
|
|
1443
|
+
// It also collided with the agent's own better-targeted `greprag memory search` (taught
|
|
1444
|
+
// by the memory primer). The memory PRIMER (the announce) stays; only the auto-inject is
|
|
1445
|
+
// gone. docs/reminder-interrupt.md (the landed map: reminder ≠ knowledge injection).
|
|
1602
1446
|
const env = {
|
|
1603
1447
|
short, turnCount, stress, armed, sessionUnread,
|
|
1604
1448
|
ownerPid, alias: (0, session_id_1.readIdentityAlias)(), assistant,
|
|
1605
|
-
frontDeskNotice,
|
|
1449
|
+
frontDeskNotice,
|
|
1606
1450
|
};
|
|
1607
|
-
//
|
|
1608
|
-
|
|
1451
|
+
// UserPromptSubmit evaluates the prompt-source modules only (command-source
|
|
1452
|
+
// modules — the collision Match — run at PreToolUse). Kill switch drops ONLY
|
|
1453
|
+
// the mechanic module — watcher-arm keeps working.
|
|
1454
|
+
const base = (0, reminder_registry_1.promptModules)();
|
|
1455
|
+
const reg = mechanicKilled() ? base.filter(m => m.id !== 'mechanic-friction') : base;
|
|
1609
1456
|
const fired = (0, reminder_registry_1.collectReminders)(env, reg);
|
|
1610
1457
|
if (!fired.length)
|
|
1611
1458
|
return;
|
|
@@ -1785,7 +1632,7 @@ function validateChip(title, prompt) {
|
|
|
1785
1632
|
* adr/spawn-task-hook-mode.md 2026-05-27 entry — Tier 2 augmentation
|
|
1786
1633
|
* attempt). `permissionDecision: deny` IS honored, so the validator
|
|
1787
1634
|
* catches missing Block 1 / Block 2 markers; the agent re-composes the
|
|
1788
|
-
* call with the templates from
|
|
1635
|
+
* call with the templates from `greprag load chip-spawn`. */
|
|
1789
1636
|
function handlePreSpawnCheck(input) {
|
|
1790
1637
|
if (input.tool_name !== 'mcp__ccd_session__spawn_task')
|
|
1791
1638
|
return;
|
|
@@ -1796,7 +1643,7 @@ function handlePreSpawnCheck(input) {
|
|
|
1796
1643
|
return;
|
|
1797
1644
|
const reason = `Chip prompt rejected by greprag PreToolUse validator. Fix the following and re-call spawn_task:\n - ` +
|
|
1798
1645
|
violations.join('\n - ') +
|
|
1799
|
-
`\n\nFull conventions:
|
|
1646
|
+
`\n\nFull conventions: run \`greprag load chip-spawn\`.`;
|
|
1800
1647
|
process.stdout.write(JSON.stringify({
|
|
1801
1648
|
hookSpecificOutput: {
|
|
1802
1649
|
hookEventName: 'PreToolUse',
|
|
@@ -1867,7 +1714,8 @@ async function coordinateGate(input) {
|
|
|
1867
1714
|
if (!short)
|
|
1868
1715
|
return; // no session id → silent
|
|
1869
1716
|
// Local + cheap: classify BEFORE any network so non-risky calls cost nothing.
|
|
1870
|
-
const
|
|
1717
|
+
const toolInput = (input.tool_input || {});
|
|
1718
|
+
const trigger = (0, coordinate_gate_1.triggerFromPreToolUse)(input.tool_name || '', toolInput);
|
|
1871
1719
|
if (!trigger)
|
|
1872
1720
|
return; // not a risky action → silent
|
|
1873
1721
|
let anchor;
|
|
@@ -1879,7 +1727,8 @@ async function coordinateGate(input) {
|
|
|
1879
1727
|
}
|
|
1880
1728
|
if (!anchor.projectId)
|
|
1881
1729
|
return; // unanchored → nothing to match
|
|
1882
|
-
|
|
1730
|
+
// I/O stays in the hook: fresh peer read off the hot path → env.collisionPeers.
|
|
1731
|
+
const peers = await (0, coordinate_gate_1.resolveCollisionPeers)({
|
|
1883
1732
|
short,
|
|
1884
1733
|
projectId: anchor.projectId,
|
|
1885
1734
|
projectName: anchor.projectName,
|
|
@@ -1887,15 +1736,26 @@ async function coordinateGate(input) {
|
|
|
1887
1736
|
apiKey: cfg.apiKey,
|
|
1888
1737
|
alias: (0, session_id_1.readIdentityAlias)(),
|
|
1889
1738
|
});
|
|
1890
|
-
if (
|
|
1739
|
+
if (peers.length === 0)
|
|
1891
1740
|
return; // no live peer in this repo → proceed
|
|
1741
|
+
// Route through the registry: build the command-read env, run the command-source
|
|
1742
|
+
// modules (the collision Match). The standalone gate is now a registry citizen.
|
|
1743
|
+
const env = {
|
|
1744
|
+
short, turnCount: 0, stress: 0, armed: false, sessionUnread: 0,
|
|
1745
|
+
alias: (0, session_id_1.readIdentityAlias)(),
|
|
1746
|
+
toolCommand: typeof toolInput.command === 'string' ? toolInput.command : '',
|
|
1747
|
+
collisionPeers: peers,
|
|
1748
|
+
};
|
|
1749
|
+
const fired = (0, reminder_registry_1.collectReminders)(env, (0, reminder_registry_1.commandModules)());
|
|
1750
|
+
if (!fired.length)
|
|
1751
|
+
return;
|
|
1892
1752
|
// EFFECT: inject as additionalContext — advisory heads-up, NO permission pause.
|
|
1893
1753
|
// `ask` was too intrusive (prompted on every git op while peers were live); inject
|
|
1894
1754
|
// keeps the agent aware without a manual-allow. adr: adr/monitor-resilience.md
|
|
1895
1755
|
process.stdout.write(JSON.stringify({
|
|
1896
1756
|
hookSpecificOutput: {
|
|
1897
1757
|
hookEventName: input.hook_event_name || 'PreToolUse',
|
|
1898
|
-
additionalContext:
|
|
1758
|
+
additionalContext: fired.map(f => f.line).join('\n\n'),
|
|
1899
1759
|
},
|
|
1900
1760
|
}) + '\n');
|
|
1901
1761
|
}
|