oh-my-codex 0.10.2 → 0.10.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/README.de.md +4 -4
- package/README.es.md +4 -4
- package/README.fr.md +4 -4
- package/README.it.md +4 -4
- package/README.ja.md +4 -4
- package/README.ko.md +4 -4
- package/README.md +13 -7
- package/README.pt.md +4 -4
- package/README.ru.md +4 -4
- package/README.tr.md +4 -4
- package/README.vi.md +4 -4
- package/README.zh-TW.md +4 -4
- package/README.zh.md +4 -4
- package/dist/agents/__tests__/native-config.test.js +37 -33
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/agents/__tests__/skill-bridge.test.d.ts +2 -0
- package/dist/agents/__tests__/skill-bridge.test.d.ts.map +1 -0
- package/dist/agents/__tests__/skill-bridge.test.js +71 -0
- package/dist/agents/__tests__/skill-bridge.test.js.map +1 -0
- package/dist/agents/native-config.d.ts +18 -6
- package/dist/agents/native-config.d.ts.map +1 -1
- package/dist/agents/native-config.js +109 -92
- package/dist/agents/native-config.js.map +1 -1
- package/dist/agents/skill-bridge.d.ts +20 -0
- package/dist/agents/skill-bridge.d.ts.map +1 -0
- package/dist/agents/skill-bridge.js +150 -0
- package/dist/agents/skill-bridge.js.map +1 -0
- package/dist/autoresearch/__tests__/contracts.test.js +37 -1
- package/dist/autoresearch/__tests__/contracts.test.js.map +1 -1
- package/dist/autoresearch/__tests__/runtime-parity-extra.test.js +10 -10
- package/dist/autoresearch/__tests__/runtime-parity-extra.test.js.map +1 -1
- package/dist/autoresearch/__tests__/runtime.test.js +2 -2
- package/dist/autoresearch/__tests__/runtime.test.js.map +1 -1
- package/dist/autoresearch/contracts.d.ts.map +1 -1
- package/dist/autoresearch/contracts.js +17 -10
- package/dist/autoresearch/contracts.js.map +1 -1
- package/dist/autoresearch/runtime.d.ts.map +1 -1
- package/dist/autoresearch/runtime.js +71 -96
- package/dist/autoresearch/runtime.js.map +1 -1
- package/dist/cli/__tests__/agents-init.test.js +2 -0
- package/dist/cli/__tests__/agents-init.test.js.map +1 -1
- package/dist/cli/__tests__/agents.test.d.ts +2 -0
- package/dist/cli/__tests__/agents.test.d.ts.map +1 -0
- package/dist/cli/__tests__/agents.test.js +114 -0
- package/dist/cli/__tests__/agents.test.js.map +1 -0
- package/dist/cli/__tests__/autoresearch-guided.test.js +156 -1
- package/dist/cli/__tests__/autoresearch-guided.test.js.map +1 -1
- package/dist/cli/__tests__/autoresearch.test.js +195 -24
- package/dist/cli/__tests__/autoresearch.test.js.map +1 -1
- package/dist/cli/__tests__/cleanup.test.d.ts +2 -0
- package/dist/cli/__tests__/cleanup.test.d.ts.map +1 -0
- package/dist/cli/__tests__/cleanup.test.js +213 -0
- package/dist/cli/__tests__/cleanup.test.js.map +1 -0
- package/dist/cli/__tests__/error-handling-warnings.test.js +1 -1
- package/dist/cli/__tests__/error-handling-warnings.test.js.map +1 -1
- package/dist/cli/__tests__/explore.test.js +3 -3
- package/dist/cli/__tests__/explore.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +521 -401
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/native-assets.test.js +72 -9
- package/dist/cli/__tests__/native-assets.test.js.map +1 -1
- package/dist/cli/__tests__/ralphthon.test.d.ts +2 -0
- package/dist/cli/__tests__/ralphthon.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ralphthon.test.js +28 -0
- package/dist/cli/__tests__/ralphthon.test.js.map +1 -0
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +36 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-prompts-overwrite.test.js +35 -5
- package/dist/cli/__tests__/setup-prompts-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-refresh.test.js +2 -2
- package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
- package/dist/cli/__tests__/setup-scope.test.js +131 -161
- package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
- package/dist/cli/__tests__/setup-skills-overwrite.test.js +10 -10
- package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js +28 -2
- package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +1 -112
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +7 -20
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/agents-init.d.ts.map +1 -1
- package/dist/cli/agents-init.js +99 -95
- package/dist/cli/agents-init.js.map +1 -1
- package/dist/cli/agents.d.ts +14 -0
- package/dist/cli/agents.d.ts.map +1 -0
- package/dist/cli/agents.js +261 -0
- package/dist/cli/agents.js.map +1 -0
- package/dist/cli/autoresearch-guided.d.ts +8 -0
- package/dist/cli/autoresearch-guided.d.ts.map +1 -1
- package/dist/cli/autoresearch-guided.js +104 -37
- package/dist/cli/autoresearch-guided.js.map +1 -1
- package/dist/cli/autoresearch-intake.d.ts +60 -0
- package/dist/cli/autoresearch-intake.d.ts.map +1 -0
- package/dist/cli/autoresearch-intake.js +318 -0
- package/dist/cli/autoresearch-intake.js.map +1 -0
- package/dist/cli/autoresearch.d.ts +3 -1
- package/dist/cli/autoresearch.d.ts.map +1 -1
- package/dist/cli/autoresearch.js +64 -10
- package/dist/cli/autoresearch.js.map +1 -1
- package/dist/cli/cleanup.d.ts +52 -0
- package/dist/cli/cleanup.d.ts.map +1 -0
- package/dist/cli/cleanup.js +302 -0
- package/dist/cli/cleanup.js.map +1 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +9 -37
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/explore.d.ts.map +1 -1
- package/dist/cli/explore.js +5 -4
- package/dist/cli/explore.js.map +1 -1
- package/dist/cli/index.d.ts +5 -7
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +610 -427
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/native-assets.d.ts +15 -1
- package/dist/cli/native-assets.d.ts.map +1 -1
- package/dist/cli/native-assets.js +134 -32
- package/dist/cli/native-assets.js.map +1 -1
- package/dist/cli/ralph.d.ts.map +1 -1
- package/dist/cli/ralph.js +38 -1
- package/dist/cli/ralph.js.map +1 -1
- package/dist/cli/ralphthon.d.ts +14 -0
- package/dist/cli/ralphthon.d.ts.map +1 -0
- package/dist/cli/ralphthon.js +234 -0
- package/dist/cli/ralphthon.js.map +1 -0
- package/dist/cli/setup.d.ts +1 -4
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +111 -76
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/sparkshell.d.ts +3 -1
- package/dist/cli/sparkshell.d.ts.map +1 -1
- package/dist/cli/sparkshell.js +35 -16
- package/dist/cli/sparkshell.js.map +1 -1
- package/dist/cli/team.d.ts.map +1 -1
- package/dist/cli/team.js +1 -0
- package/dist/cli/team.js.map +1 -1
- package/dist/cli/uninstall.d.ts +1 -1
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +82 -64
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +10 -10
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +15 -0
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/generator.d.ts +0 -1
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +53 -42
- package/dist/config/generator.js.map +1 -1
- package/dist/hooks/__tests__/agents-overlay.test.js +295 -230
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/deep-interview-contract.test.js +49 -24
- package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher-ralphthon.test.d.ts +2 -0
- package/dist/hooks/__tests__/notify-fallback-watcher-ralphthon.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/notify-fallback-watcher-ralphthon.test.js +193 -0
- package/dist/hooks/__tests__/notify-fallback-watcher-ralphthon.test.js.map +1 -0
- package/dist/hooks/agents-overlay.d.ts +1 -1
- package/dist/hooks/agents-overlay.d.ts.map +1 -1
- package/dist/hooks/agents-overlay.js +109 -106
- package/dist/hooks/agents-overlay.js.map +1 -1
- package/dist/modes/base.d.ts +1 -1
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +1 -1
- package/dist/modes/base.js.map +1 -1
- package/dist/ralphthon/__tests__/bootstrap.test.d.ts +2 -0
- package/dist/ralphthon/__tests__/bootstrap.test.d.ts.map +1 -0
- package/dist/ralphthon/__tests__/bootstrap.test.js +23 -0
- package/dist/ralphthon/__tests__/bootstrap.test.js.map +1 -0
- package/dist/ralphthon/__tests__/orchestrator.test.d.ts +2 -0
- package/dist/ralphthon/__tests__/orchestrator.test.d.ts.map +1 -0
- package/dist/ralphthon/__tests__/orchestrator.test.js +309 -0
- package/dist/ralphthon/__tests__/orchestrator.test.js.map +1 -0
- package/dist/ralphthon/__tests__/prd.test.d.ts +2 -0
- package/dist/ralphthon/__tests__/prd.test.d.ts.map +1 -0
- package/dist/ralphthon/__tests__/prd.test.js +133 -0
- package/dist/ralphthon/__tests__/prd.test.js.map +1 -0
- package/dist/ralphthon/bootstrap.d.ts +3 -0
- package/dist/ralphthon/bootstrap.d.ts.map +1 -0
- package/dist/ralphthon/bootstrap.js +84 -0
- package/dist/ralphthon/bootstrap.js.map +1 -0
- package/dist/ralphthon/orchestrator.d.ts +50 -0
- package/dist/ralphthon/orchestrator.d.ts.map +1 -0
- package/dist/ralphthon/orchestrator.js +362 -0
- package/dist/ralphthon/orchestrator.js.map +1 -0
- package/dist/ralphthon/prd.d.ts +191 -0
- package/dist/ralphthon/prd.d.ts.map +1 -0
- package/dist/ralphthon/prd.js +355 -0
- package/dist/ralphthon/prd.js.map +1 -0
- package/dist/ralphthon/runtime.d.ts +31 -0
- package/dist/ralphthon/runtime.d.ts.map +1 -0
- package/dist/ralphthon/runtime.js +104 -0
- package/dist/ralphthon/runtime.js.map +1 -0
- package/dist/ralphthon/tmux.d.ts +3 -0
- package/dist/ralphthon/tmux.d.ts.map +1 -0
- package/dist/ralphthon/tmux.js +39 -0
- package/dist/ralphthon/tmux.js.map +1 -0
- package/dist/subagents/__tests__/tracker.test.d.ts +2 -0
- package/dist/subagents/__tests__/tracker.test.d.ts.map +1 -0
- package/dist/subagents/__tests__/tracker.test.js +47 -0
- package/dist/subagents/__tests__/tracker.test.js.map +1 -0
- package/dist/subagents/tracker.d.ts +52 -0
- package/dist/subagents/tracker.d.ts.map +1 -0
- package/dist/subagents/tracker.js +175 -0
- package/dist/subagents/tracker.js.map +1 -0
- package/dist/team/__tests__/worker-bootstrap.test.js +189 -163
- package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
- package/dist/team/__tests__/worktree.test.js +1 -1
- package/dist/team/__tests__/worktree.test.js.map +1 -1
- package/dist/team/worker-bootstrap.d.ts +1 -1
- package/dist/team/worker-bootstrap.d.ts.map +1 -1
- package/dist/team/worker-bootstrap.js +58 -63
- package/dist/team/worker-bootstrap.js.map +1 -1
- package/dist/team/worktree.js +1 -1
- package/dist/team/worktree.js.map +1 -1
- package/dist/utils/__tests__/agents-md.test.d.ts +2 -0
- package/dist/utils/__tests__/agents-md.test.d.ts.map +1 -0
- package/dist/utils/__tests__/agents-md.test.js +32 -0
- package/dist/utils/__tests__/agents-md.test.js.map +1 -0
- package/dist/utils/__tests__/agents-model-table.test.d.ts +2 -0
- package/dist/utils/__tests__/agents-model-table.test.d.ts.map +1 -0
- package/dist/utils/__tests__/agents-model-table.test.js +84 -0
- package/dist/utils/__tests__/agents-model-table.test.js.map +1 -0
- package/dist/utils/__tests__/paths.test.js +78 -83
- package/dist/utils/__tests__/paths.test.js.map +1 -1
- package/dist/utils/agents-md.d.ts.map +1 -1
- package/dist/utils/agents-md.js +10 -0
- package/dist/utils/agents-md.js.map +1 -1
- package/dist/utils/agents-model-table.d.ts +16 -0
- package/dist/utils/agents-model-table.d.ts.map +1 -0
- package/dist/utils/agents-model-table.js +83 -0
- package/dist/utils/agents-model-table.js.map +1 -0
- package/dist/utils/paths.d.ts +6 -6
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +31 -31
- package/dist/utils/paths.js.map +1 -1
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js +21 -3
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
- package/dist/verification/__tests__/native-release-manifest.test.d.ts +2 -0
- package/dist/verification/__tests__/native-release-manifest.test.d.ts.map +1 -0
- package/dist/verification/__tests__/native-release-manifest.test.js +80 -0
- package/dist/verification/__tests__/native-release-manifest.test.js.map +1 -0
- package/package.json +1 -1
- package/prompts/executor.md +15 -0
- package/scripts/__tests__/smoke-packed-install.test.mjs +137 -8
- package/scripts/eval-adaptive-sort-optimization.py +24 -0
- package/scripts/eval-in-action-cat-shellout-demo.js +31 -0
- package/scripts/eval-ml-kaggle-model-optimization.py +29 -0
- package/scripts/eval-noisy-bayesopt-highdim.py +44 -0
- package/scripts/eval-noisy-latent-subspace-discovery.py +44 -0
- package/scripts/generate-native-release-manifest.mjs +14 -3
- package/scripts/notify-fallback-watcher.js +308 -6
- package/scripts/notify-hook.js +20 -0
- package/scripts/run-autoresearch-showcase.sh +75 -0
- package/scripts/smoke-packed-install.mjs +142 -10
- package/skills/deep-interview/SKILL.md +30 -1
- package/skills/omx-setup/SKILL.md +2 -2
- package/skills/skill/SKILL.md +32 -32
- package/skills/team/SKILL.md +6 -0
- package/skills/worker/SKILL.md +2 -2
- package/templates/AGENTS.md +97 -16
|
@@ -1,640 +1,760 @@
|
|
|
1
|
-
import { describe, it } from
|
|
2
|
-
import assert from
|
|
3
|
-
import { mkdir, mkdtemp, rm, writeFile } from
|
|
4
|
-
import { join } from
|
|
5
|
-
import { tmpdir } from
|
|
6
|
-
import { normalizeCodexLaunchArgs, buildTmuxShellCommand, buildTmuxPaneCommand, buildWindowsPromptCommand, buildTmuxSessionName, resolveCliInvocation, commandOwnsLocalHelp, resolveCodexLaunchPolicy, classifyCodexExecFailure, resolveSignalExitCode, parseTmuxPaneSnapshot, findHudWatchPaneIds, buildHudPaneCleanupTargets, readTopLevelTomlString, upsertTopLevelTomlString, collectInheritableTeamWorkerArgs, resolveTeamWorkerLaunchArgsEnv, injectModelInstructionsBypassArgs, resolveWorkerSparkModel, resolveSetupScopeArg,
|
|
7
|
-
import { HUD_TMUX_HEIGHT_LINES } from
|
|
8
|
-
import { DEFAULT_FRONTIER_MODEL, getTeamLowComplexityModel } from
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { normalizeCodexLaunchArgs, buildTmuxShellCommand, buildTmuxPaneCommand, buildWindowsPromptCommand, buildTmuxSessionName, resolveCliInvocation, commandOwnsLocalHelp, resolveCodexLaunchPolicy, classifyCodexExecFailure, resolveSignalExitCode, parseTmuxPaneSnapshot, findHudWatchPaneIds, buildHudPaneCleanupTargets, readTopLevelTomlString, upsertTopLevelTomlString, collectInheritableTeamWorkerArgs, resolveTeamWorkerLaunchArgsEnv, injectModelInstructionsBypassArgs, resolveWorkerSparkModel, resolveSetupScopeArg, readPersistedSetupPreferences, readPersistedSetupScope, resolveCodexHomeForLaunch, buildDetachedSessionBootstrapSteps, buildDetachedSessionFinalizeSteps, buildDetachedSessionRollbackSteps, resolveNotifyTempContract, buildNotifyTempStartupMessages, } from "../index.js";
|
|
7
|
+
import { HUD_TMUX_HEIGHT_LINES } from "../../hud/constants.js";
|
|
8
|
+
import { DEFAULT_FRONTIER_MODEL, getTeamLowComplexityModel, } from "../../config/models.js";
|
|
9
9
|
function expectedLowComplexityModel(codexHomeOverride) {
|
|
10
10
|
return getTeamLowComplexityModel(codexHomeOverride);
|
|
11
11
|
}
|
|
12
|
-
describe(
|
|
13
|
-
it(
|
|
14
|
-
assert.deepEqual(normalizeCodexLaunchArgs([
|
|
12
|
+
describe("normalizeCodexLaunchArgs", () => {
|
|
13
|
+
it("maps --madmax to codex bypass flag", () => {
|
|
14
|
+
assert.deepEqual(normalizeCodexLaunchArgs(["--madmax"]), [
|
|
15
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
16
|
+
]);
|
|
15
17
|
});
|
|
16
|
-
it(
|
|
17
|
-
assert.deepEqual(normalizeCodexLaunchArgs([
|
|
18
|
+
it("does not forward --madmax and preserves other args", () => {
|
|
19
|
+
assert.deepEqual(normalizeCodexLaunchArgs(["--model", "gpt-5", "--madmax", "--yolo"]), [
|
|
20
|
+
"--model",
|
|
21
|
+
"gpt-5",
|
|
22
|
+
"--yolo",
|
|
23
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
24
|
+
]);
|
|
18
25
|
});
|
|
19
|
-
it(
|
|
26
|
+
it("avoids duplicate bypass flags when both are present", () => {
|
|
20
27
|
assert.deepEqual(normalizeCodexLaunchArgs([
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
]), [
|
|
28
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
29
|
+
"--madmax",
|
|
30
|
+
]), ["--dangerously-bypass-approvals-and-sandbox"]);
|
|
24
31
|
});
|
|
25
|
-
it(
|
|
32
|
+
it("deduplicates repeated bypass-related flags", () => {
|
|
26
33
|
assert.deepEqual(normalizeCodexLaunchArgs([
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
]), [
|
|
32
|
-
});
|
|
33
|
-
it(
|
|
34
|
-
assert.deepEqual(normalizeCodexLaunchArgs([
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
"--madmax",
|
|
35
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
36
|
+
"--madmax",
|
|
37
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
38
|
+
]), ["--dangerously-bypass-approvals-and-sandbox"]);
|
|
39
|
+
});
|
|
40
|
+
it("leaves unrelated args unchanged", () => {
|
|
41
|
+
assert.deepEqual(normalizeCodexLaunchArgs(["--model", "gpt-5", "--yolo"]), [
|
|
42
|
+
"--model",
|
|
43
|
+
"gpt-5",
|
|
44
|
+
"--yolo",
|
|
45
|
+
]);
|
|
38
46
|
});
|
|
39
|
-
it(
|
|
40
|
-
assert.deepEqual(normalizeCodexLaunchArgs([
|
|
47
|
+
it("maps --high to reasoning override", () => {
|
|
48
|
+
assert.deepEqual(normalizeCodexLaunchArgs(["--high"]), [
|
|
49
|
+
"-c",
|
|
50
|
+
'model_reasoning_effort="high"',
|
|
51
|
+
]);
|
|
41
52
|
});
|
|
42
|
-
it(
|
|
43
|
-
assert.deepEqual(normalizeCodexLaunchArgs([
|
|
53
|
+
it("maps --xhigh to reasoning override", () => {
|
|
54
|
+
assert.deepEqual(normalizeCodexLaunchArgs(["--xhigh"]), [
|
|
55
|
+
"-c",
|
|
56
|
+
'model_reasoning_effort="xhigh"',
|
|
57
|
+
]);
|
|
44
58
|
});
|
|
45
|
-
it(
|
|
46
|
-
assert.deepEqual(normalizeCodexLaunchArgs([
|
|
59
|
+
it("uses the last reasoning shorthand when both are present", () => {
|
|
60
|
+
assert.deepEqual(normalizeCodexLaunchArgs(["--high", "--xhigh"]), [
|
|
61
|
+
"-c",
|
|
62
|
+
'model_reasoning_effort="xhigh"',
|
|
63
|
+
]);
|
|
47
64
|
});
|
|
48
|
-
it(
|
|
49
|
-
assert.deepEqual(normalizeCodexLaunchArgs([
|
|
65
|
+
it("maps --xhigh --madmax to codex-native flags only", () => {
|
|
66
|
+
assert.deepEqual(normalizeCodexLaunchArgs(["--xhigh", "--madmax"]), [
|
|
67
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
68
|
+
"-c",
|
|
69
|
+
'model_reasoning_effort="xhigh"',
|
|
70
|
+
]);
|
|
50
71
|
});
|
|
51
|
-
it(
|
|
52
|
-
assert.deepEqual(normalizeCodexLaunchArgs([
|
|
72
|
+
it("--spark is stripped from leader args (model goes to workers only)", () => {
|
|
73
|
+
assert.deepEqual(normalizeCodexLaunchArgs(["--spark", "--yolo"]), [
|
|
74
|
+
"--yolo",
|
|
75
|
+
]);
|
|
53
76
|
});
|
|
54
|
-
it(
|
|
55
|
-
assert.deepEqual(normalizeCodexLaunchArgs([
|
|
77
|
+
it("--spark alone produces no leader args", () => {
|
|
78
|
+
assert.deepEqual(normalizeCodexLaunchArgs(["--spark"]), []);
|
|
56
79
|
});
|
|
57
|
-
it(
|
|
58
|
-
assert.deepEqual(normalizeCodexLaunchArgs([
|
|
80
|
+
it("--madmax-spark adds bypass flag to leader args and is otherwise consumed", () => {
|
|
81
|
+
assert.deepEqual(normalizeCodexLaunchArgs(["--madmax-spark"]), [
|
|
82
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
83
|
+
]);
|
|
59
84
|
});
|
|
60
|
-
it(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
85
|
+
it("--madmax-spark deduplicates bypass when --madmax also present", () => {
|
|
86
|
+
assert.deepEqual(normalizeCodexLaunchArgs(["--madmax", "--madmax-spark"]), [
|
|
87
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
88
|
+
]);
|
|
64
89
|
});
|
|
65
|
-
it(
|
|
66
|
-
|
|
90
|
+
it("--madmax-spark does not inject spark model into leader args", () => {
|
|
91
|
+
const args = normalizeCodexLaunchArgs(["--madmax-spark"]);
|
|
92
|
+
assert.ok(!args.includes("--model"), "leader args must not contain --model from --madmax-spark");
|
|
93
|
+
assert.ok(!args.some((a) => a.includes("spark")), "leader args must not reference spark model");
|
|
67
94
|
});
|
|
68
|
-
it(
|
|
69
|
-
assert.deepEqual(normalizeCodexLaunchArgs([
|
|
95
|
+
it("strips detached worktree flag from leader codex args", () => {
|
|
96
|
+
assert.deepEqual(normalizeCodexLaunchArgs(["--worktree", "--yolo"]), [
|
|
97
|
+
"--yolo",
|
|
98
|
+
]);
|
|
70
99
|
});
|
|
71
|
-
it(
|
|
72
|
-
|
|
73
|
-
|
|
100
|
+
it("strips named worktree flag from leader codex args", () => {
|
|
101
|
+
assert.deepEqual(normalizeCodexLaunchArgs(["--worktree=feature/demo", "--model", "gpt-5"]), ["--model", "gpt-5"]);
|
|
102
|
+
});
|
|
103
|
+
it("does not forward notify-temp flags/selectors to leader codex args", () => {
|
|
104
|
+
const parsed = resolveNotifyTempContract([
|
|
105
|
+
"--notify-temp",
|
|
106
|
+
"--discord",
|
|
107
|
+
"--custom",
|
|
108
|
+
"openclaw:ops",
|
|
109
|
+
"--custom=my-hook",
|
|
110
|
+
"--model",
|
|
111
|
+
"gpt-5",
|
|
112
|
+
], {});
|
|
113
|
+
assert.deepEqual(normalizeCodexLaunchArgs(parsed.passthroughArgs), [
|
|
114
|
+
"--model",
|
|
115
|
+
"gpt-5",
|
|
116
|
+
]);
|
|
74
117
|
});
|
|
75
118
|
});
|
|
76
|
-
describe(
|
|
77
|
-
it(
|
|
78
|
-
const parsed = resolveNotifyTempContract([
|
|
119
|
+
describe("resolveNotifyTempContract", () => {
|
|
120
|
+
it("activates from --notify-temp with no providers", () => {
|
|
121
|
+
const parsed = resolveNotifyTempContract(["--notify-temp", "--model", "gpt-5"], {});
|
|
79
122
|
assert.equal(parsed.contract.active, true);
|
|
80
|
-
assert.equal(parsed.contract.source,
|
|
123
|
+
assert.equal(parsed.contract.source, "cli");
|
|
81
124
|
assert.deepEqual(parsed.contract.canonicalSelectors, []);
|
|
82
|
-
assert.deepEqual(parsed.passthroughArgs, [
|
|
125
|
+
assert.deepEqual(parsed.passthroughArgs, ["--model", "gpt-5"]);
|
|
83
126
|
});
|
|
84
|
-
it(
|
|
85
|
-
const parsed = resolveNotifyTempContract([
|
|
127
|
+
it("auto-activates when provider selectors are present", () => {
|
|
128
|
+
const parsed = resolveNotifyTempContract(["--discord", "--slack"], {});
|
|
86
129
|
assert.equal(parsed.contract.active, true);
|
|
87
|
-
assert.equal(parsed.contract.source,
|
|
88
|
-
assert.deepEqual(parsed.contract.canonicalSelectors, [
|
|
89
|
-
assert.equal(parsed.contract.warnings.some((line) => line.includes(
|
|
90
|
-
});
|
|
91
|
-
it(
|
|
92
|
-
const parsed = resolveNotifyTempContract([
|
|
93
|
-
assert.deepEqual(parsed.contract.canonicalSelectors, [
|
|
130
|
+
assert.equal(parsed.contract.source, "providers");
|
|
131
|
+
assert.deepEqual(parsed.contract.canonicalSelectors, ["discord", "slack"]);
|
|
132
|
+
assert.equal(parsed.contract.warnings.some((line) => line.includes("imply temp mode")), true);
|
|
133
|
+
});
|
|
134
|
+
it("supports repeated --custom forms and canonicalizes selectors", () => {
|
|
135
|
+
const parsed = resolveNotifyTempContract(["--custom", "OpenClaw:Ops", "--custom=my-hook", "--custom=", "--custom"], {});
|
|
136
|
+
assert.deepEqual(parsed.contract.canonicalSelectors, [
|
|
137
|
+
"openclaw:ops",
|
|
138
|
+
"custom:my-hook",
|
|
139
|
+
]);
|
|
94
140
|
assert.equal(parsed.contract.warnings.length >= 1, true);
|
|
95
141
|
});
|
|
96
|
-
it(
|
|
97
|
-
const parsed = resolveNotifyTempContract([
|
|
142
|
+
it("activates from OMX_NOTIFY_TEMP=1 env parity", () => {
|
|
143
|
+
const parsed = resolveNotifyTempContract(["--model", "gpt-5"], {
|
|
144
|
+
OMX_NOTIFY_TEMP: "1",
|
|
145
|
+
});
|
|
98
146
|
assert.equal(parsed.contract.active, true);
|
|
99
|
-
assert.equal(parsed.contract.source,
|
|
100
|
-
assert.deepEqual(parsed.passthroughArgs, [
|
|
147
|
+
assert.equal(parsed.contract.source, "env");
|
|
148
|
+
assert.deepEqual(parsed.passthroughArgs, ["--model", "gpt-5"]);
|
|
101
149
|
});
|
|
102
150
|
});
|
|
103
|
-
describe(
|
|
104
|
-
it(
|
|
151
|
+
describe("buildNotifyTempStartupMessages", () => {
|
|
152
|
+
it("always emits summary when temp mode is active", () => {
|
|
105
153
|
const result = buildNotifyTempStartupMessages({
|
|
106
154
|
active: true,
|
|
107
|
-
selectors: [
|
|
108
|
-
canonicalSelectors: [
|
|
155
|
+
selectors: ["discord"],
|
|
156
|
+
canonicalSelectors: ["discord"],
|
|
109
157
|
warnings: [],
|
|
110
|
-
source:
|
|
158
|
+
source: "cli",
|
|
111
159
|
}, true);
|
|
112
160
|
assert.deepEqual(result.infoLines, [
|
|
113
|
-
|
|
161
|
+
"notify temp: active | providers=discord | persistent-routing=bypassed",
|
|
114
162
|
]);
|
|
115
163
|
assert.deepEqual(result.warningLines, []);
|
|
116
164
|
});
|
|
117
|
-
it(
|
|
165
|
+
it("emits no-valid-provider warning when no provider is configured", () => {
|
|
118
166
|
const result = buildNotifyTempStartupMessages({
|
|
119
167
|
active: true,
|
|
120
168
|
selectors: [],
|
|
121
169
|
canonicalSelectors: [],
|
|
122
|
-
warnings: [
|
|
123
|
-
|
|
170
|
+
warnings: [
|
|
171
|
+
"notify temp: provider selectors imply temp mode (auto-activated)",
|
|
172
|
+
],
|
|
173
|
+
source: "providers",
|
|
124
174
|
}, false);
|
|
125
|
-
assert.equal(result.warningLines.includes(
|
|
175
|
+
assert.equal(result.warningLines.includes("notify temp: no valid providers resolved; notifications skipped"), true);
|
|
126
176
|
});
|
|
127
177
|
});
|
|
128
|
-
describe(
|
|
129
|
-
it(
|
|
130
|
-
assert.equal(resolveWorkerSparkModel([
|
|
178
|
+
describe("resolveWorkerSparkModel", () => {
|
|
179
|
+
it("returns spark model string when --spark is present", () => {
|
|
180
|
+
assert.equal(resolveWorkerSparkModel(["--spark", "--yolo"]), expectedLowComplexityModel());
|
|
131
181
|
});
|
|
132
|
-
it(
|
|
133
|
-
assert.equal(resolveWorkerSparkModel([
|
|
182
|
+
it("returns spark model string when --madmax-spark is present", () => {
|
|
183
|
+
assert.equal(resolveWorkerSparkModel(["--madmax-spark"]), expectedLowComplexityModel());
|
|
134
184
|
});
|
|
135
|
-
it(
|
|
136
|
-
assert.equal(resolveWorkerSparkModel([
|
|
185
|
+
it("returns undefined when neither spark flag is present", () => {
|
|
186
|
+
assert.equal(resolveWorkerSparkModel(["--madmax", "--yolo", "--model", "gpt-5"]), undefined);
|
|
137
187
|
});
|
|
138
|
-
it(
|
|
188
|
+
it("returns undefined for empty args", () => {
|
|
139
189
|
assert.equal(resolveWorkerSparkModel([]), undefined);
|
|
140
190
|
});
|
|
141
|
-
it(
|
|
142
|
-
const codexHome = await mkdtemp(join(tmpdir(),
|
|
191
|
+
it("reads low-complexity team model from config when codexHomeOverride is provided", async () => {
|
|
192
|
+
const codexHome = await mkdtemp(join(tmpdir(), "omx-codex-home-"));
|
|
143
193
|
try {
|
|
144
|
-
await writeFile(join(codexHome,
|
|
145
|
-
assert.equal(resolveWorkerSparkModel([
|
|
194
|
+
await writeFile(join(codexHome, ".omx-config.json"), JSON.stringify({ models: { team_low_complexity: "gpt-4.1-mini" } }));
|
|
195
|
+
assert.equal(resolveWorkerSparkModel(["--spark"], codexHome), "gpt-4.1-mini");
|
|
146
196
|
}
|
|
147
197
|
finally {
|
|
148
198
|
await rm(codexHome, { recursive: true, force: true });
|
|
149
199
|
}
|
|
150
200
|
});
|
|
151
201
|
});
|
|
152
|
-
describe(
|
|
153
|
-
it(
|
|
202
|
+
describe("resolveTeamWorkerLaunchArgsEnv (spark)", () => {
|
|
203
|
+
it("injects spark model as worker default when no explicit env model", () => {
|
|
154
204
|
assert.equal(resolveTeamWorkerLaunchArgsEnv(undefined, [], true, expectedLowComplexityModel()), `--model ${expectedLowComplexityModel()}`);
|
|
155
205
|
});
|
|
156
|
-
it(
|
|
157
|
-
assert.equal(resolveTeamWorkerLaunchArgsEnv(
|
|
206
|
+
it("explicit env model overrides spark default", () => {
|
|
207
|
+
assert.equal(resolveTeamWorkerLaunchArgsEnv("--model gpt-5", [], true, expectedLowComplexityModel()), "--model gpt-5");
|
|
158
208
|
});
|
|
159
|
-
it(
|
|
160
|
-
assert.equal(resolveTeamWorkerLaunchArgsEnv(undefined, [
|
|
209
|
+
it("inherited leader model overrides spark default", () => {
|
|
210
|
+
assert.equal(resolveTeamWorkerLaunchArgsEnv(undefined, ["--model", "gpt-4.1"], true, expectedLowComplexityModel()), "--model gpt-4.1");
|
|
161
211
|
});
|
|
162
212
|
});
|
|
163
|
-
describe(
|
|
164
|
-
it(
|
|
165
|
-
for (const command of [
|
|
213
|
+
describe("commandOwnsLocalHelp", () => {
|
|
214
|
+
it("returns true for nested commands that render their own help output", () => {
|
|
215
|
+
for (const command of [
|
|
216
|
+
"agents-init",
|
|
217
|
+
"ask",
|
|
218
|
+
"autoresearch",
|
|
219
|
+
"deepinit",
|
|
220
|
+
"hooks",
|
|
221
|
+
"hud",
|
|
222
|
+
"ralph",
|
|
223
|
+
"ralphthon",
|
|
224
|
+
"resume",
|
|
225
|
+
"session",
|
|
226
|
+
"sparkshell",
|
|
227
|
+
"team",
|
|
228
|
+
"tmux-hook",
|
|
229
|
+
]) {
|
|
166
230
|
assert.equal(commandOwnsLocalHelp(command), true, `expected ${command} to own local help`);
|
|
167
231
|
}
|
|
168
232
|
});
|
|
169
|
-
it(
|
|
170
|
-
for (const command of [
|
|
233
|
+
it("returns false for top-level help-only commands", () => {
|
|
234
|
+
for (const command of ["help", "launch", "version"]) {
|
|
171
235
|
assert.equal(commandOwnsLocalHelp(command), false, `expected ${command} to use top-level help`);
|
|
172
236
|
}
|
|
173
237
|
});
|
|
174
238
|
});
|
|
175
|
-
describe(
|
|
176
|
-
it(
|
|
177
|
-
assert.deepEqual(resolveCliInvocation([
|
|
178
|
-
command:
|
|
239
|
+
describe("resolveCliInvocation", () => {
|
|
240
|
+
it("resolves explore to explore command", () => {
|
|
241
|
+
assert.deepEqual(resolveCliInvocation(["explore", "--prompt", "find", "auth"]), {
|
|
242
|
+
command: "explore",
|
|
179
243
|
launchArgs: [],
|
|
180
244
|
});
|
|
181
245
|
});
|
|
182
|
-
it(
|
|
183
|
-
assert.deepEqual(resolveCliInvocation([
|
|
184
|
-
command:
|
|
246
|
+
it("resolves ask to ask command", () => {
|
|
247
|
+
assert.deepEqual(resolveCliInvocation(["ask", "claude", "hello"]), {
|
|
248
|
+
command: "ask",
|
|
185
249
|
launchArgs: [],
|
|
186
250
|
});
|
|
187
251
|
});
|
|
188
|
-
it(
|
|
189
|
-
assert.deepEqual(resolveCliInvocation([
|
|
190
|
-
command:
|
|
252
|
+
it("resolves autoresearch to autoresearch command", () => {
|
|
253
|
+
assert.deepEqual(resolveCliInvocation(["autoresearch", "missions/demo"]), {
|
|
254
|
+
command: "autoresearch",
|
|
191
255
|
launchArgs: [],
|
|
192
256
|
});
|
|
193
257
|
});
|
|
194
|
-
it(
|
|
195
|
-
assert.deepEqual(resolveCliInvocation([
|
|
196
|
-
command:
|
|
258
|
+
it("resolves ralphthon to ralphthon command", () => {
|
|
259
|
+
assert.deepEqual(resolveCliInvocation(["ralphthon", "--resume"]), {
|
|
260
|
+
command: "ralphthon",
|
|
197
261
|
launchArgs: [],
|
|
198
262
|
});
|
|
199
263
|
});
|
|
200
|
-
it(
|
|
201
|
-
assert.deepEqual(resolveCliInvocation([
|
|
202
|
-
command:
|
|
203
|
-
launchArgs: [
|
|
264
|
+
it("resolves session to session command", () => {
|
|
265
|
+
assert.deepEqual(resolveCliInvocation(["session", "search", "startup evidence"]), {
|
|
266
|
+
command: "session",
|
|
267
|
+
launchArgs: [],
|
|
204
268
|
});
|
|
205
269
|
});
|
|
206
|
-
it(
|
|
207
|
-
assert.deepEqual(resolveCliInvocation([
|
|
208
|
-
command:
|
|
209
|
-
launchArgs: [
|
|
270
|
+
it("resolves resume to resume command and forwards trailing args", () => {
|
|
271
|
+
assert.deepEqual(resolveCliInvocation(["resume", "--last"]), {
|
|
272
|
+
command: "resume",
|
|
273
|
+
launchArgs: ["--last"],
|
|
210
274
|
});
|
|
211
275
|
});
|
|
212
|
-
it(
|
|
213
|
-
assert.deepEqual(resolveCliInvocation([
|
|
214
|
-
command:
|
|
215
|
-
launchArgs: [
|
|
276
|
+
it("resolves resume session id and prompt as forwarded args", () => {
|
|
277
|
+
assert.deepEqual(resolveCliInvocation(["resume", "session-123", "continue here"]), {
|
|
278
|
+
command: "resume",
|
|
279
|
+
launchArgs: ["session-123", "continue here"],
|
|
216
280
|
});
|
|
217
281
|
});
|
|
218
|
-
it(
|
|
219
|
-
assert.deepEqual(resolveCliInvocation([
|
|
220
|
-
command:
|
|
221
|
-
launchArgs: [],
|
|
282
|
+
it("resolves exec to non-interactive launch passthrough and forwards trailing args", () => {
|
|
283
|
+
assert.deepEqual(resolveCliInvocation(["exec", "--model", "gpt-5", "say hi"]), {
|
|
284
|
+
command: "exec",
|
|
285
|
+
launchArgs: ["--model", "gpt-5", "say hi"],
|
|
222
286
|
});
|
|
223
287
|
});
|
|
224
|
-
it(
|
|
225
|
-
assert.deepEqual(resolveCliInvocation([
|
|
226
|
-
command:
|
|
288
|
+
it("resolves hooks to hooks command", () => {
|
|
289
|
+
assert.deepEqual(resolveCliInvocation(["hooks"]), {
|
|
290
|
+
command: "hooks",
|
|
227
291
|
launchArgs: [],
|
|
228
292
|
});
|
|
229
293
|
});
|
|
230
|
-
it(
|
|
231
|
-
assert.deepEqual(resolveCliInvocation([
|
|
232
|
-
command:
|
|
294
|
+
it("resolves agents-init to agents-init command", () => {
|
|
295
|
+
assert.deepEqual(resolveCliInvocation(["agents-init", "."]), {
|
|
296
|
+
command: "agents-init",
|
|
233
297
|
launchArgs: [],
|
|
234
298
|
});
|
|
235
299
|
});
|
|
236
|
-
it(
|
|
237
|
-
assert.deepEqual(resolveCliInvocation([
|
|
238
|
-
command:
|
|
300
|
+
it("resolves deepinit to deepinit alias command", () => {
|
|
301
|
+
assert.deepEqual(resolveCliInvocation(["deepinit", "src"]), {
|
|
302
|
+
command: "deepinit",
|
|
239
303
|
launchArgs: [],
|
|
240
304
|
});
|
|
241
305
|
});
|
|
242
|
-
it(
|
|
243
|
-
assert.deepEqual(resolveCliInvocation([
|
|
244
|
-
command:
|
|
306
|
+
it("resolves --help to the help command instead of launch", () => {
|
|
307
|
+
assert.deepEqual(resolveCliInvocation(["--help"]), {
|
|
308
|
+
command: "help",
|
|
245
309
|
launchArgs: [],
|
|
246
310
|
});
|
|
247
311
|
});
|
|
248
|
-
it(
|
|
249
|
-
assert.deepEqual(resolveCliInvocation([
|
|
250
|
-
command:
|
|
312
|
+
it("resolves --version to the version command instead of launch", () => {
|
|
313
|
+
assert.deepEqual(resolveCliInvocation(["--version"]), {
|
|
314
|
+
command: "version",
|
|
251
315
|
launchArgs: [],
|
|
252
316
|
});
|
|
253
317
|
});
|
|
254
|
-
it(
|
|
255
|
-
assert.deepEqual(resolveCliInvocation([
|
|
256
|
-
command:
|
|
257
|
-
launchArgs: [
|
|
318
|
+
it("resolves -v to the version command instead of launch", () => {
|
|
319
|
+
assert.deepEqual(resolveCliInvocation(["-v"]), {
|
|
320
|
+
command: "version",
|
|
321
|
+
launchArgs: [],
|
|
258
322
|
});
|
|
259
323
|
});
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
it('parses --scope <value> form', () => {
|
|
266
|
-
assert.equal(resolveSetupScopeArg(['--dry-run', '--scope', 'project']), 'project');
|
|
267
|
-
});
|
|
268
|
-
it('parses --scope=<value> form', () => {
|
|
269
|
-
assert.equal(resolveSetupScopeArg(['--scope=project']), 'project');
|
|
270
|
-
});
|
|
271
|
-
it('throws on invalid scope value', () => {
|
|
272
|
-
assert.throws(() => resolveSetupScopeArg(['--scope', 'workspace']), /Invalid setup scope: workspace/);
|
|
273
|
-
});
|
|
274
|
-
it('throws when --scope value is missing', () => {
|
|
275
|
-
assert.throws(() => resolveSetupScopeArg(['--scope']), /Missing setup scope value after --scope/);
|
|
324
|
+
it("keeps unknown long flags as launch passthrough args", () => {
|
|
325
|
+
assert.deepEqual(resolveCliInvocation(["--model", "gpt-5"]), {
|
|
326
|
+
command: "launch",
|
|
327
|
+
launchArgs: ["--model", "gpt-5"],
|
|
328
|
+
});
|
|
276
329
|
});
|
|
277
330
|
});
|
|
278
|
-
describe(
|
|
279
|
-
it(
|
|
280
|
-
assert.equal(
|
|
331
|
+
describe("resolveSetupScopeArg", () => {
|
|
332
|
+
it("returns undefined when scope is omitted", () => {
|
|
333
|
+
assert.equal(resolveSetupScopeArg(["--dry-run"]), undefined);
|
|
281
334
|
});
|
|
282
|
-
it(
|
|
283
|
-
assert.equal(
|
|
335
|
+
it("parses --scope <value> form", () => {
|
|
336
|
+
assert.equal(resolveSetupScopeArg(["--dry-run", "--scope", "project"]), "project");
|
|
284
337
|
});
|
|
285
|
-
it(
|
|
286
|
-
assert.equal(
|
|
338
|
+
it("parses --scope=<value> form", () => {
|
|
339
|
+
assert.equal(resolveSetupScopeArg(["--scope=project"]), "project");
|
|
287
340
|
});
|
|
288
|
-
it(
|
|
289
|
-
assert.throws(() =>
|
|
341
|
+
it("throws on invalid scope value", () => {
|
|
342
|
+
assert.throws(() => resolveSetupScopeArg(["--scope", "workspace"]), /Invalid setup scope: workspace/);
|
|
290
343
|
});
|
|
291
|
-
it(
|
|
292
|
-
assert.throws(() =>
|
|
344
|
+
it("throws when --scope value is missing", () => {
|
|
345
|
+
assert.throws(() => resolveSetupScopeArg(["--scope"]), /Missing setup scope value after --scope/);
|
|
293
346
|
});
|
|
294
347
|
});
|
|
295
|
-
describe(
|
|
296
|
-
it(
|
|
297
|
-
const wd = await mkdtemp(join(tmpdir(),
|
|
348
|
+
describe("project launch scope helpers", () => {
|
|
349
|
+
it("reads persisted setup scope when valid", async () => {
|
|
350
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-launch-scope-"));
|
|
298
351
|
try {
|
|
299
|
-
await mkdir(join(wd,
|
|
300
|
-
await writeFile(join(wd,
|
|
301
|
-
assert.equal(readPersistedSetupScope(wd),
|
|
352
|
+
await mkdir(join(wd, ".omx"), { recursive: true });
|
|
353
|
+
await writeFile(join(wd, ".omx", "setup-scope.json"), JSON.stringify({ scope: "project" }));
|
|
354
|
+
assert.equal(readPersistedSetupScope(wd), "project");
|
|
302
355
|
}
|
|
303
356
|
finally {
|
|
304
357
|
await rm(wd, { recursive: true, force: true });
|
|
305
358
|
}
|
|
306
359
|
});
|
|
307
|
-
it(
|
|
308
|
-
const wd = await mkdtemp(join(tmpdir(),
|
|
360
|
+
it("reads persisted setup preferences when skill target is present", async () => {
|
|
361
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-launch-scope-"));
|
|
309
362
|
try {
|
|
310
|
-
await mkdir(join(wd,
|
|
311
|
-
await writeFile(join(wd,
|
|
363
|
+
await mkdir(join(wd, ".omx"), { recursive: true });
|
|
364
|
+
await writeFile(join(wd, ".omx", "setup-scope.json"), JSON.stringify({ scope: "user" }));
|
|
312
365
|
assert.deepEqual(readPersistedSetupPreferences(wd), {
|
|
313
|
-
scope:
|
|
314
|
-
skillTarget: 'agents',
|
|
366
|
+
scope: "user",
|
|
315
367
|
});
|
|
316
368
|
}
|
|
317
369
|
finally {
|
|
318
370
|
await rm(wd, { recursive: true, force: true });
|
|
319
371
|
}
|
|
320
372
|
});
|
|
321
|
-
it(
|
|
322
|
-
const wd = await mkdtemp(join(tmpdir(),
|
|
373
|
+
it("ignores malformed persisted setup scope", async () => {
|
|
374
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-launch-scope-"));
|
|
323
375
|
try {
|
|
324
|
-
await mkdir(join(wd,
|
|
325
|
-
await writeFile(join(wd,
|
|
376
|
+
await mkdir(join(wd, ".omx"), { recursive: true });
|
|
377
|
+
await writeFile(join(wd, ".omx", "setup-scope.json"), "{not-json");
|
|
326
378
|
assert.equal(readPersistedSetupScope(wd), undefined);
|
|
327
379
|
}
|
|
328
380
|
finally {
|
|
329
381
|
await rm(wd, { recursive: true, force: true });
|
|
330
382
|
}
|
|
331
383
|
});
|
|
332
|
-
it(
|
|
333
|
-
const wd = await mkdtemp(join(tmpdir(),
|
|
384
|
+
it("uses project CODEX_HOME when persisted scope is project", async () => {
|
|
385
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-launch-scope-"));
|
|
334
386
|
try {
|
|
335
|
-
await mkdir(join(wd,
|
|
336
|
-
await writeFile(join(wd,
|
|
337
|
-
assert.equal(resolveCodexHomeForLaunch(wd, {}), join(wd,
|
|
387
|
+
await mkdir(join(wd, ".omx"), { recursive: true });
|
|
388
|
+
await writeFile(join(wd, ".omx", "setup-scope.json"), JSON.stringify({ scope: "project" }));
|
|
389
|
+
assert.equal(resolveCodexHomeForLaunch(wd, {}), join(wd, ".codex"));
|
|
338
390
|
}
|
|
339
391
|
finally {
|
|
340
392
|
await rm(wd, { recursive: true, force: true });
|
|
341
393
|
}
|
|
342
394
|
});
|
|
343
|
-
it(
|
|
344
|
-
const wd = await mkdtemp(join(tmpdir(),
|
|
395
|
+
it("keeps explicit CODEX_HOME override from env", async () => {
|
|
396
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-launch-scope-"));
|
|
345
397
|
try {
|
|
346
|
-
await mkdir(join(wd,
|
|
347
|
-
await writeFile(join(wd,
|
|
348
|
-
assert.equal(resolveCodexHomeForLaunch(wd, {
|
|
398
|
+
await mkdir(join(wd, ".omx"), { recursive: true });
|
|
399
|
+
await writeFile(join(wd, ".omx", "setup-scope.json"), JSON.stringify({ scope: "project" }));
|
|
400
|
+
assert.equal(resolveCodexHomeForLaunch(wd, {
|
|
401
|
+
CODEX_HOME: "/tmp/explicit-codex-home",
|
|
402
|
+
}), "/tmp/explicit-codex-home");
|
|
349
403
|
}
|
|
350
404
|
finally {
|
|
351
405
|
await rm(wd, { recursive: true, force: true });
|
|
352
406
|
}
|
|
353
407
|
});
|
|
354
408
|
it('migrates legacy "project-local" persisted scope to "project"', async () => {
|
|
355
|
-
const wd = await mkdtemp(join(tmpdir(),
|
|
409
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-launch-scope-"));
|
|
356
410
|
try {
|
|
357
|
-
await mkdir(join(wd,
|
|
358
|
-
await writeFile(join(wd,
|
|
359
|
-
assert.equal(readPersistedSetupScope(wd),
|
|
411
|
+
await mkdir(join(wd, ".omx"), { recursive: true });
|
|
412
|
+
await writeFile(join(wd, ".omx", "setup-scope.json"), JSON.stringify({ scope: "project-local" }));
|
|
413
|
+
assert.equal(readPersistedSetupScope(wd), "project");
|
|
360
414
|
}
|
|
361
415
|
finally {
|
|
362
416
|
await rm(wd, { recursive: true, force: true });
|
|
363
417
|
}
|
|
364
418
|
});
|
|
365
419
|
it('resolves CODEX_HOME for legacy "project-local" persisted scope', async () => {
|
|
366
|
-
const wd = await mkdtemp(join(tmpdir(),
|
|
420
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-launch-scope-"));
|
|
367
421
|
try {
|
|
368
|
-
await mkdir(join(wd,
|
|
369
|
-
await writeFile(join(wd,
|
|
370
|
-
assert.equal(resolveCodexHomeForLaunch(wd, {}), join(wd,
|
|
422
|
+
await mkdir(join(wd, ".omx"), { recursive: true });
|
|
423
|
+
await writeFile(join(wd, ".omx", "setup-scope.json"), JSON.stringify({ scope: "project-local" }));
|
|
424
|
+
assert.equal(resolveCodexHomeForLaunch(wd, {}), join(wd, ".codex"));
|
|
371
425
|
}
|
|
372
426
|
finally {
|
|
373
427
|
await rm(wd, { recursive: true, force: true });
|
|
374
428
|
}
|
|
375
429
|
});
|
|
376
430
|
});
|
|
377
|
-
describe(
|
|
378
|
-
it(
|
|
379
|
-
assert.equal(resolveCodexLaunchPolicy({},
|
|
431
|
+
describe("resolveCodexLaunchPolicy", () => {
|
|
432
|
+
it("uses detached tmux on macOS when outside tmux and tmux is available", () => {
|
|
433
|
+
assert.equal(resolveCodexLaunchPolicy({}, "darwin", true), "detached-tmux");
|
|
380
434
|
});
|
|
381
|
-
it(
|
|
382
|
-
assert.equal(resolveCodexLaunchPolicy({ TMUX:
|
|
435
|
+
it("uses tmux-aware launch path when already inside tmux", () => {
|
|
436
|
+
assert.equal(resolveCodexLaunchPolicy({ TMUX: "/tmp/tmux-1000/default,123,0" }, "darwin", true), "inside-tmux");
|
|
383
437
|
});
|
|
384
|
-
it(
|
|
385
|
-
assert.equal(resolveCodexLaunchPolicy({},
|
|
438
|
+
it("uses detached tmux on non-macOS hosts when outside tmux and tmux is available", () => {
|
|
439
|
+
assert.equal(resolveCodexLaunchPolicy({}, "linux", true), "detached-tmux");
|
|
386
440
|
});
|
|
387
|
-
it(
|
|
388
|
-
assert.equal(resolveCodexLaunchPolicy({},
|
|
441
|
+
it("launches directly when tmux is unavailable outside tmux", () => {
|
|
442
|
+
assert.equal(resolveCodexLaunchPolicy({}, "linux", false), "direct");
|
|
389
443
|
});
|
|
390
444
|
});
|
|
391
|
-
describe(
|
|
392
|
-
it(
|
|
393
|
-
const err = Object.assign(new Error(
|
|
445
|
+
describe("classifyCodexExecFailure", () => {
|
|
446
|
+
it("classifies child process exit status as codex exit", () => {
|
|
447
|
+
const err = Object.assign(new Error("codex exited 9"), { status: 9 });
|
|
394
448
|
const classified = classifyCodexExecFailure(err);
|
|
395
|
-
assert.equal(classified.kind,
|
|
449
|
+
assert.equal(classified.kind, "exit");
|
|
396
450
|
assert.equal(classified.exitCode, 9);
|
|
397
451
|
});
|
|
398
|
-
it(
|
|
399
|
-
const err = Object.assign(new Error(
|
|
452
|
+
it("classifies signal termination as codex exit and maps to signal-based exit code", () => {
|
|
453
|
+
const err = Object.assign(new Error("terminated"), {
|
|
454
|
+
status: null,
|
|
455
|
+
signal: "SIGTERM",
|
|
456
|
+
});
|
|
400
457
|
const classified = classifyCodexExecFailure(err);
|
|
401
|
-
assert.equal(classified.kind,
|
|
402
|
-
assert.equal(classified.signal,
|
|
403
|
-
assert.equal(classified.exitCode, resolveSignalExitCode(
|
|
458
|
+
assert.equal(classified.kind, "exit");
|
|
459
|
+
assert.equal(classified.signal, "SIGTERM");
|
|
460
|
+
assert.equal(classified.exitCode, resolveSignalExitCode("SIGTERM"));
|
|
404
461
|
});
|
|
405
|
-
it(
|
|
406
|
-
const err = Object.assign(new Error(
|
|
462
|
+
it("classifies ENOENT as launch error", () => {
|
|
463
|
+
const err = Object.assign(new Error("spawn codex ENOENT"), {
|
|
464
|
+
code: "ENOENT",
|
|
465
|
+
});
|
|
407
466
|
const classified = classifyCodexExecFailure(err);
|
|
408
|
-
assert.equal(classified.kind,
|
|
409
|
-
assert.equal(classified.code,
|
|
467
|
+
assert.equal(classified.kind, "launch-error");
|
|
468
|
+
assert.equal(classified.code, "ENOENT");
|
|
410
469
|
});
|
|
411
470
|
});
|
|
412
|
-
describe(
|
|
413
|
-
it(
|
|
471
|
+
describe("tmux HUD pane helpers", () => {
|
|
472
|
+
it("findHudWatchPaneIds detects stale HUD watch panes and excludes current pane", () => {
|
|
414
473
|
const panes = parseTmuxPaneSnapshot([
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
].join(
|
|
420
|
-
assert.deepEqual(findHudWatchPaneIds(panes,
|
|
474
|
+
"%1\tzsh\tzsh",
|
|
475
|
+
"%2\tnode\tnode /tmp/bin/omx.js hud --watch",
|
|
476
|
+
"%3\tnode\tnode /tmp/bin/omx.js hud --watch",
|
|
477
|
+
"%4\tcodex\tcodex --model gpt-5",
|
|
478
|
+
].join("\n"));
|
|
479
|
+
assert.deepEqual(findHudWatchPaneIds(panes, "%2"), ["%3"]);
|
|
421
480
|
});
|
|
422
|
-
it(
|
|
423
|
-
assert.deepEqual(buildHudPaneCleanupTargets([
|
|
481
|
+
it("buildHudPaneCleanupTargets de-dupes pane ids and includes created pane", () => {
|
|
482
|
+
assert.deepEqual(buildHudPaneCleanupTargets(["%3", "%3", "invalid"], "%4"), ["%3", "%4"]);
|
|
424
483
|
});
|
|
425
|
-
it(
|
|
484
|
+
it("buildHudPaneCleanupTargets excludes leader pane from existing ids", () => {
|
|
426
485
|
// %5 is the leader pane — it must not be included even if findHudWatchPaneIds let it through.
|
|
427
|
-
assert.deepEqual(buildHudPaneCleanupTargets([
|
|
486
|
+
assert.deepEqual(buildHudPaneCleanupTargets(["%3", "%5"], "%4", "%5"), [
|
|
487
|
+
"%3",
|
|
488
|
+
"%4",
|
|
489
|
+
]);
|
|
428
490
|
});
|
|
429
|
-
it(
|
|
491
|
+
it("buildHudPaneCleanupTargets excludes leader pane even when it matches the created HUD pane id", () => {
|
|
430
492
|
// Defensive edge case: if createHudWatchPane somehow returned the leader pane id, guard protects it.
|
|
431
|
-
assert.deepEqual(buildHudPaneCleanupTargets([
|
|
493
|
+
assert.deepEqual(buildHudPaneCleanupTargets(["%3"], "%5", "%5"), ["%3"]);
|
|
432
494
|
});
|
|
433
|
-
it(
|
|
434
|
-
assert.deepEqual(buildHudPaneCleanupTargets([
|
|
495
|
+
it("buildHudPaneCleanupTargets is a no-op guard when leaderPaneId is absent", () => {
|
|
496
|
+
assert.deepEqual(buildHudPaneCleanupTargets(["%3"], "%4"), ["%3", "%4"]);
|
|
435
497
|
});
|
|
436
498
|
});
|
|
437
|
-
describe(
|
|
438
|
-
it(
|
|
439
|
-
const steps = buildDetachedSessionBootstrapSteps(
|
|
440
|
-
assert.deepEqual(steps.map((step) => step.name), [
|
|
499
|
+
describe("detached tmux new-session sequencing", () => {
|
|
500
|
+
it("buildDetachedSessionBootstrapSteps uses shared HUD height and split-capture ordering", () => {
|
|
501
|
+
const steps = buildDetachedSessionBootstrapSteps("omx-demo", "/tmp/project", "'codex' '--model' 'gpt-5'", "'node' '/tmp/omx.js' 'hud' '--watch'", "--model gpt-5", "/tmp/codex-home", '{"active":true}');
|
|
502
|
+
assert.deepEqual(steps.map((step) => step.name), ["new-session", "split-and-capture-hud-pane"]);
|
|
441
503
|
assert.equal(steps[1]?.args[3], String(HUD_TMUX_HEIGHT_LINES));
|
|
442
|
-
assert.equal(steps[1]?.args[6],
|
|
443
|
-
assert.equal(steps[1]?.args.includes(
|
|
444
|
-
assert.equal(steps[1]?.args.includes(
|
|
445
|
-
assert.equal(steps[0]?.args.includes(
|
|
504
|
+
assert.equal(steps[1]?.args[6], "omx-demo");
|
|
505
|
+
assert.equal(steps[1]?.args.includes("-P"), true);
|
|
506
|
+
assert.equal(steps[1]?.args.includes("#{pane_id}"), true);
|
|
507
|
+
assert.equal(steps[0]?.args.includes("-e"), true);
|
|
446
508
|
assert.equal(steps[0]?.args.includes('OMX_NOTIFY_TEMP_CONTRACT={\"active\":true}'), true);
|
|
447
509
|
});
|
|
448
|
-
it(
|
|
449
|
-
const steps = buildDetachedSessionBootstrapSteps(
|
|
450
|
-
const newSession = steps.find((step) => step.name ===
|
|
510
|
+
it("buildDetachedSessionBootstrapSteps forwards temp contract env to detached tmux session", () => {
|
|
511
|
+
const steps = buildDetachedSessionBootstrapSteps("omx-demo", "/tmp/project", "'codex' '--model' 'gpt-5'", "'node' '/tmp/omx.js' 'hud' '--watch'", null, undefined, '{"active":true,"canonicalSelectors":["discord"]}');
|
|
512
|
+
const newSession = steps.find((step) => step.name === "new-session");
|
|
451
513
|
assert.ok(newSession);
|
|
452
|
-
assert.equal(newSession.args.includes(
|
|
453
|
-
|
|
454
|
-
});
|
|
455
|
-
it(
|
|
456
|
-
const hudCmd = buildWindowsPromptCommand(
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
514
|
+
assert.equal(newSession.args.includes("-e") &&
|
|
515
|
+
newSession.args.some((arg) => arg.startsWith("OMX_NOTIFY_TEMP_CONTRACT=")), true);
|
|
516
|
+
});
|
|
517
|
+
it("buildDetachedSessionBootstrapSteps starts native Windows detached sessions with powershell", () => {
|
|
518
|
+
const hudCmd = buildWindowsPromptCommand("node", [
|
|
519
|
+
"omx.js",
|
|
520
|
+
"hud",
|
|
521
|
+
"--watch",
|
|
522
|
+
]);
|
|
523
|
+
const steps = buildDetachedSessionBootstrapSteps("omx-demo", "C:/project", "'codex' '--dangerously-bypass-approvals-and-sandbox'", hudCmd, "--model gpt-5", "C:/codex-home", null, true);
|
|
524
|
+
assert.equal(steps[0]?.name, "new-session");
|
|
525
|
+
assert.equal(steps[0]?.args.at(-1), "powershell.exe");
|
|
526
|
+
assert.equal(steps[1]?.name, "split-and-capture-hud-pane");
|
|
461
527
|
assert.equal(steps[1]?.args.at(-1), hudCmd);
|
|
462
528
|
});
|
|
463
|
-
it(
|
|
464
|
-
const steps = buildDetachedSessionFinalizeSteps(
|
|
529
|
+
it("buildDetachedSessionFinalizeSteps keeps schedule after split-capture and before attach", () => {
|
|
530
|
+
const steps = buildDetachedSessionFinalizeSteps("omx-demo", "%12", "3", true);
|
|
465
531
|
const names = steps.map((step) => step.name);
|
|
466
|
-
const attachedIndex = names.indexOf(
|
|
467
|
-
const scheduleIndex = names.indexOf(
|
|
468
|
-
const attachIndex = names.indexOf(
|
|
532
|
+
const attachedIndex = names.indexOf("register-client-attached-reconcile");
|
|
533
|
+
const scheduleIndex = names.indexOf("schedule-delayed-resize");
|
|
534
|
+
const attachIndex = names.indexOf("attach-session");
|
|
469
535
|
assert.equal(attachedIndex >= 0, true);
|
|
470
536
|
assert.equal(scheduleIndex > attachedIndex, true);
|
|
471
537
|
assert.equal(scheduleIndex >= 0, true);
|
|
472
538
|
assert.equal(attachIndex > scheduleIndex, true);
|
|
473
|
-
assert.equal(names.includes(
|
|
474
|
-
assert.equal(names.includes(
|
|
475
|
-
});
|
|
476
|
-
it(
|
|
477
|
-
const steps = buildDetachedSessionFinalizeSteps(
|
|
478
|
-
const registerHook = steps.find((step) => step.name ===
|
|
479
|
-
const schedule = steps.find((step) => step.name ===
|
|
480
|
-
const reconcile = steps.find((step) => step.name ===
|
|
481
|
-
assert.match(registerHook?.args[4] ??
|
|
482
|
-
assert.match(registerHook?.args[4] ??
|
|
483
|
-
assert.match(schedule?.args[2] ??
|
|
484
|
-
assert.match(schedule?.args[2] ??
|
|
485
|
-
assert.match((reconcile?.args ?? []).join(
|
|
486
|
-
assert.match((reconcile?.args ?? []).join(
|
|
487
|
-
});
|
|
488
|
-
it(
|
|
489
|
-
const steps = buildDetachedSessionFinalizeSteps(
|
|
490
|
-
assert.deepEqual(steps.map((step) => step.name), [
|
|
491
|
-
});
|
|
492
|
-
it(
|
|
493
|
-
const steps = buildDetachedSessionFinalizeSteps(
|
|
494
|
-
assert.equal(steps.some((step) => step.name ===
|
|
495
|
-
assert.equal(steps.some((step) => step.args.includes(
|
|
496
|
-
});
|
|
497
|
-
it(
|
|
498
|
-
const steps = buildDetachedSessionRollbackSteps(
|
|
499
|
-
assert.deepEqual(steps.map((step) => step.name), [
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
assert.
|
|
505
|
-
assert.
|
|
506
|
-
assert.
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
assert.deepEqual(steps
|
|
539
|
+
assert.equal(names.includes("register-resize-hook"), true);
|
|
540
|
+
assert.equal(names.includes("reconcile-hud-resize"), true);
|
|
541
|
+
});
|
|
542
|
+
it("buildDetachedSessionFinalizeSteps uses quiet best-effort tmux resize commands", () => {
|
|
543
|
+
const steps = buildDetachedSessionFinalizeSteps("omx-demo", "%12", "3", false);
|
|
544
|
+
const registerHook = steps.find((step) => step.name === "register-resize-hook");
|
|
545
|
+
const schedule = steps.find((step) => step.name === "schedule-delayed-resize");
|
|
546
|
+
const reconcile = steps.find((step) => step.name === "reconcile-hud-resize");
|
|
547
|
+
assert.match(registerHook?.args[4] ?? "", />\/dev\/null 2>&1 \|\| true/);
|
|
548
|
+
assert.match(registerHook?.args[4] ?? "", new RegExp(`-y ${HUD_TMUX_HEIGHT_LINES}\\b`));
|
|
549
|
+
assert.match(schedule?.args[2] ?? "", />\/dev\/null 2>&1 \|\| true/);
|
|
550
|
+
assert.match(schedule?.args[2] ?? "", new RegExp(`-y ${HUD_TMUX_HEIGHT_LINES}\\b`));
|
|
551
|
+
assert.match((reconcile?.args ?? []).join(" "), />\/dev\/null 2>&1 \|\| true/);
|
|
552
|
+
assert.match((reconcile?.args ?? []).join(" "), new RegExp(`-y ${HUD_TMUX_HEIGHT_LINES}\\b`));
|
|
553
|
+
});
|
|
554
|
+
it("buildDetachedSessionFinalizeSteps skips detached resize hooks on native Windows", () => {
|
|
555
|
+
const steps = buildDetachedSessionFinalizeSteps("omx-demo", "%12", "3", true, true);
|
|
556
|
+
assert.deepEqual(steps.map((step) => step.name), ["set-mouse", "attach-session"]);
|
|
557
|
+
});
|
|
558
|
+
it("buildDetachedSessionFinalizeSteps never appends server-global terminal-overrides", () => {
|
|
559
|
+
const steps = buildDetachedSessionFinalizeSteps("omx-demo", "%12", "3", true);
|
|
560
|
+
assert.equal(steps.some((step) => step.name === "set-wsl-xt"), false);
|
|
561
|
+
assert.equal(steps.some((step) => step.args.includes("terminal-overrides")), false);
|
|
562
|
+
});
|
|
563
|
+
it("buildDetachedSessionRollbackSteps unregisters hooks before killing session", () => {
|
|
564
|
+
const steps = buildDetachedSessionRollbackSteps("omx-demo", "omx-demo:0", "omx_resize_launch_demo_0_12", "omx_attached_launch_demo_0_12");
|
|
565
|
+
assert.deepEqual(steps.map((step) => step.name), [
|
|
566
|
+
"unregister-client-attached-reconcile",
|
|
567
|
+
"unregister-resize-hook",
|
|
568
|
+
"kill-session",
|
|
569
|
+
]);
|
|
570
|
+
assert.equal(steps[0]?.args[0], "set-hook");
|
|
571
|
+
assert.equal(steps[0]?.args[1], "-u");
|
|
572
|
+
assert.equal(steps[0]?.args[2], "-t");
|
|
573
|
+
assert.equal(steps[0]?.args[3], "omx-demo:0");
|
|
574
|
+
assert.match(steps[0]?.args[4] ?? "", /^client-attached\[\d+\]$/);
|
|
575
|
+
assert.match(steps[1]?.args[4] ?? "", /^client-resized\[\d+\]$/);
|
|
576
|
+
assert.deepEqual(steps[2]?.args, ["kill-session", "-t", "omx-demo"]);
|
|
577
|
+
});
|
|
578
|
+
it("buildDetachedSessionRollbackSteps only kills session when no hook metadata exists", () => {
|
|
579
|
+
const steps = buildDetachedSessionRollbackSteps("omx-demo", null, null, null);
|
|
580
|
+
assert.deepEqual(steps.map((step) => step.name), ["kill-session"]);
|
|
511
581
|
});
|
|
512
582
|
});
|
|
513
|
-
describe(
|
|
514
|
-
it(
|
|
515
|
-
assert.equal(buildTmuxShellCommand(
|
|
583
|
+
describe("buildTmuxShellCommand", () => {
|
|
584
|
+
it("preserves quoted config values for tmux shell-command execution", () => {
|
|
585
|
+
assert.equal(buildTmuxShellCommand("codex", [
|
|
586
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
587
|
+
"-c",
|
|
588
|
+
'model_reasoning_effort="xhigh"',
|
|
589
|
+
]), `'codex' '--dangerously-bypass-approvals-and-sandbox' '-c' 'model_reasoning_effort="xhigh"'`);
|
|
516
590
|
});
|
|
517
591
|
});
|
|
518
|
-
describe(
|
|
519
|
-
it(
|
|
520
|
-
const result = buildTmuxPaneCommand(
|
|
521
|
-
assert.ok(result.startsWith("'/usr/bin/zsh' -lc "),
|
|
522
|
-
assert.ok(result.includes(
|
|
523
|
-
assert.ok(result.includes(
|
|
524
|
-
});
|
|
525
|
-
it(
|
|
526
|
-
const result = buildTmuxPaneCommand(
|
|
527
|
-
assert.ok(result.startsWith("'/bin/bash' -lc "),
|
|
528
|
-
assert.ok(result.includes(
|
|
529
|
-
assert.ok(result.includes(
|
|
530
|
-
});
|
|
531
|
-
it(
|
|
532
|
-
const result = buildTmuxPaneCommand(
|
|
533
|
-
assert.ok(result.startsWith("'/bin/fish' -lc "),
|
|
534
|
-
assert.ok(!result.includes(
|
|
535
|
-
assert.ok(result.includes(
|
|
536
|
-
});
|
|
537
|
-
it(
|
|
538
|
-
const result = buildTmuxPaneCommand(
|
|
539
|
-
assert.ok(result.startsWith("'/bin/sh' -lc "),
|
|
592
|
+
describe("buildTmuxPaneCommand", () => {
|
|
593
|
+
it("wraps command with zsh profile sourcing for zsh shell", () => {
|
|
594
|
+
const result = buildTmuxPaneCommand("codex", ["--model", "gpt-5"], "/usr/bin/zsh");
|
|
595
|
+
assert.ok(result.startsWith("'/usr/bin/zsh' -lc "), "should start with zsh login shell");
|
|
596
|
+
assert.ok(result.includes("source ~/.zshrc"), "should source .zshrc");
|
|
597
|
+
assert.ok(result.includes("exec "), "should exec the command");
|
|
598
|
+
});
|
|
599
|
+
it("wraps command with bash profile sourcing for bash shell", () => {
|
|
600
|
+
const result = buildTmuxPaneCommand("codex", [], "/bin/bash");
|
|
601
|
+
assert.ok(result.startsWith("'/bin/bash' -lc "), "should start with bash login shell");
|
|
602
|
+
assert.ok(result.includes("source ~/.bashrc"), "should source .bashrc");
|
|
603
|
+
assert.ok(result.includes("exec "), "should exec the command");
|
|
604
|
+
});
|
|
605
|
+
it("skips rc sourcing for unknown shells but still uses login flag", () => {
|
|
606
|
+
const result = buildTmuxPaneCommand("codex", [], "/bin/fish");
|
|
607
|
+
assert.ok(result.startsWith("'/bin/fish' -lc "), "should start with fish login shell");
|
|
608
|
+
assert.ok(!result.includes("source"), "should not source any rc file");
|
|
609
|
+
assert.ok(result.includes("exec "), "should exec the command");
|
|
610
|
+
});
|
|
611
|
+
it("falls back to /bin/sh when shell path is empty", () => {
|
|
612
|
+
const result = buildTmuxPaneCommand("codex", [], "");
|
|
613
|
+
assert.ok(result.startsWith("'/bin/sh' -lc "), "should fall back to /bin/sh");
|
|
540
614
|
});
|
|
541
615
|
});
|
|
542
|
-
describe(
|
|
543
|
-
it(
|
|
544
|
-
const result = buildWindowsPromptCommand(
|
|
545
|
-
|
|
616
|
+
describe("buildWindowsPromptCommand", () => {
|
|
617
|
+
it("encodes detached Windows commands for safe PowerShell prompt injection", () => {
|
|
618
|
+
const result = buildWindowsPromptCommand("codex", [
|
|
619
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
620
|
+
"-c",
|
|
621
|
+
'model_reasoning_effort="high"',
|
|
622
|
+
"it's",
|
|
623
|
+
]);
|
|
624
|
+
const prefix = "powershell.exe -NoLogo -NoExit -EncodedCommand ";
|
|
546
625
|
assert.ok(result.startsWith(prefix));
|
|
547
626
|
const payload = result.slice(prefix.length);
|
|
548
|
-
const decoded = Buffer.from(payload,
|
|
627
|
+
const decoded = Buffer.from(payload, "base64").toString("utf16le");
|
|
549
628
|
assert.equal(decoded, "$ErrorActionPreference = 'Stop'; & { & 'codex' '--dangerously-bypass-approvals-and-sandbox' '-c' 'model_reasoning_effort=\"high\"' 'it''s' }");
|
|
550
629
|
});
|
|
551
630
|
});
|
|
552
|
-
describe(
|
|
553
|
-
it(
|
|
554
|
-
const name = buildTmuxSessionName(
|
|
555
|
-
assert.equal(name,
|
|
631
|
+
describe("buildTmuxSessionName", () => {
|
|
632
|
+
it("uses detached fallback quietly outside git repos", () => {
|
|
633
|
+
const name = buildTmuxSessionName("/tmp/My Repo", "omx-1770992424158-abc123");
|
|
634
|
+
assert.equal(name, "omx-my-repo-detached-1770992424158-abc123");
|
|
556
635
|
});
|
|
557
|
-
it(
|
|
558
|
-
const name = buildTmuxSessionName(
|
|
636
|
+
it("sanitizes invalid characters", () => {
|
|
637
|
+
const name = buildTmuxSessionName("/tmp/@#$", "omx-+++");
|
|
559
638
|
assert.match(name, /^omx-(unknown|[a-z0-9-]+)-[a-z0-9-]+-(unknown|[a-z0-9-]+)$/);
|
|
560
|
-
assert.equal(name.includes(
|
|
561
|
-
assert.equal(name.includes(
|
|
639
|
+
assert.equal(name.includes("_"), false);
|
|
640
|
+
assert.equal(name.includes(" "), false);
|
|
562
641
|
});
|
|
563
|
-
it(
|
|
564
|
-
const name = buildTmuxSessionName(
|
|
642
|
+
it("includes repo name when cwd is inside .omx-worktrees", () => {
|
|
643
|
+
const name = buildTmuxSessionName("/home/user/my-repo.omx-worktrees/launch-feature-x", "omx-123-abc");
|
|
565
644
|
assert.match(name, /^omx-my-repo-launch-feature-x-/);
|
|
566
645
|
});
|
|
567
|
-
it(
|
|
568
|
-
const name = buildTmuxSessionName(
|
|
646
|
+
it("includes repo name for detached worktree paths", () => {
|
|
647
|
+
const name = buildTmuxSessionName("/projects/cool-project.omx-worktrees/launch-detached", "omx-456-def");
|
|
569
648
|
assert.match(name, /^omx-cool-project-launch-detached-/);
|
|
570
649
|
});
|
|
650
|
+
it("includes repo name when cwd is inside .omx/worktrees", () => {
|
|
651
|
+
const name = buildTmuxSessionName("/home/user/my-repo/.omx/worktrees/autoresearch-demo", "omx-789-ghi");
|
|
652
|
+
assert.match(name, /^omx-my-repo-autoresearch-demo-/);
|
|
653
|
+
});
|
|
571
654
|
});
|
|
572
|
-
describe(
|
|
573
|
-
it(
|
|
574
|
-
assert.deepEqual(collectInheritableTeamWorkerArgs([
|
|
655
|
+
describe("team worker launch arg inheritance helpers", () => {
|
|
656
|
+
it("collectInheritableTeamWorkerArgs extracts bypass, reasoning, and model overrides", () => {
|
|
657
|
+
assert.deepEqual(collectInheritableTeamWorkerArgs([
|
|
658
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
659
|
+
"-c",
|
|
660
|
+
'model_reasoning_effort="xhigh"',
|
|
661
|
+
"--model",
|
|
662
|
+
"gpt-5",
|
|
663
|
+
]), [
|
|
664
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
665
|
+
"-c",
|
|
666
|
+
'model_reasoning_effort="xhigh"',
|
|
667
|
+
"--model",
|
|
668
|
+
"gpt-5",
|
|
669
|
+
]);
|
|
575
670
|
});
|
|
576
|
-
it(
|
|
577
|
-
assert.deepEqual(collectInheritableTeamWorkerArgs([
|
|
671
|
+
it("collectInheritableTeamWorkerArgs supports --model=<value> syntax", () => {
|
|
672
|
+
assert.deepEqual(collectInheritableTeamWorkerArgs(["--model=gpt-5.3-codex"]), ["--model", "gpt-5.3-codex"]);
|
|
578
673
|
});
|
|
579
|
-
it(
|
|
580
|
-
assert.equal(resolveTeamWorkerLaunchArgsEnv('--dangerously-bypass-approvals-and-sandbox -c model_reasoning_effort="high" --model old-a --no-alt-screen --model=old-b', [
|
|
674
|
+
it("resolveTeamWorkerLaunchArgsEnv merges and normalizes with de-dupe + last reasoning/model wins", () => {
|
|
675
|
+
assert.equal(resolveTeamWorkerLaunchArgsEnv('--dangerously-bypass-approvals-and-sandbox -c model_reasoning_effort="high" --model old-a --no-alt-screen --model=old-b', [
|
|
676
|
+
"-c",
|
|
677
|
+
'model_reasoning_effort="xhigh"',
|
|
678
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
679
|
+
"--model",
|
|
680
|
+
"gpt-5",
|
|
681
|
+
], true), '--no-alt-screen --dangerously-bypass-approvals-and-sandbox -c model_reasoning_effort="xhigh" --model old-b');
|
|
581
682
|
});
|
|
582
|
-
it(
|
|
583
|
-
assert.equal(resolveTeamWorkerLaunchArgsEnv(
|
|
683
|
+
it("resolveTeamWorkerLaunchArgsEnv can opt out of leader inheritance", () => {
|
|
684
|
+
assert.equal(resolveTeamWorkerLaunchArgsEnv("--no-alt-screen", [
|
|
685
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
686
|
+
"-c",
|
|
687
|
+
'model_reasoning_effort="xhigh"',
|
|
688
|
+
], false), "--no-alt-screen");
|
|
584
689
|
});
|
|
585
|
-
it(
|
|
586
|
-
assert.equal(resolveTeamWorkerLaunchArgsEnv(
|
|
690
|
+
it("resolveTeamWorkerLaunchArgsEnv uses inherited model when env model is absent", () => {
|
|
691
|
+
assert.equal(resolveTeamWorkerLaunchArgsEnv("--no-alt-screen", ["--model=gpt-5.3-codex"], true), "--no-alt-screen --model gpt-5.3-codex");
|
|
587
692
|
});
|
|
588
|
-
it(
|
|
589
|
-
assert.equal(resolveTeamWorkerLaunchArgsEnv(
|
|
693
|
+
it("resolveTeamWorkerLaunchArgsEnv uses frontier default model when env and inherited models are absent", () => {
|
|
694
|
+
assert.equal(resolveTeamWorkerLaunchArgsEnv("--no-alt-screen", ["--dangerously-bypass-approvals-and-sandbox"], true, DEFAULT_FRONTIER_MODEL), `--no-alt-screen --dangerously-bypass-approvals-and-sandbox --model ${DEFAULT_FRONTIER_MODEL}`);
|
|
590
695
|
});
|
|
591
|
-
it(
|
|
592
|
-
assert.equal(resolveTeamWorkerLaunchArgsEnv(
|
|
696
|
+
it("resolveTeamWorkerLaunchArgsEnv keeps exactly one final model with precedence env > inherited > default", () => {
|
|
697
|
+
assert.equal(resolveTeamWorkerLaunchArgsEnv("--model env-model --model=env-model-final", ["--model", "inherited-model"], true, "fallback-model"), "--model env-model-final");
|
|
593
698
|
});
|
|
594
|
-
it(
|
|
595
|
-
assert.equal(resolveTeamWorkerLaunchArgsEnv(
|
|
699
|
+
it("resolveTeamWorkerLaunchArgsEnv prefers inherited model over default when env model is absent", () => {
|
|
700
|
+
assert.equal(resolveTeamWorkerLaunchArgsEnv("--no-alt-screen", ["--model", "inherited-model"], true, "fallback-model"), "--no-alt-screen --model inherited-model");
|
|
596
701
|
});
|
|
597
702
|
});
|
|
598
|
-
describe(
|
|
599
|
-
it(
|
|
600
|
-
const value = readTopLevelTomlString('model_reasoning_effort = "high"\n[mcp_servers.test]\nmodel_reasoning_effort = "low"\n',
|
|
601
|
-
assert.equal(value,
|
|
703
|
+
describe("readTopLevelTomlString", () => {
|
|
704
|
+
it("reads a top-level string value", () => {
|
|
705
|
+
const value = readTopLevelTomlString('model_reasoning_effort = "high"\n[mcp_servers.test]\nmodel_reasoning_effort = "low"\n', "model_reasoning_effort");
|
|
706
|
+
assert.equal(value, "high");
|
|
602
707
|
});
|
|
603
|
-
it(
|
|
604
|
-
const value = readTopLevelTomlString('[mcp_servers.test]\nmodel_reasoning_effort = "xhigh"\n',
|
|
708
|
+
it("ignores table-local values", () => {
|
|
709
|
+
const value = readTopLevelTomlString('[mcp_servers.test]\nmodel_reasoning_effort = "xhigh"\n', "model_reasoning_effort");
|
|
605
710
|
assert.equal(value, null);
|
|
606
711
|
});
|
|
607
712
|
});
|
|
608
|
-
describe(
|
|
609
|
-
it(
|
|
610
|
-
const args = injectModelInstructionsBypassArgs(
|
|
611
|
-
assert.deepEqual(args, [
|
|
713
|
+
describe("injectModelInstructionsBypassArgs", () => {
|
|
714
|
+
it("appends model_instructions_file override by default", () => {
|
|
715
|
+
const args = injectModelInstructionsBypassArgs("/tmp/my-project", ["--model", "gpt-5"], {});
|
|
716
|
+
assert.deepEqual(args, [
|
|
717
|
+
"--model",
|
|
718
|
+
"gpt-5",
|
|
719
|
+
"-c",
|
|
720
|
+
'model_instructions_file="/tmp/my-project/AGENTS.md"',
|
|
721
|
+
]);
|
|
612
722
|
});
|
|
613
|
-
it(
|
|
614
|
-
const args = injectModelInstructionsBypassArgs(
|
|
615
|
-
assert.deepEqual(args, [
|
|
723
|
+
it("does not append when bypass is disabled via env", () => {
|
|
724
|
+
const args = injectModelInstructionsBypassArgs("/tmp/my-project", ["--model", "gpt-5"], { OMX_BYPASS_DEFAULT_SYSTEM_PROMPT: "0" });
|
|
725
|
+
assert.deepEqual(args, ["--model", "gpt-5"]);
|
|
616
726
|
});
|
|
617
|
-
it(
|
|
618
|
-
const args = injectModelInstructionsBypassArgs(
|
|
619
|
-
assert.deepEqual(args, [
|
|
727
|
+
it("does not append when model_instructions_file is already set", () => {
|
|
728
|
+
const args = injectModelInstructionsBypassArgs("/tmp/my-project", ["-c", 'model_instructions_file="/tmp/custom.md"'], {});
|
|
729
|
+
assert.deepEqual(args, ["-c", 'model_instructions_file="/tmp/custom.md"']);
|
|
620
730
|
});
|
|
621
|
-
it(
|
|
622
|
-
const args = injectModelInstructionsBypassArgs(
|
|
623
|
-
|
|
731
|
+
it("respects OMX_MODEL_INSTRUCTIONS_FILE env override", () => {
|
|
732
|
+
const args = injectModelInstructionsBypassArgs("/tmp/my-project", [], {
|
|
733
|
+
OMX_MODEL_INSTRUCTIONS_FILE: "/tmp/alt instructions.md",
|
|
734
|
+
});
|
|
735
|
+
assert.deepEqual(args, [
|
|
736
|
+
"-c",
|
|
737
|
+
'model_instructions_file="/tmp/alt instructions.md"',
|
|
738
|
+
]);
|
|
624
739
|
});
|
|
625
|
-
it(
|
|
626
|
-
const args = injectModelInstructionsBypassArgs(
|
|
627
|
-
assert.deepEqual(args, [
|
|
740
|
+
it("uses session-scoped default model_instructions_file when provided", () => {
|
|
741
|
+
const args = injectModelInstructionsBypassArgs("/tmp/my-project", ["--model", "gpt-5"], {}, "/tmp/my-project/.omx/state/sessions/session-1/AGENTS.md");
|
|
742
|
+
assert.deepEqual(args, [
|
|
743
|
+
"--model",
|
|
744
|
+
"gpt-5",
|
|
745
|
+
"-c",
|
|
746
|
+
'model_instructions_file="/tmp/my-project/.omx/state/sessions/session-1/AGENTS.md"',
|
|
747
|
+
]);
|
|
628
748
|
});
|
|
629
749
|
});
|
|
630
|
-
describe(
|
|
631
|
-
it(
|
|
632
|
-
const updated = upsertTopLevelTomlString('model_reasoning_effort = "low"\n[tui]\nstatus_line = []\n',
|
|
750
|
+
describe("upsertTopLevelTomlString", () => {
|
|
751
|
+
it("replaces an existing top-level key", () => {
|
|
752
|
+
const updated = upsertTopLevelTomlString('model_reasoning_effort = "low"\n[tui]\nstatus_line = []\n', "model_reasoning_effort", "high");
|
|
633
753
|
assert.match(updated, /^model_reasoning_effort = "high"$/m);
|
|
634
754
|
assert.doesNotMatch(updated, /^model_reasoning_effort = "low"$/m);
|
|
635
755
|
});
|
|
636
|
-
it(
|
|
637
|
-
const updated = upsertTopLevelTomlString(
|
|
756
|
+
it("inserts before the first table when key is missing", () => {
|
|
757
|
+
const updated = upsertTopLevelTomlString("[tui]\nstatus_line = []\n", "model_reasoning_effort", "xhigh");
|
|
638
758
|
assert.equal(updated, 'model_reasoning_effort = "xhigh"\n[tui]\nstatus_line = []\n');
|
|
639
759
|
});
|
|
640
760
|
});
|