oh-my-codex 0.4.0 → 0.4.2

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 (93) hide show
  1. package/README.es.md +36 -0
  2. package/README.ja.md +36 -0
  3. package/README.ko.md +36 -0
  4. package/README.md +13 -1
  5. package/README.pt.md +36 -0
  6. package/README.ru.md +36 -0
  7. package/README.vi.md +36 -0
  8. package/README.zh.md +39 -0
  9. package/bin/omx.js +2 -1
  10. package/dist/config/__tests__/generator-notify.test.js +3 -3
  11. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  12. package/dist/config/generator.d.ts +1 -1
  13. package/dist/config/generator.js +8 -8
  14. package/dist/config/generator.js.map +1 -1
  15. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.d.ts +2 -0
  16. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.d.ts.map +1 -0
  17. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +427 -0
  18. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -0
  19. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.d.ts +2 -0
  20. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.d.ts.map +1 -0
  21. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +432 -0
  22. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -0
  23. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +3 -0
  24. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  25. package/dist/hooks/emulator.d.ts +1 -1
  26. package/dist/hooks/emulator.js +5 -5
  27. package/dist/hooks/emulator.js.map +1 -1
  28. package/dist/hooks/extensibility/__tests__/dispatcher.test.d.ts +2 -0
  29. package/dist/hooks/extensibility/__tests__/dispatcher.test.d.ts.map +1 -0
  30. package/dist/hooks/extensibility/__tests__/dispatcher.test.js +152 -0
  31. package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -0
  32. package/dist/hooks/extensibility/__tests__/events.test.d.ts +2 -0
  33. package/dist/hooks/extensibility/__tests__/events.test.d.ts.map +1 -0
  34. package/dist/hooks/extensibility/__tests__/events.test.js +117 -0
  35. package/dist/hooks/extensibility/__tests__/events.test.js.map +1 -0
  36. package/dist/hooks/extensibility/__tests__/loader.test.d.ts +2 -0
  37. package/dist/hooks/extensibility/__tests__/loader.test.d.ts.map +1 -0
  38. package/dist/hooks/extensibility/__tests__/loader.test.js +229 -0
  39. package/dist/hooks/extensibility/__tests__/loader.test.js.map +1 -0
  40. package/dist/hooks/extensibility/__tests__/logging.test.d.ts +2 -0
  41. package/dist/hooks/extensibility/__tests__/logging.test.d.ts.map +1 -0
  42. package/dist/hooks/extensibility/__tests__/logging.test.js +74 -0
  43. package/dist/hooks/extensibility/__tests__/logging.test.js.map +1 -0
  44. package/dist/hooks/extensibility/__tests__/plugin-runner.test.d.ts +2 -0
  45. package/dist/hooks/extensibility/__tests__/plugin-runner.test.d.ts.map +1 -0
  46. package/dist/hooks/extensibility/__tests__/plugin-runner.test.js +202 -0
  47. package/dist/hooks/extensibility/__tests__/plugin-runner.test.js.map +1 -0
  48. package/dist/hooks/extensibility/__tests__/runtime.test.d.ts +2 -0
  49. package/dist/hooks/extensibility/__tests__/runtime.test.d.ts.map +1 -0
  50. package/dist/hooks/extensibility/__tests__/runtime.test.js +117 -0
  51. package/dist/hooks/extensibility/__tests__/runtime.test.js.map +1 -0
  52. package/dist/hooks/extensibility/__tests__/sdk.test.d.ts +2 -0
  53. package/dist/hooks/extensibility/__tests__/sdk.test.d.ts.map +1 -0
  54. package/dist/hooks/extensibility/__tests__/sdk.test.js +277 -0
  55. package/dist/hooks/extensibility/__tests__/sdk.test.js.map +1 -0
  56. package/dist/hooks/extensibility/sdk.d.ts.map +1 -1
  57. package/dist/hooks/extensibility/sdk.js +10 -2
  58. package/dist/hooks/extensibility/sdk.js.map +1 -1
  59. package/dist/hud/__tests__/colors.test.d.ts +2 -0
  60. package/dist/hud/__tests__/colors.test.d.ts.map +1 -0
  61. package/dist/hud/__tests__/colors.test.js +194 -0
  62. package/dist/hud/__tests__/colors.test.js.map +1 -0
  63. package/dist/hud/__tests__/render.test.d.ts +2 -0
  64. package/dist/hud/__tests__/render.test.d.ts.map +1 -0
  65. package/dist/hud/__tests__/render.test.js +449 -0
  66. package/dist/hud/__tests__/render.test.js.map +1 -0
  67. package/dist/hud/__tests__/types.test.d.ts +2 -0
  68. package/dist/hud/__tests__/types.test.d.ts.map +1 -0
  69. package/dist/hud/__tests__/types.test.js +17 -0
  70. package/dist/hud/__tests__/types.test.js.map +1 -0
  71. package/dist/team/__tests__/tmux-session.test.js +15 -1
  72. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  73. package/dist/team/orchestrator.d.ts +1 -1
  74. package/dist/team/orchestrator.js +1 -1
  75. package/dist/team/tmux-session.d.ts +8 -0
  76. package/dist/team/tmux-session.d.ts.map +1 -1
  77. package/dist/team/tmux-session.js +28 -7
  78. package/dist/team/tmux-session.js.map +1 -1
  79. package/dist/utils/__tests__/package.test.d.ts +2 -0
  80. package/dist/utils/__tests__/package.test.d.ts.map +1 -0
  81. package/dist/utils/__tests__/package.test.js +21 -0
  82. package/dist/utils/__tests__/package.test.js.map +1 -0
  83. package/dist/utils/__tests__/paths.test.d.ts +2 -0
  84. package/dist/utils/__tests__/paths.test.d.ts.map +1 -0
  85. package/dist/utils/__tests__/paths.test.js +117 -0
  86. package/dist/utils/__tests__/paths.test.js.map +1 -0
  87. package/dist/verification/__tests__/verifier.test.d.ts +2 -0
  88. package/dist/verification/__tests__/verifier.test.d.ts.map +1 -0
  89. package/dist/verification/__tests__/verifier.test.js +94 -0
  90. package/dist/verification/__tests__/verifier.test.js.map +1 -0
  91. package/package.json +1 -1
  92. package/scripts/notify-hook.js +346 -1
  93. package/templates/AGENTS.md +1 -1
