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.
- package/Cargo.lock +5 -5
- package/Cargo.toml +1 -1
- package/dist/agents/__tests__/native-config.test.js +33 -0
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/catalog/__tests__/plugin-bundle-ssot.test.js +9 -1
- package/dist/catalog/__tests__/plugin-bundle-ssot.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-context-window-warning.test.d.ts +2 -0
- package/dist/cli/__tests__/doctor-context-window-warning.test.d.ts.map +1 -0
- package/dist/cli/__tests__/doctor-context-window-warning.test.js +122 -0
- package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-warning-copy.test.js +2 -2
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/exec.test.js +1 -0
- package/dist/cli/__tests__/exec.test.js.map +1 -1
- package/dist/cli/__tests__/explore.test.js +40 -17
- package/dist/cli/__tests__/explore.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +141 -8
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/mcp-serve.test.js +27 -1
- package/dist/cli/__tests__/mcp-serve.test.js.map +1 -1
- package/dist/cli/__tests__/ralph.test.js +59 -1
- package/dist/cli/__tests__/ralph.test.js.map +1 -1
- package/dist/cli/__tests__/setup-scope.test.js +2 -1
- package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +55 -10
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +46 -3
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +16 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +126 -15
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-serve.d.ts +1 -0
- package/dist/cli/mcp-serve.d.ts.map +1 -1
- package/dist/cli/mcp-serve.js +8 -0
- package/dist/cli/mcp-serve.js.map +1 -1
- package/dist/cli/ralph.d.ts +2 -0
- package/dist/cli/ralph.d.ts.map +1 -1
- package/dist/cli/ralph.js +17 -1
- package/dist/cli/ralph.js.map +1 -1
- package/dist/cli/team.d.ts +4 -0
- package/dist/cli/team.d.ts.map +1 -1
- package/dist/cli/team.js +47 -22
- package/dist/cli/team.js.map +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +27 -5
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/generator.d.ts +11 -2
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +114 -58
- package/dist/config/generator.js.map +1 -1
- package/dist/hooks/__tests__/agents-overlay.test.js +59 -0
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/anti-slop-workflow.test.js +109 -18
- package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
- package/dist/hooks/agents-overlay.d.ts.map +1 -1
- package/dist/hooks/agents-overlay.js +21 -0
- package/dist/hooks/agents-overlay.js.map +1 -1
- package/dist/hud/__tests__/index.test.js +30 -14
- package/dist/hud/__tests__/index.test.js.map +1 -1
- package/dist/openclaw/__tests__/dispatcher.test.js +1 -1
- package/dist/openclaw/__tests__/dispatcher.test.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +398 -14
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/stages/team-exec.d.ts +8 -4
- package/dist/pipeline/stages/team-exec.d.ts.map +1 -1
- package/dist/pipeline/stages/team-exec.js +198 -13
- package/dist/pipeline/stages/team-exec.js.map +1 -1
- package/dist/planning/__tests__/artifacts.test.js +246 -1
- package/dist/planning/__tests__/artifacts.test.js.map +1 -1
- package/dist/planning/artifact-names.d.ts +13 -0
- package/dist/planning/artifact-names.d.ts.map +1 -0
- package/dist/planning/artifact-names.js +108 -0
- package/dist/planning/artifact-names.js.map +1 -0
- package/dist/planning/artifacts.d.ts +22 -1
- package/dist/planning/artifacts.d.ts.map +1 -1
- package/dist/planning/artifacts.js +165 -50
- package/dist/planning/artifacts.js.map +1 -1
- package/dist/ralph/__tests__/persistence.test.js +21 -1
- package/dist/ralph/__tests__/persistence.test.js.map +1 -1
- package/dist/ralph/persistence.d.ts.map +1 -1
- package/dist/ralph/persistence.js +6 -4
- package/dist/ralph/persistence.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +352 -2
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +85 -6
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
- package/dist/scripts/codex-native-pre-post.js +123 -0
- package/dist/scripts/codex-native-pre-post.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker-posttooluse.js +1 -1
- package/dist/scripts/notify-hook/team-worker-posttooluse.js.map +1 -1
- package/dist/scripts/notify-hook.js +1 -1
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/scripts/sync-plugin-mirror.d.ts +1 -0
- package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
- package/dist/scripts/sync-plugin-mirror.js +8 -2
- package/dist/scripts/sync-plugin-mirror.js.map +1 -1
- package/dist/state/__tests__/skill-active.test.js +41 -0
- package/dist/state/__tests__/skill-active.test.js.map +1 -1
- package/dist/team/__tests__/api-interop.test.js +220 -0
- package/dist/team/__tests__/api-interop.test.js.map +1 -1
- package/dist/team/__tests__/model-contract.test.js +40 -9
- package/dist/team/__tests__/model-contract.test.js.map +1 -1
- package/dist/team/__tests__/repo-aware-decomposition.test.js +41 -0
- package/dist/team/__tests__/repo-aware-decomposition.test.js.map +1 -1
- package/dist/team/__tests__/runtime-cli.test.js +24 -0
- package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +446 -67
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/state.test.js +13 -0
- package/dist/team/__tests__/state.test.js.map +1 -1
- package/dist/team/__tests__/team-identity.test.d.ts +2 -0
- package/dist/team/__tests__/team-identity.test.d.ts.map +1 -0
- package/dist/team/__tests__/team-identity.test.js +166 -0
- package/dist/team/__tests__/team-identity.test.js.map +1 -0
- package/dist/team/__tests__/tmux-session.test.js +55 -1
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/worker-bootstrap.test.js +12 -0
- package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
- package/dist/team/api-interop.d.ts +1 -0
- package/dist/team/api-interop.d.ts.map +1 -1
- package/dist/team/api-interop.js +159 -129
- package/dist/team/api-interop.js.map +1 -1
- package/dist/team/delivery-log.d.ts +1 -1
- package/dist/team/delivery-log.d.ts.map +1 -1
- package/dist/team/delivery-log.js.map +1 -1
- package/dist/team/repo-aware-decomposition.d.ts +3 -0
- package/dist/team/repo-aware-decomposition.d.ts.map +1 -1
- package/dist/team/repo-aware-decomposition.js +2 -0
- package/dist/team/repo-aware-decomposition.js.map +1 -1
- package/dist/team/runtime-cli.d.ts +32 -2
- package/dist/team/runtime-cli.d.ts.map +1 -1
- package/dist/team/runtime-cli.js +78 -26
- package/dist/team/runtime-cli.js.map +1 -1
- package/dist/team/runtime.d.ts +1 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +338 -35
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/state.d.ts +9 -0
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +21 -0
- package/dist/team/state.js.map +1 -1
- package/dist/team/team-identity.d.ts +26 -0
- package/dist/team/team-identity.d.ts.map +1 -0
- package/dist/team/team-identity.js +169 -0
- package/dist/team/team-identity.js.map +1 -0
- package/dist/team/tmux-session.d.ts +18 -0
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +61 -1
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worker-bootstrap.d.ts +2 -0
- package/dist/team/worker-bootstrap.d.ts.map +1 -1
- package/dist/team/worker-bootstrap.js +10 -1
- package/dist/team/worker-bootstrap.js.map +1 -1
- package/package.json +1 -1
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/skills/ai-slop-cleaner/SKILL.md +30 -5
- package/skills/ai-slop-cleaner/SKILL.md +30 -5
- package/src/scripts/__tests__/codex-native-hook.test.ts +398 -2
- package/src/scripts/codex-native-hook.ts +115 -5
- package/src/scripts/codex-native-pre-post.ts +121 -0
- package/src/scripts/notify-hook/team-worker-posttooluse.ts +1 -1
- package/src/scripts/notify-hook.ts +1 -1
- 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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
339
|
-
|
|
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
|
-
|
|
343
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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,
|
|
698
|
-
assert.ok(await readTeamConfig(
|
|
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
|
|
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(
|
|
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(
|
|
917
|
+
await transitionDispatchRequest(expectedTeamName, request.request_id, 'pending', 'failed', { last_reason: 'test_failed_receipt' }, cwd).catch(() => { });
|
|
844
918
|
}
|
|
845
919
|
})();
|
|
846
920
|
}, 20);
|
|
847
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1260
|
-
|
|
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
|
-
|
|
1386
|
-
|
|
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
|
|
1720
|
-
const
|
|
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(
|
|
1726
|
-
const task2 = await readTask(
|
|
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
|
-
|
|
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
|
|
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(
|
|
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',
|
|
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
|
-
|
|
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,
|
|
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,
|
|
2878
|
-
assert.doesNotMatch(startupLog,
|
|
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,
|
|
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,
|
|
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(
|
|
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(
|
|
4925
|
-
const second = await readTask(
|
|
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',
|
|
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',
|
|
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/);
|