oh-my-codex 0.21.3 → 0.21.5
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 +6 -6
- package/Cargo.toml +1 -1
- package/LICENSE +21 -0
- package/README.md +6 -1
- package/crates/omx-runtime/src/lease_mutex.rs +379 -0
- package/crates/omx-runtime/src/main.rs +4 -0
- package/crates/omx-runtime/tests/execution.rs +152 -0
- package/crates/omx-sparkshell/src/codex_bridge.rs +2 -2
- package/crates/omx-sparkshell/tests/execution.rs +49 -36
- package/dist/agents/__tests__/definitions.test.js +5 -3
- package/dist/agents/__tests__/definitions.test.js.map +1 -1
- package/dist/agents/__tests__/native-config.test.js +68 -11
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/agents/definitions.d.ts +1 -1
- package/dist/agents/definitions.d.ts.map +1 -1
- package/dist/agents/definitions.js +3 -3
- package/dist/agents/definitions.js.map +1 -1
- package/dist/agents/native-config.d.ts +1 -1
- package/dist/agents/native-config.d.ts.map +1 -1
- package/dist/agents/native-config.js +1 -1
- package/dist/agents/native-config.js.map +1 -1
- package/dist/autopilot/__tests__/planner-routing.test.js +12 -2
- package/dist/autopilot/__tests__/planner-routing.test.js.map +1 -1
- package/dist/autopilot/planner-routing.d.ts.map +1 -1
- package/dist/autopilot/planner-routing.js +2 -2
- package/dist/autopilot/planner-routing.js.map +1 -1
- package/dist/cli/__tests__/agents.test.js +1 -1
- package/dist/cli/__tests__/agents.test.js.map +1 -1
- package/dist/cli/__tests__/auth.test.js +1 -1
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.js +1 -1
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-spark-routing.test.js +1 -1
- package/dist/cli/__tests__/doctor-spark-routing.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-state-root-binding.test.js +36 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +100 -4
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +87 -7
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/package-bin-contract.test.js +1 -1
- package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
- package/dist/cli/__tests__/plugin-cache-symlink-provenance-3552.test.js +17 -13
- package/dist/cli/__tests__/plugin-cache-symlink-provenance-3552.test.js.map +1 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +11 -9
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +134 -0
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/setup-reasoning-preserve.test.d.ts +2 -0
- package/dist/cli/__tests__/setup-reasoning-preserve.test.d.ts.map +1 -0
- package/dist/cli/__tests__/setup-reasoning-preserve.test.js +198 -0
- package/dist/cli/__tests__/setup-reasoning-preserve.test.js.map +1 -0
- package/dist/cli/__tests__/setup-refresh.test.js +7 -6
- package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +67 -1
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +47 -0
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/agents.d.ts.map +1 -1
- package/dist/cli/agents.js +2 -1
- package/dist/cli/agents.js.map +1 -1
- package/dist/cli/auth.d.ts.map +1 -1
- package/dist/cli/auth.js +2 -1
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli/codex-feature-probe.d.ts +2 -1
- package/dist/cli/codex-feature-probe.d.ts.map +1 -1
- package/dist/cli/codex-feature-probe.js +4 -2
- package/dist/cli/codex-feature-probe.js.map +1 -1
- package/dist/cli/codex-home.d.ts +14 -0
- package/dist/cli/codex-home.d.ts.map +1 -1
- package/dist/cli/codex-home.js +21 -0
- package/dist/cli/codex-home.js.map +1 -1
- package/dist/cli/doctor.d.ts +2 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +122 -9
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +28 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/launch-credential-provenance.d.ts +49 -0
- package/dist/cli/launch-credential-provenance.d.ts.map +1 -0
- package/dist/cli/launch-credential-provenance.js +163 -0
- package/dist/cli/launch-credential-provenance.js.map +1 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +15 -4
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/team.d.ts.map +1 -1
- package/dist/cli/team.js +23 -7
- package/dist/cli/team.js.map +1 -1
- package/dist/cli/uninstall.js +1 -1
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +7 -0
- package/dist/cli/update.js.map +1 -1
- package/dist/compat/__tests__/doctor-contract.test.js +5 -1
- package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
- package/dist/config/__tests__/codex-feature-flags.test.js +10 -1
- package/dist/config/__tests__/codex-feature-flags.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +13 -3
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/__tests__/models.test.js +9 -3
- package/dist/config/__tests__/models.test.js.map +1 -1
- package/dist/config/codex-feature-flags.d.ts +4 -0
- package/dist/config/codex-feature-flags.d.ts.map +1 -1
- package/dist/config/codex-feature-flags.js +17 -1
- package/dist/config/codex-feature-flags.js.map +1 -1
- package/dist/config/generator.d.ts +9 -2
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +23 -9
- package/dist/config/generator.js.map +1 -1
- package/dist/config/models.d.ts +6 -6
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +5 -5
- package/dist/config/models.js.map +1 -1
- package/dist/hooks/__tests__/anti-slop-workflow.test.js +2 -2
- package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
- package/dist/hooks/__tests__/code-review-skill-contract.test.js +3 -3
- package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/deep-interview-contract.test.js +6 -7
- package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +2 -2
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +16 -33
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +9 -29
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +9 -29
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js +10 -7
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/session.test.js +41 -6
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/__tests__/sunset-routing-contract.test.js +23 -0
- package/dist/hooks/__tests__/sunset-routing-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/visual-ralph-skill.test.js +3 -3
- package/dist/hooks/__tests__/visual-ralph-skill.test.js.map +1 -1
- package/dist/hooks/session.d.ts +1 -1
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +85 -29
- package/dist/hooks/session.js.map +1 -1
- package/dist/hud/__tests__/index.test.js +130 -2
- package/dist/hud/__tests__/index.test.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +76 -0
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/__tests__/render.test.js +155 -4
- package/dist/hud/__tests__/render.test.js.map +1 -1
- package/dist/hud/__tests__/roster-height-realtmux.test.d.ts +2 -0
- package/dist/hud/__tests__/roster-height-realtmux.test.d.ts.map +1 -0
- package/dist/hud/__tests__/roster-height-realtmux.test.js +70 -0
- package/dist/hud/__tests__/roster-height-realtmux.test.js.map +1 -0
- package/dist/hud/__tests__/state.test.js +92 -1
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/__tests__/tmux.test.js +24 -1
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/index.d.ts +3 -0
- package/dist/hud/index.d.ts.map +1 -1
- package/dist/hud/index.js +40 -18
- package/dist/hud/index.js.map +1 -1
- package/dist/hud/reconcile.d.ts +1 -0
- package/dist/hud/reconcile.d.ts.map +1 -1
- package/dist/hud/reconcile.js +9 -3
- package/dist/hud/reconcile.js.map +1 -1
- package/dist/hud/render.d.ts +1 -1
- package/dist/hud/render.d.ts.map +1 -1
- package/dist/hud/render.js +81 -7
- package/dist/hud/render.js.map +1 -1
- package/dist/hud/state.d.ts.map +1 -1
- package/dist/hud/state.js +38 -3
- package/dist/hud/state.js.map +1 -1
- package/dist/hud/tmux.d.ts +4 -0
- package/dist/hud/tmux.d.ts.map +1 -1
- package/dist/hud/tmux.js +66 -12
- package/dist/hud/tmux.js.map +1 -1
- package/dist/hud/types.d.ts +9 -0
- package/dist/hud/types.d.ts.map +1 -1
- package/dist/hud/types.js.map +1 -1
- package/dist/leader/__tests__/contract.test.js +9 -2
- package/dist/leader/__tests__/contract.test.js.map +1 -1
- package/dist/leader/contract.d.ts +2 -2
- package/dist/leader/contract.d.ts.map +1 -1
- package/dist/leader/contract.js +2 -2
- package/dist/leader/contract.js.map +1 -1
- package/dist/ralplan/__tests__/advisory-activation.test.js +3 -3
- package/dist/ralplan/__tests__/advisory-activation.test.js.map +1 -1
- package/dist/runtime/__tests__/bridge.test.js +91 -23
- package/dist/runtime/__tests__/bridge.test.js.map +1 -1
- package/dist/runtime/bridge.d.ts.map +1 -1
- package/dist/runtime/bridge.js +7 -1
- package/dist/runtime/bridge.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +10 -5
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/prompt-inventory.test.js +83 -2
- package/dist/scripts/__tests__/prompt-inventory.test.js.map +1 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js +75 -23
- package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
- package/dist/scripts/prompt-inventory.d.ts +17 -0
- package/dist/scripts/prompt-inventory.d.ts.map +1 -1
- package/dist/scripts/prompt-inventory.js +64 -11
- package/dist/scripts/prompt-inventory.js.map +1 -1
- package/dist/scripts/run-test-files.js +1 -0
- package/dist/scripts/run-test-files.js.map +1 -1
- package/dist/scripts/smoke-packed-install.d.ts +1 -1
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +46 -17
- package/dist/scripts/smoke-packed-install.js.map +1 -1
- package/dist/state/__tests__/mode-binding-lease.test.js +58 -2
- package/dist/state/__tests__/mode-binding-lease.test.js.map +1 -1
- package/dist/state/mode-binding-lease-helper.js +9 -41
- package/dist/state/mode-binding-lease-helper.js.map +1 -1
- package/dist/state/mode-binding-lease.d.ts.map +1 -1
- package/dist/state/mode-binding-lease.js +32 -10
- package/dist/state/mode-binding-lease.js.map +1 -1
- package/dist/team/__tests__/delegation-policy.test.js +2 -2
- package/dist/team/__tests__/delegation-policy.test.js.map +1 -1
- package/dist/team/__tests__/model-contract.test.js +10 -10
- package/dist/team/__tests__/model-contract.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +28 -15
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +5 -1
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +60 -0
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.js +2 -2
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.js.map +1 -1
- package/dist/team/__tests__/worker-runtime-identity.test.js +3 -3
- package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
- package/dist/team/model-contract.d.ts +1 -1
- package/dist/team/model-contract.d.ts.map +1 -1
- package/dist/team/runtime-cli.js +4 -2
- package/dist/team/runtime-cli.js.map +1 -1
- package/dist/team/runtime.d.ts +11 -0
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +24 -3
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +11 -5
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/state.d.ts +1 -1
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +5 -3
- package/dist/team/state.js.map +1 -1
- package/dist/team/team-identity.d.ts.map +1 -1
- package/dist/team/team-identity.js +8 -14
- package/dist/team/team-identity.js.map +1 -1
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +17 -8
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/ultragoal/__tests__/artifacts.test.js +43 -0
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/artifacts.d.ts +1 -0
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +9 -5
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/utils/__tests__/agents-model-table.test.js +13 -10
- package/dist/utils/__tests__/agents-model-table.test.js.map +1 -1
- package/dist/utils/__tests__/platform-command.test.js +10 -0
- package/dist/utils/__tests__/platform-command.test.js.map +1 -1
- package/dist/utils/platform-command.d.ts.map +1 -1
- package/dist/utils/platform-command.js +10 -4
- package/dist/utils/platform-command.js.map +1 -1
- package/dist/utils/toml.d.ts.map +1 -1
- package/dist/utils/toml.js +4 -3
- package/dist/utils/toml.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +11 -0
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/package.json +2 -2
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/skills/ai-slop-cleaner/SKILL.md +2 -3
- package/plugins/oh-my-codex/skills/best-practice-research/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/code-review/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +13 -22
- package/plugins/oh-my-codex/skills/design/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/hud/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/plan/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +9 -74
- package/plugins/oh-my-codex/skills/visual-ralph/SKILL.md +7 -7
- package/skills/ai-slop-cleaner/SKILL.md +2 -3
- package/skills/best-practice-research/SKILL.md +1 -1
- package/skills/code-review/SKILL.md +1 -1
- package/skills/deep-interview/SKILL.md +13 -22
- package/skills/design/SKILL.md +1 -1
- package/skills/hud/SKILL.md +1 -1
- package/skills/plan/SKILL.md +1 -1
- package/skills/ralplan/SKILL.md +9 -74
- package/skills/visual-ralph/SKILL.md +7 -7
- package/src/scripts/__tests__/codex-native-hook.test.ts +9 -5
- package/src/scripts/__tests__/prompt-inventory.test.ts +84 -1
- package/src/scripts/__tests__/smoke-packed-install.test.ts +75 -23
- package/src/scripts/prompt-inventory.ts +71 -12
- package/src/scripts/run-test-files.ts +1 -0
- package/src/scripts/smoke-packed-install.ts +56 -17
- package/templates/AGENTS.md +4 -7
package/Cargo.lock
CHANGED
|
@@ -32,7 +32,7 @@ checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
|
|
|
32
32
|
|
|
33
33
|
[[package]]
|
|
34
34
|
name = "omx-api"
|
|
35
|
-
version = "0.
|
|
35
|
+
version = "0.21.5"
|
|
36
36
|
dependencies = [
|
|
37
37
|
"serde",
|
|
38
38
|
"serde_json",
|
|
@@ -40,14 +40,14 @@ dependencies = [
|
|
|
40
40
|
|
|
41
41
|
[[package]]
|
|
42
42
|
name = "omx-explore-harness"
|
|
43
|
-
version = "0.
|
|
43
|
+
version = "0.21.5"
|
|
44
44
|
dependencies = [
|
|
45
45
|
"libc",
|
|
46
46
|
]
|
|
47
47
|
|
|
48
48
|
[[package]]
|
|
49
49
|
name = "omx-mux"
|
|
50
|
-
version = "0.
|
|
50
|
+
version = "0.21.5"
|
|
51
51
|
dependencies = [
|
|
52
52
|
"serde",
|
|
53
53
|
"serde_json",
|
|
@@ -55,7 +55,7 @@ dependencies = [
|
|
|
55
55
|
|
|
56
56
|
[[package]]
|
|
57
57
|
name = "omx-runtime"
|
|
58
|
-
version = "0.
|
|
58
|
+
version = "0.21.5"
|
|
59
59
|
dependencies = [
|
|
60
60
|
"fs2",
|
|
61
61
|
"libc",
|
|
@@ -67,7 +67,7 @@ dependencies = [
|
|
|
67
67
|
|
|
68
68
|
[[package]]
|
|
69
69
|
name = "omx-runtime-core"
|
|
70
|
-
version = "0.
|
|
70
|
+
version = "0.21.5"
|
|
71
71
|
dependencies = [
|
|
72
72
|
"fs2",
|
|
73
73
|
"serde",
|
|
@@ -76,7 +76,7 @@ dependencies = [
|
|
|
76
76
|
|
|
77
77
|
[[package]]
|
|
78
78
|
name = "omx-sparkshell"
|
|
79
|
-
version = "0.
|
|
79
|
+
version = "0.21.5"
|
|
80
80
|
dependencies = [
|
|
81
81
|
"omx-mux",
|
|
82
82
|
]
|
package/Cargo.toml
CHANGED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yeachan-Heo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -375,12 +375,16 @@ These are operator/support surfaces:
|
|
|
375
375
|
- `omx uninstall` removes OMX-managed wrappers from `.codex/hooks.json` but keeps the file when user hooks remain
|
|
376
376
|
- `omx update` checks npm immediately, installs the newest global OMX build, then reruns the same interactive setup refresh path
|
|
377
377
|
- launch-time update checks are throttled and prompt by default; use `OMX_AUTO_UPDATE=0` to disable them or `OMX_AUTO_UPDATE=defer` to schedule deferred updates without a prompt
|
|
378
|
-
- fresh OMX
|
|
378
|
+
- fresh OMX setup defaults to `gpt-6-astra` across frontier, standard, and spark/fast agents, with the same role reasoning defaults; existing user model configuration and explicit overrides are preserved
|
|
379
379
|
- `.omx-config.json` model/env routing is documented in [the model/env routing reference](./docs/reference/omx-config-schema-routing.md); only edit keys supported by your installed OMX version
|
|
380
380
|
- `omx doctor` verifies the install when something seems wrong; it does not prove that the active Codex profile can make an authenticated model call
|
|
381
381
|
- `omx hud --watch` is a monitoring/status surface, not the primary user workflow
|
|
382
|
+
- Active session teams show their name and worker count beside the OMX version (for example, `[OMX#0.21.4] team:checkout (3 workers) | repo/branch`) in every HUD preset, before long repository labels. When ultragoal is active, the combined ultragoal/team summary also takes priority over repository labels and other modes. Team discovery remains session-scoped; this does not show teams from other sessions or change when team startup publishes active state.
|
|
383
|
+
- Below the summary, each team agent gets a live-refreshing row with its name, last reported state (`working`, `idle`, `blocked`, `done`, `failed`, `draining`, or `unknown`), current task ID, role, tmux pane ID, and update age when available. Columns stay aligned across different name lengths and missing fields; `working` and `done` are green, while `blocked` and `failed` are yellow. The HUD grows to fit the roster and shrinks after the team stops. Missing or malformed status files display `unknown`; a reported `working` state is not a process-liveness check. Membership and status are read without modifying team files.
|
|
382
384
|
- GitGuardex finish progress is opt-in. Add `"guardex": { "enabled": true }` to the project-local `.omx/hud-config.json` to show `gx:<step>/<total> <phase>`; running review/autofix phases animate in the HUD. OMX does not read Guardex state when this flag is absent or false.
|
|
383
385
|
|
|
386
|
+
Tmux roster height is bounded by live window and leader/HUD geometry, reserving at least half of their shared rows for the leader. Workers that do not fit are represented by a `+N workers` overflow row. The watch loop remeasures after resizing; unknown geometry keeps the HUD compact rather than expanding it unchecked.
|
|
387
|
+
|
|
384
388
|
For non-team sessions, native Codex hooks are now the canonical lifecycle surface:
|
|
385
389
|
- `plugins/oh-my-codex/hooks/hooks.json` = official plugin-scoped hook registrations for plugin installs
|
|
386
390
|
- `.codex/hooks.json` = legacy/fallback native Codex hook registrations preserved for legacy installs and older Codex versions
|
|
@@ -413,6 +417,7 @@ If `Shift+Enter` still submits instead of inserting a newline inside an OMX-mana
|
|
|
413
417
|
|
|
414
418
|
- `omx sparkshell <command>` is for shell-native inspection and bounded verification
|
|
415
419
|
- for read-only repository lookups, use normal Codex repository inspection tools/subagents (the deprecated `omx explore` command has been removed)
|
|
420
|
+
- primary and fallback Sparkshell summary models both default to `gpt-6-astra`; set `OMX_SPARKSHELL_FALLBACK_MODEL` to a different model to enable a distinct fallback
|
|
416
421
|
- sparkshell env overrides are intentionally narrow: `OMX_SPARKSHELL_BIN` selects a native sidecar path, `OMX_SPARKSHELL_MODEL` selects the primary summary model, `OMX_SPARKSHELL_FALLBACK_MODEL` selects the retry model, `OMX_SPARKSHELL_MODEL_INSTRUCTIONS_FILE` selects summary instructions, and `OMX_SPARKSHELL_SUMMARY_TIMEOUT_MS` controls the local API summary timeout
|
|
417
422
|
|
|
418
423
|
Examples:
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
use fs2::FileExt;
|
|
2
|
+
use serde_json::Value;
|
|
3
|
+
use std::io::{self, BufRead, Write};
|
|
4
|
+
#[cfg(unix)]
|
|
5
|
+
use std::os::unix::ffi::OsStrExt;
|
|
6
|
+
use std::path::{Path, PathBuf};
|
|
7
|
+
use std::time::{Duration, Instant};
|
|
8
|
+
|
|
9
|
+
const LOCK_TIMEOUT: Duration = Duration::from_secs(34);
|
|
10
|
+
const SAFE_LEAF_SUFFIX: &str = ".mutex";
|
|
11
|
+
|
|
12
|
+
pub fn run(args: &[String]) -> Result<(), String> {
|
|
13
|
+
if args.len() != 4 {
|
|
14
|
+
return Err("lease-mutex requires exactly <absolute-canonical-dir> <expected-dev> <expected-ino> <safe-leaf>".to_string());
|
|
15
|
+
}
|
|
16
|
+
let leaf = validate_safe_leaf(&args[3])?;
|
|
17
|
+
#[cfg(unix)]
|
|
18
|
+
{
|
|
19
|
+
let dir = PathBuf::from(&args[0]);
|
|
20
|
+
if !dir.is_absolute() {
|
|
21
|
+
return emit_startup_error("lease-mutex directory must be absolute".to_string());
|
|
22
|
+
}
|
|
23
|
+
let expected_dev = match parse_identity(&args[1], "expected-dev") {
|
|
24
|
+
Ok(value) => value,
|
|
25
|
+
Err(error) => return emit_startup_error(error),
|
|
26
|
+
};
|
|
27
|
+
let expected_ino = match parse_identity(&args[2], "expected-ino") {
|
|
28
|
+
Ok(value) => value,
|
|
29
|
+
Err(error) => return emit_startup_error(error),
|
|
30
|
+
};
|
|
31
|
+
let lease = match UnixLease::open(&dir, expected_dev, expected_ino, leaf) {
|
|
32
|
+
Ok(lease) => lease,
|
|
33
|
+
Err(error) => return emit_startup_error(error),
|
|
34
|
+
};
|
|
35
|
+
run_protocol(lease)
|
|
36
|
+
}
|
|
37
|
+
#[cfg(not(unix))]
|
|
38
|
+
{
|
|
39
|
+
let _ = args;
|
|
40
|
+
Err("lease-mutex unsupported on this platform; native no-follow identity semantics are unavailable".to_string())
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fn validate_safe_leaf(raw: &str) -> Result<String, String> {
|
|
45
|
+
let valid_hex = raw.strip_suffix(SAFE_LEAF_SUFFIX).is_some_and(|key| {
|
|
46
|
+
key.len() == 64
|
|
47
|
+
&& key
|
|
48
|
+
.bytes()
|
|
49
|
+
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
|
|
50
|
+
});
|
|
51
|
+
if !valid_hex {
|
|
52
|
+
return Err("lease-mutex safe-leaf must match ^[a-f0-9]{64}\\.mutex$".to_string());
|
|
53
|
+
}
|
|
54
|
+
Ok(raw.to_string())
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn parse_identity(raw: &str, name: &str) -> Result<u64, String> {
|
|
58
|
+
raw.parse::<u64>()
|
|
59
|
+
.map_err(|_| format!("{name} must be a non-negative decimal integer"))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fn run_protocol<L: Lease>(mut lease: L) -> Result<(), String> {
|
|
63
|
+
let mut stdout = io::BufWriter::new(io::stdout().lock());
|
|
64
|
+
write_json(
|
|
65
|
+
&mut stdout,
|
|
66
|
+
serde_json::json!({"id":0,"ok":true,"ready":true}),
|
|
67
|
+
)?;
|
|
68
|
+
let stdin = io::stdin();
|
|
69
|
+
for line in stdin.lock().lines() {
|
|
70
|
+
let line = line.map_err(|e| format!("stdin read failed: {e}"))?;
|
|
71
|
+
if line.trim().is_empty() {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
let request: Value = match serde_json::from_str(&line) {
|
|
75
|
+
Ok(value) => value,
|
|
76
|
+
Err(error) => {
|
|
77
|
+
write_json(
|
|
78
|
+
&mut stdout,
|
|
79
|
+
serde_json::json!({"id":null,"ok":false,"error":format!("invalid JSON request: {error}")}),
|
|
80
|
+
)?;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
let id = request.get("id").cloned().unwrap_or(Value::Null);
|
|
85
|
+
let op = request.get("op").and_then(Value::as_str);
|
|
86
|
+
match op {
|
|
87
|
+
Some("assert") => match lease.verify() {
|
|
88
|
+
Ok(()) => write_json(&mut stdout, serde_json::json!({"id":id,"ok":true}))?,
|
|
89
|
+
Err(error) => write_json(
|
|
90
|
+
&mut stdout,
|
|
91
|
+
serde_json::json!({"id":id,"ok":false,"error":error}),
|
|
92
|
+
)?,
|
|
93
|
+
},
|
|
94
|
+
Some("close") => match lease.verify() {
|
|
95
|
+
Ok(()) => {
|
|
96
|
+
lease.release()?;
|
|
97
|
+
write_json(&mut stdout, serde_json::json!({"id":id,"ok":true}))?;
|
|
98
|
+
return Ok(());
|
|
99
|
+
}
|
|
100
|
+
Err(error) => {
|
|
101
|
+
let release_error = lease.release().err();
|
|
102
|
+
let message = match release_error {
|
|
103
|
+
Some(release_error) => format!("{error}; {release_error}"),
|
|
104
|
+
None => error,
|
|
105
|
+
};
|
|
106
|
+
write_json(
|
|
107
|
+
&mut stdout,
|
|
108
|
+
serde_json::json!({"id":id,"ok":false,"error":message}),
|
|
109
|
+
)?;
|
|
110
|
+
return Ok(());
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
Some(other) => write_json(
|
|
114
|
+
&mut stdout,
|
|
115
|
+
serde_json::json!({"id":id,"ok":false,"error":format!("unsupported lease-mutex operation: {other}")}),
|
|
116
|
+
)?,
|
|
117
|
+
None => write_json(
|
|
118
|
+
&mut stdout,
|
|
119
|
+
serde_json::json!({"id":id,"ok":false,"error":"request op must be assert or close"}),
|
|
120
|
+
)?,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
Ok(())
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fn write_json(out: &mut impl Write, value: Value) -> Result<(), String> {
|
|
127
|
+
serde_json::to_writer(&mut *out, &value)
|
|
128
|
+
.map_err(|e| format!("stdout JSON write failed: {e}"))?;
|
|
129
|
+
out.write_all(b"\n")
|
|
130
|
+
.map_err(|e| format!("stdout write failed: {e}"))?;
|
|
131
|
+
out.flush().map_err(|e| format!("stdout flush failed: {e}"))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fn emit_startup_error(error: String) -> Result<(), String> {
|
|
135
|
+
let mut stdout = io::stdout().lock();
|
|
136
|
+
let _ = write_json(
|
|
137
|
+
&mut stdout,
|
|
138
|
+
serde_json::json!({"id":0,"ok":false,"error":error}),
|
|
139
|
+
);
|
|
140
|
+
Err(error)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
trait Lease {
|
|
144
|
+
fn verify(&self) -> Result<(), String>;
|
|
145
|
+
fn release(&mut self) -> Result<(), String>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#[cfg(unix)]
|
|
149
|
+
struct UnixLease {
|
|
150
|
+
dir: std::fs::File,
|
|
151
|
+
mutex: std::fs::File,
|
|
152
|
+
dir_path: PathBuf,
|
|
153
|
+
expected_dev: u64,
|
|
154
|
+
expected_ino: u64,
|
|
155
|
+
mutex_dev: u64,
|
|
156
|
+
mutex_ino: u64,
|
|
157
|
+
leaf: String,
|
|
158
|
+
held: bool,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#[cfg(unix)]
|
|
162
|
+
impl UnixLease {
|
|
163
|
+
fn open(
|
|
164
|
+
path: &Path,
|
|
165
|
+
expected_dev: u64,
|
|
166
|
+
expected_ino: u64,
|
|
167
|
+
leaf: String,
|
|
168
|
+
) -> Result<Self, String> {
|
|
169
|
+
std::fs::canonicalize(path)
|
|
170
|
+
.map_err(|e| format!("lease-mutex cannot canonicalize directory: {e}"))?;
|
|
171
|
+
let dir = open_directory(path)?;
|
|
172
|
+
let dir_identity = stat_fd(dir.as_raw_fd(), "pinned directory")?;
|
|
173
|
+
if !dir_identity.is_dir {
|
|
174
|
+
return Err("lease-mutex path is not a directory".to_string());
|
|
175
|
+
}
|
|
176
|
+
if dir_identity.dev != expected_dev || dir_identity.ino != expected_ino {
|
|
177
|
+
return Err(format!("lease-mutex directory identity mismatch: expected dev={expected_dev} ino={expected_ino}, got dev={} ino={}", dir_identity.dev, dir_identity.ino));
|
|
178
|
+
}
|
|
179
|
+
let mutex = open_mutex(dir.as_raw_fd(), &leaf)?;
|
|
180
|
+
let mutex_identity = stat_fd(mutex.as_raw_fd(), "mutex file")?;
|
|
181
|
+
if !mutex_identity.is_regular || mutex_identity.nlink != 1 {
|
|
182
|
+
return Err(
|
|
183
|
+
"lease-mutex mutex leaf must be a regular file with link count 1".to_string(),
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
try_lock(&mutex)?;
|
|
187
|
+
let lease = Self {
|
|
188
|
+
dir,
|
|
189
|
+
mutex,
|
|
190
|
+
dir_path: path.to_path_buf(),
|
|
191
|
+
expected_dev,
|
|
192
|
+
expected_ino,
|
|
193
|
+
mutex_dev: mutex_identity.dev,
|
|
194
|
+
mutex_ino: mutex_identity.ino,
|
|
195
|
+
leaf,
|
|
196
|
+
held: true,
|
|
197
|
+
};
|
|
198
|
+
lease.verify()?;
|
|
199
|
+
Ok(lease)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#[cfg(unix)]
|
|
204
|
+
impl Lease for UnixLease {
|
|
205
|
+
fn verify(&self) -> Result<(), String> {
|
|
206
|
+
if !self.held {
|
|
207
|
+
return Err("lease-mutex lock is no longer held".to_string());
|
|
208
|
+
}
|
|
209
|
+
let pinned = stat_fd(self.dir.as_raw_fd(), "pinned directory")?;
|
|
210
|
+
if !pinned.is_dir || pinned.dev != self.expected_dev || pinned.ino != self.expected_ino {
|
|
211
|
+
return Err("lease-mutex pinned directory identity changed".to_string());
|
|
212
|
+
}
|
|
213
|
+
let visible = stat_at(self.dir.as_raw_fd(), &self.leaf)?;
|
|
214
|
+
if !visible.is_regular || visible.nlink != 1 {
|
|
215
|
+
return Err("lease-mutex visible mutex leaf is not a regular nlink-1 file".to_string());
|
|
216
|
+
}
|
|
217
|
+
if visible.dev != self.mutex_dev || visible.ino != self.mutex_ino {
|
|
218
|
+
return Err("lease-mutex visible mutex identity changed".to_string());
|
|
219
|
+
}
|
|
220
|
+
let opened = stat_fd(self.mutex.as_raw_fd(), "opened mutex")?;
|
|
221
|
+
if !opened.is_regular
|
|
222
|
+
|| opened.nlink != 1
|
|
223
|
+
|| opened.dev != self.mutex_dev
|
|
224
|
+
|| opened.ino != self.mutex_ino
|
|
225
|
+
{
|
|
226
|
+
return Err("lease-mutex opened mutex identity changed".to_string());
|
|
227
|
+
}
|
|
228
|
+
let path_identity = stat_path(&self.dir_path)?;
|
|
229
|
+
if path_identity.dev != self.expected_dev || path_identity.ino != self.expected_ino {
|
|
230
|
+
return Err("lease-mutex directory path identity changed".to_string());
|
|
231
|
+
}
|
|
232
|
+
Ok(())
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
fn release(&mut self) -> Result<(), String> {
|
|
236
|
+
if self.held {
|
|
237
|
+
FileExt::unlock(&self.mutex).map_err(|e| format!("lease-mutex unlock failed: {e}"))?;
|
|
238
|
+
self.held = false;
|
|
239
|
+
}
|
|
240
|
+
Ok(())
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
#[cfg(unix)]
|
|
245
|
+
use std::os::fd::{AsRawFd, FromRawFd};
|
|
246
|
+
|
|
247
|
+
#[cfg(unix)]
|
|
248
|
+
#[derive(Clone, Copy)]
|
|
249
|
+
struct Identity {
|
|
250
|
+
dev: u64,
|
|
251
|
+
ino: u64,
|
|
252
|
+
nlink: u64,
|
|
253
|
+
is_dir: bool,
|
|
254
|
+
is_regular: bool,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
#[cfg(unix)]
|
|
258
|
+
fn open_directory(path: &Path) -> Result<std::fs::File, String> {
|
|
259
|
+
let c = cstring(path, "directory")?;
|
|
260
|
+
let fd = unsafe {
|
|
261
|
+
libc::open(
|
|
262
|
+
c.as_ptr(),
|
|
263
|
+
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
|
|
264
|
+
)
|
|
265
|
+
};
|
|
266
|
+
if fd < 0 {
|
|
267
|
+
return Err(format!(
|
|
268
|
+
"lease-mutex cannot open directory without following symlinks: {}",
|
|
269
|
+
io::Error::last_os_error()
|
|
270
|
+
));
|
|
271
|
+
}
|
|
272
|
+
Ok(unsafe { std::fs::File::from_raw_fd(fd) })
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#[cfg(unix)]
|
|
276
|
+
fn open_mutex(dir_fd: i32, leaf_name: &str) -> Result<std::fs::File, String> {
|
|
277
|
+
let leaf = std::ffi::CString::new(leaf_name)
|
|
278
|
+
.map_err(|_| "lease-mutex safe-leaf contains NUL".to_string())?;
|
|
279
|
+
let flags = libc::O_RDWR | libc::O_CLOEXEC | libc::O_NOFOLLOW;
|
|
280
|
+
let mut fd = unsafe { libc::openat(dir_fd, leaf.as_ptr(), flags) };
|
|
281
|
+
if fd < 0 && io::Error::last_os_error().raw_os_error() == Some(libc::ENOENT) {
|
|
282
|
+
fd = unsafe {
|
|
283
|
+
libc::openat(
|
|
284
|
+
dir_fd,
|
|
285
|
+
leaf.as_ptr(),
|
|
286
|
+
flags | libc::O_CREAT | libc::O_EXCL,
|
|
287
|
+
0o600,
|
|
288
|
+
)
|
|
289
|
+
};
|
|
290
|
+
if fd < 0 && io::Error::last_os_error().raw_os_error() == Some(libc::EEXIST) {
|
|
291
|
+
fd = unsafe { libc::openat(dir_fd, leaf.as_ptr(), flags) };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if fd < 0 {
|
|
295
|
+
return Err(format!(
|
|
296
|
+
"lease-mutex cannot open permanent mutex leaf without following symlinks: {}",
|
|
297
|
+
io::Error::last_os_error()
|
|
298
|
+
));
|
|
299
|
+
}
|
|
300
|
+
Ok(unsafe { std::fs::File::from_raw_fd(fd) })
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
#[cfg(unix)]
|
|
304
|
+
fn try_lock(file: &std::fs::File) -> Result<(), String> {
|
|
305
|
+
let start = Instant::now();
|
|
306
|
+
loop {
|
|
307
|
+
match file.try_lock_exclusive() {
|
|
308
|
+
Ok(()) => return Ok(()),
|
|
309
|
+
Err(error)
|
|
310
|
+
if error.kind() == io::ErrorKind::WouldBlock && start.elapsed() < LOCK_TIMEOUT =>
|
|
311
|
+
{
|
|
312
|
+
std::thread::sleep(Duration::from_millis(20))
|
|
313
|
+
}
|
|
314
|
+
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
|
|
315
|
+
return Err(format!(
|
|
316
|
+
"lease-mutex lock acquisition timed out after {} ms",
|
|
317
|
+
LOCK_TIMEOUT.as_millis()
|
|
318
|
+
))
|
|
319
|
+
}
|
|
320
|
+
Err(error) => return Err(format!("lease-mutex lock acquisition failed: {error}")),
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
#[cfg(unix)]
|
|
326
|
+
fn cstring(path: &Path, label: &str) -> Result<std::ffi::CString, String> {
|
|
327
|
+
std::ffi::CString::new(path.as_os_str().as_bytes())
|
|
328
|
+
.map_err(|_| format!("lease-mutex {label} path contains an embedded NUL byte"))
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
#[cfg(unix)]
|
|
332
|
+
fn stat_path(path: &Path) -> Result<Identity, String> {
|
|
333
|
+
let c = cstring(path, "directory")?;
|
|
334
|
+
let mut st = unsafe { std::mem::zeroed() };
|
|
335
|
+
if unsafe { libc::lstat(c.as_ptr(), &mut st) } != 0 {
|
|
336
|
+
return Err(format!(
|
|
337
|
+
"lease-mutex cannot verify directory path: {}",
|
|
338
|
+
io::Error::last_os_error()
|
|
339
|
+
));
|
|
340
|
+
}
|
|
341
|
+
Ok(identity_from_stat(&st))
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
#[cfg(unix)]
|
|
345
|
+
fn stat_at(dir_fd: i32, leaf: &str) -> Result<Identity, String> {
|
|
346
|
+
let c = std::ffi::CString::new(leaf).unwrap();
|
|
347
|
+
let mut st = unsafe { std::mem::zeroed() };
|
|
348
|
+
if unsafe { libc::fstatat(dir_fd, c.as_ptr(), &mut st, libc::AT_SYMLINK_NOFOLLOW) } != 0 {
|
|
349
|
+
return Err(format!(
|
|
350
|
+
"lease-mutex cannot verify visible mutex leaf: {}",
|
|
351
|
+
io::Error::last_os_error()
|
|
352
|
+
));
|
|
353
|
+
}
|
|
354
|
+
Ok(identity_from_stat(&st))
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
#[cfg(unix)]
|
|
358
|
+
fn stat_fd(fd: i32, label: &str) -> Result<Identity, String> {
|
|
359
|
+
let mut st = unsafe { std::mem::zeroed() };
|
|
360
|
+
if unsafe { libc::fstat(fd, &mut st) } != 0 {
|
|
361
|
+
return Err(format!(
|
|
362
|
+
"lease-mutex cannot stat {label}: {}",
|
|
363
|
+
io::Error::last_os_error()
|
|
364
|
+
));
|
|
365
|
+
}
|
|
366
|
+
Ok(identity_from_stat(&st))
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
#[cfg(unix)]
|
|
370
|
+
#[allow(clippy::unnecessary_cast)]
|
|
371
|
+
fn identity_from_stat(st: &libc::stat) -> Identity {
|
|
372
|
+
Identity {
|
|
373
|
+
dev: st.st_dev as u64,
|
|
374
|
+
ino: st.st_ino as u64,
|
|
375
|
+
nlink: st.st_nlink as u64,
|
|
376
|
+
is_dir: (st.st_mode & libc::S_IFMT) == libc::S_IFDIR,
|
|
377
|
+
is_regular: (st.st_mode & libc::S_IFMT) == libc::S_IFREG,
|
|
378
|
+
}
|
|
379
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
mod lease_mutex;
|
|
2
|
+
|
|
1
3
|
use fs2::FileExt;
|
|
2
4
|
use omx_mux::{canonical_contract_summary, MuxAdapter, MuxOperation, MuxTarget, TmuxAdapter};
|
|
3
5
|
use omx_runtime_core::{runtime_contract_summary, RuntimeCommand, RuntimeEngine};
|
|
@@ -123,6 +125,7 @@ fn run() -> Result<(), String> {
|
|
|
123
125
|
);
|
|
124
126
|
Ok(())
|
|
125
127
|
}
|
|
128
|
+
Some("lease-mutex") => lease_mutex::run(&args[1..]),
|
|
126
129
|
Some("fs-rename-no-replace") => run_fs_rename_no_replace(&args[1..]),
|
|
127
130
|
Some("process-identity") => run_process_identity(&args[1..]),
|
|
128
131
|
Some("init") => {
|
|
@@ -533,6 +536,7 @@ fn print_usage() {
|
|
|
533
536
|
" snapshot [--json] [--state-dir=DIR] print a runtime snapshot\n",
|
|
534
537
|
" mux-contract print the mux boundary summary\n",
|
|
535
538
|
" exec <json> [--state-dir=DIR] process a runtime command from JSON\n",
|
|
539
|
+
" lease-mutex <absolute-canonical-dir> <expected-dev> <expected-ino> <safe-leaf> hold native lease mutex over stdin JSON protocol\n",
|
|
536
540
|
" init <state-dir> initialize a fresh state directory\n",
|
|
537
541
|
));
|
|
538
542
|
}
|
|
@@ -1,3 +1,154 @@
|
|
|
1
|
+
#[cfg(unix)]
|
|
2
|
+
mod lease_mutex_tests {
|
|
3
|
+
use super::*;
|
|
4
|
+
use std::io::{BufRead, BufReader, Write};
|
|
5
|
+
use std::os::unix::fs::MetadataExt;
|
|
6
|
+
use std::process::{Child, ChildStdout, Stdio};
|
|
7
|
+
use std::sync::mpsc;
|
|
8
|
+
use std::thread;
|
|
9
|
+
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
10
|
+
const MUTEX_LEAF: &str =
|
|
11
|
+
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.mutex";
|
|
12
|
+
|
|
13
|
+
fn temp_dir() -> std::path::PathBuf {
|
|
14
|
+
let dir = std::env::temp_dir().join(format!(
|
|
15
|
+
"omx-lease-mutex-{}-{}",
|
|
16
|
+
std::process::id(),
|
|
17
|
+
SystemTime::now()
|
|
18
|
+
.duration_since(UNIX_EPOCH)
|
|
19
|
+
.unwrap()
|
|
20
|
+
.as_nanos()
|
|
21
|
+
));
|
|
22
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
23
|
+
dir
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fn spawn_lease(dir: &std::path::Path) -> (Child, BufReader<ChildStdout>) {
|
|
27
|
+
let metadata = std::fs::metadata(dir).unwrap();
|
|
28
|
+
let mut child = Command::new(env!("CARGO_BIN_EXE_omx-runtime"))
|
|
29
|
+
.args([
|
|
30
|
+
"lease-mutex",
|
|
31
|
+
dir.to_str().unwrap(),
|
|
32
|
+
&metadata.dev().to_string(),
|
|
33
|
+
&metadata.ino().to_string(),
|
|
34
|
+
MUTEX_LEAF,
|
|
35
|
+
])
|
|
36
|
+
.stdin(Stdio::piped())
|
|
37
|
+
.stdout(Stdio::piped())
|
|
38
|
+
.stderr(Stdio::piped())
|
|
39
|
+
.spawn()
|
|
40
|
+
.unwrap();
|
|
41
|
+
let stdout = BufReader::new(child.stdout.take().unwrap());
|
|
42
|
+
(child, stdout)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn read_line(reader: &mut BufReader<ChildStdout>) -> serde_json::Value {
|
|
46
|
+
let mut line = String::new();
|
|
47
|
+
reader.read_line(&mut line).unwrap();
|
|
48
|
+
serde_json::from_str(&line).unwrap()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#[test]
|
|
52
|
+
fn lease_mutex_serializes_contenders() {
|
|
53
|
+
let dir = temp_dir();
|
|
54
|
+
let (mut first, mut first_out) = spawn_lease(&dir);
|
|
55
|
+
assert_eq!(read_line(&mut first_out)["ready"], true);
|
|
56
|
+
let (mut second, second_out) = spawn_lease(&dir);
|
|
57
|
+
let (tx, rx) = mpsc::channel();
|
|
58
|
+
thread::spawn(move || {
|
|
59
|
+
let mut reader = second_out;
|
|
60
|
+
let mut line = String::new();
|
|
61
|
+
let result = reader.read_line(&mut line).map(|_| line);
|
|
62
|
+
let _ = tx.send(result);
|
|
63
|
+
});
|
|
64
|
+
assert!(rx.recv_timeout(Duration::from_millis(150)).is_err());
|
|
65
|
+
first
|
|
66
|
+
.stdin
|
|
67
|
+
.as_mut()
|
|
68
|
+
.unwrap()
|
|
69
|
+
.write_all(b"{\"id\":1,\"op\":\"close\"}\n")
|
|
70
|
+
.unwrap();
|
|
71
|
+
assert!(first.wait().unwrap().success());
|
|
72
|
+
let line = rx.recv_timeout(Duration::from_secs(3)).unwrap().unwrap();
|
|
73
|
+
assert_eq!(
|
|
74
|
+
serde_json::from_str::<serde_json::Value>(&line).unwrap()["ready"],
|
|
75
|
+
true
|
|
76
|
+
);
|
|
77
|
+
let _ = second.kill();
|
|
78
|
+
let _ = second.wait();
|
|
79
|
+
let _ = std::fs::remove_dir_all(dir);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[test]
|
|
83
|
+
fn lease_mutex_releases_on_process_death() {
|
|
84
|
+
let dir = temp_dir();
|
|
85
|
+
let (mut first, mut first_out) = spawn_lease(&dir);
|
|
86
|
+
assert_eq!(read_line(&mut first_out)["ready"], true);
|
|
87
|
+
first.kill().unwrap();
|
|
88
|
+
first.wait().unwrap();
|
|
89
|
+
let (mut second, mut second_out) = spawn_lease(&dir);
|
|
90
|
+
assert_eq!(read_line(&mut second_out)["ready"], true);
|
|
91
|
+
second.kill().unwrap();
|
|
92
|
+
second.wait().unwrap();
|
|
93
|
+
let _ = std::fs::remove_dir_all(dir);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#[test]
|
|
97
|
+
fn lease_mutex_rejects_symlink_and_directory_identity_mismatch() {
|
|
98
|
+
use std::os::unix::fs::symlink;
|
|
99
|
+
let dir = temp_dir();
|
|
100
|
+
let target = dir.join("target");
|
|
101
|
+
std::fs::create_dir(&target).unwrap();
|
|
102
|
+
symlink(&target, dir.join("link")).unwrap();
|
|
103
|
+
let output = Command::new(env!("CARGO_BIN_EXE_omx-runtime"))
|
|
104
|
+
.args([
|
|
105
|
+
"lease-mutex",
|
|
106
|
+
dir.join("link").to_str().unwrap(),
|
|
107
|
+
"0",
|
|
108
|
+
"0",
|
|
109
|
+
MUTEX_LEAF,
|
|
110
|
+
])
|
|
111
|
+
.output()
|
|
112
|
+
.unwrap();
|
|
113
|
+
assert!(!output.status.success());
|
|
114
|
+
let metadata = std::fs::metadata(&dir).unwrap();
|
|
115
|
+
let output = Command::new(env!("CARGO_BIN_EXE_omx-runtime"))
|
|
116
|
+
.args([
|
|
117
|
+
"lease-mutex",
|
|
118
|
+
target.to_str().unwrap(),
|
|
119
|
+
&metadata.dev().to_string(),
|
|
120
|
+
&metadata.ino().to_string(),
|
|
121
|
+
MUTEX_LEAF,
|
|
122
|
+
])
|
|
123
|
+
.output()
|
|
124
|
+
.unwrap();
|
|
125
|
+
assert!(!output.status.success());
|
|
126
|
+
let _ = std::fs::remove_dir_all(dir);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#[test]
|
|
130
|
+
fn lease_mutex_assert_fails_after_directory_path_replacement() {
|
|
131
|
+
let dir = temp_dir();
|
|
132
|
+
let replacement = dir.with_extension("replacement");
|
|
133
|
+
let (mut child, mut output) = spawn_lease(&dir);
|
|
134
|
+
assert_eq!(read_line(&mut output)["ready"], true);
|
|
135
|
+
std::fs::rename(&dir, &replacement).unwrap();
|
|
136
|
+
std::fs::create_dir(&dir).unwrap();
|
|
137
|
+
child
|
|
138
|
+
.stdin
|
|
139
|
+
.as_mut()
|
|
140
|
+
.unwrap()
|
|
141
|
+
.write_all(b"{\"id\":7,\"op\":\"assert\"}\n")
|
|
142
|
+
.unwrap();
|
|
143
|
+
let response = read_line(&mut output);
|
|
144
|
+
assert_eq!(response["ok"], false);
|
|
145
|
+
assert!(response["error"].as_str().unwrap().contains("identity"));
|
|
146
|
+
let _ = child.kill();
|
|
147
|
+
let _ = child.wait();
|
|
148
|
+
let _ = std::fs::remove_dir_all(dir);
|
|
149
|
+
let _ = std::fs::remove_dir_all(replacement);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
1
152
|
use std::process::Command;
|
|
2
153
|
|
|
3
154
|
#[test]
|
|
@@ -439,6 +590,7 @@ fn concurrent_exec_queue_accepts_exactly_one_request_id() {
|
|
|
439
590
|
// ---------------------------------------------------------------------------
|
|
440
591
|
|
|
441
592
|
/// Helper: run `fs-rename-no-replace <from> <to>` and return (exit_ok, stdout, stderr).
|
|
593
|
+
#[cfg(target_os = "linux")]
|
|
442
594
|
fn run_rename_no_replace(from: &str, to: &str) -> (bool, String, String) {
|
|
443
595
|
let output = Command::new(env!("CARGO_BIN_EXE_omx-runtime"))
|
|
444
596
|
.args(["fs-rename-no-replace", from, to])
|
|
@@ -9,8 +9,8 @@ use std::time::Duration;
|
|
|
9
9
|
|
|
10
10
|
pub const DEFAULT_SUMMARY_TIMEOUT_MS: u64 = 60_000;
|
|
11
11
|
pub const DEFAULT_API_BASE_URL: &str = "http://127.0.0.1:14510";
|
|
12
|
-
pub const DEFAULT_SPARK_MODEL: &str = "gpt-
|
|
13
|
-
pub const DEFAULT_STANDARD_MODEL: &str = "gpt-
|
|
12
|
+
pub const DEFAULT_SPARK_MODEL: &str = "gpt-6-astra";
|
|
13
|
+
pub const DEFAULT_STANDARD_MODEL: &str = "gpt-6-astra";
|
|
14
14
|
|
|
15
15
|
pub fn resolve_model() -> String {
|
|
16
16
|
env::var("OMX_SPARKSHELL_MODEL")
|