@@ -10,17 +10,20 @@
10
10
  * 2. Updates state for active workflow modes
11
11
  * 3. Tracks subagent activity
12
12
  * 4. Triggers desktop notifications if configured
13
+ * 5. Auto-nudges Codex when it stalls with permission-asking patterns
13
14
  */
14
15
 
15
16
  import { writeFile, appendFile, mkdir, readFile, rename } from 'fs/promises';
16
17
  import { join, resolve as resolvePath } from 'path';
17
18
  import { existsSync } from 'fs';
18
19
  import { spawn } from 'child_process';
20
+ import { homedir } from 'os';
19
21
  import {
20
22
  normalizeTmuxHookConfig,
21
23
  pickActiveMode,
22
24
  evaluateInjectionGuards,
23
25
  buildSendKeysArgv,
26
+ DEFAULT_MARKER,
24
27
  } from './tmux-hook-engine.js';
25
28
 
26
29
  const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
@@ -233,6 +236,198 @@ function readJsonIfExists(path, fallback) {
233
236
  .catch(() => fallback);
234
237
  }
235
238
 
239
+ // ---------------------------------------------------------------------------
240
+ // Auto-nudge: detect Codex "asking for permission" stall patterns and
241
+ // automatically send a continuation prompt so the agent keeps working.
242
+ // ---------------------------------------------------------------------------
243
+
244
+ const DEFAULT_STALL_PATTERNS = [
245
+ 'if you want',
246
+ 'would you like',
247
+ 'shall i',
248
+ 'next i can',
249
+ 'do you want me to',
250
+ 'let me know if',
251
+ 'do you want',
252
+ 'want me to',
253
+ 'let me know',
254
+ 'just let me know',
255
+ 'i can also',
256
+ 'i could also',
257
+ 'ready to proceed',
258
+ 'should i',
259
+ 'whenever you',
260
+ 'say go',
261
+ 'say yes',
262
+ 'type continue',
263
+ 'and i\'ll continue',
264
+ 'and i\'ll proceed',
265
+ 'keep driving',
266
+ 'keep pushing',
267
+ 'move forward',
268
+ 'drive forward',
269
+ 'proceed from here',
270
+ 'i\'ll continue from',
271
+ ];
272
+
273
+ function normalizeAutoNudgeConfig(raw) {
274
+ if (!raw || typeof raw !== 'object') {
275
+ return {
276
+ enabled: true,
277
+ patterns: DEFAULT_STALL_PATTERNS,
278
+ response: 'yes, proceed',
279
+ delaySec: 3,
280
+ maxNudgesPerSession: Infinity,
281
+ };
282
+ }
283
+ return {
284
+ enabled: raw.enabled !== false,
285
+ patterns: Array.isArray(raw.patterns) && raw.patterns.length > 0
286
+ ? raw.patterns.filter(p => typeof p === 'string' && p.trim() !== '')
287
+ : DEFAULT_STALL_PATTERNS,
288
+ response: typeof raw.response === 'string' && raw.response.trim() !== ''
289
+ ? raw.response
290
+ : 'yes, proceed',
291
+ delaySec: typeof raw.delaySec === 'number' && raw.delaySec >= 0 && raw.delaySec <= 60
292
+ ? raw.delaySec
293
+ : 3,
294
+ maxNudgesPerSession: typeof raw.maxNudgesPerSession === 'number' && raw.maxNudgesPerSession > 0
295
+ ? raw.maxNudgesPerSession
296
+ : Infinity,
297
+ };
298
+ }
299
+
300
+ async function loadAutoNudgeConfig() {
301
+ const codexHomePath = process.env.CODEX_HOME || join(homedir(), '.codex');
302
+ const configPath = join(codexHomePath, '.omx-config.json');
303
+ const raw = await readJsonIfExists(configPath, null);
304
+ if (!raw || typeof raw !== 'object') return normalizeAutoNudgeConfig(null);
305
+ return normalizeAutoNudgeConfig(raw.autoNudge);
306
+ }
307
+
308
+ function detectStallPattern(text, patterns) {
309
+ if (!text || typeof text !== 'string') return false;
310
+ // Broader tail window (~800 chars / ~15-20 lines) for context
311
+ const tail = text.slice(-800).toLowerCase();
312
+ const lowerPatterns = patterns.map(p => p.toLowerCase());
313
+ // Focus on last few lines where stall prompts typically appear
314
+ const lines = tail.split('\n').filter(l => l.trim());
315
+ const hotZone = lines.slice(-3).join('\n');
316
+ // Primary: check last few lines (highest signal)
317
+ if (lowerPatterns.some(p => hotZone.includes(p))) return true;
318
+ // Secondary: check broader tail window
319
+ return lowerPatterns.some(p => tail.includes(p));
320
+ }
321
+
322
+ async function capturePane(paneId, lines = 10) {
323
+ try {
324
+ const result = await runProcess('tmux', [
325
+ 'capture-pane', '-t', paneId, '-p', '-l', String(lines),
326
+ ], 3000);
327
+ return result.stdout || '';
328
+ } catch {
329
+ return '';
330
+ }
331
+ }
332
+
333
+ async function resolveNudgePaneTarget(stateDir) {
334
+ // 1. Try TMUX_PANE env var (inherited from the Codex process)
335
+ const envPane = safeString(process.env.TMUX_PANE || '');
336
+ if (envPane) return envPane;
337
+
338
+ // 2. Fallback: check active mode states for tmux_pane_id
339
+ try {
340
+ const scopedDirs = await getScopedStateDirsForCurrentSession(stateDir);
341
+ for (const dir of scopedDirs) {
342
+ const files = await readdir(dir).catch(() => []);
343
+ for (const f of files) {
344
+ if (!f.endsWith('-state.json')) continue;
345
+ const path = join(dir, f);
346
+ try {
347
+ const state = JSON.parse(await readFile(path, 'utf-8'));
348
+ if (state && state.active && state.tmux_pane_id) {
349
+ return safeString(state.tmux_pane_id);
350
+ }
351
+ } catch {
352
+ // skip malformed state
353
+ }
354
+ }
355
+ }
356
+ } catch {
357
+ // Non-critical
358
+ }
359
+
360
+ return '';
361
+ }
362
+
363
+ async function maybeAutoNudge({ cwd, stateDir, logsDir, payload }) {
364
+ const config = await loadAutoNudgeConfig();
365
+ if (!config.enabled) return;
366
+
367
+ // Check nudge count against session limit
368
+ const nudgeStatePath = join(stateDir, 'auto-nudge-state.json');
369
+ let nudgeState = await readJsonIfExists(nudgeStatePath, null);
370
+ if (!nudgeState || typeof nudgeState !== 'object') {
371
+ nudgeState = { nudgeCount: 0, lastNudgeAt: '' };
372
+ }
373
+ const nudgeCount = asNumber(nudgeState.nudgeCount) ?? 0;
374
+ if (Number.isFinite(config.maxNudgesPerSession) && nudgeCount >= config.maxNudgesPerSession) return;
375
+
376
+ // Resolve pane target early (needed for both capture-pane check and sending)
377
+ const paneId = await resolveNudgePaneTarget(stateDir);
378
+
379
+ // Check last assistant message for stall patterns (fast path)
380
+ const lastMessage = safeString(payload['last-assistant-message'] || payload.last_assistant_message || '');
381
+ let detected = detectStallPattern(lastMessage, config.patterns);
382
+ let source = 'payload';
383
+
384
+ // Fallback: capture the last 10 lines of tmux pane output
385
+ if (!detected && paneId) {
386
+ const captured = await capturePane(paneId);
387
+ detected = detectStallPattern(captured, config.patterns);
388
+ source = 'capture-pane';
389
+ }
390
+
391
+ if (!detected || !paneId) return;
392
+
393
+ // Short delay to let the agent settle before nudging
394
+ if (config.delaySec > 0) {
395
+ await new Promise(r => setTimeout(r, config.delaySec * 1000));
396
+ }
397
+
398
+ const nowIso = new Date().toISOString();
399
+ try {
400
+ // Send the response text as literal bytes, then submit with double C-m
401
+ // Codex CLI needs C-m sent twice with a short delay for reliable prompt submission
402
+ const markedResponse = `${config.response} ${DEFAULT_MARKER}`;
403
+ await runProcess('tmux', ['send-keys', '-t', paneId, '-l', markedResponse], 3000);
404
+ await new Promise(r => setTimeout(r, 100));
405
+ await runProcess('tmux', ['send-keys', '-t', paneId, 'C-m'], 3000);
406
+ await new Promise(r => setTimeout(r, 100));
407
+ await runProcess('tmux', ['send-keys', '-t', paneId, 'C-m'], 3000);
408
+
409
+ nudgeState.nudgeCount = nudgeCount + 1;
410
+ nudgeState.lastNudgeAt = nowIso;
411
+ await writeFile(nudgeStatePath, JSON.stringify(nudgeState, null, 2)).catch(() => {});
412
+
413
+ await logTmuxHookEvent(logsDir, {
414
+ timestamp: nowIso,
415
+ type: 'auto_nudge',
416
+ pane_id: paneId,
417
+ response: config.response,
418
+ source,
419
+ nudge_count: nudgeState.nudgeCount,
420
+ });
421
+ } catch (err) {
422
+ await logTmuxHookEvent(logsDir, {
423
+ timestamp: nowIso,
424
+ type: 'auto_nudge',
425
+ pane_id: paneId,
426
+ error: err instanceof Error ? err.message : safeString(err),
427
+ }).catch(() => {});
428
+ }
429
+ }
430
+
236
431
  function runProcess(command, args, timeoutMs = 3000) {
237
432
  return new Promise((resolve, reject) => {
238
433
  const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] });
