@pushary/agent-hooks 0.95.8 → 0.98.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.
Files changed (82) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/bin/pushary-bell-hook.js +19 -21
  3. package/dist/bin/pushary-bell.js +6 -8
  4. package/dist/bin/pushary-claude.js +21 -23
  5. package/dist/bin/pushary-clean.js +19 -22
  6. package/dist/bin/pushary-codex-bridge.js +13 -14
  7. package/dist/bin/pushary-codex-hook.js +452 -104
  8. package/dist/bin/pushary-codex.js +8 -8
  9. package/dist/bin/pushary-connect.js +6 -6
  10. package/dist/bin/pushary-cowork.js +3 -3
  11. package/dist/bin/pushary-daemon.js +25 -27
  12. package/dist/bin/pushary-disconnect.js +6 -8
  13. package/dist/bin/pushary-doctor.js +28 -23
  14. package/dist/bin/pushary-elicitation-hook.js +19 -21
  15. package/dist/bin/pushary-gemini-bridge.js +11 -12
  16. package/dist/bin/pushary-gemini-hook.js +22 -24
  17. package/dist/bin/pushary-hook.js +24 -26
  18. package/dist/bin/pushary-login.js +7 -8
  19. package/dist/bin/pushary-logout.js +4 -4
  20. package/dist/bin/pushary-mode.js +3 -3
  21. package/dist/bin/pushary-notification-hook.js +21 -23
  22. package/dist/bin/pushary-opencode-hook.js +22 -24
  23. package/dist/bin/pushary-permission-denied-hook.js +28 -27
  24. package/dist/bin/pushary-permission-hook.js +24 -26
  25. package/dist/bin/pushary-post-hook.js +21 -23
  26. package/dist/bin/pushary-prompt-hook.js +21 -23
  27. package/dist/bin/pushary-session-end-hook.js +21 -23
  28. package/dist/bin/pushary-session-start-hook.js +24 -26
  29. package/dist/bin/pushary-setup.js +45 -40
  30. package/dist/bin/pushary-stats.js +2 -2
  31. package/dist/bin/pushary-status.js +10 -11
  32. package/dist/bin/pushary-stop-hook.js +21 -23
  33. package/dist/bin/pushary-stopfailure-hook.js +21 -23
  34. package/dist/bin/pushary-transcript-register.js +17 -19
  35. package/dist/bin/pushary-transcripts.js +5 -5
  36. package/dist/bin/pushary-upgrade.js +17 -20
  37. package/dist/bin/pushary-wait.js +3 -3
  38. package/dist/{chunk-GOPIMNVC.js → chunk-2GLOHBYB.js} +1 -1
  39. package/dist/{chunk-BQL7UDIB.js → chunk-3RVCQDC4.js} +1 -1
  40. package/dist/chunk-53QCTSWY.js +202 -0
  41. package/dist/{chunk-3YFJGKJQ.js → chunk-5B5QV3KC.js} +8 -8
  42. package/dist/{chunk-FRXZMCVA.js → chunk-5KLAETSU.js} +1 -1
  43. package/dist/{chunk-SDNTRS76.js → chunk-6C6MS57P.js} +1 -1
  44. package/dist/{chunk-A22VQ5ZL.js → chunk-6CYB77VD.js} +111 -55
  45. package/dist/{chunk-QD73WATQ.js → chunk-6LRGZJFB.js} +21 -21
  46. package/dist/{chunk-Z4JVEL7R.js → chunk-A76GNRIK.js} +191 -2
  47. package/dist/{chunk-RE25RJ5A.js → chunk-B35OCFXH.js} +4 -4
  48. package/dist/{chunk-UKMLSQXA.js → chunk-CP34FI4B.js} +2 -2
  49. package/dist/{chunk-I7LKI4WO.js → chunk-DOD3A6EU.js} +6 -8
  50. package/dist/{chunk-N6FAXQDD.js → chunk-FAJ4R7IB.js} +1 -1
  51. package/dist/{chunk-U5RIXR7I.js → chunk-HB2KB7QL.js} +1 -1
  52. package/dist/{chunk-QV266EGS.js → chunk-HFQ7WSHZ.js} +11 -14
  53. package/dist/{chunk-QJPYJVL3.js → chunk-IRLTW6KF.js} +1 -1
  54. package/dist/{chunk-Y6SCMXR6.js → chunk-J44RPHUZ.js} +2 -2
  55. package/dist/{chunk-R6LBGKWQ.js → chunk-JQU6PMH4.js} +17 -15
  56. package/dist/{chunk-4DUEIUUO.js → chunk-K6CH5NVW.js} +1 -1
  57. package/dist/{chunk-XZWUOCJZ.js → chunk-LBNAGGLV.js} +1 -1
  58. package/dist/{chunk-XS33QBJA.js → chunk-LGXUGYUH.js} +50 -1
  59. package/dist/{chunk-XO3JR3OP.js → chunk-LX4NNYII.js} +2 -2
  60. package/dist/{chunk-XEVOPOKT.js → chunk-M4TVEMLH.js} +1 -1
  61. package/dist/{chunk-TY6IJY6I.js → chunk-NIXVMMJM.js} +1 -1
  62. package/dist/{chunk-VERXHIQF.js → chunk-NQGYPTJQ.js} +1 -1
  63. package/dist/{chunk-EMGGUO2Q.js → chunk-OBYC6SN4.js} +1 -1
  64. package/dist/{chunk-XUTSZYYJ.js → chunk-OPR2BPD4.js} +1 -4
  65. package/dist/{chunk-MC5WUOJJ.js → chunk-OUAE3GKO.js} +75 -1
  66. package/dist/{chunk-ATKGKAP6.js → chunk-QXIF6R4P.js} +16 -13
  67. package/dist/chunk-SI7GYBQP.js +15 -0
  68. package/dist/{chunk-VMQU7YEJ.js → chunk-SJLGOVC6.js} +1 -1
  69. package/dist/{chunk-54W7ELDQ.js → chunk-TZHDKRMG.js} +2 -2
  70. package/dist/{chunk-F4TMY6XI.js → chunk-V47GITZV.js} +3 -3
  71. package/dist/{chunk-L7NOG547.js → chunk-VZD2GEKB.js} +1 -1
  72. package/dist/{chunk-R3OZ5ISU.js → chunk-W2MG2P46.js} +4 -4
  73. package/dist/{chunk-E47WV7BJ.js → chunk-WU6LSXJ6.js} +2 -2
  74. package/dist/{chunk-NOVA7KZ2.js → chunk-XL3RKJYQ.js} +308 -35
  75. package/dist/{chunk-5GKNAQTG.js → chunk-XOOWC62T.js} +1 -1
  76. package/dist/{chunk-SKPXQRKN.js → chunk-XPPXOKIG.js} +1 -1
  77. package/dist/{chunk-HUGIKB5O.js → chunk-YJDRHQLG.js} +6 -6
  78. package/dist/src/index.js +11 -11
  79. package/package.json +1 -1
  80. package/dist/chunk-3AJIK5AO.js +0 -57
  81. package/dist/chunk-L4ZCFMWW.js +0 -177
  82. package/dist/chunk-VDI645ZX.js +0 -18
