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.
- package/README.es.md +36 -0
- package/README.ja.md +36 -0
- package/README.ko.md +36 -0
- package/README.md +13 -1
- package/README.pt.md +36 -0
- package/README.ru.md +36 -0
- package/README.vi.md +36 -0
- package/README.zh.md +39 -0
- package/bin/omx.js +2 -1
- package/dist/config/__tests__/generator-notify.test.js +3 -3
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/generator.d.ts +1 -1
- package/dist/config/generator.js +8 -8
- package/dist/config/generator.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.d.ts +2 -0
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +427 -0
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -0
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.d.ts +2 -0
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +432 -0
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -0
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +3 -0
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/emulator.d.ts +1 -1
- package/dist/hooks/emulator.js +5 -5
- package/dist/hooks/emulator.js.map +1 -1
- package/dist/hooks/extensibility/__tests__/dispatcher.test.d.ts +2 -0
- package/dist/hooks/extensibility/__tests__/dispatcher.test.d.ts.map +1 -0
- package/dist/hooks/extensibility/__tests__/dispatcher.test.js +152 -0
- package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -0
- package/dist/hooks/extensibility/__tests__/events.test.d.ts +2 -0
- package/dist/hooks/extensibility/__tests__/events.test.d.ts.map +1 -0
- package/dist/hooks/extensibility/__tests__/events.test.js +117 -0
- package/dist/hooks/extensibility/__tests__/events.test.js.map +1 -0
- package/dist/hooks/extensibility/__tests__/loader.test.d.ts +2 -0
- package/dist/hooks/extensibility/__tests__/loader.test.d.ts.map +1 -0
- package/dist/hooks/extensibility/__tests__/loader.test.js +229 -0
- package/dist/hooks/extensibility/__tests__/loader.test.js.map +1 -0
- package/dist/hooks/extensibility/__tests__/logging.test.d.ts +2 -0
- package/dist/hooks/extensibility/__tests__/logging.test.d.ts.map +1 -0
- package/dist/hooks/extensibility/__tests__/logging.test.js +74 -0
- package/dist/hooks/extensibility/__tests__/logging.test.js.map +1 -0
- package/dist/hooks/extensibility/__tests__/plugin-runner.test.d.ts +2 -0
- package/dist/hooks/extensibility/__tests__/plugin-runner.test.d.ts.map +1 -0
- package/dist/hooks/extensibility/__tests__/plugin-runner.test.js +202 -0
- package/dist/hooks/extensibility/__tests__/plugin-runner.test.js.map +1 -0
- package/dist/hooks/extensibility/__tests__/runtime.test.d.ts +2 -0
- package/dist/hooks/extensibility/__tests__/runtime.test.d.ts.map +1 -0
- package/dist/hooks/extensibility/__tests__/runtime.test.js +117 -0
- package/dist/hooks/extensibility/__tests__/runtime.test.js.map +1 -0
- package/dist/hooks/extensibility/__tests__/sdk.test.d.ts +2 -0
- package/dist/hooks/extensibility/__tests__/sdk.test.d.ts.map +1 -0
- package/dist/hooks/extensibility/__tests__/sdk.test.js +277 -0
- package/dist/hooks/extensibility/__tests__/sdk.test.js.map +1 -0
- package/dist/hooks/extensibility/sdk.d.ts.map +1 -1
- package/dist/hooks/extensibility/sdk.js +10 -2
- package/dist/hooks/extensibility/sdk.js.map +1 -1
- package/dist/hud/__tests__/colors.test.d.ts +2 -0
- package/dist/hud/__tests__/colors.test.d.ts.map +1 -0
- package/dist/hud/__tests__/colors.test.js +194 -0
- package/dist/hud/__tests__/colors.test.js.map +1 -0
- package/dist/hud/__tests__/render.test.d.ts +2 -0
- package/dist/hud/__tests__/render.test.d.ts.map +1 -0
- package/dist/hud/__tests__/render.test.js +449 -0
- package/dist/hud/__tests__/render.test.js.map +1 -0
- package/dist/hud/__tests__/types.test.d.ts +2 -0
- package/dist/hud/__tests__/types.test.d.ts.map +1 -0
- package/dist/hud/__tests__/types.test.js +17 -0
- package/dist/hud/__tests__/types.test.js.map +1 -0
- package/dist/team/__tests__/tmux-session.test.js +15 -1
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/orchestrator.d.ts +1 -1
- package/dist/team/orchestrator.js +1 -1
- package/dist/team/tmux-session.d.ts +8 -0
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +28 -7
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/utils/__tests__/package.test.d.ts +2 -0
- package/dist/utils/__tests__/package.test.d.ts.map +1 -0
- package/dist/utils/__tests__/package.test.js +21 -0
- package/dist/utils/__tests__/package.test.js.map +1 -0
- package/dist/utils/__tests__/paths.test.d.ts +2 -0
- package/dist/utils/__tests__/paths.test.d.ts.map +1 -0
- package/dist/utils/__tests__/paths.test.js +117 -0
- package/dist/utils/__tests__/paths.test.js.map +1 -0
- package/dist/verification/__tests__/verifier.test.d.ts +2 -0
- package/dist/verification/__tests__/verifier.test.d.ts.map +1 -0
- package/dist/verification/__tests__/verifier.test.js +94 -0
- package/dist/verification/__tests__/verifier.test.js.map +1 -0
- package/package.json +1 -1
- package/scripts/notify-hook.js +346 -1
- package/templates/AGENTS.md +1 -1
package/scripts/notify-hook.js
CHANGED
|
@@ -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,
|
|
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) {
|
package/templates/AGENTS.md
CHANGED
|
@@ -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 `
|
|
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:
|