@@ -589,9 +784,14 @@ async function maybeNudgeTeamLeader({ cwd, stateDir, logsDir, preComputedLeaderS
589
784
  text = `Team ${teamName} active. Run: omx team status ${teamName}`;
590
785
  }
591
786
  const capped = text.length > 180 ? `${text.slice(0, 177)}...` : text;
787
+ const markedText = `${capped} ${DEFAULT_MARKER}`;
592
788
 
593
789
  try {
594
- await runProcess('tmux', ['send-keys', '-t', tmuxTarget, capped, 'C-m', 'C-m'], 1200);
790
+ await runProcess('tmux', ['send-keys', '-t', tmuxTarget, '-l', markedText], 3000);
791
+ await new Promise(r => setTimeout(r, 100));
792
+ await runProcess('tmux', ['send-keys', '-t', tmuxTarget, 'C-m'], 3000);
793
+ await new Promise(r => setTimeout(r, 100));
794
+ await runProcess('tmux', ['send-keys', '-t', tmuxTarget, 'C-m'], 3000);
595
795
  nudgeState.last_nudged_by_team[teamName] = { at: nowIso, last_message_id: newestId || prevMsgId || '' };
596
796
 
597
797
  // Emit team event for the nudge
@@ -909,6 +1109,133 @@ function parseTeamWorkerEnv(rawValue) {
909
1109
  return { teamName: match[1], workerName: match[2] };
910
1110
  }
911
1111
 
1112
+ function resolveAllWorkersIdleCooldownMs() {
1113
+ const raw = safeString(process.env.OMX_TEAM_ALL_IDLE_COOLDOWN_MS || '');
1114
+ const parsed = asNumber(raw);
1115
+ // Default: 60 seconds. Guard against unreasonable values.
1116
+ if (parsed !== null && parsed >= 5_000 && parsed <= 10 * 60_000) return parsed;
1117
+ return 60_000;
1118
+ }
1119
+
1120
+ async function readWorkerStatusState(stateDir, teamName, workerName) {
1121
+ if (!workerName) return 'unknown';
1122
+ const statusPath = join(stateDir, 'team', teamName, 'workers', workerName, 'status.json');
1123
+ try {
1124
+ if (!existsSync(statusPath)) return 'unknown';
1125
+ const raw = await readFile(statusPath, 'utf-8');
1126
+ const parsed = JSON.parse(raw);
1127
+ if (parsed && typeof parsed.state === 'string') return parsed.state;
1128
+ return 'unknown';
1129
+ } catch {
1130
+ return 'unknown';
1131
+ }
1132
+ }
1133
+
1134
+ async function readTeamWorkersForIdleCheck(stateDir, teamName) {
1135
+ // Try manifest.v2.json first (preferred), then config.json
1136
+ const manifestPath = join(stateDir, 'team', teamName, 'manifest.v2.json');
1137
+ const configPath = join(stateDir, 'team', teamName, 'config.json');
1138
+ const srcPath = existsSync(manifestPath) ? manifestPath : existsSync(configPath) ? configPath : null;
1139
+ if (!srcPath) return null;
1140
+
1141
+ try {
1142
+ const raw = await readFile(srcPath, 'utf-8');
1143
+ const parsed = JSON.parse(raw);
1144
+ if (!parsed || typeof parsed !== 'object') return null;
1145
+ const workers = parsed.workers;
1146
+ if (!Array.isArray(workers) || workers.length === 0) return null;
1147
+ const tmuxSession = safeString(parsed.tmux_session || '').trim();
1148
+ return { workers, tmuxSession };
1149
+ } catch {
1150
+ return null;
1151
+ }
1152
+ }
1153
+
1154
+ async function maybeNotifyLeaderAllWorkersIdle({ cwd, stateDir, logsDir, parsedTeamWorker }) {
1155
+ const { teamName, workerName } = parsedTeamWorker;
1156
+ const nowMs = Date.now();
1157
+ const nowIso = new Date(nowMs).toISOString();
1158
+
1159
+ // Only trigger check when this worker is idle
1160
+ const myState = await readWorkerStatusState(stateDir, teamName, workerName);
1161
+ if (myState !== 'idle') return;
1162
+
1163
+ // Read team config to get worker list and leader tmux target
1164
+ const teamInfo = await readTeamWorkersForIdleCheck(stateDir, teamName);
1165
+ if (!teamInfo) return;
1166
+ const { workers, tmuxSession } = teamInfo;
1167
+ if (!tmuxSession) return;
1168
+
1169
+ // Check cooldown to prevent notification spam
1170
+ const idleStatePath = join(stateDir, 'team', teamName, 'all-workers-idle.json');
1171
+ const idleState = (await readJsonIfExists(idleStatePath, null)) || {};
1172
+ const cooldownMs = resolveAllWorkersIdleCooldownMs();
1173
+ const lastNotifiedMs = asNumber(idleState.last_notified_at_ms) ?? 0;
1174
+ if ((nowMs - lastNotifiedMs) < cooldownMs) return;
1175
+
1176
+ // Check if ALL workers are idle (or done)
1177
+ const states = await Promise.all(
1178
+ workers.map(w => readWorkerStatusState(stateDir, teamName, safeString(w && w.name ? w.name : '')))
1179
+ );
1180
+ const allIdle = states.length > 0 && states.every(s => s === 'idle' || s === 'done');
1181
+ if (!allIdle) return;
1182
+
1183
+ const N = workers.length;
1184
+ const message = `[OMX] All ${N} worker${N === 1 ? '' : 's'} idle. Ready for next instructions. ${DEFAULT_MARKER}`;
1185
+
1186
+ try {
1187
+ await runProcess('tmux', ['send-keys', '-t', tmuxSession, '-l', message], 3000);
1188
+ await new Promise(r => setTimeout(r, 100));
1189
+ await runProcess('tmux', ['send-keys', '-t', tmuxSession, 'C-m'], 3000);
1190
+ await new Promise(r => setTimeout(r, 100));
1191
+ await runProcess('tmux', ['send-keys', '-t', tmuxSession, 'C-m'], 3000);
1192
+
1193
+ // Update cooldown state (atomic: use a tmp file pattern)
1194
+ const nextIdleState = {
1195
+ ...idleState,
1196
+ last_notified_at_ms: nowMs,
1197
+ last_notified_at: nowIso,
1198
+ worker_count: N,
1199
+ };
1200
+ await writeFile(idleStatePath, JSON.stringify(nextIdleState, null, 2)).catch(() => {});
1201
+
1202
+ // Emit team event for the notification
1203
+ const eventsDir = join(stateDir, 'team', teamName, 'events');
1204
+ const eventsPath = join(eventsDir, 'events.ndjson');
1205
+ try {
1206
+ await mkdir(eventsDir, { recursive: true });
1207
+ const event = {
1208
+ event_id: `all-idle-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
1209
+ team: teamName,
1210
+ type: 'all_workers_idle',
1211
+ worker: workerName,
1212
+ worker_count: N,
1213
+ created_at: nowIso,
1214
+ };
1215
+ await appendFile(eventsPath, JSON.stringify(event) + '\n');
1216
+ } catch { /* best effort */ }
1217
+
1218
+ // Log the notification
1219
+ await logTmuxHookEvent(logsDir, {
1220
+ timestamp: nowIso,
1221
+ type: 'all_workers_idle_notification',
1222
+ team: teamName,
1223
+ tmux_target: tmuxSession,
1224
+ worker: workerName,
1225
+ worker_count: N,
1226
+ });
1227
+ } catch (err) {
1228
+ await logTmuxHookEvent(logsDir, {
1229
+ timestamp: nowIso,
1230
+ type: 'all_workers_idle_notification',
1231
+ team: teamName,
1232
+ tmux_target: tmuxSession,
1233
+ worker: workerName,
1234
+ error: err instanceof Error ? err.message : safeString(err),
1235
+ }).catch(() => {});
1236
+ }
1237
+ }
1238
+
912
1239
  async function dispatchNativeHookEvent(cwd, eventName, payload, context = {}) {
913
1240
  try {
914
1241
  const { buildNativeHookEvent } = await import('../dist/hooks/extensibility/events.js');
@@ -1137,6 +1464,15 @@ async function main() {
1137
1464
  }
1138
1465
  }
1139
1466
 
1467
+ // 4.6. Notify leader when all workers are idle (worker session only)
1468
+ if (isTeamWorker && parsedTeamWorker) {
1469
+ try {
1470
+ await maybeNotifyLeaderAllWorkersIdle({ cwd, stateDir, logsDir, parsedTeamWorker });
1471
+ } catch {
1472
+ // Non-critical
1473
+ }
1474
+ }
1475
+
1140
1476
  // 5. Optional tmux prompt injection workaround (non-fatal, opt-in)
1141
1477
  // Skip for team workers - only the lead should inject prompts
1142
1478
  if (!isTeamWorker) {
@@ -1192,6 +1528,15 @@ async function main() {
1192
1528
  // Non-fatal: notification module may not be built or config may not exist
1193
1529
  }
1194
1530
  }
1531
+
1532
+ // 9. Auto-nudge: detect Codex stall patterns ("If you want...", "Shall I...", etc.)
1533
+ // and automatically send a continuation prompt so the agent keeps working.
1534
+ // Works for both leader and worker contexts.
1535
+ try {
1536
+ await maybeAutoNudge({ cwd, stateDir, logsDir, payload });
1537
+ } catch {
1538
+ // Non-critical
1539
+ }
1195
1540
  }
1196
1541
 
1197
1542
  async function readdir(dir) {
@@ -44,7 +44,7 @@ For non-trivial SDK/API/framework usage, delegate to `dependency-expert` to chec
44
44
  </delegation_rules>
45
45
 
46
46
  <child_agent_protocol>
47
- Codex CLI spawns child agents via the `spawn_agent` tool (requires `collab = true`).
47
+ Codex CLI spawns child agents via the `spawn_agent` tool (requires `multi_agent = true`).
48
48
  To inject role-specific behavior, the parent MUST read the role prompt and pass it in the spawned agent message.
49
49
 
50
50
  Delegation steps: