greprag 5.49.8 → 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.
Files changed (34) 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/reminder-registry.d.ts +12 -1
  22. package/dist/commands/reminder-registry.js +46 -4
  23. package/dist/commands/reminder-registry.js.map +1 -1
  24. package/dist/commands/reminder-types.d.ts +28 -0
  25. package/dist/commands/version-reminder.d.ts +15 -0
  26. package/dist/commands/version-reminder.js +34 -0
  27. package/dist/commands/version-reminder.js.map +1 -0
  28. package/dist/hook.js +111 -219
  29. package/dist/hook.js.map +1 -1
  30. package/dist/index.js +6 -0
  31. package/dist/index.js.map +1 -1
  32. package/package.json +1 -1
  33. package/skill/templates/chip-leader.md +188 -0
  34. 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,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,
@@ -963,44 +938,6 @@ async function fetchSessionUnread(apiUrl, apiKey, short) {
963
938
  return 0;
964
939
  }
965
940
  }
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
941
  /** Identity-drift warning rate limiter.
1005
942
  *
1006
943
  * The drift warning ("stored ID ≠ git-derived ID, run `greprag doctor`") used
@@ -1075,6 +1012,78 @@ function writeRecapOutput(text, mode) {
1075
1012
  }
1076
1013
  process.stdout.write(text);
1077
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
+ }
1078
1087
  /** SessionStart — fetch recent episodic activity for this project and print
1079
1088
  * to stdout. Claude Code injects the printed text as session context. Codex
1080
1089
  * uses the same content via `hookSpecificOutput.additionalContext`. Fires
@@ -1193,6 +1202,9 @@ async function recap(input, mode = 'plain') {
1193
1202
  const assistantDoctrine = (0, project_anchor_1.isAssistantProject)(anchor)
1194
1203
  ? (0, assistant_doctrine_1.buildAssistantDoctrineContext)(cwd)
1195
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();
1196
1208
  // ── THE single SessionStart announce assembly (docs/reminder-interrupt.md §Registry
1197
1209
  // spec) ──────────────────────────────────────────────────────────────────────────────
1198
1210
  // Every agent-facing announce — setup-warning, front-desk, checkpoint, assistant-doctrine,
@@ -1217,6 +1229,7 @@ async function recap(input, mode = 'plain') {
1217
1229
  openCheckpoints,
1218
1230
  assistantDoctrine,
1219
1231
  corpusApiDocs,
1232
+ updateAvailable,
1220
1233
  };
1221
1234
  let announceReg = mechanicKilled() ? reminder_registry_1.REGISTRY.filter(m => m.id !== 'mechanic-friction') : reminder_registry_1.REGISTRY;
1222
1235
  if (!announceShort)
@@ -1390,129 +1403,14 @@ function recordFrictionFire(projectId, tier, turnCount) {
1390
1403
  }
1391
1404
  catch { /* counter is best-effort — never block the turn */ }
1392
1405
  }
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
1406
  /** The reminder-interrupt registry CONTAINER call (docs/reminder-interrupt.md §Registry
1509
1407
  * spec) — invoked from the proven-registered `notify` hook (claude-code path). Assembles
1510
1408
  * the live env (the ONLY i/o: armed via isLocallyArmed, this session's own unread, the
1511
1409
  * Stop-stashed stress/turnCount, the front-desk envelope notice) and fires every module
1512
1410
  * 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).
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).
1516
1414
  * The announce-only modules (setup-warning / checkpoint / assistant-doctrine) stay silent per
1517
1415
  * turn — their env signals are SessionStart-only. Stacking; emitted once. Fail-open — any miss
1518
1416
  * → silent. The Mechanic kill switch drops ONLY the mechanic module; the rest keep working. */
