oh-my-codex 0.8.11 → 0.8.13

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 (162) hide show
  1. package/README.md +43 -35
  2. package/dist/agents/__tests__/definitions.test.js +1 -0
  3. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  4. package/dist/agents/definitions.d.ts.map +1 -1
  5. package/dist/agents/definitions.js +11 -0
  6. package/dist/agents/definitions.js.map +1 -1
  7. package/dist/cli/__tests__/doctor-invalid-config.test.d.ts +2 -0
  8. package/dist/cli/__tests__/doctor-invalid-config.test.d.ts.map +1 -0
  9. package/dist/cli/__tests__/doctor-invalid-config.test.js +52 -0
  10. package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -0
  11. package/dist/cli/__tests__/index.test.js +35 -3
  12. package/dist/cli/__tests__/index.test.js.map +1 -1
  13. package/dist/cli/__tests__/launch-fallback.test.d.ts +2 -0
  14. package/dist/cli/__tests__/launch-fallback.test.d.ts.map +1 -0
  15. package/dist/cli/__tests__/launch-fallback.test.js +60 -0
  16. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -0
  17. package/dist/cli/__tests__/resume.test.d.ts +2 -0
  18. package/dist/cli/__tests__/resume.test.d.ts.map +1 -0
  19. package/dist/cli/__tests__/resume.test.js +78 -0
  20. package/dist/cli/__tests__/resume.test.js.map +1 -0
  21. package/dist/cli/__tests__/session-search-help.test.d.ts +2 -0
  22. package/dist/cli/__tests__/session-search-help.test.d.ts.map +1 -0
  23. package/dist/cli/__tests__/session-search-help.test.js +36 -0
  24. package/dist/cli/__tests__/session-search-help.test.js.map +1 -0
  25. package/dist/cli/__tests__/session-search.test.d.ts +2 -0
  26. package/dist/cli/__tests__/session-search.test.d.ts.map +1 -0
  27. package/dist/cli/__tests__/session-search.test.js +77 -0
  28. package/dist/cli/__tests__/session-search.test.js.map +1 -0
  29. package/dist/cli/__tests__/setup-prompts-overwrite.test.js +2 -0
  30. package/dist/cli/__tests__/setup-prompts-overwrite.test.js.map +1 -1
  31. package/dist/cli/__tests__/team-decompose.test.js +41 -15
  32. package/dist/cli/__tests__/team-decompose.test.js.map +1 -1
  33. package/dist/cli/__tests__/team.test.js +208 -3
  34. package/dist/cli/__tests__/team.test.js.map +1 -1
  35. package/dist/cli/doctor.d.ts.map +1 -1
  36. package/dist/cli/doctor.js +26 -0
  37. package/dist/cli/doctor.js.map +1 -1
  38. package/dist/cli/index.d.ts +4 -3
  39. package/dist/cli/index.d.ts.map +1 -1
  40. package/dist/cli/index.js +73 -27
  41. package/dist/cli/index.js.map +1 -1
  42. package/dist/cli/session-search.d.ts +8 -0
  43. package/dist/cli/session-search.d.ts.map +1 -0
  44. package/dist/cli/session-search.js +133 -0
  45. package/dist/cli/session-search.js.map +1 -0
  46. package/dist/cli/team.d.ts +13 -12
  47. package/dist/cli/team.d.ts.map +1 -1
  48. package/dist/cli/team.js +123 -39
  49. package/dist/cli/team.js.map +1 -1
  50. package/dist/hooks/__tests__/agents-overlay.test.js +33 -1
  51. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  52. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +219 -0
  53. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  54. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +1 -0
  55. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  56. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +64 -1
  57. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  58. package/dist/hooks/__tests__/notify-hook-modules.test.js +7 -0
  59. package/dist/hooks/__tests__/notify-hook-modules.test.js.map +1 -1
  60. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +2 -1
  61. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  62. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +420 -5
  63. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  64. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +95 -0
  65. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  66. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +3 -0
  67. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  68. package/dist/hooks/__tests__/tmux-hook-engine.test.js +39 -1
  69. package/dist/hooks/__tests__/tmux-hook-engine.test.js.map +1 -1
  70. package/dist/hooks/agents-overlay.d.ts +6 -1
  71. package/dist/hooks/agents-overlay.d.ts.map +1 -1
  72. package/dist/hooks/agents-overlay.js +45 -4
  73. package/dist/hooks/agents-overlay.js.map +1 -1
  74. package/dist/mcp/team-server.js +1 -1
  75. package/dist/mcp/team-server.js.map +1 -1
  76. package/dist/session-history/__tests__/search.test.d.ts +2 -0
  77. package/dist/session-history/__tests__/search.test.d.ts.map +1 -0
  78. package/dist/session-history/__tests__/search.test.js +150 -0
  79. package/dist/session-history/__tests__/search.test.js.map +1 -0
  80. package/dist/session-history/search.d.ts +31 -0
  81. package/dist/session-history/search.d.ts.map +1 -0
  82. package/dist/session-history/search.js +326 -0
  83. package/dist/session-history/search.js.map +1 -0
  84. package/dist/team/__tests__/allocation-policy.test.d.ts +2 -0
  85. package/dist/team/__tests__/allocation-policy.test.d.ts.map +1 -0
  86. package/dist/team/__tests__/allocation-policy.test.js +39 -0
  87. package/dist/team/__tests__/allocation-policy.test.js.map +1 -0
  88. package/dist/team/__tests__/api-interop.test.js +140 -4
  89. package/dist/team/__tests__/api-interop.test.js.map +1 -1
  90. package/dist/team/__tests__/followup-planner.test.js +12 -0
  91. package/dist/team/__tests__/followup-planner.test.js.map +1 -1
  92. package/dist/team/__tests__/idle-nudge.test.js +6 -1
  93. package/dist/team/__tests__/idle-nudge.test.js.map +1 -1
  94. package/dist/team/__tests__/rebalance-policy.test.d.ts +2 -0
  95. package/dist/team/__tests__/rebalance-policy.test.d.ts.map +1 -0
  96. package/dist/team/__tests__/rebalance-policy.test.js +125 -0
  97. package/dist/team/__tests__/rebalance-policy.test.js.map +1 -0
  98. package/dist/team/__tests__/runtime.test.js +315 -12
  99. package/dist/team/__tests__/runtime.test.js.map +1 -1
  100. package/dist/team/__tests__/state.test.js +20 -1
  101. package/dist/team/__tests__/state.test.js.map +1 -1
  102. package/dist/team/__tests__/team-ops-contract.test.js +1 -0
  103. package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
  104. package/dist/team/__tests__/worker-bootstrap.test.js +20 -3
  105. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  106. package/dist/team/allocation-policy.d.ts +23 -0
  107. package/dist/team/allocation-policy.d.ts.map +1 -0
  108. package/dist/team/allocation-policy.js +71 -0
  109. package/dist/team/allocation-policy.js.map +1 -0
  110. package/dist/team/api-interop.d.ts +1 -1
  111. package/dist/team/api-interop.d.ts.map +1 -1
  112. package/dist/team/api-interop.js +159 -0
  113. package/dist/team/api-interop.js.map +1 -1
  114. package/dist/team/idle-nudge.js +1 -1
  115. package/dist/team/idle-nudge.js.map +1 -1
  116. package/dist/team/rebalance-policy.d.ts +19 -0
  117. package/dist/team/rebalance-policy.d.ts.map +1 -0
  118. package/dist/team/rebalance-policy.js +48 -0
  119. package/dist/team/rebalance-policy.js.map +1 -0
  120. package/dist/team/runtime.d.ts.map +1 -1
  121. package/dist/team/runtime.js +132 -17
  122. package/dist/team/runtime.js.map +1 -1
  123. package/dist/team/state/types.d.ts +3 -0
  124. package/dist/team/state/types.d.ts.map +1 -1
  125. package/dist/team/state/types.js.map +1 -1
  126. package/dist/team/state.d.ts +8 -0
  127. package/dist/team/state.d.ts.map +1 -1
  128. package/dist/team/state.js +28 -12
  129. package/dist/team/state.js.map +1 -1
  130. package/dist/team/team-ops.d.ts +2 -1
  131. package/dist/team/team-ops.d.ts.map +1 -1
  132. package/dist/team/team-ops.js +1 -0
  133. package/dist/team/team-ops.js.map +1 -1
  134. package/dist/team/tmux-session.d.ts +5 -4
  135. package/dist/team/tmux-session.d.ts.map +1 -1
  136. package/dist/team/tmux-session.js +5 -67
  137. package/dist/team/tmux-session.js.map +1 -1
  138. package/dist/team/worker-bootstrap.d.ts +1 -0
  139. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  140. package/dist/team/worker-bootstrap.js +9 -2
  141. package/dist/team/worker-bootstrap.js.map +1 -1
  142. package/package.json +2 -1
  143. package/prompts/team-executor.md +57 -0
  144. package/prompts/team-orchestrator.md +8 -0
  145. package/scripts/notify-fallback-watcher.js +295 -1
  146. package/scripts/notify-hook/auto-nudge.js +20 -4
  147. package/scripts/notify-hook/team-dispatch.js +11 -58
  148. package/scripts/notify-hook/team-leader-nudge.js +59 -12
  149. package/scripts/notify-hook/team-tmux-guard.js +28 -11
  150. package/scripts/notify-hook/team-worker.js +3 -1
  151. package/scripts/notify-hook/tmux-injection.js +12 -13
  152. package/scripts/tmux-hook-engine.js +56 -0
  153. package/skills/team/SKILL.md +14 -0
  154. package/templates/catalog-manifest.json +5 -0
  155. package/dist/rtk/__tests__/index.test.d.ts +0 -2
  156. package/dist/rtk/__tests__/index.test.d.ts.map +0 -1
  157. package/dist/rtk/__tests__/index.test.js +0 -104
  158. package/dist/rtk/__tests__/index.test.js.map +0 -1
  159. package/dist/rtk/index.d.ts +0 -130
  160. package/dist/rtk/index.d.ts.map +0 -1
  161. package/dist/rtk/index.js +0 -257
  162. package/dist/rtk/index.js.map +0 -1
