oh-my-codex 0.13.1 → 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.
Files changed (131) hide show
  1. package/Cargo.lock +5 -5
  2. package/Cargo.toml +1 -1
  3. package/README.md +2 -0
  4. package/crates/omx-explore/src/main.rs +221 -10
  5. package/dist/catalog/__tests__/generator.test.js +2 -0
  6. package/dist/catalog/__tests__/generator.test.js.map +1 -1
  7. package/dist/cli/__tests__/index.test.js +95 -1
  8. package/dist/cli/__tests__/index.test.js.map +1 -1
  9. package/dist/cli/__tests__/setup-skills-overwrite.test.js +41 -3
  10. package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
  11. package/dist/cli/__tests__/update.test.js +25 -1
  12. package/dist/cli/__tests__/update.test.js.map +1 -1
  13. package/dist/cli/index.d.ts +1 -0
  14. package/dist/cli/index.d.ts.map +1 -1
  15. package/dist/cli/index.js +70 -8
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/cli/setup.d.ts.map +1 -1
  18. package/dist/cli/setup.js +15 -0
  19. package/dist/cli/setup.js.map +1 -1
  20. package/dist/cli/update.js +1 -1
  21. package/dist/cli/update.js.map +1 -1
  22. package/dist/hooks/__tests__/analyze-routing-contract.test.d.ts +2 -0
  23. package/dist/hooks/__tests__/analyze-routing-contract.test.d.ts.map +1 -0
  24. package/dist/hooks/__tests__/analyze-routing-contract.test.js +36 -0
  25. package/dist/hooks/__tests__/analyze-routing-contract.test.js.map +1 -0
  26. package/dist/hooks/__tests__/analyze-skill-contract.test.d.ts +2 -0
  27. package/dist/hooks/__tests__/analyze-skill-contract.test.d.ts.map +1 -0
  28. package/dist/hooks/__tests__/analyze-skill-contract.test.js +48 -0
  29. package/dist/hooks/__tests__/analyze-skill-contract.test.js.map +1 -0
  30. package/dist/hooks/__tests__/keyword-detector.test.js +32 -0
  31. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  32. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +185 -8
  33. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  34. package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js +26 -0
  35. package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js.map +1 -1
  36. package/dist/hooks/__tests__/notify-hook-session-scope.test.js +44 -0
  37. package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
  38. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +126 -0
  39. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  40. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  41. package/dist/hooks/keyword-detector.js +8 -1
  42. package/dist/hooks/keyword-detector.js.map +1 -1
  43. package/dist/hud/__tests__/state.test.js +55 -0
  44. package/dist/hud/__tests__/state.test.js.map +1 -1
  45. package/dist/hud/state.d.ts.map +1 -1
  46. package/dist/hud/state.js +23 -4
  47. package/dist/hud/state.js.map +1 -1
  48. package/dist/mcp/__tests__/bootstrap.test.js +38 -0
  49. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  50. package/dist/mcp/bootstrap.d.ts +1 -1
  51. package/dist/mcp/bootstrap.d.ts.map +1 -1
  52. package/dist/mcp/bootstrap.js +11 -3
  53. package/dist/mcp/bootstrap.js.map +1 -1
  54. package/dist/notifications/__tests__/reply-listener.test.js +34 -1
  55. package/dist/notifications/__tests__/reply-listener.test.js.map +1 -1
  56. package/dist/notifications/reply-listener.d.ts +1 -0
  57. package/dist/notifications/reply-listener.d.ts.map +1 -1
  58. package/dist/notifications/reply-listener.js +14 -2
  59. package/dist/notifications/reply-listener.js.map +1 -1
  60. package/dist/scripts/__tests__/codex-native-hook.test.js +178 -15
  61. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  62. package/dist/scripts/__tests__/generate-release-body.test.d.ts +2 -0
  63. package/dist/scripts/__tests__/generate-release-body.test.d.ts.map +1 -0
  64. package/dist/scripts/__tests__/generate-release-body.test.js +144 -0
  65. package/dist/scripts/__tests__/generate-release-body.test.js.map +1 -0
  66. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  67. package/dist/scripts/codex-native-hook.js +23 -44
  68. package/dist/scripts/codex-native-hook.js.map +1 -1
  69. package/dist/scripts/generate-release-body.d.ts +34 -0
  70. package/dist/scripts/generate-release-body.d.ts.map +1 -0
  71. package/dist/scripts/generate-release-body.js +249 -0
  72. package/dist/scripts/generate-release-body.js.map +1 -0
  73. package/dist/scripts/notify-fallback-watcher.js +43 -20
  74. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  75. package/dist/scripts/notify-hook/active-team.d.ts.map +1 -1
  76. package/dist/scripts/notify-hook/active-team.js +2 -1
  77. package/dist/scripts/notify-hook/active-team.js.map +1 -1
  78. package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
  79. package/dist/scripts/notify-hook/ralph-session-resume.js +17 -2
  80. package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
  81. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  82. package/dist/scripts/notify-hook/state-io.js +16 -0
  83. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  84. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  85. package/dist/scripts/notify-hook/team-leader-nudge.js +26 -5
  86. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  87. package/dist/scripts/notify-hook.js +1 -7
  88. package/dist/scripts/notify-hook.js.map +1 -1
  89. package/dist/team/__tests__/model-contract.test.js +6 -0
  90. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  91. package/dist/team/__tests__/tmux-session.test.js +1 -1
  92. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  93. package/dist/team/__tests__/worker-runtime-identity.test.d.ts +2 -0
  94. package/dist/team/__tests__/worker-runtime-identity.test.d.ts.map +1 -0
  95. package/dist/team/__tests__/worker-runtime-identity.test.js +250 -0
  96. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -0
  97. package/dist/team/leader-activity.d.ts.map +1 -1
  98. package/dist/team/leader-activity.js +26 -15
  99. package/dist/team/leader-activity.js.map +1 -1
  100. package/dist/team/model-contract.d.ts.map +1 -1
  101. package/dist/team/model-contract.js.map +1 -1
  102. package/dist/team/runtime.d.ts.map +1 -1
  103. package/dist/team/runtime.js +9 -8
  104. package/dist/team/runtime.js.map +1 -1
  105. package/dist/team/scaling.d.ts.map +1 -1
  106. package/dist/team/scaling.js +10 -9
  107. package/dist/team/scaling.js.map +1 -1
  108. package/dist/team/tmux-session.d.ts.map +1 -1
  109. package/dist/team/tmux-session.js +3 -2
  110. package/dist/team/tmux-session.js.map +1 -1
  111. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +3 -0
  112. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
  113. package/dist/wiki/__tests__/slug-nonascii.test.js +11 -5
  114. package/dist/wiki/__tests__/slug-nonascii.test.js.map +1 -1
  115. package/dist/wiki/storage.d.ts.map +1 -1
  116. package/dist/wiki/storage.js +2 -1
  117. package/dist/wiki/storage.js.map +1 -1
  118. package/package.json +3 -1
  119. package/skills/analyze/SKILL.md +101 -134
  120. package/src/scripts/__tests__/codex-native-hook.test.ts +214 -17
  121. package/src/scripts/__tests__/generate-release-body.test.ts +166 -0
  122. package/src/scripts/codex-native-hook.ts +81 -61
  123. package/src/scripts/generate-release-body.ts +295 -0
  124. package/src/scripts/notify-fallback-watcher.ts +44 -21
  125. package/src/scripts/notify-hook/active-team.ts +2 -1
  126. package/src/scripts/notify-hook/ralph-session-resume.ts +17 -2
  127. package/src/scripts/notify-hook/state-io.ts +16 -0
  128. package/src/scripts/notify-hook/team-leader-nudge.ts +24 -4
  129. package/src/scripts/notify-hook.ts +1 -6
  130. package/templates/AGENTS.md +1 -1
  131. 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.1"
