greprag 5.74.8 → 5.74.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,184 @@
1
+ "use strict";
2
+ /** Codex checkpoint coordination — detect a successful Git commit transition
3
+ * and inject the delivery handoff at that exact lifecycle boundary.
4
+ *
5
+ * PreToolUse records the prior HEAD around shell-capable tools. PostToolUse
6
+ * proves HEAD changed via a commit reflog action and emits immediately; the
7
+ * next PreToolUse is a fallback if that lifecycle event was unavailable.
8
+ * User prompt and executor-wrapper wording are irrelevant.
9
+ * adr: adr/codex-checkpoint-coordination.md */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.checkpointCoordinationText = checkpointCoordinationText;
45
+ exports.recordCodexGitBoundary = recordCodexGitBoundary;
46
+ exports.evaluateCodexCommitResult = evaluateCodexCommitResult;
47
+ exports.evaluatePendingCodexCheckpoint = evaluatePendingCodexCheckpoint;
48
+ const crypto = __importStar(require("crypto"));
49
+ const fs = __importStar(require("fs"));
50
+ const path = __importStar(require("path"));
51
+ const child_process_1 = require("child_process");
52
+ function homeDir() {
53
+ return process.env.USERPROFILE || process.env.HOME || '';
54
+ }
55
+ function normalizedCwd(input) {
56
+ return path.resolve(input.cwd || process.cwd()).replace(/\\/g, '/').toLowerCase();
57
+ }
58
+ function statePath(input) {
59
+ const home = homeDir();
60
+ const session = (input.session_id || '').trim();
61
+ if (!home || !session)
62
+ return null;
63
+ const key = crypto.createHash('sha256')
64
+ .update(`${session}\0${normalizedCwd(input)}`)
65
+ .digest('hex')
66
+ .slice(0, 24);
67
+ return path.join(home, '.greprag', 'state', `codex-checkpoint-${key}.json`);
68
+ }
69
+ function git(cwd, args) {
70
+ try {
71
+ return (0, child_process_1.execFileSync)('git', args, {
72
+ cwd,
73
+ encoding: 'utf8',
74
+ stdio: ['ignore', 'pipe', 'ignore'],
75
+ windowsHide: true,
76
+ }).trim() || null;
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ function currentHead(cwd) {
83
+ return git(cwd, ['rev-parse', 'HEAD']);
84
+ }
85
+ function currentBranch(cwd) {
86
+ return git(cwd, ['branch', '--show-current']) || '(detached HEAD)';
87
+ }
88
+ function latestHeadAction(cwd) {
89
+ return git(cwd, ['reflog', '-1', '--format=%gs', 'HEAD']) || '';
90
+ }
91
+ function isCommitHeadAction(cwd) {
92
+ return /^commit(?: \([^)]+\))?:/i.test(latestHeadAction(cwd));
93
+ }
94
+ function isShellCapableTool(input) {
95
+ const name = (input.tool_name || '').toLowerCase();
96
+ return /(?:^|[.:/_-])(bash|shell|exec|exec_command|write_stdin)$/.test(name);
97
+ }
98
+ function writePending(file, pending) {
99
+ fs.mkdirSync(path.dirname(file), { recursive: true });
100
+ fs.writeFileSync(file, JSON.stringify(pending, null, 2) + '\n');
101
+ }
102
+ function readPending(file) {
103
+ try {
104
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ }
110
+ function clearPending(file) {
111
+ try {
112
+ fs.unlinkSync(file);
113
+ }
114
+ catch { /* already absent */ }
115
+ }
116
+ function consumePending(input, hookEventName) {
117
+ const file = statePath(input);
118
+ if (!file)
119
+ return null;
120
+ const pending = readPending(file);
121
+ if (!pending || pending.cwd !== normalizedCwd(input))
122
+ return null;
123
+ clearPending(file);
124
+ const cwd = input.cwd || process.cwd();
125
+ const afterHead = currentHead(cwd);
126
+ if (!afterHead || afterHead === pending.beforeHead || !isCommitHeadAction(cwd))
127
+ return null;
128
+ return {
129
+ hookSpecificOutput: {
130
+ hookEventName,
131
+ additionalContext: checkpointCoordinationText(currentBranch(cwd), afterHead.slice(0, 12)),
132
+ },
133
+ };
134
+ }
135
+ function checkpointCoordinationText(branch, sha) {
136
+ return `[DELIVERY COORDINATION — CHECKPOINT CREATED]
137
+
138
+ Checkpoint: ${branch} @ ${sha}
139
+
140
+ Coordinate now, before merge, push, deploy, publish, or release.
141
+
142
+ - Child task: send the LEAD your branch, SHA, checks, status, owned dirt, and blockers. The LEAD owns integration.
143
+ - LEAD/standalone task: call codex_app.list_threads unfiltered, scope to this repo/worktree, exclude yourself, and ask every live peer for its latest checkpoint, owned dirt, blockers, and sequencing needs.
144
+ - Elect exactly one delivery owner. If no peers exist, you are the owner.
145
+ - Use greprag send for cross-harness peers.
146
+
147
+ Do not begin a delivery action until coordination is settled.`;
148
+ }
149
+ /** PreToolUse leg: remember HEAD around any shell-capable action. The result
150
+ * leg classifies the actual Git transition, so nested executor syntax and
151
+ * dynamically composed commands do not matter. */
152
+ function recordCodexGitBoundary(input) {
153
+ if (input.hook_event_name !== 'PreToolUse')
154
+ return;
155
+ if (!isShellCapableTool(input))
156
+ return;
157
+ const file = statePath(input);
158
+ if (!file)
159
+ return;
160
+ const cwd = input.cwd || process.cwd();
161
+ writePending(file, {
162
+ sessionId: input.session_id || '',
163
+ cwd: normalizedCwd(input),
164
+ toolName: input.tool_name || '',
165
+ beforeHead: currentHead(cwd),
166
+ recordedAt: new Date().toISOString(),
167
+ });
168
+ }
169
+ /** Preferred result leg: emit on the successful commit's own PostToolUse. */
170
+ function evaluateCodexCommitResult(input) {
171
+ if (input.hook_event_name !== 'PostToolUse')
172
+ return null;
173
+ if (!isShellCapableTool(input))
174
+ return null;
175
+ return consumePending(input, 'PostToolUse');
176
+ }
177
+ /** Fallback result leg: if PostToolUse was unavailable, emit before the first
178
+ * later tool call. Failed/empty commits leave HEAD unchanged and clear
179
+ * silently. */
180
+ function evaluatePendingCodexCheckpoint(input) {
181
+ if (input.hook_event_name !== 'PreToolUse')
182
+ return null;
183
+ return consumePending(input, 'PreToolUse');
184
+ }
@@ -65,9 +65,10 @@ async function main() {
65
65
  const subcommand = process.argv[2];
66
66
  if (subcommand !== 'codex-chip-hook'
67
67
  && subcommand !== 'codex-pretooluse'
68
+ && subcommand !== 'codex-posttooluse'
68
69
  && subcommand !== 'codex-notify'
69
70
  && subcommand !== 'codex-store') {
70
- process.stderr.write('Usage: greprag-codex-hook <codex-chip-hook|codex-pretooluse|codex-notify|codex-store>\n');
71
+ process.stderr.write('Usage: greprag-codex-hook <codex-chip-hook|codex-pretooluse|codex-posttooluse|codex-notify|codex-store>\n');
71
72
  process.exit(1);
72
73
  }
73
74
  const input = await readInput();
@@ -81,6 +82,13 @@ async function main() {
81
82
  await runCodexNotify(input);
82
83
  return;
83
84
  }
85
+ if (subcommand === 'codex-posttooluse') {
86
+ const { evaluateCodexCommitResult } = await Promise.resolve().then(() => __importStar(require('./codex-checkpoint-hook')));
87
+ const result = evaluateCodexCommitResult(input);
88
+ if (result)
89
+ process.stdout.write(JSON.stringify(result) + '\n');
90
+ return;
91
+ }
84
92
  const { evaluateCodexChipHook } = await Promise.resolve().then(() => __importStar(require('./codex-chip-hooks')));
85
93
  const chipResult = evaluateCodexChipHook(input);
86
94
  if (subcommand === 'codex-chip-hook') {
@@ -89,6 +97,11 @@ async function main() {
89
97
  return;
90
98
  }
91
99
  let result = chipResult;
100
+ if (subcommand === 'codex-pretooluse') {
101
+ const { evaluatePendingCodexCheckpoint, recordCodexGitBoundary, } = await Promise.resolve().then(() => __importStar(require('./codex-checkpoint-hook')));
102
+ result = mergeOutputs(result, evaluatePendingCodexCheckpoint(input));
103
+ recordCodexGitBoundary(input);
104
+ }
92
105
  if (!result?.hookSpecificOutput.permissionDecision && input.tool_name === 'Bash') {
93
106
  const { runSearchGuard } = await Promise.resolve().then(() => __importStar(require('./commands/search-guard')));
94
107
  result = mergeOutputs(result, runSearchGuard(input));
@@ -38,6 +38,7 @@
38
38
  * shared-state mutation actually happens. The eval is ready for a future Write
39
39
  * adapter; only a new triggerFrom* + a `Write|Edit` matcher would be needed. */
40
40
  Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.hasGitSubcommand = hasGitSubcommand;
41
42
  exports.classifyRiskyCommand = classifyRiskyCommand;
42
43
  exports.triggerFromPreToolUse = triggerFromPreToolUse;
43
44
  exports.buildCoordinateDirective = buildCoordinateDirective;
@@ -141,6 +142,30 @@ function classifyGit(tokens) {
141
142
  return { kind: 'push', label: 'git push' };
142
143
  return null;
143
144
  }
145
+ /** True when any executable command segment invokes the requested Git
146
+ * subcommand, including `git -C <worktree> <subcommand>`. Exported so lifecycle
147
+ * hooks can key off agent-generated Git actions instead of user prompt words. */
148
+ function hasGitSubcommand(command, expected) {
149
+ if (!command || !expected)
150
+ return false;
151
+ for (const seg of commandSegments(command)) {
152
+ const tokens = shellWords(seg);
153
+ if (tokens[0] !== 'git')
154
+ continue;
155
+ let i = 1;
156
+ const optionsWithValue = new Set([
157
+ '-C', '-c', '--exec-path', '--git-dir', '--work-tree', '--namespace', '--super-prefix', '--config-env',
158
+ ]);
159
+ while (i < tokens.length && tokens[i].startsWith('-')) {
160
+ const option = tokens[i++];
161
+ if (optionsWithValue.has(option) && i < tokens.length)
162
+ i++;
163
+ }
164
+ if (tokens[i] === expected)
165
+ return true;
166
+ }
167
+ return false;
168
+ }
144
169
  /** Classify a Bash command into a risky-action trigger, or null. PURE. Matches
145
170
  * the FIRST risky segment (a chained command fires on whichever risky verb
146
171
  * appears first). */
@@ -636,9 +636,9 @@ async function runCodexInit(opts) {
636
636
  }
637
637
  console.log(`\n Codex hooks file: ${hooksPath}`);
638
638
  console.log(` Project anchor: ${anchor.anchorPath}`);
639
- console.log(' Start a fresh Codex session, then open Settings -> Settings -> Hooks and trust the GrepRAG hook definitions.');
639
+ console.log(' Open Settings -> Settings -> Hooks and trust the GrepRAG hook definitions, then fully restart Codex Desktop.');
640
640
  console.log(' Inbox messages for Codex surface on SessionStart and UserPromptSubmit hook boundaries.');
641
- console.log(' Memory hooks will activate on your next Codex session.\n');
641
+ console.log(' Hooks will activate after the Codex host restart; starting only a new task is not sufficient after hook changes.\n');
642
642
  }
643
643
  /** greprag init --global
644
644
  * Creates ~/.greprag/project.json with a stable UUID.
@@ -1130,6 +1130,19 @@ function applyCodexHooks(config) {
1130
1130
  if (removedPostToolInbox) {
1131
1131
  changes.push(`Removed ${removedPostToolInbox} Codex PostToolUse inbox hook(s); UserPromptSubmit now owns inbox steering`);
1132
1132
  }
1133
+ const checkpointHook = {
1134
+ matcher: '',
1135
+ hooks: [commandHook('codex-posttooluse', 5, 'Coordinating Codex checkpoint', 'greprag-codex-hook')],
1136
+ };
1137
+ if (!hasGrepragHook(config.hooks.PostToolUse, 'codex-posttooluse')) {
1138
+ if (!config.hooks.PostToolUse)
1139
+ config.hooks.PostToolUse = [];
1140
+ config.hooks.PostToolUse.push(checkpointHook);
1141
+ changes.push('Added Codex PostToolUse hook (successful checkpoint coordination)');
1142
+ }
1143
+ else {
1144
+ changes.push('Codex PostToolUse checkpoint hook already configured (skipped)');
1145
+ }
1133
1146
  const permissionHook = {
1134
1147
  matcher: '',
1135
1148
  hooks: [commandHook('codex-permission-context', 3, 'Loading GrepRAG approval context')],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "greprag",
3
- "version": "5.74.8",
3
+ "version": "5.74.9",
4
4
  "description": "GrepRAG — agent memory for Claude Code, Codex, and OpenCode.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {