greprag 5.49.8 → 5.49.10

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.
Files changed (36) hide show
  1. package/dist/commands/collision-reminder.d.ts +14 -0
  2. package/dist/commands/collision-reminder.js +40 -0
  3. package/dist/commands/collision-reminder.js.map +1 -0
  4. package/dist/commands/coordinate-gate.d.ts +12 -6
  5. package/dist/commands/coordinate-gate.js +24 -10
  6. package/dist/commands/coordinate-gate.js.map +1 -1
  7. package/dist/commands/friction-reminder.d.ts +22 -44
  8. package/dist/commands/friction-reminder.js +45 -63
  9. package/dist/commands/friction-reminder.js.map +1 -1
  10. package/dist/commands/init.js +7 -14
  11. package/dist/commands/init.js.map +1 -1
  12. package/dist/commands/load-primer-reminder.d.ts +28 -0
  13. package/dist/commands/load-primer-reminder.js +51 -0
  14. package/dist/commands/load-primer-reminder.js.map +1 -0
  15. package/dist/commands/load.d.ts +15 -0
  16. package/dist/commands/load.js +112 -0
  17. package/dist/commands/load.js.map +1 -0
  18. package/dist/commands/memory-reflex.d.ts +17 -104
  19. package/dist/commands/memory-reflex.js +23 -215
  20. package/dist/commands/memory-reflex.js.map +1 -1
  21. package/dist/commands/memory.js +70 -0
  22. package/dist/commands/memory.js.map +1 -1
  23. package/dist/commands/reminder-registry.d.ts +12 -1
  24. package/dist/commands/reminder-registry.js +46 -6
  25. package/dist/commands/reminder-registry.js.map +1 -1
  26. package/dist/commands/reminder-types.d.ts +28 -10
  27. package/dist/commands/version-reminder.d.ts +15 -0
  28. package/dist/commands/version-reminder.js +34 -0
  29. package/dist/commands/version-reminder.js.map +1 -0
  30. package/dist/hook.js +115 -246
  31. package/dist/hook.js.map +1 -1
  32. package/dist/index.js +10 -0
  33. package/dist/index.js.map +1 -1
  34. package/package.json +1 -1
  35. package/skill/templates/chip-leader.md +188 -0
  36. package/skill/templates/chip-spawn.md +4 -4
@@ -35,11 +35,17 @@ export interface ReminderEnv {
35
35
  setupWarning?: string | null;
36
36
  /** Front-desk unread count (the SessionStart count announce). */
37
37
  frontDeskUnread?: number;
38
- /** Open checkpoints for this project (the SessionStart landmark announce). */
39
- openCheckpoints?: OpenCheckpointSlim[];
40
38
  /** Precomputed assistant-doctrine context — the hook owns the doctrine-file read, so
41
39
  * this module half is a pass-through. Null for any non-assistant project. */
42
40
  assistantDoctrine?: string | null;
41
+ /** Deficiency-gated announce signal: the installed greprag is behind the latest
42
+ * published version. Set by the hook (daily-cached npm-registry check); null/undefined
43
+ * when current. Drives the version-upgrade announce — "new release, upgrade with X".
44
+ * docs/reminder-interrupt.md (a Deficiency-gated announce, not a new type). */
45
+ updateAvailable?: {
46
+ current: string;
47
+ latest: string;
48
+ } | null;
43
49
  /** Names of the api-docs-tagged corpus stores (the hook lists `?tag=api-docs`).
44
50
  * Drives the corpus announce: name the on-tap reference banks so an agent
45
51
  * searches them before answering from training data. Empty/undefined → the
@@ -52,14 +58,14 @@ export interface ReminderEnv {
52
58
  * the search + the confidence gate + framing; the memory-reflex module routes the
53
59
  * framed string as its per-turn reminder. Null = nothing cleared the gate this turn. */
54
60
  memoryHit?: string | null;
55
- }
56
- /** Slim open-checkpoint row (the SessionStart landmark announce, checkpoint module).
57
- * Lives here — not in checkpoint-reminder.ts — because ReminderEnv references it and a
58
- * module file importing from here would otherwise close an import cycle. */
59
- export interface OpenCheckpointSlim {
60
- nodeId: string;
61
- title: string;
62
- createdAt: string;
61
+ /** The Bash command about to run (the `command` read surface for a Match trigger).
62
+ * Present only on the PreToolUse env; undefined at UserPromptSubmit. */
63
+ toolCommand?: string | null;
64
+ /** Same-repo peers live right now (the hook does the fresh watcher read off the
65
+ * hot path and passes the result; the collision module detects purely on it). */
66
+ collisionPeers?: {
67
+ short: string;
68
+ }[];
63
69
  }
64
70
  /** A module's per-turn read of its deficiency. `tier:'silent'` = resolved → no fire. */
