oh-my-codex 0.15.2 → 0.15.3

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 (166) hide show
  1. package/Cargo.lock +5 -5
  2. package/Cargo.toml +1 -1
  3. package/dist/agents/__tests__/native-config.test.js +33 -0
  4. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  5. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js +9 -1
  6. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js.map +1 -1
  7. package/dist/cli/__tests__/doctor-context-window-warning.test.d.ts +2 -0
  8. package/dist/cli/__tests__/doctor-context-window-warning.test.d.ts.map +1 -0
  9. package/dist/cli/__tests__/doctor-context-window-warning.test.js +122 -0
  10. package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +1 -0
  11. package/dist/cli/__tests__/doctor-warning-copy.test.js +2 -2
  12. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  13. package/dist/cli/__tests__/exec.test.js +1 -0
  14. package/dist/cli/__tests__/exec.test.js.map +1 -1
  15. package/dist/cli/__tests__/explore.test.js +40 -17
  16. package/dist/cli/__tests__/explore.test.js.map +1 -1
  17. package/dist/cli/__tests__/index.test.js +141 -8
  18. package/dist/cli/__tests__/index.test.js.map +1 -1
  19. package/dist/cli/__tests__/mcp-serve.test.js +27 -1
  20. package/dist/cli/__tests__/mcp-serve.test.js.map +1 -1
  21. package/dist/cli/__tests__/ralph.test.js +59 -1
  22. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  23. package/dist/cli/__tests__/setup-scope.test.js +2 -1
  24. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  25. package/dist/cli/__tests__/team.test.js +55 -10
  26. package/dist/cli/__tests__/team.test.js.map +1 -1
  27. package/dist/cli/doctor.d.ts.map +1 -1
  28. package/dist/cli/doctor.js +46 -3
  29. package/dist/cli/doctor.js.map +1 -1
  30. package/dist/cli/index.d.ts +16 -1
  31. package/dist/cli/index.d.ts.map +1 -1
  32. package/dist/cli/index.js +126 -15
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/cli/mcp-serve.d.ts +1 -0
  35. package/dist/cli/mcp-serve.d.ts.map +1 -1
  36. package/dist/cli/mcp-serve.js +8 -0
  37. package/dist/cli/mcp-serve.js.map +1 -1
  38. package/dist/cli/ralph.d.ts +2 -0
  39. package/dist/cli/ralph.d.ts.map +1 -1
  40. package/dist/cli/ralph.js +17 -1
  41. package/dist/cli/ralph.js.map +1 -1
  42. package/dist/cli/team.d.ts +4 -0
  43. package/dist/cli/team.d.ts.map +1 -1
  44. package/dist/cli/team.js +47 -22
  45. package/dist/cli/team.js.map +1 -1
  46. package/dist/config/__tests__/generator-idempotent.test.js +27 -5
  47. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  48. package/dist/config/generator.d.ts +11 -2
  49. package/dist/config/generator.d.ts.map +1 -1
  50. package/dist/config/generator.js +114 -58
  51. package/dist/config/generator.js.map +1 -1
  52. package/dist/hooks/__tests__/agents-overlay.test.js +59 -0
  53. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  54. package/dist/hooks/__tests__/anti-slop-workflow.test.js +109 -18
  55. package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
  56. package/dist/hooks/agents-overlay.d.ts.map +1 -1
  57. package/dist/hooks/agents-overlay.js +21 -0
  58. package/dist/hooks/agents-overlay.js.map +1 -1
  59. package/dist/hud/__tests__/index.test.js +30 -14
  60. package/dist/hud/__tests__/index.test.js.map +1 -1
  61. package/dist/openclaw/__tests__/dispatcher.test.js +1 -1
  62. package/dist/openclaw/__tests__/dispatcher.test.js.map +1 -1
  63. package/dist/pipeline/__tests__/stages.test.js +398 -14
  64. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  65. package/dist/pipeline/stages/team-exec.d.ts +8 -4
  66. package/dist/pipeline/stages/team-exec.d.ts.map +1 -1
  67. package/dist/pipeline/stages/team-exec.js +198 -13
  68. package/dist/pipeline/stages/team-exec.js.map +1 -1
  69. package/dist/planning/__tests__/artifacts.test.js +246 -1
  70. package/dist/planning/__tests__/artifacts.test.js.map +1 -1
  71. package/dist/planning/artifact-names.d.ts +13 -0
  72. package/dist/planning/artifact-names.d.ts.map +1 -0
  73. package/dist/planning/artifact-names.js +108 -0
  74. package/dist/planning/artifact-names.js.map +1 -0
  75. package/dist/planning/artifacts.d.ts +22 -1
  76. package/dist/planning/artifacts.d.ts.map +1 -1
  77. package/dist/planning/artifacts.js +165 -50
  78. package/dist/planning/artifacts.js.map +1 -1
  79. package/dist/ralph/__tests__/persistence.test.js +21 -1
  80. package/dist/ralph/__tests__/persistence.test.js.map +1 -1
  81. package/dist/ralph/persistence.d.ts.map +1 -1
  82. package/dist/ralph/persistence.js +6 -4
  83. package/dist/ralph/persistence.js.map +1 -1
  84. package/dist/scripts/__tests__/codex-native-hook.test.js +352 -2
  85. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  86. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  87. package/dist/scripts/codex-native-hook.js +85 -6
  88. package/dist/scripts/codex-native-hook.js.map +1 -1
  89. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  90. package/dist/scripts/codex-native-pre-post.js +123 -0
  91. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  92. package/dist/scripts/notify-hook/team-worker-posttooluse.js +1 -1
  93. package/dist/scripts/notify-hook/team-worker-posttooluse.js.map +1 -1
  94. package/dist/scripts/notify-hook.js +1 -1
  95. package/dist/scripts/notify-hook.js.map +1 -1
  96. package/dist/scripts/sync-plugin-mirror.d.ts +1 -0
  97. package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
  98. package/dist/scripts/sync-plugin-mirror.js +8 -2
  99. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  100. package/dist/state/__tests__/skill-active.test.js +41 -0
  101. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  102. package/dist/team/__tests__/api-interop.test.js +220 -0
  103. package/dist/team/__tests__/api-interop.test.js.map +1 -1
  104. package/dist/team/__tests__/model-contract.test.js +40 -9
  105. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  106. package/dist/team/__tests__/repo-aware-decomposition.test.js +41 -0
  107. package/dist/team/__tests__/repo-aware-decomposition.test.js.map +1 -1
  108. package/dist/team/__tests__/runtime-cli.test.js +24 -0
  109. package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
  110. package/dist/team/__tests__/runtime.test.js +446 -67
  111. package/dist/team/__tests__/runtime.test.js.map +1 -1
  112. package/dist/team/__tests__/state.test.js +13 -0
  113. package/dist/team/__tests__/state.test.js.map +1 -1
  114. package/dist/team/__tests__/team-identity.test.d.ts +2 -0
  115. package/dist/team/__tests__/team-identity.test.d.ts.map +1 -0
  116. package/dist/team/__tests__/team-identity.test.js +166 -0
  117. package/dist/team/__tests__/team-identity.test.js.map +1 -0
  118. package/dist/team/__tests__/tmux-session.test.js +55 -1
  119. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  120. package/dist/team/__tests__/worker-bootstrap.test.js +12 -0
  121. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  122. package/dist/team/api-interop.d.ts +1 -0
  123. package/dist/team/api-interop.d.ts.map +1 -1
  124. package/dist/team/api-interop.js +159 -129
  125. package/dist/team/api-interop.js.map +1 -1
  126. package/dist/team/delivery-log.d.ts +1 -1
  127. package/dist/team/delivery-log.d.ts.map +1 -1
  128. package/dist/team/delivery-log.js.map +1 -1
  129. package/dist/team/repo-aware-decomposition.d.ts +3 -0
  130. package/dist/team/repo-aware-decomposition.d.ts.map +1 -1
  131. package/dist/team/repo-aware-decomposition.js +2 -0
  132. package/dist/team/repo-aware-decomposition.js.map +1 -1
  133. package/dist/team/runtime-cli.d.ts +32 -2
  134. package/dist/team/runtime-cli.d.ts.map +1 -1
  135. package/dist/team/runtime-cli.js +78 -26
  136. package/dist/team/runtime-cli.js.map +1 -1
  137. package/dist/team/runtime.d.ts +1 -1
  138. package/dist/team/runtime.d.ts.map +1 -1
  139. package/dist/team/runtime.js +338 -35
  140. package/dist/team/runtime.js.map +1 -1
  141. package/dist/team/state.d.ts +9 -0
  142. package/dist/team/state.d.ts.map +1 -1
  143. package/dist/team/state.js +21 -0
  144. package/dist/team/state.js.map +1 -1
  145. package/dist/team/team-identity.d.ts +26 -0
  146. package/dist/team/team-identity.d.ts.map +1 -0
  147. package/dist/team/team-identity.js +169 -0
  148. package/dist/team/team-identity.js.map +1 -0
  149. package/dist/team/tmux-session.d.ts +18 -0
  150. package/dist/team/tmux-session.d.ts.map +1 -1
  151. package/dist/team/tmux-session.js +61 -1
  152. package/dist/team/tmux-session.js.map +1 -1
  153. package/dist/team/worker-bootstrap.d.ts +2 -0
  154. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  155. package/dist/team/worker-bootstrap.js +10 -1
  156. package/dist/team/worker-bootstrap.js.map +1 -1
  157. package/package.json +1 -1
  158. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  159. package/plugins/oh-my-codex/skills/ai-slop-cleaner/SKILL.md +30 -5
  160. package/skills/ai-slop-cleaner/SKILL.md +30 -5
  161. package/src/scripts/__tests__/codex-native-hook.test.ts +398 -2
  162. package/src/scripts/codex-native-hook.ts +115 -5
  163. package/src/scripts/codex-native-pre-post.ts +121 -0
  164. package/src/scripts/notify-hook/team-worker-posttooluse.ts +1 -1
  165. package/src/scripts/notify-hook.ts +1 -1
  166. package/src/scripts/sync-plugin-mirror.ts +11 -2
