greprag 5.74.21 → 5.76.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.
@@ -79,13 +79,14 @@ exports.gcDeadSessions = gcDeadSessions;
79
79
  const fs = __importStar(require("fs"));
80
80
  const path = __importStar(require("path"));
81
81
  const child_process_1 = require("child_process");
82
+ const session_id_1 = require("../session-id");
82
83
  const WATCHER_DIRNAME = 'watchers';
83
84
  // Matches a greprag watcher's command line in every launch shape (npm shim, bash
84
85
  // relauncher, the supervisor's CreateProcess re-invocation). Anchored on the
85
86
  // INVOKED BINARY (`greprag` or `index.js`) immediately followed by `inbox watch`
86
87
  // — never a bare `inbox watch` substring. Retained for the owner-pid resolver's
87
88
  // claude.exe match; the cleanup path no longer scans the process table at all.
88
- const CLAUDE_PROC_RE = /^claude(\.exe)?$/i;
89
+ const CLAUDE_PROC_RE = /^(claude|grok)(\.exe)?$/i;
89
90
  /** Default per-session watcher floor: keep this many freshest live watchers, reap
90
91
  * only the surplus above it, never below it. K=2 = the live one + one margin. */
91
92
  exports.DEFAULT_WATCHER_CAP = 2;
@@ -97,9 +98,26 @@ function watchersDir() {
97
98
  const h = grepragHome();
98
99
  return h ? path.join(h, WATCHER_DIRNAME) : null;
99
100
  }
100
- function pidfilePath(short) {
101
- const dir = watchersDir();
102
- return dir ? path.join(dir, `${short}.json`) : null;
101
+ function sessionKey(short) {
102
+ return (0, session_id_1.truncateSessionId)(short) || short;
103
+ }
104
+ /** Pidfile names that belong to the same session: canonical short (16-hex
105
+ * for UUIDv7, 8-hex otherwise) plus the raw/--session string. A Grok watch
106
+ * armed with `$GROK_SESSION_ID` (dashed UUID) used to write
107
+ * `<full-uuid>.json` while Stop's isLocallyArmed looked at `<16-hex>.json`
108
+ * → perpetual UNARMED nag + singleton-guard bounce. */
109
+ function matchingPidfileNames(short) {
110
+ const want = sessionKey(short);
111
+ const names = new Set();
112
+ if (want)
113
+ names.add(want);
114
+ if (short)
115
+ names.add(short);
116
+ for (const name of allShorts()) {
117
+ if (name === short || sessionKey(name) === want)
118
+ names.add(name);
119
+ }
120
+ return [...names];
103
121
  }
104
122
  /** True iff `pid` is a live process. `process.kill(pid, 0)` sends no signal — it
105
123
  * only probes existence. EPERM = exists but another user (alive); ESRCH = gone. */
@@ -114,20 +132,32 @@ function pidAlive(pid) {
114
132
  return e?.code === 'EPERM';
115
133
  }
116
134
  }