@@ -24,25 +24,53 @@ function expectedLowComplexityModel(codexHomeOverride) {
24
24
  function withEmptyPath(fn) {
25
25
  const prev = process.env.PATH;
26
26
  process.env.PATH = '';
27
+ let restoreImmediately = true;
27
28
  try {
28
- return fn();
29
+ const result = fn();
30
+ if (result instanceof Promise) {
31
+ restoreImmediately = false;
32
+ return result.finally(() => {
33
+ if (typeof prev === 'string')
34
+ process.env.PATH = prev;
35
+ else
36
+ delete process.env.PATH;
37
+ });
38
+ }
39
+ return result;
29
40
  }
30
41
  finally {
31
- if (typeof prev === 'string')
32
- process.env.PATH = prev;
33
- else
34
- delete process.env.PATH;
42
+ if (restoreImmediately) {
43
+ if (typeof prev === 'string')
44
+ process.env.PATH = prev;
45
+ else
46
+ delete process.env.PATH;
47
+ }
35
48
  }
36
49
  }
37
50
  function withoutTeamWorkerEnv(fn) {
38
51
  const prev = process.env.OMX_TEAM_WORKER;
39
52
  delete process.env.OMX_TEAM_WORKER;
53
+ let restoreImmediately = true;
40
54
  try {
41
- return fn();
55
+ const result = fn();
56
+ if (result instanceof Promise) {
57
+ restoreImmediately = false;
58
+ return result.finally(() => {
59
+ if (typeof prev === 'string')
60
+ process.env.OMX_TEAM_WORKER = prev;
61
+ else
62
+ delete process.env.OMX_TEAM_WORKER;
63
+ });
64
+ }
65
+ return result;
42
66
  }
43
67
  finally {
44
- if (typeof prev === 'string')
45
- process.env.OMX_TEAM_WORKER = prev;
68
+ if (restoreImmediately) {
69
+ if (typeof prev === 'string')
70
+ process.env.OMX_TEAM_WORKER = prev;
71
+ else
72
+ delete process.env.OMX_TEAM_WORKER;
73
+ }
46
74
  }
47
75
  }
48
76
  async function waitForFileText(filePath, matcher, timeoutMs = 3_000) {
@@ -311,12 +339,87 @@ describe('runtime', () => {
311
339
  await rm(cwd, { recursive: true, force: true });
312
340
  }
313
341
  });
342
+ it('startTeam allows nested team invocation when parent governance enables it', async () => {
343
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-runtime-nested-allow-'));
344
+ const binDir = join(cwd, 'bin');
345
+ const fakeGeminiPath = join(binDir, 'gemini');
346
+ await mkdir(binDir, { recursive: true });
347
+ await writeFile(fakeGeminiPath, `#!/usr/bin/env bash
348
+ sleep 5
349
+ `, { mode: 0o755 });
350
+ await initTeamState('parent-team', 'parent', 'executor', 1, cwd);
351
+ const parentManifestPath = join(cwd, '.omx', 'state', 'team', 'parent-team', 'manifest.v2.json');
352
+ const parentManifest = JSON.parse(await readFile(parentManifestPath, 'utf-8'));
353
+ parentManifest.governance = { ...(parentManifest.governance || {}), nested_teams_allowed: true };
354
+ await writeFile(parentManifestPath, JSON.stringify(parentManifest, null, 2));
355
+ const prevPath = process.env.PATH;
356
+ const prevTmux = process.env.TMUX;
357
+ const prevWorker = process.env.OMX_TEAM_WORKER;
358
+ const prevStateRoot = process.env.OMX_TEAM_STATE_ROOT;
359
+ const prevLeaderCwd = process.env.OMX_TEAM_LEADER_CWD;
360
+ const prevLaunchMode = process.env.OMX_TEAM_WORKER_LAUNCH_MODE;
361
+ const prevWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
362
+ process.env.PATH = `${binDir}:${prevPath ?? ''}`;
363
+ delete process.env.TMUX;
364
+ process.env.OMX_TEAM_WORKER = 'parent-team/worker-1';
365
+ process.env.OMX_TEAM_STATE_ROOT = join(cwd, '.omx', 'state');
366
+ process.env.OMX_TEAM_LEADER_CWD = cwd;
367
+ process.env.OMX_TEAM_WORKER_LAUNCH_MODE = 'prompt';
368
+ process.env.OMX_TEAM_WORKER_CLI = 'gemini';
369
+ let runtime = null;
370
+ try {
371
+ runtime = await startTeam('nested-allowed', 'nested task', 'explore', 1, [{ subject: 's', description: 'd', owner: 'worker-1' }], cwd);
372
+ assert.equal(runtime.teamName, 'nested-allowed');
373
+ await shutdownTeam(runtime.teamName, cwd, { force: true });
374
+ runtime = null;
375
+ }
376
+ finally {
377
+ if (runtime) {
378
+ await shutdownTeam(runtime.teamName, cwd, { force: true }).catch(() => { });
379
+ }
380
+ if (typeof prevPath === 'string')
381
+ process.env.PATH = prevPath;
382
+ else
383
+ delete process.env.PATH;
384
+ if (typeof prevTmux === 'string')
385
+ process.env.TMUX = prevTmux;
386
+ else
387
+ delete process.env.TMUX;
388
+ if (typeof prevWorker === 'string')
389
+ process.env.OMX_TEAM_WORKER = prevWorker;
390
+ else
391
+ delete process.env.OMX_TEAM_WORKER;
392
+ if (typeof prevStateRoot === 'string')
393
+ process.env.OMX_TEAM_STATE_ROOT = prevStateRoot;
394
+ else
395
+ delete process.env.OMX_TEAM_STATE_ROOT;
396
+ if (typeof prevLeaderCwd === 'string')
397
+ process.env.OMX_TEAM_LEADER_CWD = prevLeaderCwd;
398
+ else
399
+ delete process.env.OMX_TEAM_LEADER_CWD;
400
+ if (typeof prevLaunchMode === 'string')
401
+ process.env.OMX_TEAM_WORKER_LAUNCH_MODE = prevLaunchMode;
402
+ else
403
+ delete process.env.OMX_TEAM_WORKER_LAUNCH_MODE;
404
+ if (typeof prevWorkerCli === 'string')
405
+ process.env.OMX_TEAM_WORKER_CLI = prevWorkerCli;
406
+ else
407
+ delete process.env.OMX_TEAM_WORKER_CLI;
408
+ await rm(cwd, { recursive: true, force: true });
409
+ }
410
+ });
314
411
  it('startTeam throws when tmux is not available', async () => {
315
412
  const cwd = await mkdtemp(join(tmpdir(), 'omx-runtime-'));
413
+ const prevLaunchMode = process.env.OMX_TEAM_WORKER_LAUNCH_MODE;
316
414
  try {
415
+ process.env.OMX_TEAM_WORKER_LAUNCH_MODE = 'interactive';
317
416
  await assert.rejects(() => withoutTeamWorkerEnv(() => withEmptyPath(() => startTeam('team-a', 'task', 'executor', 1, [{ subject: 's', description: 'd' }], cwd))), /requires tmux/i);
318
417
  }
319
418
  finally {
419
+ if (typeof prevLaunchMode === 'string')
420
+ process.env.OMX_TEAM_WORKER_LAUNCH_MODE = prevLaunchMode;
421
+ else
422
+ delete process.env.OMX_TEAM_WORKER_LAUNCH_MODE;
320
423
  await rm(cwd, { recursive: true, force: true });
321
424
  }
322
425
  });
@@ -869,8 +972,64 @@ process.on('SIGTERM', () => {
869
972
  await rm(cwd, { recursive: true, force: true });
870
973
  }
871
974
  });