@@ -1,33 +1,42 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- defersToNativeMode
4
- } from "../chunk-VDI645ZX.js";
3
+ takeHookHoldBudget
4
+ } from "../chunk-SI7GYBQP.js";
5
+ import {
6
+ blockedActionFingerprint,
7
+ codexCallKey,
8
+ consumeCodexCallDecision,
9
+ defersToNativeMode,
10
+ forgetBlockedActionHint,
11
+ hasBlockedActionHint,
12
+ recordBlockedActionHint,
13
+ recordCodexCallDecision,
14
+ userScope
15
+ } from "../chunk-53QCTSWY.js";
5
16
  import {
6
17
  isGatingMoment,
7
18
  recordKeylessMoment
8
- } from "../chunk-5GKNAQTG.js";
19
+ } from "../chunk-XOOWC62T.js";
9
20
  import {
10
21
  readHookInput
11
- } from "../chunk-XEVOPOKT.js";
22
+ } from "../chunk-M4TVEMLH.js";
12
23
  import {
13
24
  denyReasonFrom,
14
25
  isDeferAnswer
15
26
  } from "../chunk-YHG74UFF.js";
16
- import "../chunk-3YFJGKJQ.js";
17
- import "../chunk-EMGGUO2Q.js";
18
- import "../chunk-R3OZ5ISU.js";
19
- import "../chunk-XZWUOCJZ.js";
20
- import "../chunk-QJPYJVL3.js";
21
- import "../chunk-QD73WATQ.js";
22
- import "../chunk-I7LKI4WO.js";
23
- import "../chunk-QV266EGS.js";
24
- import "../chunk-GOPIMNVC.js";
25
- import "../chunk-3AJIK5AO.js";
26
- import "../chunk-SKPXQRKN.js";
27
+ import "../chunk-5B5QV3KC.js";
28
+ import "../chunk-OBYC6SN4.js";
29
+ import "../chunk-W2MG2P46.js";
30
+ import "../chunk-LBNAGGLV.js";
31
+ import "../chunk-IRLTW6KF.js";
32
+ import "../chunk-6LRGZJFB.js";
33
+ import "../chunk-DOD3A6EU.js";
34
+ import "../chunk-HFQ7WSHZ.js";
35
+ import "../chunk-2GLOHBYB.js";
36
+ import "../chunk-XPPXOKIG.js";
27
37
  import "../chunk-EQRS3SGM.js";
28
- import "../chunk-VERXHIQF.js";
38
+ import "../chunk-NQGYPTJQ.js";
29
39
  import "../chunk-XHKBHWLX.js";
30
- import "../chunk-L4ZCFMWW.js";
31
40
  import {
32
41
  CODEX_AGENT,
33
42
  DEFAULT_SESSION,
@@ -52,100 +61,97 @@ import {
52
61
  toCodexWire,
53
62
  toPolicyLookup,
54
63
  toPolicyLookups
55
- } from "../chunk-HUGIKB5O.js";
64
+ } from "../chunk-YJDRHQLG.js";
56
65
  import {
57
66
  UNNAMED_ACTION,
67
+ codexReviewerDenialsSince,
58
68
  deriveActionBody,
59
69
  deriveBlocker,
60
70
  deriveToolTarget,
61
71
  describeToolCall,
72
+ isCodexGuardianRolloutHead,
73
+ readCodexTurnContext,
74
+ scanCodexGuardianReviews,
62
75
  scopePathFor,
63
76
  sessionIdentityOf
64
- } from "../chunk-Z4JVEL7R.js";
65
- import "../chunk-XUTSZYYJ.js";
77
+ } from "../chunk-A76GNRIK.js";
78
+ import "../chunk-OPR2BPD4.js";
66
79
  import {
67
80
  DECISION_STOP_REASON,
68
81
  askUser,
82
+ cancelQuestion,
83
+ claimBlockedAction,
69
84
  isDecisionStopped,
70
85
  isLocalQuestionHandoff,
86
+ pollForAnswer,
87
+ requestGate,
71
88
  sendNotification,
72
89
  waitForQuestion
73
- } from "../chunk-XS33QBJA.js";
90
+ } from "../chunk-LGXUGYUH.js";
74
91
  import {
75
92
  getMachineId
76
- } from "../chunk-E47WV7BJ.js";
93
+ } from "../chunk-WU6LSXJ6.js";
77
94
  import {
78
95
  canonicalJson
79
- } from "../chunk-NOVA7KZ2.js";
96
+ } from "../chunk-XL3RKJYQ.js";
80
97
  import "../chunk-GMXKITVA.js";
