greprag 5.46.0 → 5.48.0
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/arm-reminder.d.ts +25 -0
- package/dist/commands/arm-reminder.js +65 -0
- package/dist/commands/arm-reminder.js.map +1 -0
- package/dist/commands/assistant-reminder.d.ts +9 -0
- package/dist/commands/assistant-reminder.js +20 -0
- package/dist/commands/assistant-reminder.js.map +1 -0
- package/dist/commands/checkpoint-reminder.d.ts +14 -0
- package/dist/commands/checkpoint-reminder.js +50 -0
- package/dist/commands/checkpoint-reminder.js.map +1 -0
- package/dist/commands/friction-reminder.d.ts +92 -0
- package/dist/commands/friction-reminder.js +147 -0
- package/dist/commands/friction-reminder.js.map +1 -0
- package/dist/commands/frontdesk-reminder.d.ts +23 -0
- package/dist/commands/frontdesk-reminder.js +40 -0
- package/dist/commands/frontdesk-reminder.js.map +1 -0
- package/dist/commands/inbox-watch-supervisor.d.ts +25 -0
- package/dist/commands/inbox-watch-supervisor.js +112 -5
- package/dist/commands/inbox-watch-supervisor.js.map +1 -1
- package/dist/commands/init.js +31 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/memory-reflex.d.ts +115 -0
- package/dist/commands/memory-reflex.js +243 -0
- package/dist/commands/memory-reflex.js.map +1 -0
- package/dist/commands/reminder-registry.d.ts +24 -0
- package/dist/commands/reminder-registry.js +76 -0
- package/dist/commands/reminder-registry.js.map +1 -0
- package/dist/commands/reminder-types.d.ts +76 -0
- package/dist/commands/reminder-types.js +8 -0
- package/dist/commands/reminder-types.js.map +1 -0
- package/dist/commands/setup-reminder.d.ts +9 -0
- package/dist/commands/setup-reminder.js +20 -0
- package/dist/commands/setup-reminder.js.map +1 -0
- package/dist/hook.js +472 -120
- package/dist/hook.js.map +1 -1
- package/dist/opencode-plugin.js +68 -8
- package/dist/opencode-plugin.js.map +1 -1
- package/package.json +1 -1
- package/skill/templates/reflex-chip.md +58 -0
package/dist/hook.js
CHANGED
|
@@ -71,6 +71,13 @@ const opencode_plugin_helpers_1 = require("./opencode-plugin-helpers");
|
|
|
71
71
|
// Mechanic dispatcher (Chip B) — PreToolUse guard + matchset boot pull/refresh.
|
|
72
72
|
// docs/mechanic-repairs.md D1/D2/D5/D8; docs/mechanic-engine-contracts.md.
|
|
73
73
|
const guard_1 = require("./guard");
|
|
74
|
+
// Reminder-interrupt registry (docs/reminder-interrupt.md §Registry spec) — the
|
|
75
|
+
// detector model: each module detects a live deficiency, fires while it holds, and
|
|
76
|
+
// auto-silences when resolved (the arm-check pattern). The hook below assembles the
|
|
77
|
+
// live env (ALL I/O) and emits; the modules are pure.
|
|
78
|
+
const reminder_registry_1 = require("./commands/reminder-registry");
|
|
79
|
+
const friction_reminder_1 = require("./commands/friction-reminder");
|
|
80
|
+
const memory_reflex_1 = require("./commands/memory-reflex");
|
|
74
81
|
const worktree_state_1 = require("./worktree-state");
|
|
75
82
|
// Ingress-trigger bridge, Adapter A (LOCAL/state half) — the Stop hook computes
|
|
76
83
|
// the deterministic state values (stress / turnCount / keyword-seen) from the
|
|
@@ -778,6 +785,37 @@ async function store(input, source = 'claude-code') {
|
|
|
778
785
|
if (source === 'codex') {
|
|
779
786
|
turn.toolCalls.push(...(0, codex_hook_events_1.readCodexSubagentToolCalls)(input));
|
|
780
787
|
}
|
|
788
|
+
// Memory-reflex EFFICACY capture (adr/memory-reflex.md). If a memory injection was surfaced
|
|
789
|
+
// this turn: (1) score the FREE overlap proxy locally → bump the `used` numerator, and (2)
|
|
790
|
+
// carry the injection onto the turn POST so the server-side Flash-Lite judge (Chip A) can
|
|
791
|
+
// score it accurately. Consume-once: the stash is always cleared, so a turn with no injection
|
|
792
|
+
// never reuses a stale sample. adr: adr/memory-reflex.md
|
|
793
|
+
let memoryInjectionForStore = null;
|
|
794
|
+
try {
|
|
795
|
+
const effShort = (0, session_id_1.truncateSessionId)(input.session_id);
|
|
796
|
+
if (effShort) {
|
|
797
|
+
const pending = consumeMemoryInjection(effShort);
|
|
798
|
+
if (pending) {
|
|
799
|
+
memoryInjectionForStore = pending.hitText;
|
|
800
|
+
if ((0, memory_reflex_1.injectionWasUsed)(pending.hitText, turn.userPrompt, turn.agentResponse)) {
|
|
801
|
+
recordMemoryUsed(pending.projectId);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
catch { /* efficacy capture is best-effort — never block the turn */ }
|
|
807
|
+
// Self-report scan (P1b, adr/memory-reflex.md). If the agent emitted a [[recall: X]] marker
|
|
808
|
+
// (taught by the memory announce) it's flagging its OWN context gap → stash the topic so the
|
|
809
|
+
// NEXT turn's notify pulls + injects it (the self-report trigger). Best-effort.
|
|
810
|
+
try {
|
|
811
|
+
const srShort = (0, session_id_1.truncateSessionId)(input.session_id);
|
|
812
|
+
if (srShort && anchor.projectId) {
|
|
813
|
+
const topic = (0, memory_reflex_1.extractRecallMarker)(turn.agentResponse);
|
|
814
|
+
if (topic)
|
|
815
|
+
stashSelfReport(srShort, anchor.projectId, topic);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
catch { /* best-effort — never block the turn */ }
|
|
781
819
|
// Empty turn — nothing to capture.
|
|
782
820
|
if (!turn.userPrompt && !turn.agentResponse && turn.toolCalls.length === 0)
|
|
783
821
|
return;
|
|
@@ -814,6 +852,12 @@ async function store(input, source = 'claude-code') {
|
|
|
814
852
|
status: turn.status,
|
|
815
853
|
userPrompt,
|
|
816
854
|
agentResponse,
|
|
855
|
+
// The memory injection surfaced this turn (when any) — the SEAM the Flash-Lite efficacy
|
|
856
|
+
// judge (Chip A) consumes server-side at ingest to score used÷injected. Scrubbed + capped
|
|
857
|
+
// like the other text fields. Omitted entirely on the common no-injection turn.
|
|
858
|
+
...(memoryInjectionForStore
|
|
859
|
+
? { memoryInjection: capField((0, secret_scrubber_1.scrubString)(memoryInjectionForStore, redaction)) }
|
|
860
|
+
: {}),
|
|
817
861
|
toolCalls,
|
|
818
862
|
filesTouched: turn.filesTouched,
|
|
819
863
|
artifacts: artifactRefs,
|
|
@@ -822,6 +866,20 @@ async function store(input, source = 'claude-code') {
|
|
|
822
866
|
provenance,
|
|
823
867
|
});
|
|
824
868
|
}
|
|
869
|
+
// ---------- Main ---------------------------------------------------------
|
|
870
|
+
// ---------- Recap (SessionStart) ------------------------------------------
|
|
871
|
+
// `ByPeriodRow`, `stripRecapNoise`, `fmtHoursAgo`, and the recap body
|
|
872
|
+
// renderer (`buildRecapBody`) are now imported from `./opencode-plugin-helpers`
|
|
873
|
+
// — see adr/opencode-monitor-relay.md 2026-06-06 (g). The recap content
|
|
874
|
+
// pushed to the system prompt is the same for Claude/Codex and the opencode
|
|
875
|
+
// plugin; per-harness wrappers (preamble, output mode, transform-hook push)
|
|
876
|
+
// stay local.
|
|
877
|
+
/** Fetch a slim summary of open checkpoints for the given project. Returns
|
|
878
|
+
* up to `limit` rows so the checkpoint module can compute the overflow
|
|
879
|
+
* (3-most-recent + "N more" hint). Returns [] on any error — the hook
|
|
880
|
+
* must never block on a checkpoint query failure. (OpenCheckpointSlim +
|
|
881
|
+
* renderCheckpointHint now live in commands/checkpoint-reminder.ts; the hook
|
|
882
|
+
* owns only this fetch.) */
|
|
825
883
|
async function fetchOpenCheckpoints(apiUrl, apiKey, projectId, limit = 10) {
|
|
826
884
|
try {
|
|
827
885
|
const url = `${apiUrl}/v1/checkpoints/${encodeURIComponent(projectId)}`
|
|
@@ -838,33 +896,6 @@ async function fetchOpenCheckpoints(apiUrl, apiKey, projectId, limit = 10) {
|
|
|
838
896
|
return [];
|
|
839
897
|
}
|
|
840
898
|
}
|
|
841
|
-
/** Render a YYYY-MM-DD date in UTC for the session-start hint. */
|
|
842
|
-
function fmtCheckpointDate(iso) {
|
|
843
|
-
return iso.slice(0, 10);
|
|
844
|
-
}
|
|
845
|
-
/** Format the open-checkpoints hint block. Shape per design doc § Hint
|
|
846
|
-
* placement: 3-most-recent rows + a single overflow line when there are
|
|
847
|
-
* more. Returns null when there are zero open checkpoints — caller
|
|
848
|
-
* treats it as silent. */
|
|
849
|
-
function renderCheckpointHint(open, projectName) {
|
|
850
|
-
if (open.length === 0)
|
|
851
|
-
return null;
|
|
852
|
-
// Server returns DESC by created_at; take the 3 most recent.
|
|
853
|
-
const top = open.slice(0, 3);
|
|
854
|
-
const overflow = open.length - top.length;
|
|
855
|
-
const lines = [];
|
|
856
|
-
lines.push(`[${open.length} checkpoint${open.length === 1 ? '' : 's'} open in ${projectName}:`);
|
|
857
|
-
for (const cp of top) {
|
|
858
|
-
lines.push(` · ${cp.nodeId} ${cp.title} (since ${fmtCheckpointDate(cp.createdAt)})`);
|
|
859
|
-
}
|
|
860
|
-
if (overflow > 0) {
|
|
861
|
-
lines.push(` · ${overflow} more — greprag checkpoint list]`);
|
|
862
|
-
}
|
|
863
|
-
else {
|
|
864
|
-
lines.push(` View one: greprag checkpoint show <id>]`);
|
|
865
|
-
}
|
|
866
|
-
return lines.join('\n');
|
|
867
|
-
}
|
|
868
899
|
/** Fetch unread inbox count for the given project context. Project-scoped
|
|
869
900
|
* count includes tenant-level messages (mail addressed to the user, not
|
|
870
901
|
* any specific project, still shows up). Returns 0 on any error so the
|
|
@@ -906,6 +937,63 @@ async function fetchFrontDeskUnread(apiUrl, apiKey) {
|
|
|
906
937
|
return 0;
|
|
907
938
|
}
|
|
908
939
|
}
|
|
940
|
+
/** Fetch this session's OWN unread count — messages addressed to `to_session_id =
|
|
941
|
+
* <short>` (mail a peer or the operator sent to THIS session). Safe + non-eavesdrop:
|
|
942
|
+
* reads only this session's mail, NOT the project-wide count the v5.6.1 removal retired
|
|
943
|
+
* (that count betrayed a sibling session's private DMs). Powers the watcher-arm module's
|
|
944
|
+
* grounded "a peer messaged you" nag. Returns 0 on any error. adr: adr/session-id-awareness.md */
|
|
945
|
+
async function fetchSessionUnread(apiUrl, apiKey, short) {
|
|
946
|
+
try {
|
|
947
|
+
const res = await fetch(`${apiUrl}/v1/inbox?count_only=1&session_id=${encodeURIComponent(short)}`, {
|
|
948
|
+
headers: { 'Authorization': `Bearer ${apiKey}` },
|
|
949
|
+
});
|
|
950
|
+
if (!res.ok)
|
|
951
|
+
return 0;
|
|
952
|
+
const data = await res.json();
|
|
953
|
+
return data.unread_count || 0;
|
|
954
|
+
}
|
|
955
|
+
catch {
|
|
956
|
+
return 0;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
/** Fetch top episodic-memory hits for a query (the memory-reflex inline path), project-
|
|
960
|
+
* scoped, with a HARD timeout so a slow search never blocks the turn. POSTs the same
|
|
961
|
+
* `/v1/memory/query` the CLI `memory search` uses, with the de-pollution `session-turn`
|
|
962
|
+
* provenance filter. Returns [] on any error/abort → the reflex stays silent. The
|
|
963
|
+
* confidence gate + framing live in buildMemoryInjection (commands/memory-reflex.ts).
|
|
964
|
+
* adr: adr/memory-reflex.md */
|
|
965
|
+
async function fetchMemoryHits(apiUrl, apiKey, projectId, query, limit = 3, timeoutMs = 2000) {
|
|
966
|
+
const ctl = new AbortController();
|
|
967
|
+
const timer = setTimeout(() => ctl.abort(), timeoutMs);
|
|
968
|
+
try {
|
|
969
|
+
const res = await fetch(`${apiUrl}/v1/memory/query`, {
|
|
970
|
+
method: 'POST',
|
|
971
|
+
headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
|
|
972
|
+
body: JSON.stringify({ query, limit, projectId, filters: { provenance: 'session-turn' } }),
|
|
973
|
+
signal: ctl.signal,
|
|
974
|
+
});
|
|
975
|
+
if (!res.ok)
|
|
976
|
+
return { hits: [], quiet: false };
|
|
977
|
+
const data = await res.json();
|
|
978
|
+
// The server's Flash-Lite efficacy verdict rides the same response (adr/memory-reflex.md).
|
|
979
|
+
const quiet = data.memoryReflexQuiet === true;
|
|
980
|
+
if (!data.ok || !Array.isArray(data.nodes))
|
|
981
|
+
return { hits: [], quiet };
|
|
982
|
+
return {
|
|
983
|
+
hits: data.nodes.map(n => ({
|
|
984
|
+
content: n.content, score: n.score, confidence: n.confidence ?? null,
|
|
985
|
+
shape: n.shape ?? null, createdAt: n.createdAt ?? null, projectName: n.projectName ?? null,
|
|
986
|
+
})),
|
|
987
|
+
quiet,
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
catch {
|
|
991
|
+
return { hits: [], quiet: false };
|
|
992
|
+
}
|
|
993
|
+
finally {
|
|
994
|
+
clearTimeout(timer);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
909
997
|
/** Identity-drift warning rate limiter.
|
|
910
998
|
*
|
|
911
999
|
* The drift warning ("stored ID ≠ git-derived ID, run `greprag doctor`") used
|
|
@@ -1070,40 +1158,51 @@ async function recap(input, mode = 'plain') {
|
|
|
1070
1158
|
// (and then read) another session's session-targeted DM. Live inbox now
|
|
1071
1159
|
// belongs exclusively to Monitor watchers; the agent learns about its own
|
|
1072
1160
|
// mail via the watcher's session-scoped stream.
|
|
1073
|
-
// Open-checkpoints
|
|
1074
|
-
//
|
|
1161
|
+
// Open-checkpoints — checkpoints are operator-triggered landmarks, not message-shape
|
|
1162
|
+
// inbox traffic, no privacy issue. The hook owns ONLY this fetch; the checkpoint module
|
|
1163
|
+
// (commands/checkpoint-reminder.ts) renders the list → hint block in its announce.
|
|
1075
1164
|
const openCheckpoints = await fetchOpenCheckpoints(cfg.apiUrl, cfg.apiKey, anchor.projectId, 10);
|
|
1076
|
-
|
|
1077
|
-
//
|
|
1078
|
-
//
|
|
1079
|
-
//
|
|
1080
|
-
//
|
|
1081
|
-
// deduped. This is NOT the v5.6.1 eavesdrop vector — see fetchFrontDeskUnread.
|
|
1082
|
-
// (Suppressing it when a live receptionist is attached is a clean follow-up.)
|
|
1083
|
-
// adr: adr/address-grammar.md (2026-06-10), docs/front-desk-switchboard.md §5
|
|
1165
|
+
// Front-desk awareness — a discreet count of cold opens + inbound email waiting at the
|
|
1166
|
+
// shared tenant front desk, so a stranger's first contact isn't stranded until someone
|
|
1167
|
+
// runs `greprag inbox`. Content is NEVER injected — just the count + the door, formatted
|
|
1168
|
+
// by the front-desk module's announce. NOT the v5.6.1 eavesdrop vector — see
|
|
1169
|
+
// fetchFrontDeskUnread. adr: adr/address-grammar.md (2026-06-10), docs/front-desk-switchboard.md §5
|
|
1084
1170
|
const frontDeskUnread = await fetchFrontDeskUnread(cfg.apiUrl, cfg.apiKey);
|
|
1085
|
-
const frontDeskHint = frontDeskUnread > 0
|
|
1086
|
-
? `[📬 ${frontDeskUnread} message${frontDeskUnread === 1 ? '' : 's'} at the front desk — \`greprag inbox --front-desk\` to read]`
|
|
1087
|
-
: null;
|
|
1088
1171
|
// Assistant doctrine auto-load — fires ONLY for the flagged assistant project
|
|
1089
|
-
// (isAssistantProject), so a normal project sees zero change.
|
|
1090
|
-
//
|
|
1091
|
-
// guaranteeing the loop loads on every session start. adr: adr/assistant-role.md
|
|
1172
|
+
// (isAssistantProject), so a normal project sees zero change. The hook owns the
|
|
1173
|
+
// doctrine-file read; the assistant-doctrine module routes the text. adr: adr/assistant-role.md
|
|
1092
1174
|
const assistantDoctrine = (0, project_anchor_1.isAssistantProject)(anchor)
|
|
1093
1175
|
? (0, assistant_doctrine_1.buildAssistantDoctrineContext)(cwd)
|
|
1094
1176
|
: null;
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1177
|
+
// ── THE single SessionStart announce assembly (docs/reminder-interrupt.md §Registry
|
|
1178
|
+
// spec) ──────────────────────────────────────────────────────────────────────────────
|
|
1179
|
+
// Every agent-facing announce — setup-warning, front-desk, checkpoint, assistant-doctrine,
|
|
1180
|
+
// watcher-arm, mechanic — is now a registry module. The hook did the I/O above; here it
|
|
1181
|
+
// stuffs the results into ONE env and collectAnnounces renders them in registry order.
|
|
1182
|
+
// Loaded once + re-fired on compact (recap matcher ''). The Mechanic kill switch drops
|
|
1183
|
+
// ONLY the mechanic announce; a missing session id drops ONLY watcher-arm.
|
|
1184
|
+
const announceShort = (0, session_id_1.truncateSessionId)(input.session_id) || '';
|
|
1185
|
+
const announceEnv = {
|
|
1186
|
+
short: announceShort, turnCount: 0, stress: 0, armed: false, sessionUnread: 0,
|
|
1187
|
+
ownerPid: announceShort ? (0, watcher_registry_1.resolveClaudeOwnerPid)(announceShort) : null,
|
|
1188
|
+
alias: (0, session_id_1.readIdentityAlias)(),
|
|
1189
|
+
assistant: (() => { try {
|
|
1190
|
+
return (0, project_anchor_1.isAssistantProject)(anchor);
|
|
1191
|
+
}
|
|
1192
|
+
catch {
|
|
1193
|
+
return false;
|
|
1194
|
+
} })(),
|
|
1195
|
+
projectName: anchor.projectName,
|
|
1196
|
+
setupWarning,
|
|
1197
|
+
frontDeskUnread,
|
|
1198
|
+
openCheckpoints,
|
|
1199
|
+
assistantDoctrine,
|
|
1106
1200
|
};
|
|
1201
|
+
let announceReg = mechanicKilled() ? reminder_registry_1.REGISTRY.filter(m => m.id !== 'mechanic-friction') : reminder_registry_1.REGISTRY;
|
|
1202
|
+
if (!announceShort)
|
|
1203
|
+
announceReg = announceReg.filter(m => m.id !== 'watcher-arm');
|
|
1204
|
+
const announceBlock = (0, reminder_registry_1.collectAnnounces)(announceEnv, announceReg).join('\n\n') || null;
|
|
1205
|
+
const preamble = () => (announceBlock ? announceBlock + '\n' : '');
|
|
1107
1206
|
// Per-project opt-out — agent flips session_start_recap=false to suppress
|
|
1108
1207
|
// the episodic injection for a specific project without disabling the
|
|
1109
1208
|
// hook globally. Setup warning + checkpoint hint still fire above.
|
|
@@ -1131,20 +1230,8 @@ async function recap(input, mode = 'plain') {
|
|
|
1131
1230
|
return;
|
|
1132
1231
|
}
|
|
1133
1232
|
const parts = [];
|
|
1134
|
-
if (
|
|
1135
|
-
parts.push(
|
|
1136
|
-
parts.push('');
|
|
1137
|
-
}
|
|
1138
|
-
if (frontDeskHint) {
|
|
1139
|
-
parts.push(frontDeskHint);
|
|
1140
|
-
parts.push('');
|
|
1141
|
-
}
|
|
1142
|
-
if (checkpointHint) {
|
|
1143
|
-
parts.push(checkpointHint);
|
|
1144
|
-
parts.push('');
|
|
1145
|
-
}
|
|
1146
|
-
if (assistantDoctrine) {
|
|
1147
|
-
parts.push(assistantDoctrine);
|
|
1233
|
+
if (announceBlock) {
|
|
1234
|
+
parts.push(announceBlock);
|
|
1148
1235
|
parts.push('');
|
|
1149
1236
|
}
|
|
1150
1237
|
parts.push(body);
|
|
@@ -1236,69 +1323,328 @@ async function notify(input, source = 'claude-code') {
|
|
|
1236
1323
|
writeAdditionalContext('UserPromptSubmit', context);
|
|
1237
1324
|
return;
|
|
1238
1325
|
}
|
|
1239
|
-
//
|
|
1240
|
-
//
|
|
1241
|
-
//
|
|
1242
|
-
//
|
|
1243
|
-
//
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
// into the arm command so the reaper checks "owner alive?" not "ancestry
|
|
1253
|
-
// reachable?". Only runs on unarmed turns (we returned above if armed).
|
|
1254
|
-
// adr: adr/monitor-resilience.md
|
|
1255
|
-
const ownerPid = (0, watcher_registry_1.resolveClaudeOwnerPid)(short);
|
|
1256
|
-
// Assistant project → arm an elevated watcher (`--assistant`) so the session
|
|
1257
|
-
// wakes on inbound tenant email, not just its own DMs. Scoped strictly to the
|
|
1258
|
-
// flagged project; every other project arms a stock watcher. Fail-open: an
|
|
1259
|
-
// anchor read failure just yields a normal arm. adr: adr/assistant-role.md
|
|
1260
|
-
let assistant = false;
|
|
1326
|
+
// Reminder-interrupt registry container (docs/reminder-interrupt.md §Registry spec):
|
|
1327
|
+
// a detector loop over the watcher-arm + mechanic-friction modules. Runs HERE — in the
|
|
1328
|
+
// proven-registered `notify` hook — so arming never depends on a freshly-merged
|
|
1329
|
+
// subcommand reaching the operator's settings. Fires whatever's deficient (stacking),
|
|
1330
|
+
// and auto-silences each module the instant its deficiency resolves.
|
|
1331
|
+
await injectReminders(input, cfg, short);
|
|
1332
|
+
}
|
|
1333
|
+
/** Has the operator hit the Mechanic kill switch (`mechanic off` → the D8 panic
|
|
1334
|
+
* switch file the guard dispatcher also checks first)? When set, the active-
|
|
1335
|
+
* Mechanic pair (SessionStart announce + per-turn friction reminder) stays quiet,
|
|
1336
|
+
* so the same switch that quiets the dispatcher quiets this pair too. Fail-quiet:
|
|
1337
|
+
* any fs error reads as NOT killed (the pair keeps working). */
|
|
1338
|
+
function mechanicKilled() {
|
|
1261
1339
|
try {
|
|
1262
|
-
|
|
1263
|
-
}
|
|
1264
|
-
catch {
|
|
1265
|
-
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
*
|
|
1270
|
-
*
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1340
|
+
return fs.existsSync((0, guard_1.killSwitchPath)());
|
|
1341
|
+
}
|
|
1342
|
+
catch {
|
|
1343
|
+
return false;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
/** Local path for this project's friction-reminder fire-counter (Measurement,
|
|
1347
|
+
* docs/reminder-interrupt.md). Sits beside the matchset cache under the shared
|
|
1348
|
+
* `~/.greprag/state` dir. */
|
|
1349
|
+
function frictionStatsPath(projectId) {
|
|
1350
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
1351
|
+
return path.join(home, '.greprag', 'state', `friction-reminder-${projectId}.json`);
|
|
1352
|
+
}
|
|
1353
|
+
/** Fire-counter I/O (the adherence DENOMINATOR). Reads the prior stats, tallies one
|
|
1354
|
+
* fire via the PURE tallyFrictionFire, writes them back. Fully best-effort: a miss
|
|
1355
|
+
* never blocks the turn. The numerator (reflex chips actually spawned) lands via
|
|
1356
|
+
* tallyReflexSpawn from the spawn path — see the report's Measurement note. */
|
|
1357
|
+
function recordFrictionFire(projectId, tier, turnCount) {
|
|
1358
|
+
try {
|
|
1359
|
+
const file = frictionStatsPath(projectId);
|
|
1360
|
+
let prior = null;
|
|
1361
|
+
try {
|
|
1362
|
+
prior = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
1363
|
+
}
|
|
1364
|
+
catch { /* none yet */ }
|
|
1365
|
+
const next = (0, friction_reminder_1.tallyFrictionFire)(prior, tier, turnCount, new Date().toISOString());
|
|
1366
|
+
const dir = path.dirname(file);
|
|
1367
|
+
if (!fs.existsSync(dir))
|
|
1368
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1369
|
+
fs.writeFileSync(file, JSON.stringify(next, null, 2) + '\n');
|
|
1370
|
+
}
|
|
1371
|
+
catch { /* counter is best-effort — never block the turn */ }
|
|
1372
|
+
}
|
|
1373
|
+
/** Local path for this project's memory-reflex fire-counter — the data the /mechanic
|
|
1374
|
+
* excessiveness monitor reads (fires · injected · silent · per-trigger). Beside the
|
|
1375
|
+
* friction counter under ~/.greprag/state. */
|
|
1376
|
+
function memoryReflexStatsPath(projectId) {
|
|
1377
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
1378
|
+
return path.join(home, '.greprag', 'state', `memory-reflex-${projectId}.json`);
|
|
1379
|
+
}
|
|
1380
|
+
/** Tally one memory-reflex fire (best-effort). `didInject` splits fire→injected vs silent
|
|
1381
|
+
* so the monitor can read the inject/fire ratio (firing without finding = noise). */
|
|
1382
|
+
function recordMemoryFire(projectId, trigger, didInject, turnCount) {
|
|
1383
|
+
try {
|
|
1384
|
+
const file = memoryReflexStatsPath(projectId);
|
|
1385
|
+
let prior = null;
|
|
1386
|
+
try {
|
|
1387
|
+
prior = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
1388
|
+
}
|
|
1389
|
+
catch { /* none yet */ }
|
|
1390
|
+
const next = (0, memory_reflex_1.tallyMemoryFire)(prior, trigger, didInject, turnCount, new Date().toISOString());
|
|
1391
|
+
const dir = path.dirname(file);
|
|
1392
|
+
if (!fs.existsSync(dir))
|
|
1393
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1394
|
+
fs.writeFileSync(file, JSON.stringify(next, null, 2) + '\n');
|
|
1395
|
+
}
|
|
1396
|
+
catch { /* counter is best-effort — never block the turn */ }
|
|
1397
|
+
}
|
|
1398
|
+
/** Stash for the memory-reflex efficacy capture — the raw hit content injected THIS turn, so
|
|
1399
|
+
* the Stop hook can score the response's overlap against it (the `used` numerator). Keyed by
|
|
1400
|
+
* session (Stop has the short id); carries projectId so Stop needn't re-derive it. Written at
|
|
1401
|
+
* inject-time, consumed + deleted at Stop. adr: adr/memory-reflex.md */
|
|
1402
|
+
function memoryReflexPendingPath(short) {
|
|
1403
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
1404
|
+
return path.join(home, '.greprag', 'state', `memory-reflex-pending-${short}.json`);
|
|
1405
|
+
}
|
|
1406
|
+
function stashMemoryInjection(short, projectId, hitText, turnCount) {
|
|
1407
|
+
try {
|
|
1408
|
+
const file = memoryReflexPendingPath(short);
|
|
1409
|
+
const dir = path.dirname(file);
|
|
1410
|
+
if (!fs.existsSync(dir))
|
|
1411
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1412
|
+
fs.writeFileSync(file, JSON.stringify({ projectId, hitText, turnCount }) + '\n');
|
|
1413
|
+
}
|
|
1414
|
+
catch { /* best-effort — a missed stash just means no efficacy sample this turn */ }
|
|
1415
|
+
}
|
|
1416
|
+
/** Read + DELETE the pending injection stash (consume-once, so a turn with no injection never
|
|
1417
|
+
* reuses a stale sample). Returns null on the common no-stash path. */
|
|
1418
|
+
function consumeMemoryInjection(short) {
|
|
1419
|
+
try {
|
|
1420
|
+
const file = memoryReflexPendingPath(short);
|
|
1421
|
+
const raw = fs.readFileSync(file, 'utf-8');
|
|
1422
|
+
try {
|
|
1423
|
+
fs.unlinkSync(file);
|
|
1424
|
+
}
|
|
1425
|
+
catch { /* already gone — fine */ }
|
|
1426
|
+
const p = JSON.parse(raw);
|
|
1427
|
+
if (!p || typeof p.projectId !== 'string' || typeof p.hitText !== 'string')
|
|
1428
|
+
return null;
|
|
1429
|
+
return { projectId: p.projectId, hitText: p.hitText };
|
|
1430
|
+
}
|
|
1431
|
+
catch {
|
|
1432
|
+
return null; // no stash this turn (the common case)
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
/** Bump the memory-reflex EFFICACY numerator (the response drew on the injection) — best-effort. */
|
|
1436
|
+
function recordMemoryUsed(projectId) {
|
|
1437
|
+
try {
|
|
1438
|
+
const file = memoryReflexStatsPath(projectId);
|
|
1439
|
+
let prior = null;
|
|
1440
|
+
try {
|
|
1441
|
+
prior = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
1442
|
+
}
|
|
1443
|
+
catch { /* none yet */ }
|
|
1444
|
+
const next = (0, memory_reflex_1.tallyMemoryUsed)(prior);
|
|
1445
|
+
const dir = path.dirname(file);
|
|
1446
|
+
if (!fs.existsSync(dir))
|
|
1447
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1448
|
+
fs.writeFileSync(file, JSON.stringify(next, null, 2) + '\n');
|
|
1449
|
+
}
|
|
1450
|
+
catch { /* counter is best-effort */ }
|
|
1451
|
+
}
|
|
1452
|
+
/** Self-report stash (P1b, adr/memory-reflex.md) — the topic the agent flagged via a
|
|
1453
|
+
* `[[recall: X]]` marker LAST turn, so the NEXT turn's notify pulls + injects it. Separate
|
|
1454
|
+
* from the efficacy-injection stash. Keyed by session; carries projectId. Written at Stop,
|
|
1455
|
+
* consumed-once at the next notify. */
|
|
1456
|
+
function selfReportPendingPath(short) {
|
|
1457
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
1458
|
+
return path.join(home, '.greprag', 'state', `memory-reflex-selfreport-${short}.json`);
|
|
1459
|
+
}
|
|
1460
|
+
function stashSelfReport(short, projectId, topic) {
|
|
1461
|
+
try {
|
|
1462
|
+
const file = selfReportPendingPath(short);
|
|
1463
|
+
const dir = path.dirname(file);
|
|
1464
|
+
if (!fs.existsSync(dir))
|
|
1465
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1466
|
+
fs.writeFileSync(file, JSON.stringify({ projectId, topic }) + '\n');
|
|
1467
|
+
}
|
|
1468
|
+
catch { /* best-effort — a missed self-report just means no pull this round */ }
|
|
1469
|
+
}
|
|
1470
|
+
/** Read + DELETE the self-report stash (consume-once). Null on the common no-marker path. */
|
|
1471
|
+
function consumeSelfReport(short) {
|
|
1472
|
+
try {
|
|
1473
|
+
const file = selfReportPendingPath(short);
|
|
1474
|
+
const raw = fs.readFileSync(file, 'utf-8');
|
|
1475
|
+
try {
|
|
1476
|
+
fs.unlinkSync(file);
|
|
1477
|
+
}
|
|
1478
|
+
catch { /* already gone — fine */ }
|
|
1479
|
+
const p = JSON.parse(raw);
|
|
1480
|
+
if (!p || typeof p.projectId !== 'string' || typeof p.topic !== 'string' || !p.topic)
|
|
1481
|
+
return null;
|
|
1482
|
+
return { projectId: p.projectId, topic: p.topic };
|
|
1483
|
+
}
|
|
1484
|
+
catch {
|
|
1485
|
+
return null;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
/** Local cache of the SERVER's auto-quiet verdict for this project (the
|
|
1489
|
+
* mechanicKilled() analog for the memory-reflex). The Flash-Lite efficacy judge
|
|
1490
|
+
* decides the threshold server-side and ships the boolean on the /v1/memory/query
|
|
1491
|
+
* response; the hook caches it here and `memoryReflexQuieted` honors it with a
|
|
1492
|
+
* TTL (quietCacheSaysSilent). Beside the fire-counter under ~/.greprag/state. */
|
|
1493
|
+
function memoryReflexQuietPath(projectId) {
|
|
1494
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
1495
|
+
return path.join(home, '.greprag', 'state', `memory-reflex-quiet-${projectId}.json`);
|
|
1496
|
+
}
|
|
1497
|
+
/** Has the server's efficacy judge QUIETED this project's memory-reflex (low
|
|
1498
|
+
* used÷injected over a meaningful sample)? Reads the local cache; honors it only
|
|
1499
|
+
* while fresh (TTL) so a recovered reflex re-probes instead of staying silenced.
|
|
1500
|
+
* Fail-quiet: any error reads as NOT quieted (the reflex keeps working) —
|
|
1501
|
+
* mirrors mechanicKilled()'s fail-open posture. */
|
|
1502
|
+
function memoryReflexQuieted(projectId) {
|
|
1503
|
+
try {
|
|
1504
|
+
const raw = fs.readFileSync(memoryReflexQuietPath(projectId), 'utf-8');
|
|
1505
|
+
return (0, memory_reflex_1.quietCacheSaysSilent)(JSON.parse(raw), Date.now());
|
|
1506
|
+
}
|
|
1507
|
+
catch {
|
|
1508
|
+
return false;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
/** Cache the server's auto-quiet verdict from a /v1/memory/query response — written
|
|
1512
|
+
* whenever the reflex actually probes (fired the search). Best-effort. */
|
|
1513
|
+
function cacheMemoryReflexQuiet(projectId, quiet) {
|
|
1514
|
+
try {
|
|
1515
|
+
const file = memoryReflexQuietPath(projectId);
|
|
1516
|
+
const dir = path.dirname(file);
|
|
1517
|
+
if (!fs.existsSync(dir))
|
|
1518
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1519
|
+
const payload = { quiet, cachedAt: new Date().toISOString() };
|
|
1520
|
+
fs.writeFileSync(file, JSON.stringify(payload) + '\n');
|
|
1521
|
+
}
|
|
1522
|
+
catch { /* best-effort — a missed cache just means the reflex re-probes next turn */ }
|
|
1523
|
+
}
|
|
1524
|
+
/** The reminder-interrupt registry CONTAINER call (docs/reminder-interrupt.md §Registry
|
|
1525
|
+
* spec) — invoked from the proven-registered `notify` hook (claude-code path). Assembles
|
|
1526
|
+
* the live env (the ONLY i/o: armed via isLocallyArmed, this session's own unread, the
|
|
1527
|
+
* Stop-stashed stress/turnCount, the front-desk envelope notice) and fires every module
|
|
1528
|
+
* whose detector reports deficient: watcher-arm (unarmed → nag if a peer messaged you, else
|
|
1529
|
+
* nudge; silent when armed), the stress-gated mechanic-friction, front-desk (a soft
|
|
1530
|
+
* "you've got mail" when a new envelope landed), and memory-reflex (auto-surfaced episodic
|
|
1531
|
+
* memory when THIS prompt signals recall — an inline, time-capped, confidence-gated search).
|
|
1532
|
+
* The announce-only modules (setup-warning / checkpoint / assistant-doctrine) stay silent per
|
|
1533
|
+
* turn — their env signals are SessionStart-only. Stacking; emitted once. Fail-open — any miss
|
|
1534
|
+
* → silent. The Mechanic kill switch drops ONLY the mechanic module; the rest keep working. */
|
|
1535
|
+
async function injectReminders(input, cfg, short) {
|
|
1536
|
+
try {
|
|
1537
|
+
const cwd = input.cwd || process.cwd();
|
|
1538
|
+
const anchor = (0, project_anchor_1.readAnchor)(cwd);
|
|
1539
|
+
const armed = (0, watcher_registry_1.isLocallyArmed)(short);
|
|
1540
|
+
const sessionUnread = armed ? 0 : await fetchSessionUnread(cfg.apiUrl, cfg.apiKey, short);
|
|
1541
|
+
const ownerPid = armed ? null : (0, watcher_registry_1.resolveClaudeOwnerPid)(short);
|
|
1542
|
+
let assistant = false;
|
|
1543
|
+
try {
|
|
1544
|
+
assistant = (0, project_anchor_1.isAssistantProject)(anchor);
|
|
1545
|
+
}
|
|
1546
|
+
catch { /* fail-open */ }
|
|
1547
|
+
// Stress/turnCount from the Stop-stashed state bag (no LLM/network here).
|
|
1548
|
+
const state = anchor.projectId
|
|
1549
|
+
? ((0, guard_1.readMatchsetCache)(anchor.projectId)?.stateValues || {})
|
|
1550
|
+
: {};
|
|
1551
|
+
const turnCount = typeof state.turnCount === 'number' ? state.turnCount : 0;
|
|
1552
|
+
const stress = typeof state.stress === 'number' ? state.stress : 0;
|
|
1553
|
+
// Front-desk envelope notice — the per-turn half of the front-desk module's pair (the
|
|
1554
|
+
// SessionStart count is the announce half). Body-free, self-dedups per machine, returns
|
|
1555
|
+
// null when nothing new. This is the network call the retired `mail` hook used to make;
|
|
1556
|
+
// it moves here so the whole announce/reminder surface rides ONE detector loop.
|
|
1557
|
+
const frontDeskNotice = await (0, front_desk_mail_1.buildFrontDeskNotice)(cfg.apiUrl, cfg.apiKey);
|
|
1558
|
+
// Memory-reflex (keyword layer) — recall intent in THIS prompt fires an inline,
|
|
1559
|
+
// time-capped, project-scoped search; a hit above the confidence gate is surfaced
|
|
1560
|
+
// inline (top 1-2, capped) so the agent sees it AS it forms its reply. A miss stays
|
|
1561
|
+
// silent (the search result is its own precision filter — no banner-blindness). Every
|
|
1562
|
+
// fire is tallied for the /mechanic excessiveness monitor. Gated on recall-intent, so
|
|
1563
|
+
// most turns skip the search entirely. docs/reminder-interrupt.md (memory-reflex).
|
|
1564
|
+
let memoryHit = null;
|
|
1565
|
+
const prompt = typeof input.prompt === 'string' ? input.prompt : '';
|
|
1566
|
+
// Pick the memory query + trigger (the hybrid): a self-report marker the agent emitted LAST
|
|
1567
|
+
// turn ([[recall: X]], stashed at Stop) takes precedence over recall-intent in THIS prompt —
|
|
1568
|
+
// the agent explicitly flagged its own gap. One injection per turn.
|
|
1569
|
+
let memQuery = null;
|
|
1570
|
+
let memTrigger = 'keyword';
|
|
1571
|
+
const selfReport = anchor.projectId ? consumeSelfReport(short) : null;
|
|
1572
|
+
if (selfReport) {
|
|
1573
|
+
memQuery = selfReport.topic;
|
|
1574
|
+
memTrigger = 'selfReport';
|
|
1575
|
+
}
|
|
1576
|
+
else if ((0, memory_reflex_1.hasRecallIntent)(prompt)) {
|
|
1577
|
+
memQuery = prompt.slice(0, 500);
|
|
1578
|
+
memTrigger = 'keyword';
|
|
1579
|
+
}
|
|
1580
|
+
// Auto-quiet gate (adr/memory-reflex.md): if the server's Flash-Lite efficacy judge has
|
|
1581
|
+
// QUIETED this project's reflex (low used÷injected over a meaningful sample), skip the search
|
|
1582
|
+
// entirely — the cached verdict expires on a TTL so a recovered reflex re-probes. mechanicKilled() analog.
|
|
1583
|
+
if (anchor.projectId && memQuery && !memoryReflexQuieted(anchor.projectId)) {
|
|
1584
|
+
const { hits, quiet } = await fetchMemoryHits(cfg.apiUrl, cfg.apiKey, anchor.projectId, memQuery);
|
|
1585
|
+
cacheMemoryReflexQuiet(anchor.projectId, quiet); // refresh the verdict from this probe
|
|
1586
|
+
memoryHit = (0, memory_reflex_1.buildMemoryInjection)(hits);
|
|
1587
|
+
recordMemoryFire(anchor.projectId, memTrigger, !!memoryHit, turnCount);
|
|
1588
|
+
// Stash what was injected so the Stop hook can score whether the response used it
|
|
1589
|
+
// (the efficacy numerator). Only when something was actually surfaced.
|
|
1590
|
+
if (memoryHit)
|
|
1591
|
+
stashMemoryInjection(short, anchor.projectId, (0, memory_reflex_1.gatedHitText)(hits), turnCount);
|
|
1592
|
+
}
|
|
1593
|
+
const env = {
|
|
1594
|
+
short, turnCount, stress, armed, sessionUnread,
|
|
1595
|
+
ownerPid, alias: (0, session_id_1.readIdentityAlias)(), assistant,
|
|
1596
|
+
frontDeskNotice, memoryHit,
|
|
1597
|
+
};
|
|
1598
|
+
// Kill switch drops ONLY the mechanic module — watcher-arm keeps working.
|
|
1599
|
+
const reg = mechanicKilled() ? reminder_registry_1.REGISTRY.filter(m => m.id !== 'mechanic-friction') : reminder_registry_1.REGISTRY;
|
|
1600
|
+
const fired = (0, reminder_registry_1.collectReminders)(env, reg);
|
|
1601
|
+
if (!fired.length)
|
|
1602
|
+
return;
|
|
1603
|
+
// Authoring rule #4 — frame nag/nudge with the act-now directive envelope; ambient
|
|
1604
|
+
// stays a soft pointer. Emit ONCE (stacking via join) so multiple due modules ride a
|
|
1605
|
+
// single additionalContext.
|
|
1606
|
+
const parts = fired.map(f => f.tier === 'ambient' ? f.line : (0, guard_1.wrapDirective)(f.line));
|
|
1607
|
+
writeAdditionalContext('UserPromptSubmit', parts.join('\n\n'));
|
|
1608
|
+
// Fire-counter (adherence denominator) for the mechanic module — best-effort.
|
|
1609
|
+
const mf = fired.find(f => f.id === 'mechanic-friction');
|
|
1610
|
+
if (mf && anchor.projectId)
|
|
1611
|
+
recordFrictionFire(anchor.projectId, mf.tier, turnCount);
|
|
1612
|
+
}
|
|
1613
|
+
catch { /* fail-open — a reminder miss never breaks the turn */ }
|
|
1614
|
+
}
|
|
1615
|
+
/** RETIRED 2026-06-20 — the per-turn reminder injection moved into the registry
|
|
1616
|
+
* container (`injectReminders`, called from `notify`), so watcher-arm + mechanic-friction
|
|
1617
|
+
* fire together under one detector loop with no double-injection. Kept as a no-op for
|
|
1618
|
+
* hook-registration stability (a fleet `friction-reminder` entry is harmless).
|
|
1619
|
+
* docs/reminder-interrupt.md §Registry spec */
|
|
1620
|
+
async function frictionReminder(_input) {
|
|
1621
|
+
// intentionally empty — see injectReminders (runs from notify)
|
|
1622
|
+
}
|
|
1623
|
+
/** Front-desk turn hook — wire as a UserPromptSubmit hook.
|
|
1275
1624
|
*
|
|
1276
|
-
*
|
|
1277
|
-
*
|
|
1625
|
+
* NOTICE ROLE RETIRED 2026-06-20 — the envelope-only "you've got mail from X" notice
|
|
1626
|
+
* moved into the reminder-interrupt registry (front-desk module, fired per turn from
|
|
1627
|
+
* `injectReminders` → the same detector loop as watcher-arm + mechanic-friction), so the
|
|
1628
|
+
* whole announce/reminder surface has ONE assembly. docs/reminder-interrupt.md §Registry spec.
|
|
1278
1629
|
*
|
|
1279
|
-
*
|
|
1280
|
-
*
|
|
1281
|
-
*
|
|
1282
|
-
*
|
|
1283
|
-
*
|
|
1630
|
+
* What REMAINS here is the attachment auto-save DRAIN — a deliberate side-effect (it WRITES
|
|
1631
|
+
* files to disk), kept OUT of the pure announce/reminder registry by design (the registry
|
|
1632
|
+
* modules are pure; all I/O + side-effects live in the hook). Auto-save (Chip E): when a
|
|
1633
|
+
* project opts in (`email_autosave=true` in .greprag/project.json, or env
|
|
1634
|
+
* GREPRAG_EMAIL_AUTOSAVE), drain new attachments to the configured dir each turn and emit a
|
|
1635
|
+
* one-line saved summary. Default OFF — no opt-in, no pull. Best-effort: a drain failure
|
|
1636
|
+
* never blocks the turn. Unconfigured / no API key → silent. */
|
|
1284
1637
|
async function mail(input) {
|
|
1285
1638
|
const cwd = input.cwd || process.cwd();
|
|
1286
1639
|
const cfg = getConfig(cwd);
|
|
1287
1640
|
if (!cfg.enabled || !cfg.apiKey)
|
|
1288
1641
|
return; // unconfigured → silent
|
|
1289
|
-
const parts = [];
|
|
1290
|
-
const notice = await (0, front_desk_mail_1.buildFrontDeskNotice)(cfg.apiUrl, cfg.apiKey);
|
|
1291
|
-
if (notice)
|
|
1292
|
-
parts.push(notice);
|
|
1293
1642
|
try {
|
|
1294
1643
|
const auto = await maybeAutoSaveAttachments(cwd, cfg.apiUrl, cfg.apiKey);
|
|
1295
1644
|
if (auto)
|
|
1296
|
-
|
|
1645
|
+
writeAdditionalContext(input.hook_event_name || 'UserPromptSubmit', auto);
|
|
1297
1646
|
}
|
|
1298
1647
|
catch { /* drain is best-effort — never block a turn */ }
|
|
1299
|
-
if (parts.length) {
|
|
1300
|
-
writeAdditionalContext(input.hook_event_name || 'UserPromptSubmit', parts.join('\n\n'));
|
|
1301
|
-
}
|
|
1302
1648
|
}
|
|
1303
1649
|
/** Per-turn attachment auto-save. Returns a one-line summary of freshly-saved
|
|
1304
1650
|
* files, or null when auto-save is off or nothing new landed. Gated on an
|
|
@@ -1565,7 +1911,7 @@ async function guardFlush(input) {
|
|
|
1565
1911
|
async function main() {
|
|
1566
1912
|
const subcommand = process.argv[2];
|
|
1567
1913
|
const validSubs = new Set([
|
|
1568
|
-
'store', 'recap', 'notify', 'mail', 'session-id', 'pre-spawn-check', 'armcheck',
|
|
1914
|
+
'store', 'recap', 'notify', 'mail', 'friction-reminder', 'session-id', 'pre-spawn-check', 'armcheck',
|
|
1569
1915
|
'collision-check', 'drain', 'coordinate-gate',
|
|
1570
1916
|
'guard', 'guard-refresh', 'guard-flush',
|
|
1571
1917
|
'context-probe', 'context-check', 'crush-wrap',
|
|
@@ -1574,7 +1920,7 @@ async function main() {
|
|
|
1574
1920
|
'codex-permission-context', 'codex-subagent-start',
|
|
1575
1921
|
]);
|
|
1576
1922
|
if (!validSubs.has(subcommand)) {
|
|
1577
|
-
process.stderr.write(`Usage: greprag-hook <store|recap|notify|mail|session-id|pre-spawn-check|armcheck|collision-check|drain|coordinate-gate|guard|guard-refresh|guard-flush|context-probe|context-check|crush-wrap|pre-compact|archive-pointer|codex-store|codex-notify|codex-inbox|codex-recap|codex-permission-context|codex-subagent-start>\n`);
|
|
1923
|
+
process.stderr.write(`Usage: greprag-hook <store|recap|notify|mail|friction-reminder|session-id|pre-spawn-check|armcheck|collision-check|drain|coordinate-gate|guard|guard-refresh|guard-flush|context-probe|context-check|crush-wrap|pre-compact|archive-pointer|codex-store|codex-notify|codex-inbox|codex-recap|codex-permission-context|codex-subagent-start>\n`);
|
|
1578
1924
|
process.exit(1);
|
|
1579
1925
|
}
|
|
1580
1926
|
let input = {};
|
|
@@ -1602,6 +1948,12 @@ async function main() {
|
|
|
1602
1948
|
else if (subcommand === 'mail') {
|
|
1603
1949
|
await mail(input);
|
|
1604
1950
|
}
|
|
1951
|
+
else if (subcommand === 'friction-reminder') {
|
|
1952
|
+
// UserPromptSubmit — the active Mechanic's dynamic friction reminder (beside
|
|
1953
|
+
// the arm directive). Stress-driven tier: silent/ambient/nudge/nag. Reads the
|
|
1954
|
+
// Stop-stashed state bag; additive + fail-open. docs/reminder-interrupt.md
|
|
1955
|
+
await frictionReminder(input);
|
|
1956
|
+
}
|
|
1605
1957
|
else if (subcommand === 'codex-notify') {
|
|
1606
1958
|
await notify(input, 'codex');
|
|
1607
1959
|
}
|