65
71
  export interface Detection {
@@ -72,6 +78,18 @@ export interface Detection {
72
78
  * unit-testable regression-lock. */
73
79
  export interface ReminderModule {
74
80
  id: string;
81
+ /** Which READ surface this module fires on (docs/reminder-interrupt.md — the landed
82
+ * map). `prompt` (default) modules run at UserPromptSubmit; `command` modules run at
83
+ * PreToolUse, reading `env.toolCommand`. The call site filters the registry by source
84
+ * so each hook event evaluates only its own modules. */
85
+ source?: 'prompt' | 'command';
86
+ /** Boot-sequence dependencies (docs/load-system.md §boot sequence): module ids whose
87
+ * announce MUST be emitted before this one's. A primer that references a concept an
88
+ * earlier primer establishes declares it here (e.g. the mechanic primer references chip
89
+ * vehicles → `dependsOn: ['chip-spawn-pointer']`; every pointer that says "greprag load"
90
+ * → `dependsOn: ['load-primer']`). collectAnnounces topologically sorts on this, so the
91
+ * boot order is a declared graph, not fragile array position. Omit = no dependency. */
92
+ dependsOn?: string[];
75
93
  /** Read the live deficiency. The gate: `silent` ⇒ the module is quiet this turn. */
76
94
  detect: (env: ReminderEnv) => Detection;
77
95
  /** SessionStart schema, loaded once (teach method + why). null = nothing to announce. */
@@ -0,0 +1,15 @@
1
+ /** version-reminder — the release-upgrade announce. A DEFICIENCY-gated announce
2
+ * (docs/reminder-interrupt.md): the Deficiency trigger ("a required state is missing")
3
+ * applied to the ANNOUNCE surface — the required state is "running the latest greprag,"
4
+ * the deficiency is "installed < latest published." When behind, the SessionStart announce
5
+ * tells the user a new release is out + how to upgrade; it auto-clears once they upgrade.
6
+ *
7
+ * This is what makes a release PROPAGATE — without it, users never learn to upgrade.
8
+ * NOT a new announce kind: it is a primer gated by Deficiency, exactly like setup-warning
9
+ * (which announces only when there's a drift gap). The hook does the I/O (the daily-cached
10
+ * npm-registry version check) and passes the verdict as env.updateAvailable; this module
11
+ * is PURE — announce-only (detect → silent, reminder → null). */
12
+ import { ReminderModule } from './reminder-types';
13
+ /** The upgrade announce — null unless the hook detected a newer published version. */
14
+ export declare function buildVersionAnnounce(current: string, latest: string): string;
15
+ export declare const versionUpgradeModule: ReminderModule;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ /** version-reminder — the release-upgrade announce. A DEFICIENCY-gated announce
3
+ * (docs/reminder-interrupt.md): the Deficiency trigger ("a required state is missing")
4
+ * applied to the ANNOUNCE surface — the required state is "running the latest greprag,"
5
+ * the deficiency is "installed < latest published." When behind, the SessionStart announce
6
+ * tells the user a new release is out + how to upgrade; it auto-clears once they upgrade.
7
+ *
8
+ * This is what makes a release PROPAGATE — without it, users never learn to upgrade.
9
+ * NOT a new announce kind: it is a primer gated by Deficiency, exactly like setup-warning
10
+ * (which announces only when there's a drift gap). The hook does the I/O (the daily-cached
11
+ * npm-registry version check) and passes the verdict as env.updateAvailable; this module
12
+ * is PURE — announce-only (detect → silent, reminder → null). */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.versionUpgradeModule = void 0;
15
+ exports.buildVersionAnnounce = buildVersionAnnounce;
16
+ /** The upgrade announce — null unless the hook detected a newer published version. */
17
+ function buildVersionAnnounce(current, latest) {
18
+ return [
19
+ `[greprag — a new release is out: v${latest} (you are on v${current}).]`,
20
+ `Upgrade: \`npm i -g greprag@latest\`. New behavior + fixes land on your next session.`,
21
+ ].join('\n');
22
+ }
23
+ exports.versionUpgradeModule = {
24
+ id: 'version-upgrade',
25
+ detect: (_env) => ({ tier: 'silent' }), // announce-only
26
+ announce: (env) => {
27
+ const u = env.updateAvailable;
28
+ if (!u || !u.current || !u.latest)
29
+ return null;
30
+ return buildVersionAnnounce(u.current, u.latest);
31
+ },
32
+ reminder: () => null,
33
+ };
34
+ //# sourceMappingURL=version-reminder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version-reminder.js","sourceRoot":"","sources":["../../src/commands/version-reminder.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;kEAUkE;;;AAKlE,oDAKC;AAND,sFAAsF;AACtF,SAAgB,oBAAoB,CAAC,OAAe,EAAE,MAAc;IAClE,OAAO;QACL,qCAAqC,MAAM,iBAAiB,OAAO,KAAK;QACxE,uFAAuF;KACxF,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAEY,QAAA,oBAAoB,GAAmB;IAClD,EAAE,EAAE,iBAAiB;IACrB,MAAM,EAAE,CAAC,IAAiB,EAAa,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,gBAAgB;IAChF,QAAQ,EAAE,CAAC,GAAgB,EAAiB,EAAE;QAC5C,MAAM,CAAC,GAAG,GAAG,CAAC,eAAe,CAAC;QAC9B,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC;QAC/C,OAAO,oBAAoB,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC;IACnD,CAAC;IACD,QAAQ,EAAE,GAAkB,EAAE,CAAC,IAAI;CACpC,CAAC"}
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,25 +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 EFFICACY capture (adr/memory-reflex.md). If a memory injection was surfaced
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 */ }
788
+ // (Memory-reflex efficacy capture removed 2026-06-22 with the auto-inject it scored.)
808
789
  // Empty turn — nothing to capture.
809
790
  if (!turn.userPrompt && !turn.agentResponse && turn.toolCalls.length === 0)
810
791
  return;
@@ -841,12 +822,6 @@ async function store(input, source = 'claude-code') {
841
822
  status: turn.status,
842
823
  userPrompt,
843
824
  agentResponse,
844
- // The memory injection surfaced this turn (when any) — the SEAM the Flash-Lite efficacy
845
- // judge (Chip A) consumes server-side at ingest to score used÷injected. Scrubbed + capped
846
- // like the other text fields. Omitted entirely on the common no-injection turn.
847
- ...(memoryInjectionForStore
848
- ? { memoryInjection: capField((0, secret_scrubber_1.scrubString)(memoryInjectionForStore, redaction)) }
849
- : {}),
850
825
  toolCalls,
851
826
  filesTouched: turn.filesTouched,
852
827
  artifacts: artifactRefs,
@@ -863,28 +838,7 @@ async function store(input, source = 'claude-code') {
863
838
  // pushed to the system prompt is the same for Claude/Codex and the opencode
864
839
  // plugin; per-harness wrappers (preamble, output mode, transform-hook push)
865
840
  // stay local.
866
- /** Fetch a slim summary of open checkpoints for the given project. Returns
867
- * up to `limit` rows so the checkpoint module can compute the overflow
868
- * (3-most-recent + "N more" hint). Returns [] on any error — the hook
869
- * must never block on a checkpoint query failure. (OpenCheckpointSlim +
870
- * renderCheckpointHint now live in commands/checkpoint-reminder.ts; the hook
871
- * owns only this fetch.) */
872
- async function fetchOpenCheckpoints(apiUrl, apiKey, projectId, limit = 10) {
873
- try {
874
- const url = `${apiUrl}/v1/checkpoints/${encodeURIComponent(projectId)}`
875
- + `?status=open&limit=${limit}`;
876
- const res = await fetch(url, { headers: { 'Authorization': `Bearer ${apiKey}` } });
877
- if (!res.ok)
878
- return [];
879
- const data = await res.json();
880
- return (data.checkpoints || []).map(c => ({
881
- nodeId: c.nodeId, title: c.title, createdAt: c.createdAt,
882
- }));
883
- }
884
- catch {
885
- return [];
886
- }
887
- }
841
+ // (fetchOpenCheckpoints removed 2026-06-22 with the open-checkpoints SessionStart announce.)
888
842
  /** Fetch unread inbox count for the given project context. Project-scoped
889
843
  * count includes tenant-level messages (mail addressed to the user, not
890
844
  * any specific project, still shows up). Returns 0 on any error so the
@@ -963,44 +917,6 @@ async function fetchSessionUnread(apiUrl, apiKey, short) {
963
917
  return 0;
964
918
  }
965
919
  }
966
- /** Fetch top episodic-memory hits for a query (the memory-reflex inline path), project-
967
- * scoped, with a HARD timeout so a slow search never blocks the turn. POSTs the same
968
- * `/v1/memory/query` the CLI `memory search` uses, with the de-pollution `session-turn`
969
- * provenance filter. Returns [] on any error/abort → the reflex stays silent. The
970
- * confidence gate + framing live in buildMemoryInjection (commands/memory-reflex.ts).
971
- * adr: adr/memory-reflex.md */
972
- async function fetchMemoryHits(apiUrl, apiKey, projectId, query, limit = 3, timeoutMs = 2000) {
973
- const ctl = new AbortController();
974
- const timer = setTimeout(() => ctl.abort(), timeoutMs);
975
- try {
976
- const res = await fetch(`${apiUrl}/v1/memory/query`, {
977
- method: 'POST',
978
- headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
979
- body: JSON.stringify({ query, limit, projectId, filters: { provenance: 'session-turn' } }),
980
- signal: ctl.signal,
981
- });
982
- if (!res.ok)
983
- return { hits: [], quiet: false };
984
- const data = await res.json();
985
- // The server's Flash-Lite efficacy verdict rides the same response (adr/memory-reflex.md).
986
- const quiet = data.memoryReflexQuiet === true;
987
- if (!data.ok || !Array.isArray(data.nodes))
988
- return { hits: [], quiet };
989
- return {
990
- hits: data.nodes.map(n => ({
991
- content: n.content, score: n.score, confidence: n.confidence ?? null,
992
- shape: n.shape ?? null, createdAt: n.createdAt ?? null, projectName: n.projectName ?? null,
993
- })),
994
- quiet,
995
- };
996
- }
997
- catch {
998
- return { hits: [], quiet: false };
999
- }
1000
- finally {
1001
- clearTimeout(timer);
1002
- }
1003
- }
1004
920
  /** Identity-drift warning rate limiter.
1005
921
  *
1006
922
  * The drift warning ("stored ID ≠ git-derived ID, run `greprag doctor`") used
@@ -1075,6 +991,78 @@ function writeRecapOutput(text, mode) {
1075
991
  }
1076
992
  process.stdout.write(text);
1077
993
  }
994
+ /** This CLI's installed version (from the bundled package.json). Null if unreadable. */
995
+ function installedVersion() {
996
+ try {
997
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
998
+ return typeof pkg.version === 'string' ? pkg.version : null;
999
+ }
1000
+ catch {
1001
+ return null;
1002
+ }
1003
+ }
1004
+ /** Is semver `b` strictly greater than `a`? Plain numeric major.minor.patch compare
1005
+ * (greprag never ships pre-release tags). Non-numeric segments collapse to 0. */
1006
+ function isNewer(a, b) {
1007
+ const pa = a.split('.').map(n => parseInt(n, 10) || 0);
1008
+ const pb = b.split('.').map(n => parseInt(n, 10) || 0);
1009
+ for (let i = 0; i < 3; i++) {
1010
+ if ((pb[i] || 0) > (pa[i] || 0))
1011
+ return true;
1012
+ if ((pb[i] || 0) < (pa[i] || 0))
1013
+ return false;
1014
+ }
1015
+ return false;
1016
+ }
1017
+ const VERSION_CHECK_TTL_MS = 24 * 60 * 60 * 1000; // once a day
1018
+ function versionCheckPath() {
1019
+ const home = process.env.HOME || process.env.USERPROFILE || '';
1020
+ return path.join(home, '.greprag', 'state', 'version-check.json');
1021
+ }
1022
+ /** The Deficiency-gated upgrade signal (version-reminder.ts). Returns {current, latest}
1023
+ * when the installed CLI is behind the latest published version, else null. The npm-registry
1024
+ * read is cached daily (a slow/offline check never blocks SessionStart — stale cache or no
1025
+ * cache → no announce, fail-quiet). */
1026
+ async function checkForUpdate() {
1027
+ const current = installedVersion();
1028
+ if (!current)
1029
+ return null;
1030
+ let latest = null;
1031
+ try {
1032
+ const raw = fs.readFileSync(versionCheckPath(), 'utf-8');
1033
+ const c = JSON.parse(raw);
1034
+ const age = c.checkedAt ? Date.now() - Date.parse(c.checkedAt) : Infinity;
1035
+ if (typeof c.latest === 'string' && Number.isFinite(age) && age < VERSION_CHECK_TTL_MS) {
1036
+ latest = c.latest; // fresh cache hit
1037
+ }
1038
+ }
1039
+ catch { /* no/bad cache — refetch */ }
1040
+ if (!latest) {
1041
+ try {
1042
+ const ctl = new AbortController();
1043
+ const timer = setTimeout(() => ctl.abort(), 1500);
1044
+ const res = await fetch('https://registry.npmjs.org/greprag/latest', { signal: ctl.signal });
1045
+ clearTimeout(timer);
1046
+ if (res.ok) {
1047
+ const data = await res.json();
1048
+ if (typeof data.version === 'string') {
1049
+ latest = data.version;
1050
+ try {
1051
+ const file = versionCheckPath();
1052
+ if (!fs.existsSync(path.dirname(file)))
1053
+ fs.mkdirSync(path.dirname(file), { recursive: true });
1054
+ fs.writeFileSync(file, JSON.stringify({ latest, checkedAt: new Date().toISOString() }) + '\n');
1055
+ }
1056
+ catch { /* cache write best-effort */ }
1057
+ }
1058
+ }
1059
+ }
1060
+ catch { /* offline / slow → no announce this session */ }
1061
+ }
1062
+ if (latest && isNewer(current, latest))
1063
+ return { current, latest };
1064
+ return null;
1065
+ }
1078
1066
  /** SessionStart — fetch recent episodic activity for this project and print
1079
1067
  * to stdout. Claude Code injects the printed text as session context. Codex
1080
1068
  * uses the same content via `hookSpecificOutput.additionalContext`. Fires
@@ -1173,10 +1161,9 @@ async function recap(input, mode = 'plain') {
1173
1161
  // (and then read) another session's session-targeted DM. Live inbox now
1174
1162
  // belongs exclusively to Monitor watchers; the agent learns about its own
1175
1163
  // mail via the watcher's session-scoped stream.
1176
- // Open-checkpoints checkpoints are operator-triggered landmarks, not message-shape
1177
- // inbox traffic, no privacy issue. The hook owns ONLY this fetch; the checkpoint module
1178
- // (commands/checkpoint-reminder.ts) renders the list hint block in its announce.
1179
- const openCheckpoints = await fetchOpenCheckpoints(cfg.apiUrl, cfg.apiKey, anchor.projectId, 10);
1164
+ // (Open-checkpoints announce removed 2026-06-22 operator found stale bookmarks
1165
+ // cluttering every SessionStart. The `greprag checkpoint` command stays; only the
1166
+ // auto-announce is gone. No fetch here anymore.)
1180
1167
  // Front-desk awareness — a discreet count of cold opens + inbound email waiting at the
1181
1168
  // shared tenant front desk, so a stranger's first contact isn't stranded until someone
1182
1169
  // runs `greprag inbox`. Content is NEVER injected — just the count + the door, formatted
@@ -1193,6 +1180,9 @@ async function recap(input, mode = 'plain') {
1193
1180
  const assistantDoctrine = (0, project_anchor_1.isAssistantProject)(anchor)
1194
1181
  ? (0, assistant_doctrine_1.buildAssistantDoctrineContext)(cwd)
1195
1182
  : null;
1183
+ // Deficiency-gated upgrade announce: daily-cached npm check → tell the user to
1184
+ // upgrade when behind. This is how a release actually propagates to everyone.
1185
+ const updateAvailable = await checkForUpdate();
1196
1186
  // ── THE single SessionStart announce assembly (docs/reminder-interrupt.md §Registry
1197
1187
  // spec) ──────────────────────────────────────────────────────────────────────────────
1198
1188
  // Every agent-facing announce — setup-warning, front-desk, checkpoint, assistant-doctrine,
@@ -1214,9 +1204,9 @@ async function recap(input, mode = 'plain') {
1214
1204
  projectName: anchor.projectName,
1215
1205
  setupWarning,
1216
1206
  frontDeskUnread,
1217
- openCheckpoints,
1218
1207
  assistantDoctrine,
1219
1208
  corpusApiDocs,
1209
+ updateAvailable,
1220
1210
  };
1221
1211
  let announceReg = mechanicKilled() ? reminder_registry_1.REGISTRY.filter(m => m.id !== 'mechanic-friction') : reminder_registry_1.REGISTRY;
1222
1212
  if (!announceShort)
@@ -1390,129 +1380,14 @@ function recordFrictionFire(projectId, tier, turnCount) {
1390
1380
  }
1391
1381
  catch { /* counter is best-effort — never block the turn */ }
1392
1382
  }
1393
- /** Local path for this project's memory-reflex fire-counter — the data the /mechanic
1394
- * excessiveness monitor reads (fires · injected · silent · per-trigger). Beside the
1395
- * friction counter under ~/.greprag/state. */
1396
- function memoryReflexStatsPath(projectId) {
1397
- const home = process.env.HOME || process.env.USERPROFILE || '';
1398
- return path.join(home, '.greprag', 'state', `memory-reflex-${projectId}.json`);
1399
- }
1400
- /** Tally one memory-reflex fire (best-effort). `didInject` splits fire→injected vs silent
1401
- * so the monitor can read the inject/fire ratio (firing without finding = noise). */
1402
- function recordMemoryFire(projectId, trigger, didInject, turnCount) {
1403
- try {
1404
- const file = memoryReflexStatsPath(projectId);
1405
- let prior = null;
1406
- try {
1407
- prior = JSON.parse(fs.readFileSync(file, 'utf-8'));
1408
- }
1409
- catch { /* none yet */ }
1410
- const next = (0, memory_reflex_1.tallyMemoryFire)(prior, trigger, didInject, turnCount, new Date().toISOString());
1411
- const dir = path.dirname(file);
1412
- if (!fs.existsSync(dir))
1413
- fs.mkdirSync(dir, { recursive: true });
1414
- fs.writeFileSync(file, JSON.stringify(next, null, 2) + '\n');
1415
- }
1416
- catch { /* counter is best-effort — never block the turn */ }
1417
- }
1418
- /** Stash for the memory-reflex efficacy capture — the raw hit content injected THIS turn, so
1419
- * the Stop hook can score the response's overlap against it (the `used` numerator). Keyed by
1420
- * session (Stop has the short id); carries projectId so Stop needn't re-derive it. Written at
1421
- * inject-time, consumed + deleted at Stop. adr: adr/memory-reflex.md */
1422
- function memoryReflexPendingPath(short) {
1423
- const home = process.env.HOME || process.env.USERPROFILE || '';
1424
- return path.join(home, '.greprag', 'state', `memory-reflex-pending-${short}.json`);
1425
- }
1426
- function stashMemoryInjection(short, projectId, hitText, turnCount) {
1427
- try {
1428
- const file = memoryReflexPendingPath(short);
1429
- const dir = path.dirname(file);
1430
- if (!fs.existsSync(dir))
1431
- fs.mkdirSync(dir, { recursive: true });
1432
- fs.writeFileSync(file, JSON.stringify({ projectId, hitText, turnCount }) + '\n');
1433
- }
1434
- catch { /* best-effort — a missed stash just means no efficacy sample this turn */ }
1435
- }
1436
- /** Read + DELETE the pending injection stash (consume-once, so a turn with no injection never
1437
- * reuses a stale sample). Returns null on the common no-stash path. */
1438
- function consumeMemoryInjection(short) {
1439
- try {
1440
- const file = memoryReflexPendingPath(short);
1441
- const raw = fs.readFileSync(file, 'utf-8');
1442
- try {
1443
- fs.unlinkSync(file);
1444
- }
1445
- catch { /* already gone — fine */ }
1446
- const p = JSON.parse(raw);
1447
- if (!p || typeof p.projectId !== 'string' || typeof p.hitText !== 'string')
1448
- return null;
1449
- return { projectId: p.projectId, hitText: p.hitText };
1450
- }
1451
- catch {
1452
- return null; // no stash this turn (the common case)
1453
- }
1454
- }
1455
- /** Bump the memory-reflex EFFICACY numerator (the response drew on the injection) — best-effort. */
1456
- function recordMemoryUsed(projectId) {
1457
- try {
1458
- const file = memoryReflexStatsPath(projectId);
1459
- let prior = null;
1460
- try {
1461
- prior = JSON.parse(fs.readFileSync(file, 'utf-8'));
1462
- }
1463
- catch { /* none yet */ }
1464
- const next = (0, memory_reflex_1.tallyMemoryUsed)(prior);
1465
- const dir = path.dirname(file);
1466
- if (!fs.existsSync(dir))
1467
- fs.mkdirSync(dir, { recursive: true });
1468
- fs.writeFileSync(file, JSON.stringify(next, null, 2) + '\n');
1469
- }
1470
- catch { /* counter is best-effort */ }
1471
- }
1472
- /** Local cache of the SERVER's auto-quiet verdict for this project (the
1473
- * mechanicKilled() analog for the memory-reflex). The Flash-Lite efficacy judge
1474
- * decides the threshold server-side and ships the boolean on the /v1/memory/query
1475
- * response; the hook caches it here and `memoryReflexQuieted` honors it with a
1476
- * TTL (quietCacheSaysSilent). Beside the fire-counter under ~/.greprag/state. */
1477
- function memoryReflexQuietPath(projectId) {
1478
- const home = process.env.HOME || process.env.USERPROFILE || '';
1479
- return path.join(home, '.greprag', 'state', `memory-reflex-quiet-${projectId}.json`);
1480
- }
1481
- /** Has the server's efficacy judge QUIETED this project's memory-reflex (low
1482
- * used÷injected over a meaningful sample)? Reads the local cache; honors it only
1483
- * while fresh (TTL) so a recovered reflex re-probes instead of staying silenced.
1484
- * Fail-quiet: any error reads as NOT quieted (the reflex keeps working) —
1485
- * mirrors mechanicKilled()'s fail-open posture. */
1486
- function memoryReflexQuieted(projectId) {
1487
- try {
1488
- const raw = fs.readFileSync(memoryReflexQuietPath(projectId), 'utf-8');
1489
- return (0, memory_reflex_1.quietCacheSaysSilent)(JSON.parse(raw), Date.now());
1490
- }
1491
- catch {
1492
- return false;
1493
- }
1494
- }
1495
- /** Cache the server's auto-quiet verdict from a /v1/memory/query response — written
1496
- * whenever the reflex actually probes (fired the search). Best-effort. */
1497
- function cacheMemoryReflexQuiet(projectId, quiet) {
1498
- try {
1499
- const file = memoryReflexQuietPath(projectId);
1500
- const dir = path.dirname(file);
1501
- if (!fs.existsSync(dir))
1502
- fs.mkdirSync(dir, { recursive: true });
1503
- const payload = { quiet, cachedAt: new Date().toISOString() };
1504
- fs.writeFileSync(file, JSON.stringify(payload) + '\n');
1505
- }
1506
- catch { /* best-effort — a missed cache just means the reflex re-probes next turn */ }
1507
- }
1508
1383
  /** The reminder-interrupt registry CONTAINER call (docs/reminder-interrupt.md §Registry
1509
1384
  * spec) — invoked from the proven-registered `notify` hook (claude-code path). Assembles
1510
1385
  * the live env (the ONLY i/o: armed via isLocallyArmed, this session's own unread, the
1511
1386
  * Stop-stashed stress/turnCount, the front-desk envelope notice) and fires every module
1512
1387
  * whose detector reports deficient: watcher-arm (unarmed → nag if a peer messaged you, else
1513
- * nudge; silent when armed), the stress-gated mechanic-friction, front-desk (a soft
1514
- * "you've got mail" when a new envelope landed), and memory-reflex (auto-surfaced episodic
1515
- * memory when THIS prompt signals recall an inline, time-capped, confidence-gated search).
1388
+ * nudge; silent when armed), the stress-gated mechanic-friction, and front-desk (a soft
1389
+ * "you've got mail" when a new envelope landed). The memory PRIMER announces at SessionStart;
1390
+ * the auto-inject was dropped (knowledge injection, not a reminder docs/reminder-interrupt.md).
1516
1391
  * The announce-only modules (setup-warning / checkpoint / assistant-doctrine) stay silent per
1517
1392
  * turn — their env signals are SessionStart-only. Stacking; emitted once. Fail-open — any miss
1518
1393
  * → silent. The Mechanic kill switch drops ONLY the mechanic module; the rest keep working. */
@@ -1539,41 +1414,22 @@ async function injectReminders(input, cfg, short) {
1539
1414
  // null when nothing new. This is the network call the retired `mail` hook used to make;
1540
1415
  // it moves here so the whole announce/reminder surface rides ONE detector loop.
1541
1416
  const frontDeskNotice = await (0, front_desk_mail_1.buildFrontDeskNotice)(cfg.apiUrl, cfg.apiKey);
1542
- // Memory-reflex (keyword layer) recall intent in THIS prompt fires an inline,
1543
- // time-capped, project-scoped search; a hit above the confidence gate is surfaced
1544
- // inline (top 1-2, capped) so the agent sees it AS it forms its reply. A miss stays
1545
- // silent (the search result is its own precision filter no banner-blindness). Every
1546
- // fire is tallied for the /mechanic excessiveness monitor. Gated on recall-intent, so
1547
- // most turns skip the search entirely. docs/reminder-interrupt.md (memory-reflex).
1548
- let memoryHit = null;
1549
- const prompt = typeof input.prompt === 'string' ? input.prompt : '';
1550
- // Pick the memory query: recall-intent in THIS prompt fires an inline search. One
1551
- // injection per turn. (The self-report [[recall:]] marker was removed 2026-06-22.)
1552
- let memQuery = null;
1553
- const memTrigger = 'keyword';
1554
- if ((0, memory_reflex_1.hasRecallIntent)(prompt)) {
1555
- memQuery = prompt.slice(0, 500);
1556
- }
1557
- // Auto-quiet gate (adr/memory-reflex.md): if the server's Flash-Lite efficacy judge has
1558
- // QUIETED this project's reflex (low used÷injected over a meaningful sample), skip the search
1559
- // entirely — the cached verdict expires on a TTL so a recovered reflex re-probes. mechanicKilled() analog.
1560
- if (anchor.projectId && memQuery && !memoryReflexQuieted(anchor.projectId)) {
1561
- const { hits, quiet } = await fetchMemoryHits(cfg.apiUrl, cfg.apiKey, anchor.projectId, memQuery);
1562
- cacheMemoryReflexQuiet(anchor.projectId, quiet); // refresh the verdict from this probe
1563
- memoryHit = (0, memory_reflex_1.buildMemoryInjection)(hits);
1564
- recordMemoryFire(anchor.projectId, memTrigger, !!memoryHit, turnCount);
1565
- // Stash what was injected so the Stop hook can score whether the response used it
1566
- // (the efficacy numerator). Only when something was actually surfaced.
1567
- if (memoryHit)
1568
- stashMemoryInjection(short, anchor.projectId, (0, memory_reflex_1.gatedHitText)(hits), turnCount);
1569
- }
1417
+ // NOTE: the per-turn memory AUTO-INJECT (recall-intent search surface a hit) was
1418
+ // DROPPED 2026-06-22. It was knowledge-injection, NOT a behavioral reminder a
1419
+ // different (undeveloped) system that was jammed into this registry for lack of a slot.
1420
+ // It also collided with the agent's own better-targeted `greprag memory search` (taught
1421
+ // by the memory primer). The memory PRIMER (the announce) stays; only the auto-inject is
1422
+ // gone. docs/reminder-interrupt.md (the landed map: reminder ≠ knowledge injection).
1570
1423
  const env = {
1571
1424
  short, turnCount, stress, armed, sessionUnread,
1572
1425
  ownerPid, alias: (0, session_id_1.readIdentityAlias)(), assistant,
1573
- frontDeskNotice, memoryHit,
1426
+ frontDeskNotice,
1574
1427
  };
1575
- // Kill switch drops ONLY the mechanic module watcher-arm keeps working.
1576
- const reg = mechanicKilled() ? reminder_registry_1.REGISTRY.filter(m => m.id !== 'mechanic-friction') : reminder_registry_1.REGISTRY;
1428
+ // UserPromptSubmit evaluates the prompt-source modules only (command-source
1429
+ // modules the collision Match run at PreToolUse). Kill switch drops ONLY
1430
+ // the mechanic module — watcher-arm keeps working.
1431
+ const base = (0, reminder_registry_1.promptModules)();
1432
+ const reg = mechanicKilled() ? base.filter(m => m.id !== 'mechanic-friction') : base;
1577
1433
  const fired = (0, reminder_registry_1.collectReminders)(env, reg);
1578
1434
  if (!fired.length)
1579
1435
  return;
@@ -1753,7 +1609,7 @@ function validateChip(title, prompt) {
1753
1609
  * adr/spawn-task-hook-mode.md 2026-05-27 entry — Tier 2 augmentation
1754
1610
  * attempt). `permissionDecision: deny` IS honored, so the validator
1755
1611
  * catches missing Block 1 / Block 2 markers; the agent re-composes the
1756
- * call with the templates from `~/.claude/docs/chip-spawn.md`. */
1612
+ * call with the templates from `greprag load chip-spawn`. */
1757
1613
  function handlePreSpawnCheck(input) {
1758
1614
  if (input.tool_name !== 'mcp__ccd_session__spawn_task')
1759
1615
  return;
@@ -1764,7 +1620,7 @@ function handlePreSpawnCheck(input) {
1764
1620
  return;
1765
1621
  const reason = `Chip prompt rejected by greprag PreToolUse validator. Fix the following and re-call spawn_task:\n - ` +
1766
1622
  violations.join('\n - ') +
1767
- `\n\nFull conventions: ~/.claude/docs/chip-spawn.md.`;
1623
+ `\n\nFull conventions: run \`greprag load chip-spawn\`.`;
1768
1624
  process.stdout.write(JSON.stringify({
1769
1625
  hookSpecificOutput: {
1770
1626
  hookEventName: 'PreToolUse',
@@ -1835,7 +1691,8 @@ async function coordinateGate(input) {
1835
1691
  if (!short)
1836
1692
  return; // no session id → silent
1837
1693
  // Local + cheap: classify BEFORE any network so non-risky calls cost nothing.
1838
- const trigger = (0, coordinate_gate_1.triggerFromPreToolUse)(input.tool_name || '', (input.tool_input || {}));
1694
+ const toolInput = (input.tool_input || {});
1695
+ const trigger = (0, coordinate_gate_1.triggerFromPreToolUse)(input.tool_name || '', toolInput);
1839
1696
  if (!trigger)
1840
1697
  return; // not a risky action → silent
1841
1698
  let anchor;
@@ -1847,7 +1704,8 @@ async function coordinateGate(input) {
1847
1704
  }
1848
1705
  if (!anchor.projectId)
1849
1706
  return; // unanchored → nothing to match
1850
- const directive = await (0, coordinate_gate_1.runCoordinateGate)(trigger, {
1707
+ // I/O stays in the hook: fresh peer read off the hot path → env.collisionPeers.
1708
+ const peers = await (0, coordinate_gate_1.resolveCollisionPeers)({
1851
1709
  short,
1852
1710
  projectId: anchor.projectId,
1853
1711
  projectName: anchor.projectName,
@@ -1855,15 +1713,26 @@ async function coordinateGate(input) {
1855
1713
  apiKey: cfg.apiKey,
1856
1714
  alias: (0, session_id_1.readIdentityAlias)(),
1857
1715
  });
1858
- if (!directive)
1716
+ if (peers.length === 0)
1859
1717
  return; // no live peer in this repo → proceed
1718
+ // Route through the registry: build the command-read env, run the command-source
1719
+ // modules (the collision Match). The standalone gate is now a registry citizen.
1720
+ const env = {
1721
+ short, turnCount: 0, stress: 0, armed: false, sessionUnread: 0,
1722
+ alias: (0, session_id_1.readIdentityAlias)(),
1723
+ toolCommand: typeof toolInput.command === 'string' ? toolInput.command : '',
1724
+ collisionPeers: peers,
1725
+ };
1726
+ const fired = (0, reminder_registry_1.collectReminders)(env, (0, reminder_registry_1.commandModules)());
1727
+ if (!fired.length)
1728
+ return;
1860
1729
  // EFFECT: inject as additionalContext — advisory heads-up, NO permission pause.
1861
1730
  // `ask` was too intrusive (prompted on every git op while peers were live); inject
1862
1731
  // keeps the agent aware without a manual-allow. adr: adr/monitor-resilience.md
1863
1732
  process.stdout.write(JSON.stringify({
1864
1733
  hookSpecificOutput: {
1865
1734
  hookEventName: input.hook_event_name || 'PreToolUse',
1866
- additionalContext: directive,
1735
+ additionalContext: fired.map(f => f.line).join('\n\n'),
1867
1736
  },
1868
1737
  }) + '\n');
1869
1738
  }