81
98
  import "../chunk-DINDL2CF.js";
82
- import "../chunk-VMQU7YEJ.js";
99
+ import "../chunk-SJLGOVC6.js";
83
100
  import "../chunk-EYRGBYZG.js";
84
101
  import "../chunk-SAF6HGAA.js";
85
102
  import "../chunk-7QLSKOSU.js";
86
103
  import "../chunk-KZERVKTD.js";
87
- import "../chunk-L7NOG547.js";
104
+ import {
105
+ writeFileAtomic
106
+ } from "../chunk-VZD2GEKB.js";
88
107
  import {
89
108
  getApiKey
90
109
  } from "../chunk-2UMNXADU.js";
91
110
  import {
111
+ CODEX_RESCUE_TOOL_NAME,
92
112
  DECISION_LINE_MAX,
113
+ GATE_BLOCK_REASON_MAX_LENGTH,
114
+ GATE_WIRE,
93
115
  KILL_REASON,
116
+ codexStopRescueOutput,
94
117
  effectiveWaitSeconds,
118
+ hookHoldWindowMs,
95
119
  hookWaitClamped,
96
120
  hookWaitDeadline,
97
121
  resolveGate
98
- } from "../chunk-MC5WUOJJ.js";
122
+ } from "../chunk-OUAE3GKO.js";
99
123
 
100
124
  // bin/pushary-codex-hook.ts
101
125
  import { basename } from "path";
102
126
 
103
- // src/codex-call-decision.ts
127
+ // src/codex-reviewer.ts
128
+ import { createHash as createHash2 } from "crypto";
129
+
130
+ // src/codex-rollout.ts
104
131
  import { createHash } from "crypto";