117
- /** Read a session's entry LIST. Back-compat: a legacy single-object pidfile reads
118
- * as a 1-element list. */
135
+ /** Read a session's entry LIST. Merges alias pidfiles (dashed UUID, 16-hex,
136
+ * raw --session). Back-compat: a legacy single-object pidfile reads as a
137
+ * 1-element list. */
119
138
  function readWatcherEntries(short) {
120
- try {
121
- const p = pidfilePath(short);
122
- if (!p)
123
- return [];
124
- const parsed = JSON.parse(fs.readFileSync(p, 'utf-8'));
125
- const arr = Array.isArray(parsed) ? parsed : [parsed];
126
- return arr.filter((r) => !!r && typeof r.pid === 'number');
127
- }
128
- catch {
139
+ const dir = watchersDir();
140
+ if (!dir)
129
141
  return [];
142
+ const seen = new Set();
143
+ const out = [];
144
+ for (const name of matchingPidfileNames(short)) {
145
+ try {
146
+ const parsed = JSON.parse(fs.readFileSync(path.join(dir, `${name}.json`), 'utf-8'));
147
+ const arr = Array.isArray(parsed) ? parsed : [parsed];
148
+ for (const r of arr) {
149
+ if (!r || typeof r.pid !== 'number')
150
+ continue;
151
+ const rec = r;
152
+ if (seen.has(rec.pid))
153
+ continue;
154
+ seen.add(rec.pid);
155
+ out.push(rec);
156
+ }
157
+ }
158
+ catch { /* missing alias is fine */ }
130
159
  }
160
+ return out;
131
161
  }
132
162
  function writeWatcherEntries(short, entries) {
133
163
  try {
@@ -135,12 +165,21 @@ function writeWatcherEntries(short, entries) {
135
165
  if (!dir)
136
166
  return;
137
167
  fs.mkdirSync(dir, { recursive: true });
138
- const p = path.join(dir, `${short}.json`);
168
+ const key = sessionKey(short);
169
+ const canonical = path.join(dir, `${key}.json`);
170
+ for (const name of matchingPidfileNames(short)) {
171
+ if (name === key)
172
+ continue;
173
+ try {
174
+ fs.rmSync(path.join(dir, `${name}.json`), { force: true });
175
+ }
176
+ catch { /* best-effort */ }
177
+ }
139
178
  if (entries.length === 0) {
140
- fs.rmSync(p, { force: true });
179
+ fs.rmSync(canonical, { force: true });
141
180
  return;
142
181
  }
143
- fs.writeFileSync(p, JSON.stringify(entries));
182
+ fs.writeFileSync(canonical, JSON.stringify(entries));
144
183
  }
145
184
  catch { /* best-effort — a failed write only means re-arm, never a crash */ }
146
185
  }
@@ -155,12 +194,18 @@ function writeWatcherPidfile(short, pid, ownerPid) {
155
194
  function removeWatcherEntry(short, pid) {
156
195
  writeWatcherEntries(short, readWatcherEntries(short).filter(r => r.pid !== pid));
157
196
  }
158
- /** Remove a session's whole pidfile (legacy / full sweep). */
197
+ /** Remove a session's whole pidfile (legacy / full sweep), including aliases. */
159
198
  function removeWatcherPidfile(short) {
160
199
  try {
161
- const p = pidfilePath(short);
162
- if (p)
163
- fs.rmSync(p, { force: true });
200
+ const dir = watchersDir();
201
+ if (!dir)
202
+ return;
203
+ for (const name of matchingPidfileNames(short)) {
204
+ try {
205
+ fs.rmSync(path.join(dir, `${name}.json`), { force: true });
206
+ }
207
+ catch { /* best-effort */ }
208
+ }
164
209
  }
165
210
  catch { /* best-effort */ }
166
211
  }
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ /** Grok Build session files + the recap sidecar SessionStart can write
3
+ * (Grok ignores SessionStart stdout, so the always-on rule points here).
4
+ * adr: adr/grok-platform.md */
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.grokSessionDir = grokSessionDir;
40
+ exports.readLatestGrokUserPrompt = readLatestGrokUserPrompt;
41
+ exports.grokSidecarPath = grokSidecarPath;
42
+ exports.writeGrokSidecar = writeGrokSidecar;
43
+ exports.appendGrokSidecar = appendGrokSidecar;
44
+ const fs = __importStar(require("fs"));
45
+ const path = __importStar(require("path"));
46
+ const os = __importStar(require("os"));
47
+ function grokHome() {
48
+ if (process.env.GROK_HOME)
49
+ return process.env.GROK_HOME;
50
+ return path.join(os.homedir(), '.grok');
51
+ }
52
+ function grokSessionDir(cwd, sessionId) {
53
+ const encoded = encodeURIComponent(path.resolve(cwd));
54
+ return path.join(grokHome(), 'sessions', encoded, sessionId);
55
+ }
56
+ function extractText(content) {
57
+ if (typeof content === 'string')
58
+ return content.trim();
59
+ if (!Array.isArray(content))
60
+ return '';
61
+ return content
62
+ .filter((b) => b?.type === 'text' && typeof b.text === 'string')
63
+ .map((b) => String(b.text))
64
+ .join('\n')
65
+ .trim();
66
+ }
67
+ function unwrapUserQuery(text) {
68
+ const m = text.match(/<user_query>\s*([\s\S]*?)\s*<\/user_query>/i);
69
+ return (m?.[1] || text).trim();
70
+ }
71
+ /** Latest real user prompt from Grok chat_history.jsonl. */
72
+ function readLatestGrokUserPrompt(cwd, sessionId) {
73
+ if (!sessionId)
74
+ return '';
75
+ const file = path.join(grokSessionDir(cwd, sessionId), 'chat_history.jsonl');
76
+ let raw;
77
+ try {
78
+ raw = fs.readFileSync(file, 'utf-8');
79
+ }
80
+ catch {
81
+ return '';
82
+ }
83
+ const lines = raw.split(/\n/);
84
+ for (let i = lines.length - 1; i >= 0; i--) {
85
+ const line = lines[i].trim();
86
+ if (!line)
87
+ continue;
88
+ let entry;
89
+ try {
90
+ entry = JSON.parse(line);
91
+ }
92
+ catch {
93
+ continue;
94
+ }
95
+ if (entry.type !== 'user')
96
+ continue;
97
+ const text = unwrapUserQuery(extractText(entry.content));
98
+ if (text)
99
+ return text;
100
+ }
101
+ return '';
102
+ }
103
+ function grokSidecarPath(short) {
104
+ return path.join(os.homedir(), '.greprag', 'grok-context', `${short}.md`);
105
+ }
106
+ function writeGrokSidecar(short, text) {
107
+ if (!short || !text)
108
+ return;
109
+ try {
110
+ const file = grokSidecarPath(short);
111
+ fs.mkdirSync(path.dirname(file), { recursive: true });
112
+ fs.writeFileSync(file, text);
113
+ }
114
+ catch { /* sidecar is best-effort */ }
115
+ }
116
+ function appendGrokSidecar(short, text) {
117
+ if (!short || !text)
118
+ return;
119
+ try {
120
+ const file = grokSidecarPath(short);
121
+ fs.mkdirSync(path.dirname(file), { recursive: true });
122
+ fs.appendFileSync(file, (fs.existsSync(file) ? '\n\n' : '') + text);
123
+ }
124
+ catch { /* sidecar is best-effort */ }
125
+ }
package/dist/harness.js CHANGED
@@ -8,6 +8,7 @@ exports.REPAIR_TARGET_HARNESSES = [
8
8
  'claude-code',
9
9
  'codex',
10
10
  'opencode',
11
+ 'grok',
11
12
  'all',
12
13
  ];
13
14
  function normalizeHarness(raw) {
@@ -20,6 +21,8 @@ function normalizeHarness(raw) {
20
21
  return 'codex';
21
22
  if (v === 'opencode' || v === 'open-code' || v === 'open_code')
22
23
  return 'opencode';
24
+ if (v === 'grok' || v === 'grok-build' || v === 'grok_build')
25
+ return 'grok';
23
26
  if (v === 'all' || v === 'any' || v === 'universal')
24
27
  return 'all';
25
28
  return null;
@@ -30,6 +33,9 @@ function inferCurrentHarness() {
30
33
  if (h && h !== 'all')
31
34
  return h;
32
35
  }
36
+ // Grok sets CLAUDE_PROJECT_DIR as a compat alias. Detect Grok BEFORE Claude.
37
+ if (process.env.GROK_SESSION_ID || process.env.GROK_HOOK_EVENT)
38
+ return 'grok';
33
39
  if (process.env.CODEX_THREAD_ID)
34
40
  return 'codex';
35
41
  if (process.env.GREPRAG_OPENCODE_SESSION_ID || process.env.OPENCODE_SESSION_ID)
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ /** Once-per-event stamp so leaked Claude greprag hooks + dedicated
3
+ * ~/.grok/hooks/greprag.json cannot double-store / double-recap.
4
+ * adr: adr/grok-platform.md */
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.claimHookOnce = claimHookOnce;
40
+ const fs = __importStar(require("fs"));
41
+ const os = __importStar(require("os"));
42
+ const path = __importStar(require("path"));
43
+ const TTL_MS = 60_000;
44
+ function stampPath(sessionId, event, sub, turnId) {
45
+ const safe = [sessionId, event, sub, turnId].join('-').replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 180);
46
+ return path.join(os.homedir(), '.greprag', 'hook-once', `${safe}.json`);
47
+ }
48
+ /** True = this process should run the subcommand. False = duplicate, skip. */
49
+ function claimHookOnce(sessionId, event, subcommand, turnId) {
50
+ if (!sessionId)
51
+ return true;
52
+ const key = turnId || 'noturn';
53
+ const file = stampPath(sessionId, event || 'unknown', subcommand, key);
54
+ try {
55
+ fs.mkdirSync(path.dirname(file), { recursive: true });
56
+ if (fs.existsSync(file)) {
57
+ const age = Date.now() - fs.statSync(file).mtimeMs;
58
+ if (age < TTL_MS)
59
+ return false;
60
+ }
61
+ fs.writeFileSync(file, JSON.stringify({ ts: Date.now(), subcommand }));
62
+ return true;
63
+ }
64
+ catch {
65
+ return true;
66
+ }
67
+ }
@@ -33,12 +33,55 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.normalizeHookInput = normalizeHookInput;
37
+ exports.isGrokProcess = isGrokProcess;
38
+ exports.isShellTool = isShellTool;
36
39
  exports.ensureEnv = ensureEnv;
37
40
  exports.getHookConfig = getHookConfig;
38
41
  exports.apiCall = apiCall;
39
42
  exports.writeAdditionalContext = writeAdditionalContext;
40
43
  const fs = __importStar(require("fs"));
41
44
  const path = __importStar(require("path"));
45
+ function asString(v) {
46
+ return typeof v === 'string' && v ? v : undefined;
47
+ }
48
+ function asObj(v) {
49
+ return v && typeof v === 'object' && !Array.isArray(v) ? v : undefined;
50
+ }
51
+ /** Grok sends camelCase; Claude/Codex send snake_case. One bag for every hook.
52
+ * adr: adr/grok-platform.md */
53
+ function normalizeHookInput(raw) {
54
+ const r = (raw && typeof raw === 'object') ? raw : {};
55
+ const event = asString(r.hook_event_name) || asString(r.hookEventName)
56
+ || asString(process.env.GROK_HOOK_EVENT);
57
+ const session = asString(r.session_id) || asString(r.sessionId)
58
+ || asString(process.env.GROK_SESSION_ID)
59
+ || asString(process.env.CLAUDE_CODE_SESSION_ID)
60
+ || asString(process.env.CODEX_THREAD_ID);
61
+ const cwd = asString(r.cwd) || asString(r.workspaceRoot)
62
+ || asString(process.env.GROK_WORKSPACE_ROOT);
63
+ const toolInput = asObj(r.tool_input) || asObj(r.toolInput);
64
+ return {
65
+ session_id: session,
66
+ turn_id: asString(r.turn_id) || asString(r.promptId) || asString(r.prompt_id),
67
+ transcript_path: asString(r.transcript_path) || asString(r.transcriptPath),
68
+ cwd,
69
+ hook_event_name: event,
70
+ prompt: asString(r.prompt),
71
+ model: asString(r.model),
72
+ last_assistant_message: asString(r.last_assistant_message) || asString(r.lastAssistantMessage),
73
+ tool_name: asString(r.tool_name) || asString(r.toolName),
74
+ tool_input: toolInput,
75
+ stop_hook_active: r.stop_hook_active === true || r.stopHookActive === true,
76
+ };
77
+ }
78
+ function isGrokProcess() {
79
+ return !!(process.env.GROK_SESSION_ID || process.env.GROK_HOOK_EVENT);
80
+ }
81
+ function isShellTool(name) {
82
+ const n = (name || '').toLowerCase();
83
+ return n === 'bash' || n === 'run_terminal_command' || n === 'shell';
84
+ }
42
85
  const API_URL_DEFAULT = 'https://api.greprag.com';
43
86
  function loadEnvFile(filePath) {
44
87
  try {
package/dist/hook.js CHANGED
@@ -50,6 +50,9 @@ const mechanic_role_1 = require("./mechanic-role");
50
50
  const assistant_doctrine_1 = require("./assistant-doctrine");
51
51
  const session_id_1 = require("./session-id");
52
52
  const hook_runtime_1 = require("./hook-runtime");
53
+ const harness_1 = require("./harness");
54
+ const hook_once_1 = require("./hook-once");
55
+ const grok_session_1 = require("./grok-session");
53
56
  const codex_prompt_cache_1 = require("./codex-prompt-cache");
54
57
  // adr: adr/memory-provenance-capture.md — classify harness-injected user text
55
58
  // (skill bodies, continuation summaries, chip prompts) pre-LLM at capture and
@@ -1060,12 +1063,19 @@ async function store(input, source = 'claude-code') {
1060
1063
  if (!anchor.memoryCapture)
1061
1064
  return;
1062
1065
  const turn = parseLatestTurn(input.transcript_path);
1063
- if (source === 'codex' && (!turn.userPrompt || !turn.agentResponse)) {
1066
+ if ((source === 'codex' || source === 'grok') && (!turn.userPrompt || !turn.agentResponse)) {
1064
1067
  const cached = (0, codex_prompt_cache_1.readCodexPromptCache)(input);
1065
1068
  if (!turn.userPrompt && cached) {
1066
1069
  turn.userPrompt = cached.prompt;
1067
1070
  turn.provenance = (0, turn_provenance_1.classifyUserText)(cached.prompt);
1068
1071
  }
1072
+ if (!turn.userPrompt && source === 'grok') {
1073
+ const fromDisk = (0, grok_session_1.readLatestGrokUserPrompt)(cwd, input.session_id);
1074
+ if (fromDisk) {
1075
+ turn.userPrompt = fromDisk;
1076
+ turn.provenance = (0, turn_provenance_1.classifyUserText)(fromDisk);
1077
+ }
1078
+ }
1069
1079
  if (!turn.agentResponse && typeof input.last_assistant_message === 'string') {
1070
1080
  turn.agentResponse = input.last_assistant_message.trim();
1071
1081
  }
@@ -1082,7 +1092,7 @@ async function store(input, source = 'claude-code') {
1082
1092
  turn.filesTouched = Array.from(filesSet).sort();
1083
1093
  }
1084
1094
  }
1085
- if (source === 'codex') {
1095
+ if (source === 'codex' || source === 'grok') {
1086
1096
  turn.toolCalls.push(...(0, codex_hook_events_1.readCodexSubagentToolCalls)(input));
1087
1097
  }
1088
1098
  // (Memory-reflex efficacy capture removed 2026-06-22 with the auto-inject it scored.)
@@ -1182,7 +1192,7 @@ async function store(input, source = 'claude-code') {
1182
1192
  // is network-backed. Codex Stop spools it to a detached worker so the app's
1183
1193
  // UI-critical Stop path does not wait on judge/distill calls; Claude Code keeps
1184
1194
  // the prior synchronous lifecycle.
1185
- if (source === 'codex') {
1195
+ if (source === 'codex' || source === 'grok') {
1186
1196
  (0, inline_atom_background_1.spawnInlineAtomObserver)({
1187
1197
  cwd,
1188
1198
  sessionId: input.session_id,
@@ -1319,7 +1329,17 @@ function shouldEmitDriftWarning(storedId, derivedId) {
1319
1329
  return true;
1320
1330
  return (Date.now() - lastMs) >= DRIFT_WARNING_TTL_MS;
1321
1331
  }
1322
- function writeRecapOutput(text, mode) {
1332
+ function grokSidecarHead(short, full) {
1333
+ const arm = (0, session_id_1.armMonitorCommand)(short, null, false, false, 'grok');
1334
+ return (0, session_id_1.buildSessionIdContext)(short, (0, session_id_1.readIdentityAlias)())
1335
+ + `\n\nARM (Grok monitor persistent:true): \`${arm}\`\n`
1336
+ + `Send with --from-session ${full || short} (full UUID or 16-hex, never 8-hex).\n`
1337
+ + 'Chip: `greprag load grok-chip-spawn` then `greprag grok spawn`. Helper: spawn_subagent. Parent keeps ONE watch. Peers: `greprag inbox watchers` then `greprag send`.\n'
1338
+ + 'If a greprag inbox monitor is already listed, do not start another. Instant exit = already armed.\n\n';
1339
+ }
1340
+ function writeRecapOutput(text, mode, grokShort, grokFull) {
1341
+ if (grokShort)
1342
+ (0, grok_session_1.writeGrokSidecar)(grokShort, grokSidecarHead(grokShort, grokFull) + (text || ''));
1323
1343
  if (!text)
1324
1344
  return;
1325
1345
  if (mode === 'additionalContext') {
@@ -1427,8 +1447,9 @@ function activeCommandDrift() {
1427
1447
  * Storage and display are both UTC. The agent can compute local time itself
1428
1448
  * if needed — a server-side UTC display avoids straddle-day confusion. */
1429
1449
  async function recap(input, mode = 'plain', opts = {}) {
1430
- const platform = opts.platform || 'claude-code';
1431
- const hasLocalMonitorLifecycle = platform === 'claude-code';
1450
+ const platform = opts.platform || (0, harness_1.inferCurrentHarness)() || 'claude-code';
1451
+ const hasLocalMonitorLifecycle = platform === 'claude-code' || platform === 'grok';
1452
+ const grokShort = platform === 'grok' ? ((0, session_id_1.truncateSessionId)(input.session_id) || null) : null;
1432
1453
  let sessionOwnerPid = null;
1433
1454
  // SessionStart watcher cleanup (best-effort, side-effect only — never writes to
1434
1455
  // stdout, which carries this hook's additionalContext JSON). Three snapshot-free
@@ -1476,7 +1497,10 @@ async function recap(input, mode = 'plain', opts = {}) {
1476
1497
  // session started outside a project gets the session-id line (its own anchor-free
1477
1498
  // hook) but never the inbox rules — the exact gap a fresh unanchored session hit.
1478
1499
  // adr: adr/address-grammar.md
1479
- writeRecapOutput((0, inbox_primer_reminder_1.buildInboxPrimer)({ short: (0, session_id_1.truncateSessionId)(input.session_id) || '' }) + '\n', mode);
1500
+ writeRecapOutput((0, inbox_primer_reminder_1.buildInboxPrimer)({
1501
+ short: (0, session_id_1.truncateSessionId)(input.session_id) || '',
1502
+ platform,
1503
+ }) + '\n', mode, grokShort, input.session_id);
1480
1504
  return;
1481
1505
  }
1482
1506
  // Mechanic matchset boot pull (D5) — rides the existing SessionStart call
@@ -1696,7 +1720,7 @@ async function recap(input, mode = 'plain', opts = {}) {
1696
1720
  announceReg = (0, reminder_registry_1.compactReannounceModules)(announceReg);
1697
1721
  const announceBlock = (0, reminder_registry_1.collectAnnounces)(announceEnv, announceReg).join('\n\n') || null;
1698
1722
  if (opts.compact) {
1699
- writeRecapOutput(announceBlock ? announceBlock + '\n' : '', mode);
1723
+ writeRecapOutput(announceBlock ? announceBlock + '\n' : '', mode, grokShort, input.session_id);
1700
1724
  return;
1701
1725
  }
1702
1726
  const preamble = () => (announceBlock ? announceBlock + '\n' : '');
@@ -1705,7 +1729,7 @@ async function recap(input, mode = 'plain', opts = {}) {
1705
1729
  // hook globally. Setup warnings still fire above.
1706
1730
  if (!anchor.sessionStartRecap) {
1707
1731
  const out = preamble();
1708
- writeRecapOutput(out, mode);
1732
+ writeRecapOutput(out, mode, grokShort, input.session_id);
1709
1733
  return;
1710
1734
  }
1711
1735
  // Recap body is hourlies-only — daily summaries used to render here (one
@@ -1722,7 +1746,7 @@ async function recap(input, mode = 'plain', opts = {}) {
1722
1746
  // if they're pending.
1723
1747
  if (!body) {
1724
1748
  const out = preamble();
1725
- writeRecapOutput(out, mode);
1749
+ writeRecapOutput(out, mode, grokShort, input.session_id);
1726
1750
  return;
1727
1751
  }
1728
1752
  const parts = [];
@@ -1731,7 +1755,7 @@ async function recap(input, mode = 'plain', opts = {}) {
1731
1755
  parts.push('');
1732
1756
  }
1733
1757
  parts.push(body);
1734
- writeRecapOutput(parts.join('\n') + '\n', mode);
1758
+ writeRecapOutput(parts.join('\n') + '\n', mode, grokShort, input.session_id);
1735
1759
  }
1736
1760
  /** Arm-state detection moved LOCAL (2026-06-04). The former `isSessionArmed`
1737
1761
  * asked the server's watcher registry "does this session have a live socket?"
@@ -1814,8 +1838,14 @@ async function procedureCheck(input) {
1814
1838
  }
1815
1839
  }
1816
1840
  async function notify(input, source = 'claude-code', chipContext) {
1817
- if (source === 'codex')
1841
+ const harness = (0, harness_1.inferCurrentHarness)() || source;
1842
+ if (source === 'codex' || harness === 'grok')
1818
1843
  (0, codex_prompt_cache_1.cacheCodexPrompt)(input);
1844
+ if (harness === 'grok') {
1845
+ // UserPromptSubmit is observe-only on Grok — stdout cannot inject.
1846
+ // Cache the prompt for Stop capture, then return.
1847
+ return;
1848
+ }
1819
1849
  // Codex Desktop reliably executes only the first UserPromptSubmit command.
1820
1850
  // Procedure therefore shares the proven codex-notify bridge instead of
1821
1851
  // depending on a sibling registration that the harness may skip.
@@ -1993,7 +2023,9 @@ async function collisionCheck(input) {
1993
2023
  * poll live-tails with no double-delivery. Closes the dead-window gap where a
1994
2024
  * session-directed message that lands between watcher death and re-arm sits
1995
2025
  * unread until a manual `greprag inbox`. Inbound-only — the front desk (cold
1996
- * opens + email) stays with the human-scoped `mail` hook. All real logic lives
2026
+ * opens + email) stays with the human-scoped `mail` hook. Injects only when
2027
+ * unarmed (`isLocallyArmed`); a live watcher already delivered the body.
2028
+ * Grok also wires this on Stop as the unarmed floor. All real logic lives
1997
2029
  * in ./commands/inbox-drain (pure + tested). Best-effort: any miss → silent,
1998
2030
  * never blocks SessionStart. adr: adr/monitor-resilience.md */
1999
2031
  async function drain(input) {
@@ -2004,9 +2036,14 @@ async function drain(input) {
2004
2036
  const short = (0, session_id_1.truncateSessionId)(input.session_id);
2005
2037
  if (!short)
2006
2038
  return; // no session id → silent
2007
- const result = await (0, inbox_drain_1.runInboxDrain)({ session: short, apiUrl: cfg.apiUrl, apiKey: cfg.apiKey });
2039
+ const result = await (0, inbox_drain_1.runInboxDrain)({
2040
+ session: short, apiUrl: cfg.apiUrl, apiKey: cfg.apiKey,
2041
+ armed: (0, watcher_registry_1.isLocallyArmed)(short),
2042
+ });
2008
2043
  if (result.context) {
2009
2044
  (0, hook_runtime_1.writeAdditionalContext)(input.hook_event_name || 'SessionStart', result.context);
2045
+ if ((0, harness_1.inferCurrentHarness)() === 'grok')
2046
+ (0, grok_session_1.appendGrokSidecar)(short, result.context);
2010
2047
  }
2011
2048
  }
2012
2049
  function validateChip(title, prompt) {
@@ -2208,14 +2245,19 @@ async function main() {
2208
2245
  chunks.push(chunk);
2209
2246
  }
2210
2247
  const raw = Buffer.concat(chunks).toString('utf-8').trim();
2211
- if (raw)
2212
- input = JSON.parse(raw);
2248
+ input = (0, hook_runtime_1.normalizeHookInput)(raw ? JSON.parse(raw) : {});
2213
2249
  }
2214
2250
  catch {
2215
2251
  process.exit(0);
2216
2252
  }
2253
+ if (!(0, hook_once_1.claimHookOnce)(input.session_id, input.hook_event_name, subcommand, input.turn_id)) {
2254
+ process.exit(0);
2255
+ }
2256
+ const harness = (0, harness_1.inferCurrentHarness)();
2217
2257
  if (subcommand === 'recap') {
2218
- await recap(input);
2258
+ await recap(input, harness === 'grok' ? 'additionalContext' : 'plain', {
2259
+ platform: harness === 'grok' ? 'grok' : undefined,
2260
+ });
2219
2261
  }
2220
2262
  else if (subcommand === 'recompact') {
2221
2263
  // PostCompact — re-present the Interrupt System's announce modules (default-on;
@@ -2228,7 +2270,7 @@ async function main() {
2228
2270
  await recap(input, 'additionalContext', { platform: 'codex' });
2229
2271
  }
2230
2272
  else if (subcommand === 'notify') {
2231
- await notify(input);
2273
+ await notify(input, harness === 'grok' ? 'grok' : 'claude-code');
2232
2274
  }
2233
2275
  else if (subcommand === 'mail') {
2234
2276
  await mail(input);
@@ -2273,6 +2315,23 @@ async function main() {
2273
2315
  // hook's job now (it injects the arm directive on any turn the session is
2274
2316
  // found to have no live watcher), so SessionStart no longer arms.
2275
2317
  (0, session_id_1.handleSessionIdHook)(input);
2318
+ if (harness === 'grok') {
2319
+ const short = (0, session_id_1.truncateSessionId)(input.session_id);
2320
+ if (short) {
2321
+ let existing = '';
2322
+ try {
2323
+ existing = fs.readFileSync((0, grok_session_1.grokSidecarPath)(short), 'utf-8');
2324
+ }
2325
+ catch { /* none yet */ }
2326
+ if (!existing.includes('ARM (Grok monitor')) {
2327
+ const block = grokSidecarHead(short, input.session_id);
2328
+ if (existing)
2329
+ (0, grok_session_1.appendGrokSidecar)(short, block);
2330
+ else
2331
+ (0, grok_session_1.writeGrokSidecar)(short, block);
2332
+ }
2333
+ }
2334
+ }
2276
2335
  }
2277
2336
  else if (subcommand === 'pre-spawn-check') {
2278
2337
  handlePreSpawnCheck(input);
@@ -2361,7 +2420,15 @@ async function main() {
2361
2420
  // the next turn's PreToolUse `source:"state"` rules read a fresh stress /
2362
2421
  // turnCount reading. Order: store first (the primary job), then the
2363
2422
  // best-effort state stash. docs/ingress-trigger-bridge.md
2364
- await store(input);
2423
+ await store(input, harness === 'grok' ? 'grok' : 'claude-code');
2424
+ if (harness === 'grok') {
2425
+ // Floor: inject unread mail only when unarmed. Do NOT nag UNARMED here —
2426
+ // Grok Stop fires every turn, and a pidfile-alias miss made that a
2427
+ // re-arm loop (watch already live → singleton-guard exits instantly →
2428
+ // model arms again). Arm teaching is sidecar + rules.
2429
+ // adr: adr/grok-platform.md
2430
+ await drain(input);
2431
+ }
2365
2432
  stateUpdate(input);
2366
2433
  }
2367
2434
  }