oh-my-codex 0.13.0 → 0.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +5 -5
- package/Cargo.toml +1 -1
- package/README.md +40 -6
- package/crates/omx-explore/src/main.rs +221 -10
- package/dist/catalog/__tests__/generator.test.js +2 -0
- package/dist/catalog/__tests__/generator.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +150 -1
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/setup-skills-overwrite.test.js +41 -3
- package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +25 -1
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +73 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +15 -0
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/update.js +1 -1
- package/dist/cli/update.js.map +1 -1
- package/dist/hooks/__tests__/agents-overlay.test.js +20 -2
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/analyze-routing-contract.test.d.ts +2 -0
- package/dist/hooks/__tests__/analyze-routing-contract.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/analyze-routing-contract.test.js +36 -0
- package/dist/hooks/__tests__/analyze-routing-contract.test.js.map +1 -0
- package/dist/hooks/__tests__/analyze-skill-contract.test.d.ts +2 -0
- package/dist/hooks/__tests__/analyze-skill-contract.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/analyze-skill-contract.test.js +48 -0
- package/dist/hooks/__tests__/analyze-skill-contract.test.js.map +1 -0
- package/dist/hooks/__tests__/keyword-detector.test.js +32 -0
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +185 -8
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js +26 -0
- package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js +44 -0
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +126 -0
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/session.test.js +21 -0
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/agents-overlay.d.ts.map +1 -1
- package/dist/hooks/agents-overlay.js +9 -0
- package/dist/hooks/agents-overlay.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +8 -1
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +9 -0
- package/dist/hooks/session.js.map +1 -1
- package/dist/hud/__tests__/state.test.js +55 -0
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/state.d.ts.map +1 -1
- package/dist/hud/state.js +23 -4
- package/dist/hud/state.js.map +1 -1
- package/dist/mcp/__tests__/bootstrap.test.js +38 -0
- package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
- package/dist/mcp/bootstrap.d.ts +1 -1
- package/dist/mcp/bootstrap.d.ts.map +1 -1
- package/dist/mcp/bootstrap.js +11 -3
- package/dist/mcp/bootstrap.js.map +1 -1
- package/dist/notifications/__tests__/reply-listener.test.js +34 -1
- package/dist/notifications/__tests__/reply-listener.test.js.map +1 -1
- package/dist/notifications/reply-listener.d.ts +1 -0
- package/dist/notifications/reply-listener.d.ts.map +1 -1
- package/dist/notifications/reply-listener.js +14 -2
- package/dist/notifications/reply-listener.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +248 -15
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/generate-release-body.test.d.ts +2 -0
- package/dist/scripts/__tests__/generate-release-body.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/generate-release-body.test.js +144 -0
- package/dist/scripts/__tests__/generate-release-body.test.js.map +1 -0
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +39 -49
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/generate-release-body.d.ts +34 -0
- package/dist/scripts/generate-release-body.d.ts.map +1 -0
- package/dist/scripts/generate-release-body.js +249 -0
- package/dist/scripts/generate-release-body.js.map +1 -0
- package/dist/scripts/notify-fallback-watcher.js +43 -20
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/active-team.d.ts.map +1 -1
- package/dist/scripts/notify-hook/active-team.js +2 -1
- package/dist/scripts/notify-hook/active-team.js.map +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.js +17 -2
- package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
- package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
- package/dist/scripts/notify-hook/state-io.js +16 -0
- package/dist/scripts/notify-hook/state-io.js.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.js +26 -5
- package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
- package/dist/scripts/notify-hook.js +1 -7
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/team/__tests__/model-contract.test.js +6 -0
- package/dist/team/__tests__/model-contract.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +1 -1
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/worker-runtime-identity.test.d.ts +2 -0
- package/dist/team/__tests__/worker-runtime-identity.test.d.ts.map +1 -0
- package/dist/team/__tests__/worker-runtime-identity.test.js +250 -0
- package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -0
- package/dist/team/leader-activity.d.ts.map +1 -1
- package/dist/team/leader-activity.js +26 -15
- package/dist/team/leader-activity.js.map +1 -1
- package/dist/team/model-contract.d.ts.map +1 -1
- package/dist/team/model-contract.js.map +1 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +9 -8
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +10 -9
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +3 -2
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js +3 -0
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
- package/dist/wiki/__tests__/slug-nonascii.test.js +11 -5
- package/dist/wiki/__tests__/slug-nonascii.test.js.map +1 -1
- package/dist/wiki/storage.d.ts.map +1 -1
- package/dist/wiki/storage.js +2 -1
- package/dist/wiki/storage.js.map +1 -1
- package/package.json +3 -1
- package/skills/analyze/SKILL.md +101 -134
- package/src/scripts/__tests__/codex-native-hook.test.ts +297 -17
- package/src/scripts/__tests__/generate-release-body.test.ts +166 -0
- package/src/scripts/codex-native-hook.ts +99 -66
- package/src/scripts/generate-release-body.ts +295 -0
- package/src/scripts/notify-fallback-watcher.ts +44 -21
- package/src/scripts/notify-hook/active-team.ts +2 -1
- package/src/scripts/notify-hook/ralph-session-resume.ts +17 -2
- package/src/scripts/notify-hook/state-io.ts +16 -0
- package/src/scripts/notify-hook/team-leader-nudge.ts +24 -4
- package/src/scripts/notify-hook.ts +1 -6
- package/templates/AGENTS.md +1 -1
- package/templates/catalog-manifest.json +2 -4
package/Cargo.lock
CHANGED
|
@@ -32,11 +32,11 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
|
|
32
32
|
|
|
33
33
|
[[package]]
|
|
34
34
|
name = "omx-explore-harness"
|
|
35
|
-
version = "0.13.
|
|
35
|
+
version = "0.13.2"
|
|
36
36
|
|
|
37
37
|
[[package]]
|
|
38
38
|
name = "omx-mux"
|
|
39
|
-
version = "0.13.
|
|
39
|
+
version = "0.13.2"
|
|
40
40
|
dependencies = [
|
|
41
41
|
"serde",
|
|
42
42
|
"serde_json",
|
|
@@ -44,7 +44,7 @@ dependencies = [
|
|
|
44
44
|
|
|
45
45
|
[[package]]
|
|
46
46
|
name = "omx-runtime"
|
|
47
|
-
version = "0.13.
|
|
47
|
+
version = "0.13.2"
|
|
48
48
|
dependencies = [
|
|
49
49
|
"omx-mux",
|
|
50
50
|
"omx-runtime-core",
|
|
@@ -53,7 +53,7 @@ dependencies = [
|
|
|
53
53
|
|
|
54
54
|
[[package]]
|
|
55
55
|
name = "omx-runtime-core"
|
|
56
|
-
version = "0.13.
|
|
56
|
+
version = "0.13.2"
|
|
57
57
|
dependencies = [
|
|
58
58
|
"fs2",
|
|
59
59
|
"serde",
|
|
@@ -62,7 +62,7 @@ dependencies = [
|
|
|
62
62
|
|
|
63
63
|
[[package]]
|
|
64
64
|
name = "omx-sparkshell"
|
|
65
|
-
version = "0.13.
|
|
65
|
+
version = "0.13.2"
|
|
66
66
|
dependencies = [
|
|
67
67
|
"omx-mux",
|
|
68
68
|
]
|
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -72,6 +72,7 @@ $team 3:executor "execute the approved plan in parallel"
|
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
That is the main path.
|
|
75
|
+
Before you treat the runtime as ready, run the quick-start smoke test below: `omx doctor` verifies the install shape, while `omx exec` proves the active Codex runtime can actually authenticate and complete a model call from the current environment.
|
|
75
76
|
Start OMX strongly, clarify first when needed, approve the plan, then choose `$team` for coordinated parallel execution or `$ralph` for the persistent completion loop.
|
|
76
77
|
|
|
77
78
|
## What OMX is for
|
|
@@ -90,12 +91,22 @@ If you want plain Codex with no extra workflow layer, you probably do not need O
|
|
|
90
91
|
|
|
91
92
|
- Node.js 20+
|
|
92
93
|
- Codex CLI installed: `npm install -g @openai/codex`
|
|
93
|
-
- Codex auth configured
|
|
94
|
+
- Codex auth configured and visible in the same shell/profile that will run OMX
|
|
94
95
|
- `tmux` on macOS/Linux if you want the recommended durable team runtime
|
|
95
96
|
- `psmux` on native Windows only if you intentionally want the less-supported Windows team path
|
|
96
97
|
|
|
97
98
|
### A good first session
|
|
98
99
|
|
|
100
|
+
After install, check both boundaries:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
omx doctor
|
|
104
|
+
codex login status
|
|
105
|
+
omx exec --skip-git-repo-check -C . "Reply with exactly OMX-EXEC-OK"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`omx doctor` catches missing OMX files, hooks, and runtime prerequisites. The real smoke test catches auth, profile, and provider/base-URL problems that only appear when Codex performs an actual request.
|
|
109
|
+
|
|
99
110
|
Launch OMX the recommended way:
|
|
100
111
|
|
|
101
112
|
```bash
|
|
@@ -135,10 +146,12 @@ Most users should think of OMX as **better task routing + better workflow + bett
|
|
|
135
146
|
## Start here if you are new
|
|
136
147
|
|
|
137
148
|
1. Run `omx setup`
|
|
138
|
-
2.
|
|
139
|
-
3.
|
|
140
|
-
4.
|
|
141
|
-
5.
|
|
149
|
+
2. Run `omx doctor`
|
|
150
|
+
3. Run a real execution smoke test: `codex login status` and `omx exec --skip-git-repo-check -C . "Reply with exactly OMX-EXEC-OK"`
|
|
151
|
+
4. Launch with `omx --madmax --high`
|
|
152
|
+
5. Use `$deep-interview "..."` when the request or boundaries are still unclear
|
|
153
|
+
6. Use `$ralplan "..."` to approve the plan and review tradeoffs
|
|
154
|
+
7. Choose `$team` for coordinated parallel execution or `$ralph` for persistent completion loops
|
|
142
155
|
|
|
143
156
|
## Recommended workflow
|
|
144
157
|
|
|
@@ -177,7 +190,7 @@ These are operator/support surfaces:
|
|
|
177
190
|
- `omx setup` installs prompts, skills, AGENTS scaffolding, `.codex/config.toml`, and OMX-managed native Codex hooks in `.codex/hooks.json`
|
|
178
191
|
- setup refresh preserves non-OMX hook entries in `.codex/hooks.json` and only rewrites OMX-managed wrappers
|
|
179
192
|
- `omx uninstall` removes OMX-managed wrappers from `.codex/hooks.json` but keeps the file when user hooks remain
|
|
180
|
-
- `omx doctor` verifies the install when something seems wrong
|
|
193
|
+
- `omx doctor` verifies the install when something seems wrong; it does not prove that the active Codex profile can make an authenticated model call
|
|
181
194
|
- `omx hud --watch` is a monitoring/status surface, not the primary user workflow
|
|
182
195
|
|
|
183
196
|
For non-team sessions, native Codex hooks are now the canonical lifecycle surface:
|
|
@@ -187,6 +200,26 @@ For non-team sessions, native Codex hooks are now the canonical lifecycle surfac
|
|
|
187
200
|
|
|
188
201
|
See [Codex native hook mapping](./docs/codex-native-hooks.md) for the current native / fallback matrix.
|
|
189
202
|
|
|
203
|
+
|
|
204
|
+
### Troubleshooting false-green readiness
|
|
205
|
+
|
|
206
|
+
A green `omx doctor` means the install and local runtime wiring look sane. If real execution still fails, check the environment Codex actually uses:
|
|
207
|
+
|
|
208
|
+
- Run `codex login status` and `omx exec --skip-git-repo-check -C . "Reply with exactly OMX-EXEC-OK"` from the same shell/profile that will launch OMX.
|
|
209
|
+
- In custom HOME, profile, container, or service shells, confirm the active `~/.codex` (or `CODEX_HOME`) is the one with the expected auth and config. Do not assume your normal user `~/.codex` is visible there.
|
|
210
|
+
- If you depend on a local OpenAI-compatible proxy, confirm the active `~/.codex/config.toml` includes the expected `openai_base_url`; otherwise a proxy-issued key can be sent to the default endpoint and fail with `401 Unauthorized`, `Missing bearer or basic authentication in header`, or `Incorrect API key provided`.
|
|
211
|
+
- If `omx doctor --team` or resume reports a stale team such as `resume_blocker` or a missing tmux session, clean the dead runtime state before retrying:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
omx team shutdown <team-name> --force --confirm-issues
|
|
215
|
+
omx cancel
|
|
216
|
+
omx doctor --team
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Only use the forced team shutdown for a team you have confirmed is dead or intentionally abandoned.
|
|
220
|
+
|
|
221
|
+
If `Shift+Enter` still submits instead of inserting a newline inside an OMX-managed tmux session, see [Troubleshooting execution readiness](./docs/troubleshooting.md#shiftenter-submits-instead-of-inserting-a-newline-in-tmux-backed-omx-sessions). Current OMX already enables tmux extended-key forwarding around its own Codex launch paths, so a persistent failure is usually a tmux terminal-capability/discoverability problem rather than a net-new OMX feature gap.
|
|
222
|
+
|
|
190
223
|
### Explore and sparkshell
|
|
191
224
|
|
|
192
225
|
- `omx explore --prompt "..."` is for read-only repository lookup
|
|
@@ -251,6 +284,7 @@ If this happens, try:
|
|
|
251
284
|
- [Skills reference](./docs/skills.html)
|
|
252
285
|
- [Codex native hook mapping](./docs/codex-native-hooks.md)
|
|
253
286
|
- [Integrations](./docs/integrations.html)
|
|
287
|
+
- [Troubleshooting execution readiness](./docs/troubleshooting.md)
|
|
254
288
|
- [OpenClaw / notification gateway guide](./docs/openclaw-integration.md)
|
|
255
289
|
- [Contributing](./CONTRIBUTING.md)
|
|
256
290
|
- [Changelog](./CHANGELOG.md)
|
|
@@ -12,6 +12,8 @@ const CODEX_BIN_ENV: &str = "OMX_EXPLORE_CODEX_BIN";
|
|
|
12
12
|
const HARNESS_ROOT_ENV: &str = "OMX_EXPLORE_ROOT";
|
|
13
13
|
const INTERNAL_DIRECT_WRAPPER_FLAG: &str = "--internal-allowlist-direct";
|
|
14
14
|
const INTERNAL_SHELL_WRAPPER_FLAG: &str = "--internal-allowlist-shell";
|
|
15
|
+
const TEMP_ALLOWLIST_DIR_PREFIX: &str = "omx-explore-allowlist-";
|
|
16
|
+
const SHELL_STARTUP_ENV_VARS: &[&str] = &["BASH_ENV", "ENV", "PROMPT_COMMAND"];
|
|
15
17
|
const WINDOWS_UNSUPPORTED_ALLOWLIST_MESSAGE: &str =
|
|
16
18
|
"omx explore built-in harness is not ready on Windows because its allowlist runtime relies on POSIX sh/bash wrappers. Set OMX_EXPLORE_BIN to a compatible custom harness, prefer `omx sparkshell` for shell-native read-only lookups, or run `omx doctor` for readiness details.";
|
|
17
19
|
|
|
@@ -226,6 +228,7 @@ fn invoke_codex(args: &Args, model: &str, prompt_contract: &str) -> io::Result<A
|
|
|
226
228
|
.env(HARNESS_ROOT_ENV, &args.cwd)
|
|
227
229
|
.env("PATH", &allowlist.bin_dir)
|
|
228
230
|
.env("SHELL", &allowlist.shell_path);
|
|
231
|
+
sanitize_explore_subprocess_env(&mut command);
|
|
229
232
|
let output = command.output()?;
|
|
230
233
|
|
|
231
234
|
let markdown = read_to_string(&output_path).ok();
|
|
@@ -580,20 +583,46 @@ fn build_direct_wrapper(self_exe: &Path, command: &str) -> Result<String, String
|
|
|
580
583
|
|
|
581
584
|
fn resolve_host_command(command: &str) -> Option<PathBuf> {
|
|
582
585
|
let candidate = Path::new(command);
|
|
583
|
-
if candidate.is_absolute()
|
|
586
|
+
if candidate.is_absolute()
|
|
587
|
+
&& is_usable_host_command(candidate)
|
|
588
|
+
&& !is_omx_explore_allowlist_path(candidate)
|
|
589
|
+
{
|
|
584
590
|
return Some(candidate.to_path_buf());
|
|
585
591
|
}
|
|
586
592
|
|
|
587
593
|
let path = env::var_os("PATH")?;
|
|
588
594
|
for entry in env::split_paths(&path) {
|
|
589
595
|
let resolved = entry.join(command);
|
|
590
|
-
if is_usable_host_command(&resolved) {
|
|
596
|
+
if is_usable_host_command(&resolved) && !is_omx_explore_allowlist_path(&resolved) {
|
|
591
597
|
return Some(resolved);
|
|
592
598
|
}
|
|
593
599
|
}
|
|
594
600
|
None
|
|
595
601
|
}
|
|
596
602
|
|
|
603
|
+
fn is_omx_explore_allowlist_path(path: &Path) -> bool {
|
|
604
|
+
fn is_under_allowlist_bin(path: &Path) -> bool {
|
|
605
|
+
path.ancestors().any(|ancestor| {
|
|
606
|
+
let is_allowlist_root = ancestor
|
|
607
|
+
.file_name()
|
|
608
|
+
.and_then(|name| name.to_str())
|
|
609
|
+
.is_some_and(|name| name.starts_with(TEMP_ALLOWLIST_DIR_PREFIX));
|
|
610
|
+
if !is_allowlist_root {
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
path.strip_prefix(ancestor)
|
|
614
|
+
.ok()
|
|
615
|
+
.and_then(|relative| relative.components().next())
|
|
616
|
+
.is_some_and(|component| component.as_os_str() == "bin")
|
|
617
|
+
})
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
is_under_allowlist_bin(path)
|
|
621
|
+
|| canonicalize_existing_prefix(path)
|
|
622
|
+
.as_deref()
|
|
623
|
+
.is_some_and(is_under_allowlist_bin)
|
|
624
|
+
}
|
|
625
|
+
|
|
597
626
|
fn is_usable_host_command(path: &Path) -> bool {
|
|
598
627
|
let metadata = match path.metadata() {
|
|
599
628
|
Ok(metadata) => metadata,
|
|
@@ -617,6 +646,12 @@ fn shell_quote(value: &str) -> String {
|
|
|
617
646
|
format!("'{}'", value.replace('\'', "'\"'\"'"))
|
|
618
647
|
}
|
|
619
648
|
|
|
649
|
+
fn sanitize_explore_subprocess_env(command: &mut Command) {
|
|
650
|
+
for key in SHELL_STARTUP_ENV_VARS {
|
|
651
|
+
command.env_remove(key);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
620
655
|
fn run_internal_direct_wrapper<I>(mut args: I) -> Result<(), String>
|
|
621
656
|
where
|
|
622
657
|
I: Iterator<Item = OsString>,
|
|
@@ -653,6 +688,7 @@ where
|
|
|
653
688
|
if real_shell.ends_with("bash") {
|
|
654
689
|
child.arg("--noprofile").arg("--norc");
|
|
655
690
|
}
|
|
691
|
+
sanitize_explore_subprocess_env(&mut child);
|
|
656
692
|
let status = child
|
|
657
693
|
.arg("-lc")
|
|
658
694
|
.arg(&command)
|
|
@@ -735,7 +771,7 @@ fn validate_direct_command(command_name: &str, args: &[String]) -> Result<(), St
|
|
|
735
771
|
);
|
|
736
772
|
}
|
|
737
773
|
}
|
|
738
|
-
"find"
|
|
774
|
+
"find"
|
|
739
775
|
if args.iter().any(|arg| {
|
|
740
776
|
matches!(
|
|
741
777
|
arg.as_str(),
|
|
@@ -749,13 +785,14 @@ fn validate_direct_command(command_name: &str, args: &[String]) -> Result<(), St
|
|
|
749
785
|
| "-fprintf"
|
|
750
786
|
| "-fls"
|
|
751
787
|
)
|
|
752
|
-
})
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
788
|
+
}) =>
|
|
789
|
+
{
|
|
790
|
+
return Err(
|
|
791
|
+
"find actions that execute, delete, or write files are not allowed in omx explore"
|
|
792
|
+
.to_string(),
|
|
793
|
+
);
|
|
758
794
|
}
|
|
795
|
+
"find" => {}
|
|
759
796
|
"cat" => {
|
|
760
797
|
let operands = non_option_operands(args);
|
|
761
798
|
if operands.is_empty() {
|
|
@@ -987,7 +1024,17 @@ mod tests {
|
|
|
987
1024
|
#[test]
|
|
988
1025
|
fn resolve_codex_binary_resolves_bare_env_override_from_path() {
|
|
989
1026
|
let _guard = env_lock();
|
|
990
|
-
let root =
|
|
1027
|
+
let root = TempDirGuard {
|
|
1028
|
+
path: env::temp_dir().join(format!(
|
|
1029
|
+
"omx-explore-resolve-codex-binary-{}-{}",
|
|
1030
|
+
std::process::id(),
|
|
1031
|
+
SystemTime::now()
|
|
1032
|
+
.duration_since(UNIX_EPOCH)
|
|
1033
|
+
.unwrap_or_default()
|
|
1034
|
+
.as_nanos()
|
|
1035
|
+
)),
|
|
1036
|
+
};
|
|
1037
|
+
create_dir_all(&root.path).expect("create temp root");
|
|
991
1038
|
let bin_dir = root.path.join("bin");
|
|
992
1039
|
create_dir_all(&bin_dir).expect("create bin");
|
|
993
1040
|
let fake_codex = bin_dir.join("codex-custom");
|
|
@@ -1271,6 +1318,72 @@ exec node "$basedir/../@openai/codex/bin/codex.js" "$@"
|
|
|
1271
1318
|
assert!(!wrapper.contains("exit 127"));
|
|
1272
1319
|
}
|
|
1273
1320
|
|
|
1321
|
+
#[cfg(unix)]
|
|
1322
|
+
#[test]
|
|
1323
|
+
fn resolve_host_command_skips_omx_explore_allowlist_wrappers() {
|
|
1324
|
+
let _guard = env_lock();
|
|
1325
|
+
let allowlist_root = temp_allowlist_dir().expect("allowlist root");
|
|
1326
|
+
let real_root = TempDirGuard {
|
|
1327
|
+
path: env::temp_dir().join(format!(
|
|
1328
|
+
"omx-explore-host-resolution-{}-{}",
|
|
1329
|
+
std::process::id(),
|
|
1330
|
+
SystemTime::now()
|
|
1331
|
+
.duration_since(UNIX_EPOCH)
|
|
1332
|
+
.unwrap_or_default()
|
|
1333
|
+
.as_nanos()
|
|
1334
|
+
)),
|
|
1335
|
+
};
|
|
1336
|
+
create_dir_all(&real_root.path).expect("create real root");
|
|
1337
|
+
let real_bin = real_root.path.join("real-bin");
|
|
1338
|
+
let allowlist_bin = allowlist_root
|
|
1339
|
+
.path
|
|
1340
|
+
.join(format!("{TEMP_ALLOWLIST_DIR_PREFIX}fixture"))
|
|
1341
|
+
.join("bin");
|
|
1342
|
+
create_dir_all(&real_bin).expect("create real bin");
|
|
1343
|
+
create_dir_all(&allowlist_bin).expect("create allowlist bin");
|
|
1344
|
+
|
|
1345
|
+
let real_grep = real_bin.join("grep");
|
|
1346
|
+
write_executable(&real_grep, "#!/bin/sh\nexit 0\n").expect("write real grep");
|
|
1347
|
+
let wrapper_grep = allowlist_bin.join("grep");
|
|
1348
|
+
write_executable(&wrapper_grep, "#!/bin/sh\nexit 0\n").expect("write wrapper grep");
|
|
1349
|
+
|
|
1350
|
+
let original_path = env::var_os("PATH");
|
|
1351
|
+
unsafe {
|
|
1352
|
+
env::set_var(
|
|
1353
|
+
"PATH",
|
|
1354
|
+
env::join_paths([allowlist_bin.as_path(), real_bin.as_path()]).expect("join path"),
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
let resolved = resolve_host_command("grep");
|
|
1359
|
+
|
|
1360
|
+
match original_path {
|
|
1361
|
+
Some(value) => unsafe { env::set_var("PATH", value) },
|
|
1362
|
+
None => unsafe { env::remove_var("PATH") },
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
assert_eq!(resolved, Some(real_grep));
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
#[cfg(unix)]
|
|
1369
|
+
#[test]
|
|
1370
|
+
fn resolve_host_command_rejects_absolute_allowlist_wrapper_paths() {
|
|
1371
|
+
let _guard = env_lock();
|
|
1372
|
+
let root = temp_allowlist_dir().expect("temp root");
|
|
1373
|
+
let wrapper_dir = root
|
|
1374
|
+
.path
|
|
1375
|
+
.join(format!("{TEMP_ALLOWLIST_DIR_PREFIX}fixture"))
|
|
1376
|
+
.join("bin");
|
|
1377
|
+
create_dir_all(&wrapper_dir).expect("create wrapper dir");
|
|
1378
|
+
let wrapper = wrapper_dir.join("grep");
|
|
1379
|
+
write_executable(&wrapper, "#!/bin/sh\nexit 0\n").expect("write wrapper");
|
|
1380
|
+
|
|
1381
|
+
assert_eq!(
|
|
1382
|
+
resolve_host_command(wrapper.to_str().expect("wrapper path")),
|
|
1383
|
+
None
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1274
1387
|
#[test]
|
|
1275
1388
|
fn discover_codex_support_dirs_includes_home_omx_and_codex_when_present() {
|
|
1276
1389
|
let _guard = env_lock();
|
|
@@ -1520,6 +1633,104 @@ exit 17
|
|
|
1520
1633
|
let _error = result.expect_err("both attempts should fail");
|
|
1521
1634
|
}
|
|
1522
1635
|
|
|
1636
|
+
#[test]
|
|
1637
|
+
fn invoke_codex_clears_shell_startup_env_before_launching_codex() {
|
|
1638
|
+
let _guard = env_lock();
|
|
1639
|
+
let root = temp_allowlist_dir().expect("temp root");
|
|
1640
|
+
let repo = root.path.join("repo");
|
|
1641
|
+
create_dir_all(&repo).expect("create repo");
|
|
1642
|
+
let prompt_file = root.path.join("prompt.md");
|
|
1643
|
+
write(&prompt_file, "contract").expect("write prompt");
|
|
1644
|
+
let capture_path = root.path.join("capture.txt");
|
|
1645
|
+
let fake_codex = root.path.join("codex-stub");
|
|
1646
|
+
write_executable(
|
|
1647
|
+
&fake_codex,
|
|
1648
|
+
&format!(
|
|
1649
|
+
r#"#!/bin/sh
|
|
1650
|
+
set -eu
|
|
1651
|
+
output_path=""
|
|
1652
|
+
while [ "$#" -gt 0 ]; do
|
|
1653
|
+
if [ "$1" = "-o" ]; then
|
|
1654
|
+
shift
|
|
1655
|
+
output_path="$1"
|
|
1656
|
+
fi
|
|
1657
|
+
shift
|
|
1658
|
+
done
|
|
1659
|
+
printf 'BASH_ENV=%s\nENV=%s\nPROMPT_COMMAND=%s\n' "${{BASH_ENV:-}}" "${{ENV:-}}" "${{PROMPT_COMMAND:-}}" > {}
|
|
1660
|
+
printf '# Answer\nok\n' > "$output_path"
|
|
1661
|
+
"#,
|
|
1662
|
+
shell_quote(&capture_path.display().to_string())
|
|
1663
|
+
),
|
|
1664
|
+
)
|
|
1665
|
+
.expect("write fake codex");
|
|
1666
|
+
|
|
1667
|
+
unsafe {
|
|
1668
|
+
env::set_var(CODEX_BIN_ENV, &fake_codex);
|
|
1669
|
+
env::set_var("BASH_ENV", "/tmp/bash-env-should-not-leak");
|
|
1670
|
+
env::set_var("ENV", "/tmp/env-should-not-leak");
|
|
1671
|
+
env::set_var("PROMPT_COMMAND", "printf should-not-run");
|
|
1672
|
+
}
|
|
1673
|
+
let attempt = invoke_codex(
|
|
1674
|
+
&Args {
|
|
1675
|
+
cwd: repo.clone(),
|
|
1676
|
+
prompt: "find tests".to_string(),
|
|
1677
|
+
prompt_file,
|
|
1678
|
+
spark_model: "spark-model".to_string(),
|
|
1679
|
+
fallback_model: "fallback-model".to_string(),
|
|
1680
|
+
},
|
|
1681
|
+
"spark-model",
|
|
1682
|
+
"contract",
|
|
1683
|
+
)
|
|
1684
|
+
.expect("invoke codex");
|
|
1685
|
+
unsafe {
|
|
1686
|
+
env::remove_var(CODEX_BIN_ENV);
|
|
1687
|
+
env::remove_var("BASH_ENV");
|
|
1688
|
+
env::remove_var("ENV");
|
|
1689
|
+
env::remove_var("PROMPT_COMMAND");
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
assert_eq!(attempt.status_code, 0);
|
|
1693
|
+
assert_eq!(
|
|
1694
|
+
read_to_string(&capture_path).expect("read capture"),
|
|
1695
|
+
"BASH_ENV=\nENV=\nPROMPT_COMMAND=\n"
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
#[test]
|
|
1700
|
+
fn sanitize_explore_subprocess_env_blocks_bash_env_startup_hooks() {
|
|
1701
|
+
let _guard = env_lock();
|
|
1702
|
+
let root = temp_allowlist_dir().expect("temp root");
|
|
1703
|
+
let bash_env_log = root.path.join("bash-env.log");
|
|
1704
|
+
let bash_env = root.path.join("bash-env.sh");
|
|
1705
|
+
write(
|
|
1706
|
+
&bash_env,
|
|
1707
|
+
format!(
|
|
1708
|
+
"printf 'startup:%s\\n' \"$BASH_ENV\" >> {}\n",
|
|
1709
|
+
shell_quote(&bash_env_log.display().to_string())
|
|
1710
|
+
),
|
|
1711
|
+
)
|
|
1712
|
+
.expect("write bash env");
|
|
1713
|
+
let bash_path = resolve_host_command("bash").expect("host bash path");
|
|
1714
|
+
|
|
1715
|
+
unsafe {
|
|
1716
|
+
env::set_var("BASH_ENV", &bash_env);
|
|
1717
|
+
}
|
|
1718
|
+
let mut child = Command::new(&bash_path);
|
|
1719
|
+
child
|
|
1720
|
+
.arg("--noprofile")
|
|
1721
|
+
.arg("--norc")
|
|
1722
|
+
.arg("-lc")
|
|
1723
|
+
.arg("true");
|
|
1724
|
+
sanitize_explore_subprocess_env(&mut child);
|
|
1725
|
+
let status = child.status().expect("run bash");
|
|
1726
|
+
unsafe {
|
|
1727
|
+
env::remove_var("BASH_ENV");
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
assert!(status.success());
|
|
1731
|
+
assert_eq!(read_to_string(&bash_env_log).unwrap_or_default(), "");
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1523
1734
|
#[test]
|
|
1524
1735
|
fn print_attempt_output_requires_markdown_artifact() {
|
|
1525
1736
|
let result = print_attempt_output(AttemptResult {
|
|
@@ -30,8 +30,10 @@ describe('catalog reader/contract', () => {
|
|
|
30
30
|
assert.equal(contract.counts.skillCount, expected.skills);
|
|
31
31
|
assert.equal(contract.counts.promptCount, expected.agents);
|
|
32
32
|
assert.ok(contract.aliases.some((a) => a.name === 'swarm' && a.canonical === 'team'));
|
|
33
|
+
assert.ok(!contract.aliases.some((a) => a.name === 'analyze'));
|
|
33
34
|
assert.ok(contract.internalHidden.includes('worker'));
|
|
34
35
|
assert.ok(contract.coreSkills.includes('autopilot'));
|
|
36
|
+
assert.ok(contract.skills.some((s) => s.name === 'analyze' && s.status === 'active'));
|
|
35
37
|
assert.ok(contract.skills.some((s) => s.name === 'ask-claude' && s.status === 'active'));
|
|
36
38
|
assert.ok(contract.skills.some((s) => s.name === 'ask-gemini' && s.status === 'active'));
|
|
37
39
|
assert.ok(contract.skills.some((s) => s.name === 'ai-slop-cleaner' && s.status === 'active'));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generator.test.js","sourceRoot":"","sources":["../../../src/catalog/__tests__/generator.test.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"generator.test.js","sourceRoot":"","sources":["../../../src/catalog/__tests__/generator.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, it, mock } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, utimesSync } from "node:fs";
|
|
4
4
|
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
@@ -457,6 +457,7 @@ describe("reapStaleNotifyFallbackWatcher", () => {
|
|
|
457
457
|
await writeFile(pidPath, JSON.stringify({ pid: 4321, started_at: "2026-04-05T00:00:00.000Z" }), "utf-8");
|
|
458
458
|
const killed = [];
|
|
459
459
|
await reapStaleNotifyFallbackWatcher(pidPath, {
|
|
460
|
+
isWatcherProcess: () => true,
|
|
460
461
|
tryKillPid(pid, signal) {
|
|
461
462
|
killed.push({ pid, signal });
|
|
462
463
|
return true;
|
|
@@ -504,6 +505,7 @@ describe("reapStaleNotifyFallbackWatcher", () => {
|
|
|
504
505
|
const warned = [];
|
|
505
506
|
await reapStaleNotifyFallbackWatcher(pidPath, {
|
|
506
507
|
readFile: async (path, encoding) => readFile(path, encoding),
|
|
508
|
+
isWatcherProcess: () => true,
|
|
507
509
|
tryKillPid() {
|
|
508
510
|
throw new Error("permission denied");
|
|
509
511
|
},
|
|
@@ -984,7 +986,9 @@ describe("detached tmux new-session sequencing", () => {
|
|
|
984
986
|
assert.match(leaderCmd, /acquireTmuxExtendedKeysLease/);
|
|
985
987
|
assert.match(leaderCmd, /omx_detached_session_cleanup\(\)/);
|
|
986
988
|
assert.match(leaderCmd, /trap omx_detached_session_cleanup 0 INT TERM HUP;/);
|
|
989
|
+
assert.match(leaderCmd, /exec 3<&0;/);
|
|
987
990
|
assert.match(leaderCmd, /omx_codex_pid=\$!;/);
|
|
991
|
+
assert.match(leaderCmd, /<\&3 &/);
|
|
988
992
|
assert.match(leaderCmd, /wait "\$omx_codex_pid";/);
|
|
989
993
|
assert.match(leaderCmd, /kill -TERM "\$omx_codex_pid"/);
|
|
990
994
|
assert.match(leaderCmd, /releaseTmuxExtendedKeysLease/);
|
|
@@ -993,6 +997,59 @@ describe("detached tmux new-session sequencing", () => {
|
|
|
993
997
|
assert.match(leaderCmd, /"omx-demo"/);
|
|
994
998
|
assert.match(leaderCmd, /exit \$status/);
|
|
995
999
|
});
|
|
1000
|
+
it("detached leader command keeps stdin open for the Codex child", async () => {
|
|
1001
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-detached-leader-stdin-"));
|
|
1002
|
+
const fakeBin = join(cwd, "bin");
|
|
1003
|
+
const stdinLogPath = join(cwd, "stdin.log");
|
|
1004
|
+
try {
|
|
1005
|
+
await mkdir(fakeBin, { recursive: true });
|
|
1006
|
+
await writeFile(join(fakeBin, "codex"), `#!/bin/sh
|
|
1007
|
+
if IFS= read -r line; then
|
|
1008
|
+
printf 'stdin:%s\n' "$line" > "${stdinLogPath}"
|
|
1009
|
+
else
|
|
1010
|
+
printf 'stdin:EOF\n' > "${stdinLogPath}"
|
|
1011
|
+
fi
|
|
1012
|
+
`);
|
|
1013
|
+
await chmod(join(fakeBin, "codex"), 0o755);
|
|
1014
|
+
await writeFile(join(fakeBin, "tmux"), `#!/bin/sh
|
|
1015
|
+
case "$1" in
|
|
1016
|
+
display-message)
|
|
1017
|
+
if [ "$3" = '#{socket_path}' ] || [ "$4" = '#{socket_path}' ]; then
|
|
1018
|
+
printf '/tmp/tmux-test.sock\n'
|
|
1019
|
+
else
|
|
1020
|
+
printf '0\n'
|
|
1021
|
+
fi
|
|
1022
|
+
;;
|
|
1023
|
+
show-options)
|
|
1024
|
+
printf 'off\n'
|
|
1025
|
+
;;
|
|
1026
|
+
set-option|kill-session)
|
|
1027
|
+
;;
|
|
1028
|
+
esac
|
|
1029
|
+
exit 0
|
|
1030
|
+
`);
|
|
1031
|
+
await chmod(join(fakeBin, "tmux"), 0o755);
|
|
1032
|
+
const steps = buildDetachedSessionBootstrapSteps("omx-demo", cwd, buildTmuxPaneCommand("codex", [], "/bin/sh"), "'node' '/tmp/omx.js' 'hud' '--watch'", null);
|
|
1033
|
+
const leaderCmd = steps[0]?.args.at(-1);
|
|
1034
|
+
assert.equal(typeof leaderCmd, "string");
|
|
1035
|
+
const result = (await import("node:child_process")).spawnSync("/bin/sh", ["-c", leaderCmd], {
|
|
1036
|
+
cwd,
|
|
1037
|
+
env: {
|
|
1038
|
+
...process.env,
|
|
1039
|
+
PATH: `${fakeBin}:/usr/bin:/bin`,
|
|
1040
|
+
HOME: cwd,
|
|
1041
|
+
},
|
|
1042
|
+
input: "hello from leader\n",
|
|
1043
|
+
encoding: "utf-8",
|
|
1044
|
+
});
|
|
1045
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
1046
|
+
const stdinLog = await readFile(stdinLogPath, "utf-8");
|
|
1047
|
+
assert.match(stdinLog, /stdin:hello from leader/);
|
|
1048
|
+
}
|
|
1049
|
+
finally {
|
|
1050
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
996
1053
|
it("detached leader command preserves cwd and cleanup without shell-quote breakage", async () => {
|
|
997
1054
|
const cwd = await mkdtemp(join(tmpdir(), "omx-detached-leader-"));
|
|
998
1055
|
const fakeBin = join(cwd, "bin");
|
|
@@ -1252,6 +1309,98 @@ exit 0
|
|
|
1252
1309
|
["run"],
|
|
1253
1310
|
]);
|
|
1254
1311
|
});
|
|
1312
|
+
it("reapStaleNotifyFallbackWatcher skips kill when process identity does not match a watcher", async () => {
|
|
1313
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-reap-pid-identity-"));
|
|
1314
|
+
const pidPath = join(cwd, "watcher.pid");
|
|
1315
|
+
await writeFile(pidPath, JSON.stringify({ pid: 99999, started_at: new Date().toISOString() }));
|
|
1316
|
+
const killed = [];
|
|
1317
|
+
await reapStaleNotifyFallbackWatcher(pidPath, {
|
|
1318
|
+
isWatcherProcess: () => false,
|
|
1319
|
+
tryKillPid: (pid) => { killed.push(pid); return true; },
|
|
1320
|
+
});
|
|
1321
|
+
assert.equal(killed.length, 0, "should not kill a process that is not a watcher");
|
|
1322
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1323
|
+
});
|
|
1324
|
+
it("reapStaleNotifyFallbackWatcher sends SIGTERM only after confirming watcher identity", async () => {
|
|
1325
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-reap-pid-confirmed-"));
|
|
1326
|
+
const pidPath = join(cwd, "watcher.pid");
|
|
1327
|
+
await writeFile(pidPath, JSON.stringify({ pid: 12345, started_at: new Date().toISOString() }));
|
|
1328
|
+
const killed = [];
|
|
1329
|
+
await reapStaleNotifyFallbackWatcher(pidPath, {
|
|
1330
|
+
isWatcherProcess: () => true,
|
|
1331
|
+
tryKillPid: (pid) => { killed.push(pid); return true; },
|
|
1332
|
+
});
|
|
1333
|
+
assert.deepEqual(killed, [12345], "should SIGTERM the verified watcher process");
|
|
1334
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1335
|
+
});
|
|
1336
|
+
it("reuses legacy plain-text PID parsing without widening stale reap semantics across PID reuse", async () => {
|
|
1337
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-reap-legacy-pid-"));
|
|
1338
|
+
try {
|
|
1339
|
+
const pidPath = join(cwd, "watcher.pid");
|
|
1340
|
+
await writeFile(pidPath, "12345\n", "utf-8");
|
|
1341
|
+
const observed = [];
|
|
1342
|
+
await reapStaleNotifyFallbackWatcher(pidPath, {
|
|
1343
|
+
isWatcherProcess(pid) {
|
|
1344
|
+
observed.push(pid);
|
|
1345
|
+
return false;
|
|
1346
|
+
},
|
|
1347
|
+
tryKillPid: (pid) => {
|
|
1348
|
+
observed.push(pid);
|
|
1349
|
+
return true;
|
|
1350
|
+
},
|
|
1351
|
+
});
|
|
1352
|
+
assert.deepEqual(observed, [12345], "legacy plain-text PID files should still identity-check reused PIDs before any kill");
|
|
1353
|
+
}
|
|
1354
|
+
finally {
|
|
1355
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1356
|
+
}
|
|
1357
|
+
});
|
|
1358
|
+
it("reaps watcher-record PIDs only after the record path confirms watcher identity across PID reuse", async () => {
|
|
1359
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-reap-record-pid-"));
|
|
1360
|
+
try {
|
|
1361
|
+
const pidPath = join(cwd, "watcher.pid");
|
|
1362
|
+
await writeFile(pidPath, JSON.stringify({ pid: 54321, started_at: "2026-04-05T00:00:00.000Z" }), "utf-8");
|
|
1363
|
+
const observed = [];
|
|
1364
|
+
await reapStaleNotifyFallbackWatcher(pidPath, {
|
|
1365
|
+
isWatcherProcess(pid) {
|
|
1366
|
+
observed.push({ step: "identity", pid });
|
|
1367
|
+
return true;
|
|
1368
|
+
},
|
|
1369
|
+
tryKillPid(pid) {
|
|
1370
|
+
observed.push({ step: "kill", pid });
|
|
1371
|
+
return true;
|
|
1372
|
+
},
|
|
1373
|
+
});
|
|
1374
|
+
assert.deepEqual(observed, [
|
|
1375
|
+
{ step: "identity", pid: 54321 },
|
|
1376
|
+
{ step: "kill", pid: 54321 },
|
|
1377
|
+
]);
|
|
1378
|
+
}
|
|
1379
|
+
finally {
|
|
1380
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1381
|
+
}
|
|
1382
|
+
});
|
|
1383
|
+
it("acquireTmuxExtendedKeysLease recovers from a stale lock left by a crashed process", async () => {
|
|
1384
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-tmux-stale-lock-"));
|
|
1385
|
+
const leaseDir = join(cwd, ".omx", "state", "tmux-extended-keys");
|
|
1386
|
+
const lockDir = join(leaseDir, "tmp-stale-sock.lock");
|
|
1387
|
+
mkdirSync(lockDir, { recursive: true });
|
|
1388
|
+
const staleTime = new Date(Date.now() - 60_000);
|
|
1389
|
+
utimesSync(lockDir, staleTime, staleTime);
|
|
1390
|
+
const calls = [];
|
|
1391
|
+
const execStub = (_file, args) => {
|
|
1392
|
+
calls.push([...args]);
|
|
1393
|
+
if (args[0] === "display-message")
|
|
1394
|
+
return "/tmp/stale-sock";
|
|
1395
|
+
return "";
|
|
1396
|
+
};
|
|
1397
|
+
const lease = acquireTmuxExtendedKeysLease(cwd, execStub);
|
|
1398
|
+
assert.equal(typeof lease, "string", "lease should succeed after stale lock recovery");
|
|
1399
|
+
assert.ok(!existsSync(lockDir), "stale lock directory should be removed");
|
|
1400
|
+
if (lease)
|
|
1401
|
+
releaseTmuxExtendedKeysLease(cwd, lease, execStub);
|
|
1402
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1403
|
+
});
|
|
1255
1404
|
it("buildDetachedSessionFinalizeSteps keeps schedule after split-capture and before attach", () => {
|
|
1256
1405
|
const steps = buildDetachedSessionFinalizeSteps("omx-demo", "%12", "3", true);
|
|
1257
1406
|
const names = steps.map((step) => step.name);
|