105
- import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "fs";
106
- import { tmpdir, userInfo } from "os";
107
- import { join } from "path";
108
- var CODEX_DECISION_TTL_MS = 60 * 1e3;
109
- var userScope = () => {
110
- try {
111
- const info = userInfo();
112
- const id = typeof info.uid === "number" && info.uid >= 0 ? String(info.uid) : info.username;
113
- return id.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 128) || "default";
114
- } catch {
115
- return "default";
116
- }
117
- };
118
- var decisionDir = () => join(tmpdir(), `pushary-codex-decisions-${userScope()}`);
119
- var decisionFile = (key) => join(decisionDir(), key);
120
- var PRESENTATION_ONLY_INPUT_KEYS = ["description"];
121
- var identifyingToolInput = (toolInput) => {
122
- if (!toolInput) return {};
123
- const identifying = {};
124
- for (const [key, value] of Object.entries(toolInput)) {
125
- if (PRESENTATION_ONLY_INPUT_KEYS.includes(key)) continue;
126
- identifying[key] = value;
127
- }
128
- return identifying;
129
- };
130
- var codexCallKey = (identity) => {
131
- if (!identity.sessionId || !identity.turnId || !identity.toolName) return null;
132
- const material = canonicalJson([
133
- identity.sessionId,
134
- identity.turnId,
135
- identity.toolName,
136
- identifyingToolInput(identity.toolInput)
137
- ]);
138
- return createHash("sha256").update(material, "utf-8").digest("hex").slice(0, 32);
139
- };
140
- var parseDecision = (raw) => {
132
+ import { closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, rmSync, statSync } from "fs";
133
+ import { tmpdir } from "os";
134
+ import { dirname, isAbsolute, join, relative } from "path";
135
+ var CODEX_ROLLOUT_TAIL_MAX_BYTES = 8 * 1024 * 1024;
136
+ var CODEX_ROLLOUT_HEAD_MAX_BYTES = 1024 * 1024;
137
+ var CODEX_GUARDIAN_READ_MAX_BYTES = 8 * 1024 * 1024;
138
+ var CODEX_GUARDIAN_SCAN_MAX_DAYS = 31;
139
+ var CODEX_GUARDIAN_SCAN_MAX_FILES = 2e3;
140
+ var CODEX_ROLLOUT_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
141
+ var READ_CHUNK_BYTES = 64 * 1024;
142
+ var TURN_CONTEXT_LINE_MAX_BYTES = 1024 * 1024;
143
+ var MTIME_SLACK_MS = 2e3;
144
+ var NEWLINE = 10;
145
+ var TURN_CONTEXT_MARKER = '"turn_context"';
146
+ var ROLLOUT_FILE = /^rollout-.*\.jsonl$/;
147
+ var DATE_SEGMENTS = /^(\d{4})\/(\d{2})\/(\d{2})$/;
148
+ var cacheKey = (...parts) => createHash("sha256").update(parts.join("\n"), "utf-8").digest("hex").slice(0, 32);
149
+ var turnCacheDir = () => join(tmpdir(), `pushary-codex-turns-${userScope()}`);
150
+ var guardianCursorDir = () => join(tmpdir(), `pushary-codex-guardian-${userScope()}`);
151
+ var readJsonRecord = (path) => {
141
152
  try {
142
- const parsed = JSON.parse(raw);
143
- if (parsed.kind === "approved") return { kind: "approved" };
144
- if (parsed.kind === "handled") return { kind: "handled" };
145
- if (parsed.kind === "denied") {
146
- return { kind: "denied", reason: typeof parsed.reason === "string" ? parsed.reason : "" };
147
- }
148
- return null;
153
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
154
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
149
155
  } catch {
150
156
  return null;
151
157
  }
@@ -155,7 +161,7 @@ var sweepExpired = (dir, nowMs) => {
155
161
  for (const name of readdirSync(dir)) {
156
162
  const path = join(dir, name);
157
163
  try {
158
- if (nowMs - statSync(path).mtimeMs >= CODEX_DECISION_TTL_MS) rmSync(path, { force: true });
164
+ if (nowMs - statSync(path).mtimeMs >= CODEX_ROLLOUT_CACHE_TTL_MS) rmSync(path, { force: true });
159
165
  } catch {
160
166
  continue;
161
167
  }
@@ -164,27 +170,365 @@ var sweepExpired = (dir, nowMs) => {
164
170
  return;
165
171
  }
166
172
  };
167
- var recordCodexCallDecision = (key, decision) => {
168
- if (!key) return;
173
+ var writeJsonRecord = (dir, name, record) => {
169
174
  try {
170
- const dir = decisionDir();
171
175
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 448 });
172
176
  else sweepExpired(dir, Date.now());
173
- writeFileSync(decisionFile(key), JSON.stringify(decision), { encoding: "utf-8", mode: 384 });
177
+ writeFileAtomic(join(dir, name), JSON.stringify(record), 384);
178
+ return true;
174
179
  } catch {
175
- return;
180
+ return false;
181
+ }
182
+ };
183
+ var readBytes = (path, offset, length) => {
184
+ let fd;
185
+ try {
186
+ fd = openSync(path, "r");
187
+ const buffer = Buffer.alloc(length);
188
+ const read = readSync(fd, buffer, 0, length, offset);
189
+ return read === length ? buffer : null;
190
+ } catch {
191
+ return null;
192
+ } finally {
193
+ if (fd !== void 0) closeSync(fd);
194
+ }
195
+ };
196
+ var locate = (region, base, from, to, read, marker) => {
197
+ if (to <= from) return null;
198
+ const text = region.toString("utf-8", from, to);
199
+ if (!text.includes(marker)) return null;
200
+ const value = read(text);
201
+ return value ? { value, offset: base + from, length: to - from } : null;
202
+ };
203
+ var findLastRolloutLine = (path, maxBytes, read, marker) => {
204
+ let fd;
205
+ try {
206
+ fd = openSync(path, "r");
207
+ const size = fstatSync(fd).size;
208
+ const floor = Math.max(0, size - maxBytes);
209
+ let carry = Buffer.alloc(0);
210
+ let position = size;
211
+ while (position > floor) {
212
+ const start = Math.max(floor, position - READ_CHUNK_BYTES);
213
+ const chunk = Buffer.alloc(position - start);
214
+ if (readSync(fd, chunk, 0, chunk.length, start) !== chunk.length) return null;
215
+ const region = carry.length > 0 ? Buffer.concat([chunk, carry]) : chunk;
216
+ let lineEnd = region.length;
217
+ let newline = region.lastIndexOf(NEWLINE, lineEnd - 1);
218
+ while (newline >= 0) {
219
+ const found = locate(region, start, newline + 1, lineEnd, read, marker);
220
+ if (found) return found;
221
+ lineEnd = newline;
222
+ newline = lineEnd > 0 ? region.lastIndexOf(NEWLINE, lineEnd - 1) : -1;
223
+ }
224
+ if (start === 0) return locate(region, 0, 0, lineEnd, read, marker);
225
+ carry = region.subarray(0, lineEnd);
226
+ position = start;
227
+ }
228
+ return null;
229
+ } catch {
230
+ return null;
231
+ } finally {
232
+ if (fd !== void 0) closeSync(fd);
233
+ }
234
+ };
235
+ var isWholeLineAt = (path, offset, length) => {
236
+ const before = offset > 0 ? 1 : 0;
237
+ const bytes = readBytes(path, offset - before, length + before + 1);
238
+ if (!bytes) return null;
239
+ if (before === 1 && bytes[0] !== NEWLINE) return null;
240
+ if (bytes[bytes.length - 1] !== NEWLINE) return null;
241
+ return bytes.toString("utf-8", before, before + length);
242
+ };
243
+ var cachedTurn = (file, transcriptPath, turnId) => {
244
+ const cached = readJsonRecord(file);
245
+ if (!cached || cached.transcriptPath !== transcriptPath || cached.turnId !== turnId) return null;
246
+ const { offset, length } = cached;
247
+ if (typeof offset !== "number" || typeof length !== "number") return null;
248
+ if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(length) || length <= 0) return null;
249
+ if (length > TURN_CONTEXT_LINE_MAX_BYTES) return null;
250
+ const line = isWholeLineAt(transcriptPath, offset, length);
251
+ const context = line === null ? null : readCodexTurnContext(line);
252
+ return context?.turnId === turnId ? context : null;
253
+ };
254
+ var readCodexTurn = (call, limits = {}) => {
255
+ const { transcriptPath, sessionId, turnId } = call;
256
+ if (!transcriptPath || !isAbsolute(transcriptPath) || !sessionId || !turnId) return null;
257
+ const name = cacheKey(sessionId, turnId);
258
+ const cached = cachedTurn(join(turnCacheDir(), name), transcriptPath, turnId);
259
+ if (cached) return cached;
260
+ const found = findLastRolloutLine(
261
+ transcriptPath,
262
+ limits.tailMaxBytes ?? CODEX_ROLLOUT_TAIL_MAX_BYTES,
263
+ readCodexTurnContext,
264
+ TURN_CONTEXT_MARKER
265
+ );
266
+ if (!found || found.value.turnId !== turnId) return null;
267
+ writeJsonRecord(turnCacheDir(), name, { transcriptPath, turnId, offset: found.offset, length: found.length });
268
+ return found.value;
269
+ };
270
+ var readFirstLine = (path, maxBytes) => {
271
+ let fd;
272
+ try {
273
+ fd = openSync(path, "r");
274
+ let collected = Buffer.alloc(0);
275
+ while (collected.length < maxBytes) {
276
+ const chunk = Buffer.alloc(Math.min(READ_CHUNK_BYTES, maxBytes - collected.length));
277
+ const read = readSync(fd, chunk, 0, chunk.length, collected.length);
278
+ if (read === 0) return null;
279
+ const newline = chunk.subarray(0, read).indexOf(NEWLINE);
280
+ if (newline >= 0) return Buffer.concat([collected, chunk.subarray(0, newline)]).toString("utf-8");
281
+ collected = Buffer.concat([collected, chunk.subarray(0, read)]);
282
+ }
283
+ return null;
284
+ } catch {
285
+ return null;
286
+ } finally {
287
+ if (fd !== void 0) closeSync(fd);
288
+ }
289
+ };
290
+ var sessionsLayoutOf = (transcriptPath) => {
291
+ const dayDir = dirname(transcriptPath);
292
+ const root = dirname(dirname(dirname(dayDir)));
293
+ const match = DATE_SEGMENTS.exec(relative(root, dayDir).split(/[\\/]/).join("/"));
294
+ if (!match) return null;
295
+ return { root, year: Number(match[1]), month: Number(match[2]), day: Number(match[3]) };
296
+ };
297
+ var isInside = (root, path) => {
298
+ const within = relative(root, path);
299
+ return within.length > 0 && !within.startsWith("..") && !isAbsolute(within);
300
+ };
301
+ var isGuardianRollout = (path, sessionId, headMaxBytes) => {
302
+ const head = readFirstLine(path, headMaxBytes);
303
+ return head !== null && isCodexGuardianRolloutHead(head, sessionId);
304
+ };
305
+ var pad = (value) => String(value).padStart(2, "0");
306
+ var dayDirectories = (layout, now) => {
307
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
308
+ const directories = [];
309
+ for (let offset = 0; offset <= CODEX_GUARDIAN_SCAN_MAX_DAYS; offset++) {
310
+ const date = new Date(layout.year, layout.month - 1, layout.day + offset);
311
+ if (offset > 0 && date.getTime() > today) break;
312
+ directories.push(join(layout.root, String(date.getFullYear()), pad(date.getMonth() + 1), pad(date.getDate())));
313
+ }
314
+ return directories;
315
+ };
316
+ var createdAtMs = (transcriptPath, turnStartedAt) => {
317
+ try {
318
+ const birth = statSync(transcriptPath).birthtimeMs;
319
+ return birth > 0 ? birth : Date.parse(turnStartedAt);
320
+ } catch {
321
+ return Date.parse(turnStartedAt);
322
+ }
323
+ };
324
+ var discoverGuardianRollout = (call, layout, now, limits) => {
325
+ const sinceMs = createdAtMs(call.transcriptPath, call.turnStartedAt) - MTIME_SLACK_MS;
326
+ if (!Number.isFinite(sinceMs)) return null;
327
+ const headMaxBytes = limits.headMaxBytes ?? CODEX_ROLLOUT_HEAD_MAX_BYTES;
328
+ const maxFiles = limits.maxFiles ?? CODEX_GUARDIAN_SCAN_MAX_FILES;
329
+ let examined = 0;
330
+ let best = null;
331
+ for (const directory of dayDirectories(layout, now)) {
332
+ let names;
333
+ try {
334
+ names = readdirSync(directory).sort();
335
+ } catch {
336
+ continue;
337
+ }
338
+ for (const name of names) {
339
+ if (!ROLLOUT_FILE.test(name)) continue;
340
+ const path = join(directory, name);
341
+ if (path === call.transcriptPath) continue;
342
+ if (++examined > maxFiles) return best?.path ?? null;
343
+ let mtimeMs;
344
+ try {
345
+ const stat = statSync(path);
346
+ if (!stat.isFile()) continue;
347
+ mtimeMs = stat.mtimeMs;
348
+ } catch {
349
+ continue;
350
+ }
351
+ if (mtimeMs < sinceMs || best && mtimeMs <= best.mtimeMs) continue;
352
+ if (isGuardianRollout(path, call.sessionId, headMaxBytes)) best = { path, mtimeMs };
353
+ }
176
354
  }
355
+ return best?.path ?? null;
356
+ };
357
+ var readGuardianCursor = (file, sessionId) => {
358
+ const cursor = readJsonRecord(file);
359
+ if (!cursor || cursor.sessionId !== sessionId || typeof cursor.path !== "string") return null;
360
+ const offset = cursor.offset;
361
+ if (typeof offset !== "number" || !Number.isSafeInteger(offset) || offset < 0) return null;
362
+ return { path: cursor.path, offset };
177
363
  };
178
- var consumeCodexCallDecision = (key) => {
179
- if (!key) return null;
180
- const path = decisionFile(key);
364
+ var readCompleteLines = (path, from, maxBytes) => {
365
+ let fd;
181
366
  try {
182
- const fresh = Date.now() - statSync(path).mtimeMs < CODEX_DECISION_TTL_MS;
183
- const raw = readFileSync(path, "utf-8");
184
- rmSync(path, { force: true });
185
- return fresh ? parseDecision(raw) : null;
367
+ fd = openSync(path, "r");
368
+ const size = fstatSync(fd).size;
369
+ let start = from > size ? 0 : from;
370
+ const skipsPartial = size - start > maxBytes;
371
+ if (skipsPartial) start = size - maxBytes;
372
+ const buffer = Buffer.alloc(size - start);
373
+ if (buffer.length > 0 && readSync(fd, buffer, 0, buffer.length, start) !== buffer.length) return null;
374
+ let lineStart = 0;
375
+ if (skipsPartial) {
376
+ const newline = buffer.indexOf(NEWLINE);
377
+ if (newline < 0) return { lines: [], end: from };
378
+ lineStart = newline + 1;
379
+ }
380
+ const lines = [];
381
+ for (let newline = buffer.indexOf(NEWLINE, lineStart); newline >= 0; newline = buffer.indexOf(NEWLINE, lineStart)) {
382
+ lines.push({ offset: start + lineStart, text: buffer.toString("utf-8", lineStart, newline) });
383
+ lineStart = newline + 1;
384
+ }
385
+ return { lines, end: start + lineStart };
186
386
  } catch {
187
387
  return null;
388
+ } finally {
389
+ if (fd !== void 0) closeSync(fd);
390
+ }
391
+ };
392
+ var takeCodexReviewerDenials = (call, options = {}) => {
393
+ const { transcriptPath, sessionId, turnStartedAt } = call;
394
+ if (!isAbsolute(transcriptPath) || !sessionId || !Number.isFinite(Date.parse(turnStartedAt))) return null;
395
+ const layout = sessionsLayoutOf(transcriptPath);
396
+ if (!layout) return null;
397
+ const headMaxBytes = options.headMaxBytes ?? CODEX_ROLLOUT_HEAD_MAX_BYTES;
398
+ const cursorName = cacheKey(sessionId);
399
+ const cursor = readGuardianCursor(join(guardianCursorDir(), cursorName), sessionId);
400
+ const remembered = cursor && isInside(layout.root, cursor.path) && cursor.path !== transcriptPath && isGuardianRollout(cursor.path, sessionId, headMaxBytes) ? cursor : null;
401
+ const guardianPath = remembered?.path ?? discoverGuardianRollout(call, layout, options.now ?? /* @__PURE__ */ new Date(), options);
402
+ if (!guardianPath) return null;
403
+ const from = remembered?.offset ?? 0;
404
+ const read = readCompleteLines(guardianPath, from, options.guardianMaxBytes ?? CODEX_GUARDIAN_READ_MAX_BYTES);
405
+ if (!read) return null;
406
+ const scan = scanCodexGuardianReviews(read.lines);
407
+ const next = scan.pendingOffset ?? read.end;
408
+ const denials = codexReviewerDenialsSince(scan.reviews, turnStartedAt);
409
+ if (!remembered || next !== from) {
410
+ const saved = writeJsonRecord(guardianCursorDir(), cursorName, { sessionId, path: guardianPath, offset: next });
411
+ if (!saved && denials.length > 0) return null;
412
+ }
413
+ return { guardianPath, denials };
414
+ };
415
+
416
+ // src/codex-reviewer.ts
417
+ var START_MS = Date.now();
418
+ var CLAIM_TIMEOUT_MS = 4e3;
419
+ var GATE_TIMEOUT_MS = 15e3;
420
+ var CANCEL_TIMEOUT_MS = 4e3;
421
+ var CODEX_RESCUE_STOP_RESERVE_MS = 2e4;
422
+ var codexRescueEndsAt = (startMs, holdBudgetSeconds) => startMs + hookHoldWindowMs("codex", holdBudgetSeconds) - CODEX_RESCUE_STOP_RESERVE_MS;
423
+ var codexRescueDeadline = (startMs, deadlineSeconds, nowMs, holdBudgetSeconds) => Math.min(hookWaitDeadline(startMs, deadlineSeconds, "codex", nowMs), codexRescueEndsAt(startMs, holdBudgetSeconds));
424
+ var codexApprovalsReviewer = (call) => readCodexTurn({ transcriptPath: call.transcript_path, sessionId: call.session_id, turnId: call.turn_id })?.approvalsReviewer;
425
+ var claimCodexReviewerRetry = async (apiKey, call) => {
426
+ if (!call.tool_name || !call.tool_input) return false;
427
+ const fingerprint = blockedActionFingerprint({
428
+ source: "codex",
429
+ sessionId: call.session_id,
430
+ toolName: call.tool_name,
431
+ toolInput: call.tool_input,
432
+ cwd: call.cwd
433
+ });
434
+ if (!fingerprint || !hasBlockedActionHint(fingerprint)) return false;
435
+ const granted = await claimBlockedAction(apiKey, {
436
+ v: GATE_WIRE,
437
+ source: "codex",
438
+ event: "PermissionRequest",
439
+ toolName: call.tool_name,
440
+ toolInputs: [call.tool_input],
441
+ sessionId: call.session_id,
442
+ machineId: getMachineId(),
443
+ cwd: call.cwd,
444
+ repoKey: repoKeyFor(call.cwd),
445
+ permissionMode: call.permission_mode
446
+ }, CLAIM_TIMEOUT_MS);
447
+ if (granted !== null) forgetBlockedActionHint(fingerprint);
448
+ return granted === true;
449
+ };
450
+ var blockReasonOf = (rationale) => rationale?.trim().slice(0, GATE_BLOCK_REASON_MAX_LENGTH).trim() || void 0;
451
+ var reviewToolUseId = (guardianPath, denial) => `codex-review-${createHash2("sha256").update(canonicalJson([guardianPath, denial.offset, denial.plannedAction]), "utf-8").digest("hex").slice(0, 40)}`;
452
+ var isRescueApproval = (output) => {
453
+ if (!output) return false;
454
+ try {
455
+ const parsed = JSON.parse(output);
456
+ return parsed?.decision === "block" && typeof parsed.reason === "string" && parsed.reason.length > 0;
457
+ } catch {
458
+ return false;
459
+ }
460
+ };
461
+ var isAffirmativeAnswer = (answer) => answer.answered && !isDecisionStopped(answer) && answer.value === "yes";
462
+ var approveOnce = (fingerprint, output) => recordBlockedActionHint(fingerprint) ? output : void 0;
463
+ var rescueRequest = (call, sessionId, guardianPath, denial) => {
464
+ const cwd = denial.cwd ?? call.cwd;
465
+ const blockReason = blockReasonOf(denial.rationale);
466
+ return {
467
+ v: GATE_WIRE,
468
+ source: "codex",
469
+ event: "Stop",
470
+ toolName: CODEX_RESCUE_TOOL_NAME,
471
+ toolInputs: [{ command: denial.command }],
472
+ sessionId,
473
+ toolUseId: reviewToolUseId(guardianPath, denial),
474
+ machineId: getMachineId(),
475
+ cwd,
476
+ ...call.cwd ? { sessionCwd: call.cwd } : {},
477
+ repoKey: repoKeyFor(cwd),
478
+ permissionMode: call.permission_mode,
479
+ createDecision: true,
480
+ localSurfaceAvailable: false,
481
+ ...blockReason ? { blockReason } : {},
482
+ approvalsReviewer: "auto_review",
483
+ blockedBy: "auto_review"
484
+ };
485
+ };
486
+ var askAboutDenial = async (apiKey, call, sessionId, guardianPath, denial, holdBudgetSeconds) => {
487
+ const request = rescueRequest(call, sessionId, guardianPath, denial);
488
+ const fingerprint = blockedActionFingerprint({
489
+ source: "codex",
490
+ sessionId,
491
+ toolName: request.toolName,
492
+ toolInput: request.toolInputs[0],
493
+ cwd: request.cwd
494
+ });
495
+ if (!fingerprint) return void 0;
496
+ const reply = await requestGate(apiKey, request, GATE_TIMEOUT_MS);
497
+ if (!reply) return void 0;
498
+ if (reply.kind === "no_opinion") return isRescueApproval(reply.output) ? approveOnce(fingerprint, reply.output) : void 0;
499
+ if (reply.kind !== "ask" || !reply.correlationId || reply.deadlineSeconds === void 0) return void 0;
500
+ if (reply.noDevices) {
501
+ await cancelQuestion(apiKey, reply.correlationId, AbortSignal.timeout(CANCEL_TIMEOUT_MS)).catch(() => void 0);
502
+ return void 0;
503
+ }
504
+ const deadline = codexRescueDeadline(START_MS, reply.deadlineSeconds, Date.now(), holdBudgetSeconds);
505
+ const answer = await pollForAnswer(apiKey, reply.correlationId, deadline);
506
+ if (!isAffirmativeAnswer(answer)) return void 0;
507
+ const approve = reply.settlementOutputs?.approve;
508
+ return approveOnce(
509
+ fingerprint,
510
+ isRescueApproval(approve) ? approve : JSON.stringify(codexStopRescueOutput("allow", denial.command))
511
+ );
512
+ };
513
+ var rescueCodexReviewerDenial = async (call, holdBudgetSeconds) => {
514
+ let apiKey;
515
+ try {
516
+ apiKey = getApiKey();
517
+ } catch {
518
+ return void 0;
519
+ }
520
+ try {
521
+ const { session_id: sessionId, transcript_path: transcriptPath } = call;
522
+ if (!sessionId || !transcriptPath) return void 0;
523
+ if (holdBudgetSeconds === void 0 || codexRescueEndsAt(START_MS, holdBudgetSeconds) - Date.now() <= GATE_TIMEOUT_MS) return void 0;
524
+ const turn = readCodexTurn({ transcriptPath, sessionId, turnId: call.turn_id });
525
+ if (turn?.approvalsReviewer !== "auto_review" || !turn.startedAt) return void 0;
526
+ const taken = takeCodexReviewerDenials({ transcriptPath, sessionId, turnStartedAt: turn.startedAt });
527
+ const denial = taken?.denials.at(-1);
528
+ if (!taken || !denial) return void 0;
529
+ return await askAboutDenial(apiKey, call, sessionId, taken.guardianPath, denial, holdBudgetSeconds);
530
+ } catch {
531
+ return void 0;
188
532
  }
189
533
  };
190
534
 
@@ -195,7 +539,7 @@ var callKeyFor = (input) => codexCallKey({
195
539
  toolName: input.tool_name,
196
540
  toolInput: input.tool_input
197
541
  });
198
- var START_MS = Date.now();
542
+ var START_MS2 = Date.now();
199
543
  var agentNameFor = (input) => `Codex - ${basename(input.cwd ?? process.cwd())}`;
200
544
  var describeFor = (input, lookup) => input.tool_name === "apply_patch" ? describeApplyPatch(input.tool_input?.command) ?? describeToolCall(lookup.tool, lookup.input, "hook") : describeToolCall(lookup.tool, lookup.input, "hook");
201
545
  var pushQuestion = async (apiKey, input, lookup, waitSeconds, mode) => {
@@ -221,8 +565,8 @@ var pushQuestion = async (apiKey, input, lookup, waitSeconds, mode) => {
221
565
  toolUseId: input.tool_use_id
222
566
  }).catch(() => ({ correlationId: "", status: "unavailable" }));
223
567
  const now = Date.now();
224
- const deadline = hookWaitDeadline(START_MS, waitSeconds, "codex", now);
225
- const clampCut = hookWaitClamped(START_MS, waitSeconds, "codex", now);
568
+ const deadline = hookWaitDeadline(START_MS2, waitSeconds, "codex", now);
569
+ const clampCut = hookWaitClamped(START_MS2, waitSeconds, "codex", now);
226
570
  const answer = await waitForQuestion(apiKey, result, deadline);
227
571
  return { answer, localHandoff: !answer.answered && isLocalQuestionHandoff(result), clampCut };
228
572
  };
@@ -258,23 +602,21 @@ var codexGate = async (apiKey, input) => {
258
602
  });