@@ -1539,41 +1437,22 @@ async function injectReminders(input, cfg, short) {
1539
1437
  // null when nothing new. This is the network call the retired `mail` hook used to make;
1540
1438
  // it moves here so the whole announce/reminder surface rides ONE detector loop.
1541
1439
  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
- }
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).
1570
1446
  const env = {
1571
1447
  short, turnCount, stress, armed, sessionUnread,
1572
1448
  ownerPid, alias: (0, session_id_1.readIdentityAlias)(), assistant,
1573
- frontDeskNotice, memoryHit,
1449
+ frontDeskNotice,
1574
1450
  };
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;
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;
1577
1456
  const fired = (0, reminder_registry_1.collectReminders)(env, reg);
1578
1457
  if (!fired.length)
1579
1458
  return;
@@ -1753,7 +1632,7 @@ function validateChip(title, prompt) {
1753
1632
  * adr/spawn-task-hook-mode.md 2026-05-27 entry — Tier 2 augmentation
1754
1633
  * attempt). `permissionDecision: deny` IS honored, so the validator
1755
1634
  * catches missing Block 1 / Block 2 markers; the agent re-composes the
1756
- * call with the templates from `~/.claude/docs/chip-spawn.md`. */
1635
+ * call with the templates from `greprag load chip-spawn`. */
1757
1636
  function handlePreSpawnCheck(input) {
1758
1637
  if (input.tool_name !== 'mcp__ccd_session__spawn_task')
1759
1638
  return;
@@ -1764,7 +1643,7 @@ function handlePreSpawnCheck(input) {
1764
1643
  return;
1765
1644
  const reason = `Chip prompt rejected by greprag PreToolUse validator. Fix the following and re-call spawn_task:\n - ` +
1766
1645
  violations.join('\n - ') +
1767
- `\n\nFull conventions: ~/.claude/docs/chip-spawn.md.`;
1646
+ `\n\nFull conventions: run \`greprag load chip-spawn\`.`;
1768
1647
  process.stdout.write(JSON.stringify({
1769
1648
  hookSpecificOutput: {
1770
1649
  hookEventName: 'PreToolUse',
@@ -1835,7 +1714,8 @@ async function coordinateGate(input) {
1835
1714
  if (!short)
1836
1715
  return; // no session id → silent
1837
1716
  // 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 || {}));
1717
+ const toolInput = (input.tool_input || {});
1718
+ const trigger = (0, coordinate_gate_1.triggerFromPreToolUse)(input.tool_name || '', toolInput);
1839
1719
  if (!trigger)
1840
1720
  return; // not a risky action → silent
1841
1721
  let anchor;
@@ -1847,7 +1727,8 @@ async function coordinateGate(input) {
1847
1727
  }
1848
1728
  if (!anchor.projectId)
1849
1729
  return; // unanchored → nothing to match
1850
- const directive = await (0, coordinate_gate_1.runCoordinateGate)(trigger, {
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)({
1851
1732
  short,
1852
1733
  projectId: anchor.projectId,
1853
1734
  projectName: anchor.projectName,
@@ -1855,15 +1736,26 @@ async function coordinateGate(input) {
1855
1736
  apiKey: cfg.apiKey,
1856
1737
  alias: (0, session_id_1.readIdentityAlias)(),
1857
1738
  });
1858
- if (!directive)
1739
+ if (peers.length === 0)
1859
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;
1860
1752
  // EFFECT: inject as additionalContext — advisory heads-up, NO permission pause.
1861
1753
  // `ask` was too intrusive (prompted on every git op while peers were live); inject
1862
1754
  // keeps the agent aware without a manual-allow. adr: adr/monitor-resilience.md
1863
1755
  process.stdout.write(JSON.stringify({
1864
1756
  hookSpecificOutput: {
1865
1757
  hookEventName: input.hook_event_name || 'PreToolUse',
1866
- additionalContext: directive,
1758
+ additionalContext: fired.map(f => f.line).join('\n\n'),
1867
1759
  },
1868
1760
  }) + '\n');
1869
1761
  }