@@ -1,7 +1,7 @@
1
1
  import { afterEach, beforeEach, describe, it } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import { execFileSync, spawn } from 'child_process';
4
- import { mkdtemp, rm, writeFile, readFile, mkdir, chmod } from 'fs/promises';
4
+ import { mkdtemp, rm, writeFile, readFile, mkdir, chmod, readdir } from 'fs/promises';
5
5
  import { join } from 'path';
6
6
  import { tmpdir } from 'os';
7
7
  import { existsSync } from 'fs';
@@ -11,6 +11,7 @@ import { monitorTeam, shutdownTeam, resumeTeam, startTeam, assignTask, sendWorke
11
11
  import { resolveAgentReasoningEffort, resolveTeamLowComplexityDefaultModel } from '../model-contract.js';
12
12
  import { readTeamEvents } from '../state/events.js';
13
13
  import { sanitizeTeamName } from '../tmux-session.js';
14
+ import { buildInternalTeamName, resolveTeamIdentityScope } from '../team-identity.js';
14
15
  async function initRepo() {
15
16
  const cwd = await mkdtemp(join(tmpdir(), 'omx-runtime-worktree-repo-'));
16
17
  execFileSync('git', ['init'], { cwd, stdio: 'ignore' });
@@ -47,6 +48,56 @@ async function attachDirtyWorkerRepo(teamName, cwd, repoName) {
47
48
  function expectedLowComplexityModel(codexHomeOverride) {
48
49
  return resolveTeamLowComplexityDefaultModel(codexHomeOverride);
49
50
  }
51
+ function withIsolatedDefaultModelEnv(run) {
52
+ const savedEnv = new Map();
53
+ for (const key of [
54
+ 'CODEX_HOME',
55
+ 'OMX_DEFAULT_FRONTIER_MODEL',
56
+ 'OMX_DEFAULT_STANDARD_MODEL',
57
+ 'OMX_DEFAULT_SPARK_MODEL',
58
+ 'OMX_SPARK_MODEL',
59
+ ]) {
60
+ savedEnv.set(key, process.env[key]);
61
+ delete process.env[key];
62
+ }
63
+ process.env.CODEX_HOME = join(tmpdir(), `omx-runtime-defaults-${process.pid}-${Date.now()}`);
64
+ try {
65
+ return run();
66
+ }
67
+ finally {
68
+ for (const [key, value] of savedEnv.entries()) {
69
+ if (typeof value === 'string')
70
+ process.env[key] = value;
71
+ else
72
+ delete process.env[key];
73
+ }
74
+ }
75
+ }
76
+ async function withIsolatedDefaultModelEnvAsync(run) {
77
+ const savedEnv = new Map();
78
+ for (const key of [
79
+ 'CODEX_HOME',
80
+ 'OMX_DEFAULT_FRONTIER_MODEL',
81
+ 'OMX_DEFAULT_STANDARD_MODEL',
82
+ 'OMX_DEFAULT_SPARK_MODEL',
83
+ 'OMX_SPARK_MODEL',
84
+ ]) {
85
+ savedEnv.set(key, process.env[key]);
86
+ delete process.env[key];
87
+ }
88
+ process.env.CODEX_HOME = join(tmpdir(), `omx-runtime-defaults-${process.pid}-${Date.now()}`);
89
+ try {
90
+ return await run();
91
+ }
92
+ finally {
93
+ for (const [key, value] of savedEnv.entries()) {
94
+ if (typeof value === 'string')
95
+ process.env[key] = value;
96
+ else
97
+ delete process.env[key];
98
+ }
99
+ }
100
+ }
50
101
  async function readTeamDeliveryLog(cwd) {
51
102
  const path = join(cwd, '.omx', 'logs', `team-delivery-${new Date().toISOString().slice(0, 10)}.jsonl`);
52
103
  const raw = await readFile(path, 'utf-8').catch(() => '');
@@ -57,7 +108,7 @@ async function readTeamDeliveryLog(cwd) {
57
108
  .map((line) => JSON.parse(line));
58
109
  }
59
110
  async function markPendingInboxDispatchesDelivered(teamName, cwd, opts = {}) {
60
- const requests = await listDispatchRequests(teamName, cwd, { kind: 'inbox' }).catch(() => []);
111
+ const requests = await listDispatchRequests(await resolveRuntimeTeamName(cwd, teamName), cwd, { kind: 'inbox' }).catch(() => []);
61
112
  for (const request of requests) {
62
113
  if (request.status !== 'pending')
63
114
  continue;
@@ -72,7 +123,7 @@ async function markPendingInboxDispatchesDelivered(teamName, cwd, opts = {}) {
72
123
  }
73
124
  }
74
125
  async function markPendingInboxDispatchesNotified(teamName, cwd, opts = {}) {
75
- const requests = await listDispatchRequests(teamName, cwd, { kind: 'inbox' }).catch(() => []);
126
+ const requests = await listDispatchRequests(await resolveRuntimeTeamName(cwd, teamName), cwd, { kind: 'inbox' }).catch(() => []);
76
127
  for (const request of requests) {
77
128
  if (request.status !== 'pending')
78
129
  continue;
@@ -168,6 +219,16 @@ async function waitForFileText(filePath, matcher, timeoutMs = 3_000) {
168
219
  }
169
220
  throw new Error(`timed out waiting for ${filePath}`);
170
221
  }
222
+ async function resolveRuntimeTeamName(cwd, requestedName) {
223
+ const teamsRoot = join(cwd, '.omx', 'state', 'team');
224
+ const entries = await readdir(teamsRoot, { withFileTypes: true }).catch(() => []);
225
+ const prefix = requestedName.slice(0, 18);
226
+ const names = entries
227
+ .filter((entry) => entry.isDirectory() && (entry.name === requestedName || entry.name.startsWith(`${requestedName}-`) || entry.name.startsWith(prefix)))
228
+ .map((entry) => entry.name)
229
+ .sort((a, b) => a.length - b.length || a.localeCompare(b));
230
+ return names[0] ?? requestedName;
231
+ }
171
232
  async function writeFakePromptWorkerBinary(binaryPath, scriptBody, options = {}) {
172
233
  const bootstrap = options.emitStartupEvidence === false
173
234
  ? ''
@@ -175,7 +236,7 @@ async function writeFakePromptWorkerBinary(binaryPath, scriptBody, options = {})
175
236
  const fs = require('fs');
176
237
  const path = require('path');
177
238
  const stateRoot = process.env.OMX_TEAM_STATE_ROOT;
178
- const worker = String(process.env.OMX_TEAM_WORKER || '');
239
+ const worker = String(process.env.OMX_TEAM_INTERNAL_WORKER || process.env.OMX_TEAM_WORKER || '');
179
240
  const [teamName, workerName] = worker.split('/');
180
241
  if (stateRoot && teamName && workerName) {
181
242
  const workerDir = path.join(stateRoot, 'team', teamName, 'workers', workerName);
@@ -318,29 +379,35 @@ describe('runtime', () => {
318
379
  assert.deepEqual(args, ['--no-alt-screen', '--model', expectedLowComplexityModel()]);
319
380
  });
320
381
  it('resolveWorkerLaunchArgsFromEnv reads low-complexity model from config when present', async () => {
321
- const previousCodexHome = process.env.CODEX_HOME;
322
- const tempCodexHome = await mkdtemp(join(tmpdir(), 'omx-codex-home-'));
323
- await writeFile(join(tempCodexHome, '.omx-config.json'), JSON.stringify({ models: { team_low_complexity: 'gpt-4.1-mini' } }));
324
- process.env.CODEX_HOME = tempCodexHome;
325
- try {
326
- const args = resolveWorkerLaunchArgsFromEnv({ OMX_TEAM_WORKER_LAUNCH_ARGS: '--no-alt-screen' }, 'explore');
327
- assert.deepEqual(args, ['--no-alt-screen', '--model', 'gpt-4.1-mini']);
328
- }
329
- finally {
330
- if (typeof previousCodexHome === 'string')
331
- process.env.CODEX_HOME = previousCodexHome;
332
- else
333
- delete process.env.CODEX_HOME;
334
- await rm(tempCodexHome, { recursive: true, force: true });
335
- }
382
+ await withIsolatedDefaultModelEnvAsync(async () => {
383
+ const previousCodexHome = process.env.CODEX_HOME;
384
+ const tempCodexHome = await mkdtemp(join(tmpdir(), 'omx-codex-home-'));
385
+ await writeFile(join(tempCodexHome, '.omx-config.json'), JSON.stringify({ models: { team_low_complexity: 'gpt-4.1-mini' } }));
386
+ process.env.CODEX_HOME = tempCodexHome;
387
+ try {
388
+ const args = resolveWorkerLaunchArgsFromEnv({ OMX_TEAM_WORKER_LAUNCH_ARGS: '--no-alt-screen' }, 'explore');
389
+ assert.deepEqual(args, ['--no-alt-screen', '--model', 'gpt-4.1-mini']);
390
+ }
391
+ finally {
392
+ if (typeof previousCodexHome === 'string')
393
+ process.env.CODEX_HOME = previousCodexHome;
394
+ else
395
+ delete process.env.CODEX_HOME;
396
+ await rm(tempCodexHome, { recursive: true, force: true });
397
+ }
398
+ });
336
399
  });
337
400
  it('resolveWorkerLaunchArgsFromEnv injects the frontier default model for executor workers', () => {
338
- const args = resolveWorkerLaunchArgsFromEnv({ OMX_TEAM_WORKER_LAUNCH_ARGS: '--no-alt-screen' }, 'executor');
339
- assert.deepEqual(args, ['--no-alt-screen', '--model', 'gpt-5.5']);
401
+ withIsolatedDefaultModelEnv(() => {
402
+ const args = resolveWorkerLaunchArgsFromEnv({ OMX_TEAM_WORKER_LAUNCH_ARGS: '--no-alt-screen' }, 'executor');
403
+ assert.deepEqual(args, ['--no-alt-screen', '--model', 'gpt-5.5']);
404
+ });
340
405
  });
341
406
  it('resolveWorkerLaunchArgsFromEnv uses medium reasoning for executor launch defaults', () => {
342
- const args = resolveWorkerLaunchArgsFromEnv({ OMX_TEAM_WORKER_LAUNCH_ARGS: '--no-alt-screen' }, 'executor', undefined, resolveAgentReasoningEffort('executor'), 'codex');
343
- assert.deepEqual(args, ['--no-alt-screen', '-c', 'model_reasoning_effort="medium"', '--model', 'gpt-5.5']);
407
+ withIsolatedDefaultModelEnv(() => {
408
+ const args = resolveWorkerLaunchArgsFromEnv({ OMX_TEAM_WORKER_LAUNCH_ARGS: '--no-alt-screen' }, 'executor', undefined, resolveAgentReasoningEffort('executor'), 'codex');
409
+ assert.deepEqual(args, ['--no-alt-screen', '-c', 'model_reasoning_effort="medium"', '--model', 'gpt-5.5']);
410
+ });
344
411
  });
345
412
  it('resolveWorkerLaunchArgsFromEnv treats *-low aliases as low complexity', () => {
346
413
  const args = resolveWorkerLaunchArgsFromEnv({ OMX_TEAM_WORKER_LAUNCH_ARGS: '--no-alt-screen' }, 'executor-low');
@@ -366,10 +433,12 @@ describe('runtime', () => {
366
433
  const originalLog = console.log;
367
434
  console.log = (...args) => { logs.push(args.join(' ')); };
368
435
  try {
369
- const lowArgs = resolveWorkerLaunchArgsFromEnv({ OMX_TEAM_WORKER_LAUNCH_ARGS: '--no-alt-screen' }, 'executor', undefined, 'low', 'codex');
370
- const highArgs = resolveWorkerLaunchArgsFromEnv({ OMX_TEAM_WORKER_LAUNCH_ARGS: '--no-alt-screen' }, 'executor', undefined, 'high', 'codex');
371
- assert.deepEqual(lowArgs, ['--no-alt-screen', '-c', 'model_reasoning_effort="low"', '--model', 'gpt-5.5']);
372
- assert.deepEqual(highArgs, ['--no-alt-screen', '-c', 'model_reasoning_effort="high"', '--model', 'gpt-5.5']);
436
+ withIsolatedDefaultModelEnv(() => {
437
+ const lowArgs = resolveWorkerLaunchArgsFromEnv({ OMX_TEAM_WORKER_LAUNCH_ARGS: '--no-alt-screen' }, 'executor', undefined, 'low', 'codex');
438
+ const highArgs = resolveWorkerLaunchArgsFromEnv({ OMX_TEAM_WORKER_LAUNCH_ARGS: '--no-alt-screen' }, 'executor', undefined, 'high', 'codex');
439
+ assert.deepEqual(lowArgs, ['--no-alt-screen', '-c', 'model_reasoning_effort="low"', '--model', 'gpt-5.5']);
440
+ assert.deepEqual(highArgs, ['--no-alt-screen', '-c', 'model_reasoning_effort="high"', '--model', 'gpt-5.5']);
441
+ });
373
442
  }
374
443
  finally {
375
444
  console.log = originalLog;
@@ -451,7 +520,10 @@ describe('runtime', () => {
451
520
  const originalLog = console.log;
452
521
  console.log = (...args) => { logs.push(args.join(' ')); };
453
522
  try {
454
- const codexArgs = resolveWorkerLaunchArgsFromEnv({ OMX_TEAM_WORKER_LAUNCH_ARGS: '--no-alt-screen' }, 'executor', undefined, 'high', 'codex');
523
+ let codexArgs = [];
524
+ withIsolatedDefaultModelEnv(() => {
525
+ codexArgs = resolveWorkerLaunchArgsFromEnv({ OMX_TEAM_WORKER_LAUNCH_ARGS: '--no-alt-screen' }, 'executor', undefined, 'high', 'codex');
526
+ });
455
527
  const claudeArgs = resolveWorkerLaunchArgsFromEnv({ OMX_TEAM_WORKER_LAUNCH_ARGS: '--no-alt-screen --model claude-3-7-sonnet' }, 'executor', undefined, 'low', 'claude');
456
528
  const geminiArgs = resolveWorkerLaunchArgsFromEnv({ OMX_TEAM_WORKER_LAUNCH_ARGS: '--model gemini-2.0-pro' }, 'executor', undefined, 'low', 'gemini');
457
529
  assert.deepEqual(codexArgs, ['--no-alt-screen', '-c', 'model_reasoning_effort="high"', '--model', 'gpt-5.5']);
@@ -680,22 +752,23 @@ esac
680
752
  delete process.env.OMX_TEAM_STARTUP_EVIDENCE_TIMEOUT_MS;
681
753
  process.env.OMX_TEAM_STARTUP_DISPATCH_RETRIES = '1';
682
754
  process.env.OMX_TEAM_STARTUP_DISPATCH_RETRY_DELAY_MS = '50';
755
+ const expectedTeamName = buildInternalTeamName('team-startup-window', resolveTeamIdentityScope(process.env));
683
756
  receiptNotifier = setInterval(() => {
684
- void markPendingInboxDispatchesNotified('team-startup-window', cwd, {
757
+ void markPendingInboxDispatchesNotified(expectedTeamName, cwd, {
685
758
  toWorker: 'worker-1',
686
759
  lastReason: 'test_notified_receipt',
687
760
  }).catch(() => { });
688
761
  }, 20);
689
762
  progressWriter = setTimeout(() => {
690
- void writeWorkerStatus('team-startup-window', 'worker-1', {
763
+ void writeWorkerStatus(expectedTeamName, 'worker-1', {
691
764
  state: 'working',
692
765
  current_task_id: '1',
693
766
  updated_at: new Date().toISOString(),
694
767
  }, cwd).catch(() => { });
695
768
  }, 6_000);
696
769
  const runtime = await withoutTeamWorkerEnv(() => startTeam('team-startup-window', 'interactive startup should wait for slow Codex evidence', 'executor', 1, [{ subject: 's', description: 'd', owner: 'worker-1' }], cwd));
697
- assert.equal(runtime.teamName, 'team-startup-window');
698
- assert.ok(await readTeamConfig('team-startup-window', cwd));
770
+ assert.equal(runtime.teamName, expectedTeamName);
771
+ assert.ok(await readTeamConfig(runtime.teamName, cwd));
699
772
  });
700
773
  }
701
774
  finally {
@@ -744,7 +817,7 @@ esac
744
817
  await rm(cwd, { recursive: true, force: true });
745
818
  }
746
819
  });
747
- it('startTeam rejects interactive startup when tmux fallback never produces worker startup evidence', async () => {
820
+ it('startTeam records recoverable issue when tmux fallback never produces worker startup evidence', async () => {
748
821
  const cwd = await mkdtemp(join(tmpdir(), 'omx-runtime-startup-no-evidence-'));
749
822
  const prevTmux = process.env.TMUX;
750
823
  const prevTmuxPane = process.env.TMUX_PANE;
@@ -834,24 +907,28 @@ esac
834
907
  process.env.OMX_TEAM_STARTUP_EVIDENCE_TIMEOUT_MS = '500';
835
908
  process.env.OMX_TEAM_STARTUP_DISPATCH_RETRIES = '1';
836
909
  process.env.OMX_TEAM_STARTUP_DISPATCH_RETRY_DELAY_MS = '50';
910
+ const expectedTeamName = buildInternalTeamName('team-startup-no-evidence', resolveTeamIdentityScope(process.env));
837
911
  receiptFailer = setInterval(() => {
838
912
  void (async () => {
839
- const requests = await listDispatchRequests('team-startup-no-evidence', cwd, { kind: 'inbox' }).catch(() => []);
913
+ const requests = await listDispatchRequests(expectedTeamName, cwd, { kind: 'inbox' }).catch(() => []);
840
914
  for (const request of requests) {
841
915
  if (request.status !== 'pending')
842
916
  continue;
843
- await transitionDispatchRequest('team-startup-no-evidence', request.request_id, 'pending', 'failed', { last_reason: 'test_failed_receipt' }, cwd).catch(() => { });
917
+ await transitionDispatchRequest(expectedTeamName, request.request_id, 'pending', 'failed', { last_reason: 'test_failed_receipt' }, cwd).catch(() => { });
844
918
  }
845
919
  })();
846
920
  }, 20);
847
- await assert.rejects(() => withoutTeamWorkerEnv(() => startTeam('team-startup-no-evidence', 'interactive startup must observe worker evidence', 'executor', 1, [{ subject: 's', description: 'd', owner: 'worker-1' }], cwd)), /worker_notify_failed/);
921
+ const runtime = await withoutTeamWorkerEnv(() => startTeam('team-startup-no-evidence', 'interactive startup records missing worker evidence without aborting live panes', 'executor', 1, [{ subject: 's', description: 'd', owner: 'worker-1' }], cwd));
848
922
  if (receiptFailer) {
849
923
  clearInterval(receiptFailer);
850
924
  receiptFailer = null;
851
925
  }
852
- assert.equal(await readTeamConfig('team-startup-no-evidence', cwd), null);
926
+ assert.ok(await readTeamConfig(runtime.teamName, cwd));
927
+ const workerStatus = await readWorkerStatus(runtime.teamName, 'worker-1', cwd);
928
+ assert.ok(['unknown', 'idle'].includes(workerStatus.state));
853
929
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
854
930
  assert.match(tmuxLog, /send-keys -t %2 -l --/);
931
+ await shutdownTeam(runtime.teamName, cwd, { force: true }).catch(() => { });
855
932
  });
856
933
  }
857
934
  finally {
@@ -956,7 +1033,8 @@ sleep 5
956
1033
  let runtime = null;
957
1034
  try {
958
1035
  runtime = await startTeam('nested-allowed', 'nested task', 'explore', 1, [{ subject: 's', description: 'd', owner: 'worker-1' }], cwd);
959
- assert.equal(runtime.teamName, 'nested-allowed');
1036
+ assert.match(runtime.teamName, /^nested-allowed-[a-f0-9]{8}$/);
1037
+ assert.equal(runtime.config.display_name, 'nested-allowed');
960
1038
  await shutdownTeam(runtime.teamName, cwd, { force: true });
961
1039
  runtime = null;
962
1040
  }
@@ -1008,6 +1086,54 @@ sleep 5
1008
1086
  await rm(cwd, { recursive: true, force: true });
1009
1087
  }
1010
1088
  });
1089
+ it('shutdownTeam with path-like display input cannot remove state outside the team directory', async () => {
1090
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-runtime-shutdown-unsafe-'));
1091
+ try {
1092
+ const victim = join(cwd, '.omx', 'state', 'victim');
1093
+ await mkdir(victim, { recursive: true });
1094
+ await writeFile(join(victim, 'keep.txt'), 'keep');
1095
+ await shutdownTeam('../../victim', cwd, { force: true });
1096
+ assert.equal(existsSync(join(victim, 'keep.txt')), true);
1097
+ }
1098
+ finally {
1099
+ await rm(cwd, { recursive: true, force: true });
1100
+ }
1101
+ });
1102
+ it('startTeam blocks duplicate no-session/no-tmux prompt-mode starts with stable cwd leader identity', async () => {
1103
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-runtime-prompt-duplicate-nosession-'));
1104
+ const binDir = join(cwd, 'bin');
1105
+ const fakeCodexPath = join(binDir, 'codex');
1106
+ await mkdir(binDir, { recursive: true });
1107
+ await writeFakePromptWorkerBinary(fakeCodexPath, `setTimeout(() => {}, 5000);
1108
+ process.on('SIGTERM', () => process.exit(0));`);
1109
+ let runtime = null;
1110
+ try {
1111
+ await withPromptModeCodexEnv(binDir, {
1112
+ OMX_SESSION_ID: undefined,
1113
+ CODEX_SESSION_ID: undefined,
1114
+ SESSION_ID: undefined,
1115
+ TMUX_PANE: undefined,
1116
+ }, async () => {
1117
+ runtime = await withoutTeamWorkerEnv(() => startTeam('first-prompt-team', 'first no-session prompt team', 'executor', 1, [{ subject: 's', description: 'd', owner: 'worker-1' }], cwd));
1118
+ assert.equal(runtime.config.worker_launch_mode, 'prompt');
1119
+ assert.match(runtime.teamName, /^first-prompt-team-[a-f0-9]{8}$/);
1120
+ assert.equal(runtime.config.display_name, 'first-prompt-team');
1121
+ assert.equal(runtime.config.identity_source, 'run-id');
1122
+ const manifest = JSON.parse(await readFile(join(cwd, '.omx', 'state', 'team', runtime.teamName, 'manifest.v2.json'), 'utf-8'));
1123
+ assert.equal(manifest.leader?.session_id, `cwd:${cwd}`);
1124
+ await assert.rejects(() => withoutTeamWorkerEnv(() => startTeam('second-prompt-team', 'second no-session prompt team must be blocked', 'executor', 1, [{ subject: 's2', description: 'd2', owner: 'worker-1' }], cwd)), /leader_session_conflict: active team exists \(first-prompt-team-[a-f0-9]{8}\)/);
1125
+ const teamEntries = await readdir(join(cwd, '.omx', 'state', 'team'), { withFileTypes: true });
1126
+ assert.equal(teamEntries.some((entry) => entry.isDirectory() && entry.name.startsWith('second-prompt-team-')), false, 'blocked duplicate start must not create a second prompt-mode team state directory');
1127
+ });
1128
+ }
1129
+ finally {
1130
+ const runtimeToShutdown = runtime;
1131
+ if (runtimeToShutdown) {
1132
+ await shutdownTeam(runtimeToShutdown.teamName, cwd, { force: true }).catch(() => { });
1133
+ }
1134
+ await rm(cwd, { recursive: true, force: true });
1135
+ }
1136
+ });
1011
1137
  it('startTeam rejects duplicate active same-name team state without mutating existing files', async () => {
1012
1138
  const cwd = await mkdtemp(join(tmpdir(), 'omx-runtime-duplicate-team-'));
1013
1139
  const prevSessionId = process.env.OMX_SESSION_ID;
@@ -1256,8 +1382,9 @@ case "\${1:-}" in
1256
1382
  split-window)
1257
1383
  case "$*" in
1258
1384
  *" -h "*)
1259
- mkdir -p "${cwd}/.omx/state/team/team-interactive-cleanup/workers/worker-1"
1260
- cat > "${cwd}/.omx/state/team/team-interactive-cleanup/workers/worker-1/status.json" <<'EOF'
1385
+ team_dir=$(find "${cwd}/.omx/state/team" -maxdepth 1 -type d -name 'team-interactive*' | head -n 1)
1386
+ mkdir -p "$team_dir/workers/worker-1"
1387
+ cat > "$team_dir/workers/worker-1/status.json" <<'EOF'
1261
1388
  {
1262
1389
  "state": "working",
1263
1390
  "current_task_id": "1",
@@ -1382,8 +1509,9 @@ case "\${1:-}" in
1382
1509
  split-window)
1383
1510
  case "$*" in
1384
1511
  *" -h "*)
1385
- mkdir -p "${cwd}/.omx/state/team/team-pane-pid/workers/worker-1"
1386
- cat > "${cwd}/.omx/state/team/team-pane-pid/workers/worker-1/status.json" <<'EOF'
1512
+ team_dir=$(find "${cwd}/.omx/state/team" -maxdepth 1 -type d -name 'team-pane-pid*' | head -n 1)
1513
+ mkdir -p "$team_dir/workers/worker-1"
1514
+ cat > "$team_dir/workers/worker-1/status.json" <<'EOF'
1387
1515
  {
1388
1516
  "state": "working",
1389
1517
  "current_task_id": "1",
@@ -1464,6 +1592,253 @@ esac
1464
1592
  assert.equal(applyIndex < saveIndex, true);
1465
1593
  assert.equal(saveIndex < readyIndex, true);
1466
1594
  });
1595
+ it('startTeam sends startup direct trigger before slow readiness wait when pane is safe', async () => {
1596
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-runtime-startup-direct-fast-'));
1597
+ const previousTmux = process.env.TMUX;
1598
+ const previousTmuxPane = process.env.TMUX_PANE;
1599
+ const previousLaunchMode = process.env.OMX_TEAM_WORKER_LAUNCH_MODE;
1600
+ const previousWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
1601
+ const previousReadyTimeout = process.env.OMX_TEAM_READY_TIMEOUT_MS;
1602
+ const previousStartupEvidenceTimeout = process.env.OMX_TEAM_STARTUP_EVIDENCE_TIMEOUT_MS;
1603
+ const previousStartupDispatchRetries = process.env.OMX_TEAM_STARTUP_DISPATCH_RETRIES;
1604
+ let runtimeTeamName = null;
1605
+ try {
1606
+ await withMockTmuxFixture({
1607
+ dirPrefix: 'omx-runtime-startup-direct-fast-bin-',
1608
+ tmuxScript: () => `#!/bin/sh
1609
+ set -eu
1610
+ order_file="${cwd}/startup-order.log"
1611
+ case "$1" in
1612
+ -V)
1613
+ echo "tmux 3.4"
1614
+ exit 0
1615
+ ;;
1616
+ display-message)
1617
+ case "$*" in
1618
+ *"#{window_width}"*) echo "120" ;;
1619
+ *) echo "leader:0 %1" ;;
1620
+ esac
1621
+ exit 0
1622
+ ;;
1623
+ list-panes)
1624
+ case "$*" in
1625
+ *"pane_current_command"*) printf "%%1\tnode\t'codex'\n" ;;
1626
+ *"#{pane_dead} #{pane_pid}"*) echo "0 4242" ;;
1627
+ *"#{pane_dead}"*) echo "0" ;;
1628
+ *"#{pane_pid}"*) echo "4242" ;;
1629
+ *) exit 0 ;;
1630
+ esac
1631
+ exit 0
1632
+ ;;
1633
+ capture-pane)
1634
+ printf '%s\n' capture >> "$order_file"
1635
+ printf 'OpenAI Codex\nmodel: test\ndirectory: /tmp/demo\n'
1636
+ exit 0
1637
+ ;;
1638
+ send-keys)
1639
+ printf '%s\n' send-keys >> "$order_file"
1640
+ exit 0
1641
+ ;;
1642
+ split-window)
1643
+ echo "%2"
1644
+ exit 0
1645
+ ;;
1646
+ set-hook|run-shell|select-layout|set-window-option|select-pane|kill-pane|kill-session|resize-pane)
1647
+ exit 0
1648
+ ;;
1649
+ *)
1650
+ exit 0
1651
+ ;;
1652
+ esac
1653
+ `,
1654
+ binaries: [{ name: 'codex', content: '#!/usr/bin/env node\nprocess.stdin.resume();\n' }],
1655
+ }, async () => {
1656
+ delete process.env.TMUX;
1657
+ process.env.TMUX_PANE = '%1';
1658
+ process.env.OMX_TEAM_WORKER_LAUNCH_MODE = 'interactive';
1659
+ process.env.OMX_TEAM_WORKER_CLI = 'codex';
1660
+ process.env.OMX_TEAM_READY_TIMEOUT_MS = '5000';
1661
+ process.env.OMX_TEAM_STARTUP_EVIDENCE_TIMEOUT_MS = '50';
1662
+ process.env.OMX_TEAM_STARTUP_DISPATCH_RETRIES = '1';
1663
+ const runtime = await withoutTeamWorkerEnv(() => startTeam('team-startup-direct-fast', 'startup direct trigger falls back to evidence-gated dispatch', 'executor', 1, [{ subject: 'w1', description: 'worker one', owner: 'worker-1' }], cwd));
1664
+ runtimeTeamName = runtime.teamName;
1665
+ const order = (await readFile(join(cwd, 'startup-order.log'), 'utf-8')).trim().split('\n');
1666
+ assert.ok(order.includes('send-keys'), `expected direct send-keys, got ${order.join(',')}`);
1667
+ const timing = JSON.parse(await readFile(join(cwd, '.omx', 'state', 'team', runtime.teamName, 'startup-timing.json'), 'utf-8'));
1668
+ assert.ok(timing.events.some((event) => event.phase === 'split_returned'));
1669
+ assert.ok(timing.events.some((event) => event.phase === 'identity_inbox_written'));
1670
+ assert.ok(timing.events.some((event) => event.phase === 'direct_fallback' && /startup_direct_trigger_sent/.test(event.reason ?? '')));
1671
+ assert.ok(timing.events.some((event) => event.phase === 'startup_evidence' && event.reason === 'none' && event.ok === false));
1672
+ assert.equal(timing.events.some((event) => event.phase === 'ready_wait_start'), false);
1673
+ const workerStatus = await readWorkerStatus(runtime.teamName, 'worker-1', cwd);
1674
+ assert.equal(workerStatus?.state, 'unknown');
1675
+ assert.match(workerStatus?.reason ?? '', /startup_direct_no_evidence/);
1676
+ });
1677
+ }
1678
+ finally {
1679
+ if (runtimeTeamName)
1680
+ await shutdownTeam(runtimeTeamName, cwd, { force: true }).catch(() => { });
1681
+ if (typeof previousTmux === 'string')
1682
+ process.env.TMUX = previousTmux;
1683
+ else
1684
+ delete process.env.TMUX;
1685
+ if (typeof previousTmuxPane === 'string')
1686
+ process.env.TMUX_PANE = previousTmuxPane;
1687
+ else
1688
+ delete process.env.TMUX_PANE;
1689
+ if (typeof previousLaunchMode === 'string')
1690
+ process.env.OMX_TEAM_WORKER_LAUNCH_MODE = previousLaunchMode;
1691
+ else
1692
+ delete process.env.OMX_TEAM_WORKER_LAUNCH_MODE;
1693
+ if (typeof previousWorkerCli === 'string')
1694
+ process.env.OMX_TEAM_WORKER_CLI = previousWorkerCli;
1695
+ else
1696
+ delete process.env.OMX_TEAM_WORKER_CLI;
1697
+ if (typeof previousReadyTimeout === 'string')
1698
+ process.env.OMX_TEAM_READY_TIMEOUT_MS = previousReadyTimeout;
1699
+ else
1700
+ delete process.env.OMX_TEAM_READY_TIMEOUT_MS;
1701
+ if (typeof previousStartupEvidenceTimeout === 'string')
1702
+ process.env.OMX_TEAM_STARTUP_EVIDENCE_TIMEOUT_MS = previousStartupEvidenceTimeout;
1703
+ else
1704
+ delete process.env.OMX_TEAM_STARTUP_EVIDENCE_TIMEOUT_MS;
1705
+ if (typeof previousStartupDispatchRetries === 'string')
1706
+ process.env.OMX_TEAM_STARTUP_DISPATCH_RETRIES = previousStartupDispatchRetries;
1707
+ else
1708
+ delete process.env.OMX_TEAM_STARTUP_DISPATCH_RETRIES;
1709
+ await rm(cwd, { recursive: true, force: true });
1710
+ }
1711
+ });
1712
+ it('startTeam treats a confirmed ready prompt as startup evidence after hook notification', async () => {
1713
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-runtime-ready-prompt-evidence-'));
1714
+ const previousTmux = process.env.TMUX;
1715
+ const previousTmuxPane = process.env.TMUX_PANE;
1716
+ const previousLaunchMode = process.env.OMX_TEAM_WORKER_LAUNCH_MODE;
1717
+ const previousWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
1718
+ const previousReadyTimeout = process.env.OMX_TEAM_READY_TIMEOUT_MS;
1719
+ const previousStartupEvidenceTimeout = process.env.OMX_TEAM_STARTUP_EVIDENCE_TIMEOUT_MS;
1720
+ const previousStartupDispatchRetries = process.env.OMX_TEAM_STARTUP_DISPATCH_RETRIES;
1721
+ let receiptNotifier = null;
1722
+ let runtimeTeamName = null;
1723
+ try {
1724
+ await withMockTmuxFixture({
1725
+ dirPrefix: 'omx-runtime-ready-prompt-evidence-bin-',
1726
+ tmuxScript: () => `#!/bin/sh
1727
+ set -eu
1728
+ count_file="${cwd}/capture-count"
1729
+ case "$1" in
1730
+ -V)
1731
+ echo "tmux 3.4"
1732
+ exit 0
1733
+ ;;
1734
+ display-message)
1735
+ case "$*" in
1736
+ *"#{window_width}"*) echo "120" ;;
1737
+ *) echo "leader:0 %1" ;;
1738
+ esac
1739
+ exit 0
1740
+ ;;
1741
+ list-panes)
1742
+ case "$*" in
1743
+ *"pane_current_command"*) printf "%%1\tnode\t'codex'\n" ;;
1744
+ *"#{pane_dead} #{pane_pid}"*) echo "0 4242" ;;
1745
+ *"-t %2"*"#{pane_pid}"*) echo "4242" ;;
1746
+ *"#{pane_dead}"*) echo "0" ;;
1747
+ *"#{pane_pid}"*) echo "4242" ;;
1748
+ *) exit 0 ;;
1749
+ esac
1750
+ exit 0
1751
+ ;;
1752
+ capture-pane)
1753
+ count=0
1754
+ if [ -f "$count_file" ]; then count=$(cat "$count_file"); fi
1755
+ count=$((count + 1))
1756
+ printf '%s' "$count" > "$count_file"
1757
+ if [ "$count" -eq 1 ]; then
1758
+ printf 'OpenAI Codex\nmodel: loading\nLoading workspace...\n'
1759
+ else
1760
+ printf 'OpenAI Codex\nmodel: test\n› \n'
1761
+ fi
1762
+ exit 0
1763
+ ;;
1764
+ split-window)
1765
+ echo "%2"
1766
+ exit 0
1767
+ ;;
1768
+ set-hook|run-shell|select-layout|set-window-option|select-pane|send-keys|kill-pane|kill-session|resize-pane)
1769
+ exit 0
1770
+ ;;
1771
+ *)
1772
+ exit 0
1773
+ ;;
1774
+ esac
1775
+ `,
1776
+ binaries: [{ name: 'codex', content: '#!/usr/bin/env node\nprocess.stdin.resume();\n' }],
1777
+ }, async () => {
1778
+ delete process.env.TMUX;
1779
+ process.env.TMUX_PANE = '%1';
1780
+ process.env.OMX_TEAM_WORKER_LAUNCH_MODE = 'interactive';
1781
+ process.env.OMX_TEAM_WORKER_CLI = 'codex';
1782
+ process.env.OMX_TEAM_READY_TIMEOUT_MS = '5000';
1783
+ process.env.OMX_TEAM_STARTUP_EVIDENCE_TIMEOUT_MS = '500';
1784
+ process.env.OMX_TEAM_STARTUP_DISPATCH_RETRIES = '1';
1785
+ receiptNotifier = setInterval(() => {
1786
+ void markPendingInboxDispatchesNotified('team-ready-prompt-evidence', cwd);
1787
+ }, 20);
1788
+ const runtime = await withoutTeamWorkerEnv(() => startTeam('team-ready-prompt-evidence', 'interactive ready prompt should settle startup evidence after notification', 'executor', 1, [{ subject: 'w1', description: 'worker one', owner: 'worker-1' }], cwd));
1789
+ runtimeTeamName = runtime.teamName;
1790
+ const workerStatus = await readWorkerStatus(runtime.teamName, 'worker-1', cwd);
1791
+ assert.equal(workerStatus.state, 'unknown');
1792
+ assert.equal(workerStatus.reason, undefined);
1793
+ const requests = await listDispatchRequests(runtime.teamName, cwd, { kind: 'inbox' });
1794
+ assert.equal(requests.at(-1)?.status, 'notified');
1795
+ const captureCount = Number.parseInt(await readFile(join(cwd, 'capture-count'), 'utf-8'), 10);
1796
+ assert.ok(captureCount >= 2, `expected ready wait capture after bootstrapping, got ${captureCount}`);
1797
+ const timing = JSON.parse(await readFile(join(cwd, '.omx', 'state', 'team', runtime.teamName, 'startup-timing.json'), 'utf-8'));
1798
+ assert.ok(timing.events.some((event) => event.phase === 'ready_wait_start'));
1799
+ assert.ok(timing.events.some((event) => event.phase === 'ready_wait_end' && event.ok === true));
1800
+ });
1801
+ }
1802
+ finally {
1803
+ if (receiptNotifier)
1804
+ clearInterval(receiptNotifier);
1805
+ if (runtimeTeamName)
1806
+ await shutdownTeam(runtimeTeamName, cwd, { force: true }).catch(() => { });
1807
+ if (typeof previousTmux === 'string')
1808
+ process.env.TMUX = previousTmux;
1809
+ else
1810
+ delete process.env.TMUX;
1811
+ if (typeof previousTmuxPane === 'string')
1812
+ process.env.TMUX_PANE = previousTmuxPane;
1813
+ else
1814
+ delete process.env.TMUX_PANE;
1815
+ if (typeof previousLaunchMode === 'string')
1816
+ process.env.OMX_TEAM_WORKER_LAUNCH_MODE = previousLaunchMode;
1817
+ else
1818
+ delete process.env.OMX_TEAM_WORKER_LAUNCH_MODE;
1819
+ if (typeof previousWorkerCli === 'string')
1820
+ process.env.OMX_TEAM_WORKER_CLI = previousWorkerCli;
1821
+ else
1822
+ delete process.env.OMX_TEAM_WORKER_CLI;
1823
+ if (typeof previousReadyTimeout === 'string')
1824
+ process.env.OMX_TEAM_READY_TIMEOUT_MS = previousReadyTimeout;
1825
+ else
1826
+ delete process.env.OMX_TEAM_READY_TIMEOUT_MS;
1827
+ if (typeof previousStartupEvidenceTimeout === 'string') {
1828
+ process.env.OMX_TEAM_STARTUP_EVIDENCE_TIMEOUT_MS = previousStartupEvidenceTimeout;
1829
+ }
1830
+ else {
1831
+ delete process.env.OMX_TEAM_STARTUP_EVIDENCE_TIMEOUT_MS;
1832
+ }
1833
+ if (typeof previousStartupDispatchRetries === 'string') {
1834
+ process.env.OMX_TEAM_STARTUP_DISPATCH_RETRIES = previousStartupDispatchRetries;
1835
+ }
1836
+ else {
1837
+ delete process.env.OMX_TEAM_STARTUP_DISPATCH_RETRIES;
1838
+ }
1839
+ await rm(cwd, { recursive: true, force: true });
1840
+ }
1841
+ });
1467
1842
  it('startTeam starts worker-2 readiness before delayed worker-1 readiness settles', async () => {
1468
1843
  const cwd = await mkdtemp(join(tmpdir(), 'omx-runtime-parallel-ready-'));
1469
1844
  const previousTmux = process.env.TMUX;
@@ -1704,7 +2079,7 @@ process.on('SIGTERM', () => process.exit(0));
1704
2079
  process.env.OMX_TEAM_STARTUP_DISPATCH_RETRY_DELAY_MS = '50';
1705
2080
  receiptFailer = setInterval(() => {
1706
2081
  void (async () => {
1707
- const requests = await listDispatchRequests(teamName, cwd, { kind: 'inbox' }).catch(() => []);
2082
+ const requests = await listDispatchRequests(await resolveRuntimeTeamName(cwd, teamName), cwd, { kind: 'inbox' }).catch(() => []);
1708
2083
  for (const request of requests) {
1709
2084
  if (request.status !== 'pending')
1710
2085
  continue;
@@ -1716,14 +2091,15 @@ process.on('SIGTERM', () => process.exit(0));
1716
2091
  { subject: 'worker-1 task', description: 'd', owner: 'worker-1' },
1717
2092
  { subject: 'worker-2 task', description: 'd', owner: 'worker-2' },
1718
2093
  ], cwd));
1719
- const worker1Status = await readWorkerStatus(teamName, 'worker-1', cwd);
1720
- const worker2Status = await readWorkerStatus(teamName, 'worker-2', cwd);
2094
+ const runtimeTeamName = runtime.teamName;
2095
+ const worker1Status = await readWorkerStatus(runtimeTeamName, 'worker-1', cwd);
2096
+ const worker2Status = await readWorkerStatus(runtimeTeamName, 'worker-2', cwd);
1721
2097
  assert.equal(worker1Status.state, 'unknown');
1722
2098
  assert.equal(worker2Status.state, 'unknown');
1723
2099
  assert.match(worker1Status.reason ?? '', /startup_no_evidence|fallback_attempted_but_unconfirmed/);
1724
2100
  assert.match(worker2Status.reason ?? '', /startup_no_evidence|fallback_attempted_but_unconfirmed/);
1725
- const task1 = await readTask(teamName, '1', cwd);
1726
- const task2 = await readTask(teamName, '2', cwd);
2101
+ const task1 = await readTask(runtimeTeamName, '1', cwd);
2102
+ const task2 = await readTask(runtimeTeamName, '2', cwd);
1727
2103
  assert.equal(task1?.status, 'pending');
1728
2104
  assert.equal(task2?.status, 'pending');
1729
2105
  });
@@ -2149,7 +2525,7 @@ process.on('SIGTERM', () => process.exit(0));
2149
2525
  },
2150
2526
  ],
2151
2527
  }, async () => {
2152
- const sanitizedTeamName = sanitizeTeamName('team-materialize-before-evidence');
2528
+ let runtimeTeamName = sanitizeTeamName('team-materialize-before-evidence');
2153
2529
  delete process.env.TMUX;
2154
2530
  process.env.TMUX_PANE = '%1';
2155
2531
  process.env.OMX_TEAM_WORKER_LAUNCH_MODE = 'interactive';
@@ -2160,11 +2536,13 @@ process.on('SIGTERM', () => process.exit(0));
2160
2536
  process.env.OMX_TEAM_STARTUP_DISPATCH_RETRY_DELAY_MS = '50';
2161
2537
  receiptFailer = setInterval(() => {
2162
2538
  void (async () => {
2163
- const requests = await listDispatchRequests('team-materialize-before-evidence', cwd, { kind: 'inbox' }).catch(() => []);
2539
+ const activeTeamName = await resolveRuntimeTeamName(cwd, 'team-materialize-before-evidence');
2540
+ runtimeTeamName = activeTeamName;
2541
+ const requests = await listDispatchRequests(activeTeamName, cwd, { kind: 'inbox' }).catch(() => []);
2164
2542
  for (const request of requests) {
2165
2543
  if (request.status !== 'pending')
2166
2544
  continue;
2167
- await transitionDispatchRequest('team-materialize-before-evidence', request.request_id, 'pending', 'failed', { last_reason: 'test_failed_receipt' }, cwd).catch(() => { });
2545
+ await transitionDispatchRequest(activeTeamName, request.request_id, 'pending', 'failed', { last_reason: 'test_failed_receipt' }, cwd).catch(() => { });
2168
2546
  }
2169
2547
  })();
2170
2548
  }, 20);
@@ -2173,11 +2551,12 @@ process.on('SIGTERM', () => process.exit(0));
2173
2551
  { subject: 'w2', description: 'worker two', owner: 'worker-2' },
2174
2552
  ], cwd));
2175
2553
  const observedTeamPromise = teamPromise.then((runtime) => ({ ok: true, runtime }), (error) => ({ ok: false, error }));
2176
- const workerOneIdentity = join(cwd, '.omx', 'state', 'team', sanitizedTeamName, 'workers', 'worker-1', 'identity.json');
2177
- const workerTwoIdentity = join(cwd, '.omx', 'state', 'team', sanitizedTeamName, 'workers', 'worker-2', 'identity.json');
2178
- const workerTwoInbox = join(cwd, '.omx', 'state', 'team', sanitizedTeamName, 'workers', 'worker-2', 'inbox.md');
2179
2554
  let materializedAllWorkers = false;
2180
2555
  for (let attempt = 0; attempt < 200; attempt += 1) {
2556
+ runtimeTeamName = await resolveRuntimeTeamName(cwd, 'team-materialize-before-evidence');
2557
+ const workerOneIdentity = join(cwd, '.omx', 'state', 'team', runtimeTeamName, 'workers', 'worker-1', 'identity.json');
2558
+ const workerTwoIdentity = join(cwd, '.omx', 'state', 'team', runtimeTeamName, 'workers', 'worker-2', 'identity.json');
2559
+ const workerTwoInbox = join(cwd, '.omx', 'state', 'team', runtimeTeamName, 'workers', 'worker-2', 'inbox.md');
2181
2560
  if (existsSync(workerOneIdentity)
2182
2561
  && existsSync(workerTwoIdentity)
2183
2562
  && existsSync(workerTwoInbox)) {
@@ -2190,7 +2569,7 @@ process.on('SIGTERM', () => process.exit(0));
2190
2569
  const outcome = await observedTeamPromise;
2191
2570
  assert.equal(outcome.ok, false);
2192
2571
  assert.match(String(outcome.error), /worker_notify_failed:worker-1/);
2193
- assert.equal(existsSync(join(cwd, '.omx', 'state', 'team', sanitizedTeamName)), false);
2572
+ assert.equal(existsSync(join(cwd, '.omx', 'state', 'team', runtimeTeamName)), false);
2194
2573
  });
2195
2574
  }
2196
2575
  finally {
@@ -2361,7 +2740,7 @@ sleep 5
2361
2740
  assert.equal((runtime.config.workers[0]?.pid ?? 0) > 0, true);
2362
2741
  const expectedArgv = [
2363
2742
  '-i',
2364
- 'Read .omx/state/team/team-gemini-prompt/workers/worker-1/inbox.md, start work now, report concrete progress, then continue assigned work or next feasible task.',
2743
+ `Read .omx/state/team/${runtime.teamName}/workers/worker-1/inbox.md, start work now, report concrete progress, then continue assigned work or next feasible task.`,
2365
2744
  ];
2366
2745
  let argv = null;
2367
2746
  for (let attempt = 0; attempt < 50; attempt += 1) {
@@ -2491,10 +2870,10 @@ process.on('SIGTERM', () => process.exit(0));
2491
2870
  delete process.env.OMX_DEFAULT_STANDARD_MODEL;
2492
2871
  let runtime = null;
2493
2872
  try {
2494
- runtime = await withMockPromptModeCodexAllowed(() => withoutTeamWorkerEnv(() => startTeam('team-role-routing', 'heuristic routing handoff', 'executor', 2, [
2873
+ runtime = await withIsolatedDefaultModelEnvAsync(async () => await withMockPromptModeCodexAllowed(() => withoutTeamWorkerEnv(() => startTeam('team-role-routing', 'heuristic routing handoff', 'executor', 2, [
2495
2874
  { subject: 'test routing report only', description: 'test routing report only', owner: 'worker-1', role: 'test-engineer' },
2496
2875
  { subject: 'document routing report only', description: 'document routing report only', owner: 'worker-2', role: 'writer' },
2497
- ], cwd)));
2876
+ ], cwd))));
2498
2877
  assert.equal(runtime.config.worker_launch_mode, 'prompt');
2499
2878
  assert.equal(runtime.config.workers[0]?.role, 'test-engineer');
2500
2879
  assert.equal(runtime.config.workers[1]?.role, 'writer');
@@ -2872,20 +3251,20 @@ process.on('SIGTERM', () => process.exit(0));
2872
3251
  assert.notEqual(workerPath, repo);
2873
3252
  const workerAgents = await readFile(join(workerPath, 'AGENTS.md'), 'utf-8');
2874
3253
  assert.match(workerAgents, /Team Worker Runtime Instructions/);
2875
- assert.match(workerAgents, /team-detached-worktree-paths/);
3254
+ assert.match(workerAgents, new RegExp(runtime.teamName));
2876
3255
  const startupLog = await waitForFileText(stdinLogPath, (content) => content.includes('/workers/worker-1/inbox.md'));
2877
- assert.match(startupLog, /\$OMX_TEAM_STATE_ROOT\/team\/team-detached-worktree-paths\/workers\/worker-1\/inbox\.md/);
2878
- assert.doesNotMatch(startupLog, /Read \.omx\/state\/team\/team-detached-worktree-paths\/workers\/worker-1\/inbox\.md/);
3256
+ assert.match(startupLog, new RegExp(`\\$OMX_TEAM_STATE_ROOT/team/${runtime.teamName}/workers/worker-1/inbox\\.md`));
3257
+ assert.doesNotMatch(startupLog, new RegExp(`Read \\.omx/state/team/${runtime.teamName}/workers/worker-1/inbox\\.md`));
2879
3258
  const envLog = JSON.parse(await waitForFileText(envLogPath, (content) => content.includes('teamStateRoot')));
2880
3259
  assert.equal(envLog.cwd, workerPath);
2881
3260
  assert.equal(envLog.teamStateRoot, join(repo, '.omx', 'state'));
2882
3261
  assert.equal(envLog.worker, 'team-detached-worktree-paths/worker-1');
2883
3262
  const rootAgents = await readFile(join(workerPath, 'AGENTS.md'), 'utf-8');
2884
3263
  assert.match(rootAgents, /Team Worker Runtime Instructions/);
2885
- assert.match(rootAgents, /Inbox path: .*team-detached-worktree-paths\/workers\/worker-1\/inbox\.md/);
3264
+ assert.match(rootAgents, new RegExp(`Inbox path: .*${runtime.teamName}/workers/worker-1/inbox\\.md`));
2886
3265
  await sendWorkerMessage(runtime.teamName, 'leader-fixed', 'worker-1', 'follow-up', repo);
2887
3266
  const mailboxLog = await waitForFileText(stdinLogPath, (content) => content.includes('/mailbox/worker-1.json'));
2888
- assert.match(mailboxLog, /\$OMX_TEAM_STATE_ROOT\/team\/team-detached-worktree-paths\/mailbox\/worker-1\.json/);
3267
+ assert.match(mailboxLog, new RegExp(`\\$OMX_TEAM_STATE_ROOT/team/${runtime.teamName}/mailbox/worker-1\\.json`));
2889
3268
  await shutdownTeam(runtime.teamName, repo, { force: true });
2890
3269
  runtime = null;
2891
3270
  }
@@ -4861,7 +5240,7 @@ esac
4861
5240
  let runtime = null;
4862
5241
  try {
4863
5242
  runtime = await withPromptModeCodexEnv(binDir, {}, () => withoutTeamWorkerEnv(() => startTeam('team-delegation-persist', 'delegation persistence test', 'executor', 1, [{ subject: 'Investigate runtime assignment', description: 'Search runtime and debug assignTask behavior' }], cwd)));
4864
- const task = await readTask('team-delegation-persist', '1', cwd);
5243
+ const task = await readTask(runtime.teamName, '1', cwd);
4865
5244
  assert.equal(task?.delegation?.mode, 'auto');
4866
5245
  assert.equal(task?.delegation?.child_model, 'gpt-5.4-mini');
4867
5246
  assert.equal(task?.delegation?.required_parallel_probe, true);
@@ -4921,17 +5300,17 @@ esac
4921
5300
  },
4922
5301
  },
4923
5302
  })));
4924
- const first = await readTask('team-dag-remap', '1', cwd);
4925
- const second = await readTask('team-dag-remap', '2', cwd);
5303
+ const first = await readTask(runtime.teamName, '1', cwd);
5304
+ const second = await readTask(runtime.teamName, '2', cwd);
4926
5305
  assert.deepEqual(first?.depends_on, []);
4927
5306
  assert.deepEqual(first?.blocked_by, undefined);
4928
5307
  assert.deepEqual(second?.depends_on, ['1']);
4929
5308
  assert.deepEqual(second?.blocked_by, ['1']);
4930
- const report = JSON.parse(await readFile(join(cwd, '.omx', 'state', 'team', 'team-dag-remap', 'decomposition-report.json'), 'utf-8'));
5309
+ const report = JSON.parse(await readFile(join(cwd, '.omx', 'state', 'team', runtime.teamName, 'decomposition-report.json'), 'utf-8'));
4931
5310
  assert.deepEqual(report.node_id_to_task_id, { impl: '1', verify: '2' });
4932
5311
  assert.deepEqual(report.task_hints?.['2']?.depends_on, ['1']);
4933
5312
  assert.deepEqual(report.task_hints?.['2']?.symbolic_depends_on, ['impl']);
4934
- const inbox = await readFile(join(cwd, '.omx', 'state', 'team', 'team-dag-remap', 'workers', 'worker-2', 'inbox.md'), 'utf-8');
5313
+ const inbox = await readFile(join(cwd, '.omx', 'state', 'team', runtime.teamName, 'workers', 'worker-2', 'inbox.md'), 'utf-8');
4935
5314
  assert.match(inbox, /Blocked by: 1/);
4936
5315
  assert.doesNotMatch(inbox, /Blocked by: impl/);
4937
5316
  assert.doesNotMatch(inbox, /Depends on: impl/);