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.
- 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/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 -104
- package/dist/commands/memory-reflex.js +23 -215
- package/dist/commands/memory-reflex.js.map +1 -1
- package/dist/commands/memory.js +70 -0
- package/dist/commands/memory.js.map +1 -1
- package/dist/commands/reminder-registry.d.ts +12 -1
- package/dist/commands/reminder-registry.js +46 -6
- package/dist/commands/reminder-registry.js.map +1 -1
- package/dist/commands/reminder-types.d.ts +28 -10
- 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 +115 -246
- package/dist/hook.js.map +1 -1
- package/dist/index.js +10 -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
|
@@ -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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1177
|
-
//
|
|
1178
|
-
//
|
|
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)
|
|
1515
|
-
*
|
|
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
|
-
//
|
|
1543
|
-
//
|
|
1544
|
-
//
|
|
1545
|
-
//
|
|
1546
|
-
//
|
|
1547
|
-
//
|
|
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,
|
|
1426
|
+
frontDeskNotice,
|
|
1574
1427
|
};
|
|
1575
|
-
//
|
|
1576
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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:
|
|
1735
|
+
additionalContext: fired.map(f => f.line).join('\n\n'),
|
|
1867
1736
|
},
|
|
1868
1737
|
}) + '\n');
|
|
1869
1738
|
}
|