259
603
  return { verdict, lookup };
260
604
  };
261
- var fromRecordedDecision = (decision) => {
262
- if (decision.kind === "approved") return codexAllow();
263
- if (decision.kind === "denied") return codexDeny(decision.reason);
264
- return codexPass();
265
- };
266
605
  var PUSHARY_OWN_TOOL_PREFIX = "mcp__pushary__";
267
606
  var isPusharyOwnTool = (toolName) => typeof toolName === "string" && toolName.startsWith(PUSHARY_OWN_TOOL_PREFIX);
268
607
  var decidePermissionRequest = async (input) => {
269
608
  if (isPusharyOwnTool(input.tool_name)) return codexPass();
270
- const recorded = consumeCodexCallDecision(callKeyFor(input));
271
- if (recorded) return fromRecordedDecision(recorded);
272
609
  try {
273
610
  const apiKey = getApiKey();
274
611
  const { verdict, lookup } = await codexGate(apiKey, input);
275
612
  if (verdict.kind === "kill") return codexDeny(KILL_REASON);
276
- if (verdict.kind === "allow") return codexAllow();
277
613
  if (verdict.kind === "deny") return codexDeny(verdict.reason);
614
+ const recorded = consumeCodexCallDecision(callKeyFor(input));
615
+ if (recorded?.kind === "denied") return codexDeny(recorded.reason);
616
+ if (recorded?.kind === "handled") return codexPass();
617
+ if (await claimCodexReviewerRetry(apiKey, input)) return codexAllow();
618
+ if (codexApprovalsReviewer(input) === "auto_review") return codexPass();
619
+ if (verdict.kind === "allow") return codexAllow();
278
620
  if (verdict.kind === "defer") return codexPass();
279
621
  const toolPolicy = verdict.policy;
280
622
  if (toolPolicy.mode === "terminal_only") return codexPass();
@@ -283,7 +625,8 @@ var decidePermissionRequest = async (input) => {
283
625
  return codexPass();
284
626
  }
285
627
  const waitSeconds = toolPolicy.mode === "push_first" ? toolPolicy.pushFirstSeconds : effectiveWaitSeconds(toolPolicy.timeoutAction, toolPolicy.timeoutSeconds, "codex");
286
- const { answer, localHandoff, clampCut } = await pushQuestion(apiKey, input, lookup, waitSeconds, toolPolicy.mode);
628
+ const asked = recorded ? { ...input, tool_use_id: recorded.toolUseId } : input;
629
+ const { answer, localHandoff, clampCut } = await pushQuestion(apiKey, asked, lookup, waitSeconds, toolPolicy.mode);
287
630
  if (isDecisionStopped(answer)) return codexDeny(DECISION_STOP_REASON);
288
631
  if (localHandoff) return codexPass();
289
632
  if (answer.answered) {
@@ -305,6 +648,10 @@ var recordPreToolUse = (key, outcome) => {
305
648
  );
306
649
  return outcome;
307
650
  };
651
+ var recordApproval = (key, input) => {
652
+ if (input.tool_use_id) recordCodexCallDecision(key, { kind: "approved", toolUseId: input.tool_use_id });
653
+ return codexPass();
654
+ };
308
655
  var permissionRequestNeverFires = (permissionMode) => permissionMode === "bypassPermissions";
309
656
  var decidePushFirstBeforeTheTool = async (apiKey, input, lookup, pushFirstSeconds, callKey) => {
310
657
  let pushed;
@@ -320,10 +667,7 @@ var decidePushFirstBeforeTheTool = async (apiKey, input, lookup, pushFirstSecond
320
667
  }
321
668
  if (answer.answered) {
322
669
  if (isDeferAnswer(answer.value)) return recordPreToolUse(callKey, codexPass());
323
- if (answer.value === "yes") {
324
- recordCodexCallDecision(callKey, { kind: "approved" });
325
- return codexPass();
326
- }
670
+ if (answer.value === "yes") return recordApproval(callKey, input);
327
671
  return recordPreToolUse(callKey, codexDeny(denyReasonFrom(answer.value, answer.note)));
328
672
  }
329
673
  return recordPreToolUse(callKey, codexPass());
@@ -353,6 +697,7 @@ var decidePreToolUse = async (input) => {
353
697
  if (verdict.kind === "kill") return codexDeny(KILL_REASON);
354
698
  if (verdict.kind === "allow") return codexPass();
355
699
  if (verdict.kind === "deny") return codexDeny(verdict.reason);
700
+ if (codexApprovalsReviewer(input) === "auto_review") return codexPass();
356
701
  if (defersToNativeMode(input.permission_mode, input.tool_name ?? "")) return codexPass();
357
702
  if (verdict.kind === "defer") return codexPass();
358
703
  const toolPolicy = verdict.policy;
@@ -369,10 +714,7 @@ var decidePreToolUse = async (input) => {
369
714
  if (localHandoff) return recordPreToolUse(callKey, codexPass());
370
715
  if (answer.answered) {
371
716
  if (isDeferAnswer(answer.value)) return recordPreToolUse(callKey, codexPass());
372
- if (answer.value === "yes") {
373
- recordCodexCallDecision(callKey, { kind: "approved" });
374
- return codexPass();
375
- }
717
+ if (answer.value === "yes") return recordApproval(callKey, input);
376
718
  return recordPreToolUse(callKey, codexDeny(denyReasonFrom(answer.value, answer.note)));
377
719
  }
378
720
  if (clampCut) return recordPreToolUse(callKey, codexPass());
@@ -448,12 +790,18 @@ var main = async () => {
448
790
  session_id: input.session_id
449
791
  }, CODEX_AGENT));
450
792
  break;
451
- case "Stop":
793
+ case "Stop": {
794
+ const rescued = await rescueCodexReviewerDenial(input, takeHookHoldBudget());
795
+ if (rescued) {
796
+ process.stdout.write(rescued);
797
+ break;
798
+ }
452
799
  await handleStop({
453
800
  cwd: input.cwd,
454
801
  session_id: input.session_id
455
802
  }, CODEX_AGENT);
456
803
  break;
804
+ }
457
805
  case "SessionStart":
458
806
  await reportSessionStart(input);
459
807
  break;