975
+ it('monitorTeam surfaces reclaimed work pickup attempts when an idle worker is available', async () => {
976
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-runtime-reassign-reclaimed-'));
977
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
978
+ delete process.env.OMX_TEAM_STATE_ROOT;
979
+ let sleeper1 = null;
980
+ let sleeper2 = null;
981
+ try {
982
+ await initTeamState('team-runtime-reassign', 'reassign reclaimed test', 'executor', 2, cwd);
983
+ const task = await createTask('team-runtime-reassign', { subject: 'write docs', description: 'document feature', status: 'pending', role: 'writer' }, cwd);
984
+ const claim = await claimTask('team-runtime-reassign', task.id, 'worker-1', null, cwd);
985
+ assert.ok(claim.ok);
986
+ if (!claim.ok)
987
+ throw new Error('claim failed');
988
+ const taskPath = join(cwd, '.omx', 'state', 'team', 'team-runtime-reassign', 'tasks', `task-${task.id}.json`);
989
+ const current = JSON.parse(await readFile(taskPath, 'utf-8'));
990
+ current.claim.leased_until = new Date(Date.now() - 1000).toISOString();
991
+ await writeAtomic(taskPath, JSON.stringify(current, null, 2));
992
+ const manifestPath = join(cwd, '.omx', 'state', 'team', 'team-runtime-reassign', 'manifest.v2.json');
993
+ const manifest = JSON.parse(await readFile(manifestPath, 'utf-8'));
994
+ sleeper1 = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { stdio: 'ignore', detached: false });
995
+ sleeper2 = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { stdio: 'ignore', detached: false });
996
+ manifest.policy = { ...(manifest.policy || {}), worker_launch_mode: 'prompt' };
997
+ manifest.workers[0].role = 'executor';
998
+ manifest.workers[1].role = 'writer';
999
+ manifest.workers[0].pid = sleeper1.pid;
1000
+ manifest.workers[1].pid = sleeper2.pid;
1001
+ await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
1002
+ await writeAtomic(join(cwd, '.omx', 'state', 'team', 'team-runtime-reassign', 'workers', 'worker-1', 'status.json'), JSON.stringify({ state: 'working', current_task_id: task.id, updated_at: new Date().toISOString() }, null, 2));
1003
+ await writeAtomic(join(cwd, '.omx', 'state', 'team', 'team-runtime-reassign', 'workers', 'worker-2', 'status.json'), JSON.stringify({ state: 'idle', updated_at: new Date().toISOString() }, null, 2));
1004
+ const snapshot = await monitorTeam('team-runtime-reassign', cwd);
1005
+ assert.ok(snapshot);
1006
+ const reread = await readTask('team-runtime-reassign', task.id, cwd);
1007
+ assert.equal(reread?.status, 'pending');
1008
+ assert.equal(reread?.owner, undefined);
1009
+ assert.equal(snapshot?.recommendations.some((r) => r.includes(`Unable to assign task-${task.id} to worker-2: worker_notify_failed`)), true);
1010
+ }
1011
+ finally {
1012
+ try {
1013
+ if (sleeper1?.pid)
1014
+ process.kill(sleeper1.pid, 'SIGKILL');
1015
+ }
1016
+ catch { }
1017
+ try {
1018
+ if (sleeper2?.pid)
1019
+ process.kill(sleeper2.pid, 'SIGKILL');
1020
+ }
1021
+ catch { }
1022
+ if (typeof prevTeamStateRoot === 'string')
1023
+ process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
1024
+ else
1025
+ delete process.env.OMX_TEAM_STATE_ROOT;
1026
+ await rm(cwd, { recursive: true, force: true });
1027
+ }
1028
+ });
872
1029
  it('monitorTeam reclaims expired task claims and surfaces the recovery in recommendations', async () => {
873
1030
  const cwd = await mkdtemp(join(tmpdir(), 'omx-runtime-reclaim-'));
1031
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
1032
+ delete process.env.OMX_TEAM_STATE_ROOT;
874
1033
  try {
875
1034
  await initTeamState('team-runtime-reclaim', 'reclaim test', 'executor', 2, cwd);
876
1035
  const t = await createTask('team-runtime-reclaim', { subject: 'task', description: 'd', status: 'pending' }, cwd);
@@ -890,6 +1049,10 @@ process.on('SIGTERM', () => {
890
1049
  assert.equal(snapshot?.recommendations.some((r) => r.includes(`task-${t.id}`) && r.includes('Reclaimed expired claim')), true);
891
1050
  }
892
1051
  finally {
1052
+ if (typeof prevTeamStateRoot === 'string')
1053
+ process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
1054
+ else
1055
+ delete process.env.OMX_TEAM_STATE_ROOT;
893
1056
  await rm(cwd, { recursive: true, force: true });
894
1057
  }
895
1058
  });
@@ -960,6 +1123,45 @@ process.on('SIGTERM', () => {
960
1123
  await rm(cwd, { recursive: true, force: true });
961
1124
  }
962
1125
  });
1126
+ it('monitorTeam propagates linked terminal state into Ralph without waiting for notify-hook', async () => {
1127
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-runtime-linked-ralph-monitor-'));
1128
+ try {
1129
+ await initTeamState('team-linked-ralph-monitor', 'linked runtime sync test', 'executor', 1, cwd);
1130
+ await createTask('team-linked-ralph-monitor', {
1131
+ subject: 'code change',
1132
+ description: 'implement feature',
1133
+ status: 'completed',
1134
+ owner: 'worker-1',
1135
+ requires_code_change: false,
1136
+ }, cwd);
1137
+ await writeFile(join(cwd, '.omx', 'state', 'team-state.json'), JSON.stringify({
1138
+ active: true,
1139
+ current_phase: 'team-exec',
1140
+ linked_ralph: true,
1141
+ team_name: 'team-linked-ralph-monitor',
1142
+ }, null, 2));
1143
+ await writeFile(join(cwd, '.omx', 'state', 'ralph-state.json'), JSON.stringify({
1144
+ active: true,
1145
+ iteration: 1,
1146
+ max_iterations: 10,
1147
+ current_phase: 'executing',
1148
+ started_at: '2026-03-11T00:00:00.000Z',
1149
+ linked_team: true,
1150
+ }, null, 2));
1151
+ const snapshot = await monitorTeam('team-linked-ralph-monitor', cwd);
1152
+ assert.ok(snapshot);
1153
+ assert.equal(snapshot?.phase, 'complete');
1154
+ const ralphState = JSON.parse(await readFile(join(cwd, '.omx', 'state', 'ralph-state.json'), 'utf-8'));
1155
+ assert.equal(ralphState.active, false);
1156
+ assert.equal(ralphState.current_phase, 'complete');
1157
+ assert.equal(ralphState.linked_team_terminal_phase, 'complete');
1158
+ assert.ok(typeof ralphState.linked_team_terminal_at === 'string' && ralphState.linked_team_terminal_at);
1159
+ assert.ok(typeof ralphState.completed_at === 'string' && ralphState.completed_at);
1160
+ }
1161
+ finally {
1162
+ await rm(cwd, { recursive: true, force: true });
1163
+ }
1164
+ });
963
1165
  it('monitorTeam emits worker_state_changed, worker_idle, and task_completed events based on transitions', async () => {
964
1166
  const cwd = await mkdtemp(join(tmpdir(), 'omx-runtime-'));
965
1167
  try {
@@ -1006,6 +1208,26 @@ process.on('SIGTERM', () => {
1006
1208
  await rm(cwd, { recursive: true, force: true });
1007
1209
  }
1008
1210
  });
1211
+ it('shutdownTeam honors governance cleanup override when active tasks remain', async () => {
1212
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-runtime-shutdown-gate-override-'));
1213
+ try {
1214
+ await initTeamState('team-shutdown-gate-override', 'shutdown gate override test', 'executor', 1, cwd);
1215
+ await createTask('team-shutdown-gate-override', { subject: 'pending', description: 'd', status: 'pending' }, cwd);
1216
+ const manifestPath = join(cwd, '.omx', 'state', 'team', 'team-shutdown-gate-override', 'manifest.v2.json');
1217
+ const manifest = JSON.parse(await readFile(manifestPath, 'utf-8'));
1218
+ manifest.governance = {
1219
+ ...(manifest.governance || {}),
1220
+ cleanup_requires_all_workers_inactive: false,
1221
+ };
1222
+ await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
1223
+ await shutdownTeam('team-shutdown-gate-override', cwd);
1224
+ const teamRoot = join(cwd, '.omx', 'state', 'team', 'team-shutdown-gate-override');
1225
+ assert.equal(existsSync(teamRoot), false);
1226
+ }
1227
+ finally {
1228
+ await rm(cwd, { recursive: true, force: true });
1229
+ }
1230
+ });
1009
1231
  it('shutdownTeam blocks when failed tasks remain (completion gate)', async () => {
1010
1232
  const cwd = await mkdtemp(join(tmpdir(), 'omx-runtime-shutdown-gate-failed-'));
1011
1233
  try {
@@ -1289,7 +1511,7 @@ esac
1289
1511
  await rm(fakeBinDir, { recursive: true, force: true });
1290
1512
  }
1291
1513
  });
1292
- it('shutdownTeam preserves leader and hud exclusions in teardown', async () => {
1514
+ it('shutdownTeam preserves leader exclusion while tearing down the hud pane', async () => {
1293
1515
  const cwd = await mkdtemp(join(tmpdir(), 'omx-runtime-shutdown-exclusions-'));
1294
1516
  const fakeBinDir = await mkdtemp(join(tmpdir(), 'omx-runtime-shutdown-exclusions-bin-'));
1295
1517
  const tmuxLogPath = join(fakeBinDir, 'tmux.log');
@@ -1332,7 +1554,7 @@ esac
1332
1554
  await shutdownTeam('team-shutdown-exclusions', cwd, { force: true });
1333
1555
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
1334
1556
  assert.doesNotMatch(tmuxLog, /kill-pane -t %11/);
1335
- assert.doesNotMatch(tmuxLog, /kill-pane -t %12/);
1557
+ assert.match(tmuxLog, /kill-pane -t %12/);
1336
1558
  assert.match(tmuxLog, /kill-pane -t %13/);
1337
1559
  }
1338
1560
  finally {
@@ -1402,6 +1624,37 @@ esac
1402
1624
  await rm(cwd, { recursive: true, force: true });
1403
1625
  }
1404
1626
  });
1627
+ it('shutdownTeam ralph=true propagates linked Ralph cancellation before cleanup', async () => {
1628
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-runtime-linked-ralph-shutdown-'));
1629
+ try {
1630
+ await initTeamState('team-linked-ralph-shutdown', 'linked shutdown sync test', 'executor', 1, cwd);
1631
+ await createTask('team-linked-ralph-shutdown', { subject: 'done', description: 'd', status: 'completed' }, cwd);
1632
+ await writeFile(join(cwd, '.omx', 'state', 'team-state.json'), JSON.stringify({
1633
+ active: true,
1634
+ current_phase: 'team-exec',
1635
+ linked_ralph: true,
1636
+ team_name: 'team-linked-ralph-shutdown',
1637
+ }, null, 2));
1638
+ await writeFile(join(cwd, '.omx', 'state', 'ralph-state.json'), JSON.stringify({
1639
+ active: true,
1640
+ iteration: 1,
1641
+ max_iterations: 10,
1642
+ current_phase: 'executing',
1643
+ started_at: '2026-03-11T00:00:00.000Z',
1644
+ linked_team: true,
1645
+ }, null, 2));
1646
+ await shutdownTeam('team-linked-ralph-shutdown', cwd, { ralph: true });
1647
+ const ralphState = JSON.parse(await readFile(join(cwd, '.omx', 'state', 'ralph-state.json'), 'utf-8'));
1648
+ assert.equal(ralphState.active, false);
1649
+ assert.equal(ralphState.current_phase, 'cancelled');
1650
+ assert.equal(ralphState.linked_team_terminal_phase, 'cancelled');
1651
+ assert.ok(typeof ralphState.linked_team_terminal_at === 'string' && ralphState.linked_team_terminal_at);
1652
+ assert.ok(typeof ralphState.completed_at === 'string' && ralphState.completed_at);
1653
+ }
1654
+ finally {
1655
+ await rm(cwd, { recursive: true, force: true });
1656
+ }
1657
+ });
1405
1658
  it('shutdownTeam ralph=false still throws on failed tasks (normal path unchanged)', async () => {
1406
1659
  const cwd = await mkdtemp(join(tmpdir(), 'omx-runtime-ralph-normal-'));
1407
1660
  try {
@@ -1477,7 +1730,7 @@ esac
1477
1730
  const task = await createTask('team-delegation', { subject: 'x', description: 'd', status: 'pending', requires_code_change: false }, cwd);
1478
1731
  const manifestPath = join(cwd, '.omx', 'state', 'team', 'team-delegation', 'manifest.v2.json');
1479
1732
  const manifest = JSON.parse(await readFile(manifestPath, 'utf-8'));
1480
- manifest.policy.delegation_only = true;
1733
+ manifest.governance = { ...(manifest.governance || {}), delegation_only: true };
1481
1734
  await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
1482
1735
  await assert.rejects(() => assignTask('team-delegation', 'leader-fixed', task.id, cwd), /delegation_only_violation/);
1483
1736
  }
@@ -1541,7 +1794,7 @@ esac
1541
1794
  const task = await createTask('team-approval', { subject: 'x', description: 'd', status: 'pending', requires_code_change: true }, cwd);
1542
1795
  const manifestPath = join(cwd, '.omx', 'state', 'team', 'team-approval', 'manifest.v2.json');
1543
1796
  const manifest = JSON.parse(await readFile(manifestPath, 'utf-8'));
1544
- manifest.policy.plan_approval_required = true;
1797
+ manifest.governance = { ...(manifest.governance || {}), plan_approval_required: true };
1545
1798
  await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
1546
1799
  await assert.rejects(() => assignTask('team-approval', 'worker-1', task.id, cwd), /plan_approval_required/);
1547
1800
  }
@@ -1696,6 +1949,56 @@ esac
1696
1949
  await rm(cwd, { recursive: true, force: true });
1697
1950
  }
1698
1951
  });
1952
+ it('sendWorkerMessage hook-preferred path injects leader mailbox read guidance when leader pane exists', async () => {
1953
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-runtime-leader-inject-'));
1954
+ const fakeBinDir = await mkdtemp(join(tmpdir(), 'omx-runtime-leader-inject-bin-'));
1955
+ const tmuxLogPath = join(fakeBinDir, 'tmux.log');
1956
+ const tmuxStubPath = join(fakeBinDir, 'tmux');
1957
+ const previousPath = process.env.PATH;
1958
+ try {
1959
+ await writeFile(tmuxStubPath, `#!/bin/sh
1960
+ set -eu
1961
+ printf '%s\n' "$*" >> "${tmuxLogPath}"
1962
+ case "\${1:-}" in
1963
+ send-keys)
1964
+ exit 0
1965
+ ;;
1966
+ *)
1967
+ exit 0
1968
+ ;;
1969
+ esac
1970
+ `);
1971
+ await chmod(tmuxStubPath, 0o755);
1972
+ process.env.PATH = `${fakeBinDir}:${previousPath ?? ''}`;
1973
+ await initTeamState('team-leader-inject', 'leader injection test', 'executor', 1, cwd);
1974
+ const cfg = await readTeamConfig('team-leader-inject', cwd);
1975
+ assert.ok(cfg);
1976
+ if (!cfg)
1977
+ throw new Error('missing team config');
1978
+ cfg.leader_pane_id = '%55';
1979
+ await saveTeamConfig(cfg, cwd);
1980
+ const manifestPath = join(cwd, '.omx', 'state', 'team', 'team-leader-inject', 'manifest.v2.json');
1981
+ const manifest = JSON.parse(await readFile(manifestPath, 'utf-8'));
1982
+ manifest.policy = { ...(manifest.policy || {}), dispatch_ack_timeout_ms: 100 };
1983
+ await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
1984
+ await sendWorkerMessage('team-leader-inject', 'worker-1', 'leader-fixed', 'hello leader', cwd);
1985
+ const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
1986
+ assert.match(tmuxLog, /send-keys -t %55 -l -- Read \.omx\/state\/team\/team-leader-inject\/mailbox\/leader-fixed\.json; worker-1 sent a new message\. Reply with the next concrete step\./);
1987
+ const mailbox = await listMailboxMessages('team-leader-inject', 'leader-fixed', cwd);
1988
+ assert.ok(mailbox.some((m) => typeof m.notified_at === 'string' && m.notified_at.length > 0));
1989
+ const requests = await listDispatchRequests('team-leader-inject', cwd, { kind: 'mailbox', to_worker: 'leader-fixed' });
1990
+ const latest = requests[requests.length - 1];
1991
+ assert.equal(latest?.status, 'notified');
1992
+ }
1993
+ finally {
1994
+ if (typeof previousPath === 'string')
1995
+ process.env.PATH = previousPath;
1996
+ else
1997
+ delete process.env.PATH;
1998
+ await rm(cwd, { recursive: true, force: true });
1999
+ await rm(fakeBinDir, { recursive: true, force: true });
2000
+ }
2001
+ });
1699
2002
  it('sendWorkerMessage hook-preferred path for leader waits for receipt then falls back to direct notify', async () => {
1700
2003
  const cwd = await mkdtemp(join(tmpdir(), 'omx-runtime-leader-hook-'));
1701
2004
  try {