35
+ version = "0.13.2"
36
36
 
37
37
  [[package]]
38
38
  name = "omx-mux"
39
- version = "0.13.1"
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.1"
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.1"
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.1"
65
+ version = "0.13.2"
66
66
  dependencies = [
67
67
  "omx-mux",
68
68
  ]
package/Cargo.toml CHANGED
@@ -10,7 +10,7 @@ resolver = "2"
10
10
 
11
11
  [workspace.package]
12
12
 
13
- version = "0.13.1"
13
+ version = "0.13.2"
14
14
 
15
15
  edition = "2021"
16
16
  license = "MIT"
package/README.md CHANGED
@@ -218,6 +218,8 @@ omx doctor --team
218
218
 
219
219
  Only use the forced team shutdown for a team you have confirmed is dead or intentionally abandoned.
220
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
+
221
223
  ### Explore and sparkshell
222
224
 
223
225
  - `omx explore --prompt "..."` is for read-only repository lookup
@@ -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() && is_usable_host_command(candidate) {
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
- return Err(
754
- "find actions that execute, delete, or write files are not allowed in omx explore"
755
- .to_string(),
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 = temp_allowlist_dir().expect("temp 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
  },
@@ -1307,6 +1309,98 @@ exit 0
1307
1309
  ["run"],
1308
1310
  ]);
1309
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
+ });
1310
1404
  it("buildDetachedSessionFinalizeSteps keeps schedule after split-capture and before attach", () => {
1311
1405
  const steps = buildDetachedSessionFinalizeSteps("omx-demo", "%12", "3", true);
1312
1406
  const names = steps.map((step) => step.name);