oh-my-codex 0.17.3 → 0.18.1
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 +13 -5
- package/Cargo.toml +2 -1
- package/README.md +44 -19
- package/crates/omx-api/Cargo.toml +19 -0
- package/crates/omx-api/src/lib.rs +2997 -0
- package/crates/omx-api/src/main.rs +10 -0
- package/crates/omx-api/tests/cli.rs +558 -0
- package/crates/omx-explore/src/main.rs +4 -0
- package/crates/omx-sparkshell/src/codex_bridge.rs +437 -123
- package/crates/omx-sparkshell/src/exec.rs +127 -1
- package/crates/omx-sparkshell/src/main.rs +829 -30
- package/crates/omx-sparkshell/src/prompt.rs +25 -3
- package/crates/omx-sparkshell/src/redaction.rs +241 -0
- package/crates/omx-sparkshell/tests/execution.rs +702 -237
- package/dist/cli/__tests__/api.test.d.ts +2 -0
- package/dist/cli/__tests__/api.test.d.ts.map +1 -0
- package/dist/cli/__tests__/api.test.js +175 -0
- package/dist/cli/__tests__/api.test.js.map +1 -0
- package/dist/cli/__tests__/ask.test.js +72 -5
- package/dist/cli/__tests__/ask.test.js.map +1 -1
- package/dist/cli/__tests__/autoresearch-goal.test.js +14 -1
- package/dist/cli/__tests__/autoresearch-goal.test.js.map +1 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js +15 -7
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +76 -3
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/explore.test.js +23 -0
- package/dist/cli/__tests__/explore.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +171 -5
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/install-docs-contract.test.d.ts +2 -0
- package/dist/cli/__tests__/install-docs-contract.test.d.ts.map +1 -0
- package/dist/cli/__tests__/install-docs-contract.test.js +55 -0
- package/dist/cli/__tests__/install-docs-contract.test.js.map +1 -0
- package/dist/cli/__tests__/launch-fallback.test.js +191 -0
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/package-bin-contract.test.js +4 -3
- package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
- package/dist/cli/__tests__/question.test.js +27 -41
- package/dist/cli/__tests__/question.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +232 -35
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js +25 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-packaging.test.js +1 -0
- package/dist/cli/__tests__/sparkshell-packaging.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +227 -4
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +72 -1
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/__tests__/version-sync-contract.test.js +4 -0
- package/dist/cli/__tests__/version-sync-contract.test.js.map +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
- package/dist/cli/api.d.ts +26 -0
- package/dist/cli/api.d.ts.map +1 -0
- package/dist/cli/api.js +153 -0
- package/dist/cli/api.js.map +1 -0
- package/dist/cli/codex-feature-probe.d.ts +5 -0
- package/dist/cli/codex-feature-probe.d.ts.map +1 -1
- package/dist/cli/codex-feature-probe.js +13 -7
- package/dist/cli/codex-feature-probe.js.map +1 -1
- package/dist/cli/doctor.d.ts +7 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +119 -10
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/explore.d.ts +2 -0
- package/dist/cli/explore.d.ts.map +1 -1
- package/dist/cli/explore.js +43 -1
- package/dist/cli/explore.js.map +1 -1
- package/dist/cli/index.d.ts +12 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +460 -87
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/native-assets.d.ts +2 -1
- package/dist/cli/native-assets.d.ts.map +1 -1
- package/dist/cli/native-assets.js +1 -0
- package/dist/cli/native-assets.js.map +1 -1
- package/dist/cli/plugin-marketplace.d.ts +2 -0
- package/dist/cli/plugin-marketplace.d.ts.map +1 -1
- package/dist/cli/plugin-marketplace.js +15 -1
- package/dist/cli/plugin-marketplace.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +71 -11
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/sparkshell.d.ts +7 -1
- package/dist/cli/sparkshell.d.ts.map +1 -1
- package/dist/cli/sparkshell.js +31 -4
- package/dist/cli/sparkshell.js.map +1 -1
- package/dist/cli/ultragoal.d.ts +1 -1
- package/dist/cli/ultragoal.d.ts.map +1 -1
- package/dist/cli/ultragoal.js +184 -10
- package/dist/cli/ultragoal.js.map +1 -1
- package/dist/cli/update.d.ts +2 -0
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +14 -3
- package/dist/cli/update.js.map +1 -1
- package/dist/compat/__tests__/doctor-contract.test.js +3 -0
- package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
- package/dist/config/__tests__/codex-feature-flags.test.js +11 -1
- package/dist/config/__tests__/codex-feature-flags.test.js.map +1 -1
- package/dist/config/__tests__/codex-hooks.test.js +19 -8
- package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
- package/dist/config/__tests__/commit-lore-guard.test.d.ts +2 -0
- package/dist/config/__tests__/commit-lore-guard.test.d.ts.map +1 -0
- package/dist/config/__tests__/commit-lore-guard.test.js +20 -0
- package/dist/config/__tests__/commit-lore-guard.test.js.map +1 -0
- 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 +4 -0
- package/dist/config/codex-feature-flags.js.map +1 -1
- package/dist/config/codex-hooks.js +6 -6
- package/dist/config/codex-hooks.js.map +1 -1
- package/dist/config/commit-lore-guard.d.ts +1 -0
- package/dist/config/commit-lore-guard.d.ts.map +1 -1
- package/dist/config/commit-lore-guard.js +29 -3
- package/dist/config/commit-lore-guard.js.map +1 -1
- package/dist/config/generator.d.ts +3 -1
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +114 -10
- package/dist/config/generator.js.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -0
- package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.js +5 -1
- package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +10 -6
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts +2 -0
- package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/best-practice-research-skill.test.js +27 -0
- package/dist/hooks/__tests__/best-practice-research-skill.test.js.map +1 -0
- package/dist/hooks/__tests__/consensus-execution-handoff.test.d.ts +1 -1
- package/dist/hooks/__tests__/consensus-execution-handoff.test.js +13 -11
- package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
- package/dist/hooks/__tests__/deep-interview-contract.test.js +4 -3
- package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +15 -3
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +6 -0
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +33 -0
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +4 -0
- package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
- package/dist/hooks/extensibility/__tests__/dispatcher.test.js +26 -3
- package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
- package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
- package/dist/hooks/extensibility/dispatcher.js +29 -14
- package/dist/hooks/extensibility/dispatcher.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +8 -3
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/keyword-registry.d.ts.map +1 -1
- package/dist/hooks/keyword-registry.js +1 -0
- package/dist/hooks/keyword-registry.js.map +1 -1
- package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
- package/dist/hooks/prompt-guidance-contract.js +3 -2
- package/dist/hooks/prompt-guidance-contract.js.map +1 -1
- package/dist/hud/__tests__/hud-tmux-injection.test.js +14 -8
- package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +4 -4
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/__tests__/resource-leak-watch.test.d.ts +2 -0
- package/dist/hud/__tests__/resource-leak-watch.test.d.ts.map +1 -0
- package/dist/hud/__tests__/resource-leak-watch.test.js +28 -0
- package/dist/hud/__tests__/resource-leak-watch.test.js.map +1 -0
- package/dist/hud/__tests__/tmux.test.js +23 -18
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/index.d.ts +1 -1
- package/dist/hud/index.d.ts.map +1 -1
- package/dist/hud/index.js +10 -4
- package/dist/hud/index.js.map +1 -1
- package/dist/hud/tmux.d.ts.map +1 -1
- package/dist/hud/tmux.js +9 -8
- package/dist/hud/tmux.js.map +1 -1
- package/dist/mcp/__tests__/bootstrap.test.js +75 -1
- package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
- package/dist/mcp/bootstrap.d.ts +3 -1
- package/dist/mcp/bootstrap.d.ts.map +1 -1
- package/dist/mcp/bootstrap.js +71 -2
- package/dist/mcp/bootstrap.js.map +1 -1
- package/dist/notifications/__tests__/http-client-resource.test.d.ts +2 -0
- package/dist/notifications/__tests__/http-client-resource.test.d.ts.map +1 -0
- package/dist/notifications/__tests__/http-client-resource.test.js +41 -0
- package/dist/notifications/__tests__/http-client-resource.test.js.map +1 -0
- package/dist/notifications/__tests__/verbosity.test.js +20 -0
- package/dist/notifications/__tests__/verbosity.test.js.map +1 -1
- package/dist/notifications/config.d.ts.map +1 -1
- package/dist/notifications/config.js +6 -3
- package/dist/notifications/config.js.map +1 -1
- package/dist/notifications/http-client.d.ts.map +1 -1
- package/dist/notifications/http-client.js +78 -27
- package/dist/notifications/http-client.js.map +1 -1
- package/dist/notifications/types.d.ts +2 -0
- package/dist/notifications/types.d.ts.map +1 -1
- package/dist/openclaw/__tests__/dispatcher.test.js +49 -1
- package/dist/openclaw/__tests__/dispatcher.test.js.map +1 -1
- package/dist/openclaw/dispatcher.d.ts +7 -4
- package/dist/openclaw/dispatcher.d.ts.map +1 -1
- package/dist/openclaw/dispatcher.js +32 -69
- package/dist/openclaw/dispatcher.js.map +1 -1
- package/dist/pipeline/__tests__/orchestrator.test.js +65 -3
- package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +50 -5
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/index.d.ts +8 -2
- package/dist/pipeline/index.d.ts.map +1 -1
- package/dist/pipeline/index.js +5 -2
- package/dist/pipeline/index.js.map +1 -1
- package/dist/pipeline/orchestrator.d.ts +5 -4
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +56 -15
- package/dist/pipeline/orchestrator.js.map +1 -1
- package/dist/pipeline/stages/code-review.d.ts +2 -2
- package/dist/pipeline/stages/code-review.d.ts.map +1 -1
- package/dist/pipeline/stages/code-review.js +5 -3
- package/dist/pipeline/stages/code-review.js.map +1 -1
- package/dist/pipeline/stages/deep-interview.d.ts +15 -0
- package/dist/pipeline/stages/deep-interview.d.ts.map +1 -0
- package/dist/pipeline/stages/deep-interview.js +32 -0
- package/dist/pipeline/stages/deep-interview.js.map +1 -0
- package/dist/pipeline/stages/ralph-verify.d.ts +5 -5
- package/dist/pipeline/stages/ralph-verify.d.ts.map +1 -1
- package/dist/pipeline/stages/ralph-verify.js +2 -2
- package/dist/pipeline/stages/ralph-verify.js.map +1 -1
- package/dist/pipeline/stages/ultragoal.d.ts +19 -0
- package/dist/pipeline/stages/ultragoal.d.ts.map +1 -0
- package/dist/pipeline/stages/ultragoal.js +38 -0
- package/dist/pipeline/stages/ultragoal.js.map +1 -0
- package/dist/pipeline/stages/ultraqa.d.ts +30 -0
- package/dist/pipeline/stages/ultraqa.d.ts.map +1 -0
- package/dist/pipeline/stages/ultraqa.js +46 -0
- package/dist/pipeline/stages/ultraqa.js.map +1 -0
- package/dist/pipeline/types.d.ts +8 -6
- package/dist/pipeline/types.d.ts.map +1 -1
- package/dist/pipeline/types.js +2 -2
- package/dist/scripts/__tests__/codex-native-hook.test.js +1488 -117
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-dispatcher.test.js +183 -1
- package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js +27 -2
- package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
- package/dist/scripts/__tests__/verify-native-agents.test.js +16 -1
- package/dist/scripts/__tests__/verify-native-agents.test.js.map +1 -1
- package/dist/scripts/build-api.d.ts +2 -0
- package/dist/scripts/build-api.d.ts.map +1 -0
- package/dist/scripts/build-api.js +44 -0
- package/dist/scripts/build-api.js.map +1 -0
- package/dist/scripts/cleanup-explore-harness.js +1 -0
- package/dist/scripts/cleanup-explore-harness.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +364 -16
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
- package/dist/scripts/codex-native-pre-post.js +98 -25
- package/dist/scripts/codex-native-pre-post.js.map +1 -1
- package/dist/scripts/notify-dispatcher.js +88 -0
- package/dist/scripts/notify-dispatcher.js.map +1 -1
- package/dist/scripts/notify-hook/process-runner.d.ts.map +1 -1
- package/dist/scripts/notify-hook/process-runner.js +39 -17
- package/dist/scripts/notify-hook/process-runner.js.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.js +36 -14
- package/dist/scripts/notify-hook/team-dispatch.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 -11
- package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts +2 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.js +45 -1
- package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.js +27 -14
- package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
- package/dist/scripts/run-provider-advisor.js +9 -3
- package/dist/scripts/run-provider-advisor.js.map +1 -1
- package/dist/scripts/smoke-packed-install.d.ts +4 -1
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +101 -1
- package/dist/scripts/smoke-packed-install.js.map +1 -1
- package/dist/scripts/sync-plugin-mirror.js +2 -2
- package/dist/scripts/sync-plugin-mirror.js.map +1 -1
- package/dist/scripts/verify-native-agents.js +2 -2
- package/dist/scripts/verify-native-agents.js.map +1 -1
- package/dist/sidecar/__tests__/resource-leak-watch.test.d.ts +2 -0
- package/dist/sidecar/__tests__/resource-leak-watch.test.d.ts.map +1 -0
- package/dist/sidecar/__tests__/resource-leak-watch.test.js +38 -0
- package/dist/sidecar/__tests__/resource-leak-watch.test.js.map +1 -0
- package/dist/sidecar/index.d.ts +1 -1
- package/dist/sidecar/index.d.ts.map +1 -1
- package/dist/sidecar/index.js +29 -12
- package/dist/sidecar/index.js.map +1 -1
- package/dist/state/__tests__/operations-ralph-phase.test.js +88 -1
- package/dist/state/__tests__/operations-ralph-phase.test.js.map +1 -1
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +11 -0
- package/dist/state/operations.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +2 -2
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +207 -22
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/tmux-session.d.ts +1 -0
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +73 -28
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/ultragoal/__tests__/artifacts.test.js +714 -10
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/__tests__/docs-contract.test.js +57 -1
- package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
- package/dist/ultragoal/__tests__/steering-fixtures.d.ts +68 -0
- package/dist/ultragoal/__tests__/steering-fixtures.d.ts.map +1 -0
- package/dist/ultragoal/__tests__/steering-fixtures.js +259 -0
- package/dist/ultragoal/__tests__/steering-fixtures.js.map +1 -0
- package/dist/ultragoal/__tests__/steering-fixtures.test.d.ts +2 -0
- package/dist/ultragoal/__tests__/steering-fixtures.test.d.ts.map +1 -0
- package/dist/ultragoal/__tests__/steering-fixtures.test.js +65 -0
- package/dist/ultragoal/__tests__/steering-fixtures.test.js.map +1 -0
- package/dist/ultragoal/artifacts.d.ts +97 -2
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +811 -256
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/utils/__tests__/sleep-resource.test.d.ts +2 -0
- package/dist/utils/__tests__/sleep-resource.test.d.ts.map +1 -0
- package/dist/utils/__tests__/sleep-resource.test.js +39 -0
- package/dist/utils/__tests__/sleep-resource.test.js.map +1 -0
- package/dist/utils/sleep.d.ts.map +1 -1
- package/dist/utils/sleep.js +17 -6
- package/dist/utils/sleep.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +85 -10
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js +1 -0
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
- package/package.json +5 -3
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +4 -3
- package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +56 -0
- package/plugins/oh-my-codex/hooks/hooks.json +77 -0
- package/plugins/oh-my-codex/skills/autopilot/SKILL.md +77 -47
- package/plugins/oh-my-codex/skills/best-practice-research/SKILL.md +83 -0
- package/plugins/oh-my-codex/skills/cancel/SKILL.md +2 -2
- package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +9 -8
- package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/pipeline/SKILL.md +22 -11
- package/plugins/oh-my-codex/skills/plan/SKILL.md +8 -8
- package/plugins/oh-my-codex/skills/ralph/SKILL.md +7 -0
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +5 -5
- package/plugins/oh-my-codex/skills/team/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +38 -4
- package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +1 -1
- package/prompts/planner.md +1 -1
- package/prompts/researcher.md +15 -10
- package/skills/autopilot/SKILL.md +77 -47
- package/skills/best-practice-research/SKILL.md +83 -0
- package/skills/cancel/SKILL.md +2 -2
- package/skills/deep-interview/SKILL.md +9 -8
- package/skills/omx-setup/SKILL.md +1 -1
- package/skills/pipeline/SKILL.md +22 -11
- package/skills/plan/SKILL.md +8 -8
- package/skills/ralph/SKILL.md +7 -0
- package/skills/ralplan/SKILL.md +5 -5
- package/skills/team/SKILL.md +1 -1
- package/skills/ultragoal/SKILL.md +38 -4
- package/skills/ultrawork/SKILL.md +1 -1
- package/src/scripts/__tests__/codex-native-hook.test.ts +1758 -166
- package/src/scripts/__tests__/notify-dispatcher.test.ts +223 -1
- package/src/scripts/__tests__/smoke-packed-install.test.ts +39 -2
- package/src/scripts/__tests__/verify-native-agents.test.ts +21 -1
- package/src/scripts/build-api.ts +48 -0
- package/src/scripts/cleanup-explore-harness.ts +1 -0
- package/src/scripts/codex-native-hook.ts +416 -18
- package/src/scripts/codex-native-pre-post.ts +119 -25
- package/src/scripts/notify-dispatcher.ts +97 -0
- package/src/scripts/notify-hook/process-runner.ts +40 -16
- package/src/scripts/notify-hook/team-dispatch.ts +36 -13
- package/src/scripts/notify-hook/team-leader-nudge.ts +25 -11
- package/src/scripts/notify-hook/team-tmux-guard.ts +49 -0
- package/src/scripts/notify-hook/team-worker-stop.ts +24 -13
- package/src/scripts/run-provider-advisor.ts +11 -3
- package/src/scripts/smoke-packed-install.ts +107 -0
- package/src/scripts/sync-plugin-mirror.ts +3 -3
- package/src/scripts/verify-native-agents.ts +2 -2
- package/templates/catalog-manifest.json +7 -0
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
use std::env;
|
|
2
2
|
use std::fs;
|
|
3
|
+
use std::io::{Read, Write};
|
|
4
|
+
use std::net::{TcpListener, TcpStream};
|
|
3
5
|
use std::path::{Path, PathBuf};
|
|
4
6
|
use std::process::Command;
|
|
7
|
+
use std::sync::{Arc, Mutex};
|
|
8
|
+
use std::thread::{self, JoinHandle};
|
|
5
9
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
6
10
|
|
|
7
11
|
fn sparkshell_bin() -> &'static str {
|
|
@@ -32,6 +36,80 @@ fn write_executable(path: &Path, body: &str) {
|
|
|
32
36
|
}
|
|
33
37
|
}
|
|
34
38
|
|
|
39
|
+
fn start_api_server<F>(expected_requests: usize, mut handler: F) -> (String, JoinHandle<()>)
|
|
40
|
+
where
|
|
41
|
+
F: FnMut(String) -> (u16, String) + Send + 'static,
|
|
42
|
+
{
|
|
43
|
+
let listener = TcpListener::bind("127.0.0.1:0").expect("bind api server");
|
|
44
|
+
let address = listener.local_addr().expect("api address");
|
|
45
|
+
let handle = thread::spawn(move || {
|
|
46
|
+
for _ in 0..expected_requests {
|
|
47
|
+
let (stream, _) = listener.accept().expect("accept api request");
|
|
48
|
+
let request = read_http_request(stream.try_clone().expect("clone stream"));
|
|
49
|
+
let (status, body) = handler(request);
|
|
50
|
+
write_http_response(stream, status, &body);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
(format!("http://{}", address), handle)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fn read_http_request(mut stream: TcpStream) -> String {
|
|
57
|
+
let mut buffer = Vec::new();
|
|
58
|
+
let mut scratch = [0; 1024];
|
|
59
|
+
loop {
|
|
60
|
+
let count = stream.read(&mut scratch).expect("read request");
|
|
61
|
+
assert!(count > 0, "connection closed before request headers");
|
|
62
|
+
buffer.extend_from_slice(&scratch[..count]);
|
|
63
|
+
if buffer.windows(4).any(|window| window == b"\r\n\r\n") {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
let request = String::from_utf8_lossy(&buffer).into_owned();
|
|
68
|
+
let content_length = request
|
|
69
|
+
.lines()
|
|
70
|
+
.find_map(|line| {
|
|
71
|
+
line.strip_prefix("Content-Length:")
|
|
72
|
+
.or_else(|| line.strip_prefix("content-length:"))
|
|
73
|
+
.and_then(|value| value.trim().parse::<usize>().ok())
|
|
74
|
+
})
|
|
75
|
+
.unwrap_or(0);
|
|
76
|
+
let body_start = buffer
|
|
77
|
+
.windows(4)
|
|
78
|
+
.position(|window| window == b"\r\n\r\n")
|
|
79
|
+
.expect("header boundary")
|
|
80
|
+
+ 4;
|
|
81
|
+
while buffer.len() - body_start < content_length {
|
|
82
|
+
let count = stream.read(&mut scratch).expect("read request body");
|
|
83
|
+
assert!(count > 0, "connection closed before request body");
|
|
84
|
+
buffer.extend_from_slice(&scratch[..count]);
|
|
85
|
+
}
|
|
86
|
+
String::from_utf8_lossy(&buffer).into_owned()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
fn write_http_response(mut stream: TcpStream, status: u16, body: &str) {
|
|
90
|
+
let reason = if (200..300).contains(&status) {
|
|
91
|
+
"OK"
|
|
92
|
+
} else {
|
|
93
|
+
"ERROR"
|
|
94
|
+
};
|
|
95
|
+
let response = format!(
|
|
96
|
+
"HTTP/1.1 {status} {reason}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
|
|
97
|
+
body.len()
|
|
98
|
+
);
|
|
99
|
+
stream
|
|
100
|
+
.write_all(response.as_bytes())
|
|
101
|
+
.expect("write response");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fn response_json(text: &str) -> String {
|
|
105
|
+
format!(
|
|
106
|
+
"{{\"object\":\"response\",\"output_text\":\"{}\"}}",
|
|
107
|
+
text.replace('\\', "\\\\")
|
|
108
|
+
.replace('"', "\\\"")
|
|
109
|
+
.replace('\n', "\\n")
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
35
113
|
#[test]
|
|
36
114
|
fn raw_mode_preserves_stdout_and_stderr() {
|
|
37
115
|
let output = Command::new(sparkshell_bin())
|
|
@@ -48,27 +126,19 @@ fn raw_mode_preserves_stdout_and_stderr() {
|
|
|
48
126
|
}
|
|
49
127
|
|
|
50
128
|
#[test]
|
|
51
|
-
fn
|
|
52
|
-
let
|
|
53
|
-
let
|
|
54
|
-
let
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
prompt_log.display()
|
|
62
|
-
),
|
|
63
|
-
);
|
|
129
|
+
fn summary_mode_uses_local_api_and_model_override() {
|
|
130
|
+
let request_log = Arc::new(Mutex::new(String::new()));
|
|
131
|
+
let request_log_for_server = Arc::clone(&request_log);
|
|
132
|
+
let (base_url, server) = start_api_server(1, move |request| {
|
|
133
|
+
*request_log_for_server.lock().expect("request log") = request;
|
|
134
|
+
(
|
|
135
|
+
200,
|
|
136
|
+
response_json("- summary: command produced long output\n- warnings: stderr was empty"),
|
|
137
|
+
)
|
|
138
|
+
});
|
|
64
139
|
|
|
65
|
-
let path = format!(
|
|
66
|
-
"{}:{}",
|
|
67
|
-
temp.display(),
|
|
68
|
-
env::var("PATH").unwrap_or_default()
|
|
69
|
-
);
|
|
70
140
|
let output = Command::new(sparkshell_bin())
|
|
71
|
-
.env("
|
|
141
|
+
.env("OMX_API_BASE_URL", base_url)
|
|
72
142
|
.env("OMX_SPARKSHELL_LINES", "1")
|
|
73
143
|
.env("OMX_SPARKSHELL_MODEL", "spark-test-model")
|
|
74
144
|
.arg("sh")
|
|
@@ -76,6 +146,7 @@ fn summary_mode_uses_codex_exec_and_model_override() {
|
|
|
76
146
|
.arg("printf 'one\ntwo\n'")
|
|
77
147
|
.output()
|
|
78
148
|
.expect("run sparkshell");
|
|
149
|
+
server.join().expect("api server");
|
|
79
150
|
|
|
80
151
|
assert!(output.status.success());
|
|
81
152
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
@@ -83,42 +154,65 @@ fn summary_mode_uses_codex_exec_and_model_override() {
|
|
|
83
154
|
assert!(stdout.contains("- warnings: stderr was empty"));
|
|
84
155
|
assert!(String::from_utf8_lossy(&output.stderr).is_empty());
|
|
85
156
|
|
|
86
|
-
let
|
|
87
|
-
assert!(
|
|
88
|
-
assert!(
|
|
89
|
-
assert!(
|
|
90
|
-
assert!(
|
|
157
|
+
let request = request_log.lock().expect("request log");
|
|
158
|
+
assert!(request.starts_with("POST /v1/responses HTTP/1.1"));
|
|
159
|
+
assert!(request.contains("\"model\":\"spark-test-model\""));
|
|
160
|
+
assert!(request.contains("\"reasoning\":{\"effort\":\"low\"}"));
|
|
161
|
+
assert!(request.contains("Command family: generic-shell"));
|
|
162
|
+
assert!(request.contains("<<<STDOUT"));
|
|
163
|
+
assert!(request.contains("one\\ntwo"));
|
|
164
|
+
}
|
|
91
165
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
166
|
+
#[test]
|
|
167
|
+
fn summary_mode_redacts_secret_like_output_before_prompt_request() {
|
|
168
|
+
let request_log = Arc::new(Mutex::new(String::new()));
|
|
169
|
+
let request_log_for_server = Arc::clone(&request_log);
|
|
170
|
+
let (base_url, server) = start_api_server(1, move |request| {
|
|
171
|
+
*request_log_for_server.lock().expect("request log") = request;
|
|
172
|
+
(200, response_json("- summary: redacted output summarized"))
|
|
173
|
+
});
|
|
96
174
|
|
|
97
|
-
let
|
|
175
|
+
let output = Command::new(sparkshell_bin())
|
|
176
|
+
.env("OMX_API_BASE_URL", base_url)
|
|
177
|
+
.env("OMX_SPARKSHELL_LINES", "1")
|
|
178
|
+
.env("CHILD_API_TOKEN", "super-secret-token")
|
|
179
|
+
.env("CHILD_BEARER", "bearer-secret-token")
|
|
180
|
+
.arg("sh")
|
|
181
|
+
.arg("-c")
|
|
182
|
+
.arg("printf 'API_TOKEN=%s\\nline-2\\n' \"$CHILD_API_TOKEN\"; printf 'Authorization: Bearer %s\\n' \"$CHILD_BEARER\" >&2")
|
|
183
|
+
.output()
|
|
184
|
+
.expect("run sparkshell");
|
|
185
|
+
server.join().expect("api server");
|
|
186
|
+
|
|
187
|
+
assert!(output.status.success());
|
|
188
|
+
assert!(String::from_utf8_lossy(&output.stdout).contains("redacted output summarized"));
|
|
189
|
+
|
|
190
|
+
let request = request_log.lock().expect("request log");
|
|
191
|
+
assert!(request.contains("API_TOKEN=[REDACTED]"));
|
|
192
|
+
assert!(request.contains("Authorization: Bearer [REDACTED]"));
|
|
193
|
+
assert!(request.contains("line-2"));
|
|
194
|
+
assert!(!request.contains("super-secret-token"));
|
|
195
|
+
assert!(!request.contains("bearer-secret-token"));
|
|
98
196
|
}
|
|
99
197
|
|
|
100
198
|
#[test]
|
|
101
199
|
fn summary_mode_injects_model_instructions_file_override() {
|
|
102
|
-
let temp = unique_temp_dir("
|
|
103
|
-
let codex = temp.join("codex");
|
|
104
|
-
let args_log = temp.join("args.log");
|
|
200
|
+
let temp = unique_temp_dir("api-instructions-file");
|
|
105
201
|
let instructions_file = temp.join("sparkshell-lightweight-AGENTS.md");
|
|
106
|
-
write_executable(
|
|
107
|
-
&codex,
|
|
108
|
-
&format!(
|
|
109
|
-
"#!/bin/sh\nprintf '%s\n' \"$@\" > '{}'\nprintf '%s\n' '- summary: command produced long output'\n",
|
|
110
|
-
args_log.display()
|
|
111
|
-
),
|
|
112
|
-
);
|
|
113
202
|
fs::write(&instructions_file, "# sparkshell instructions\n").expect("write instructions file");
|
|
114
203
|
|
|
115
|
-
let
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
204
|
+
let request_log = Arc::new(Mutex::new(String::new()));
|
|
205
|
+
let request_log_for_server = Arc::clone(&request_log);
|
|
206
|
+
let (base_url, server) = start_api_server(1, move |request| {
|
|
207
|
+
*request_log_for_server.lock().expect("request log") = request;
|
|
208
|
+
(
|
|
209
|
+
200,
|
|
210
|
+
response_json("- summary: command produced long output"),
|
|
211
|
+
)
|
|
212
|
+
});
|
|
213
|
+
|
|
120
214
|
let output = Command::new(sparkshell_bin())
|
|
121
|
-
.env("
|
|
215
|
+
.env("OMX_API_BASE_URL", base_url)
|
|
122
216
|
.env("OMX_SPARKSHELL_LINES", "1")
|
|
123
217
|
.env(
|
|
124
218
|
"OMX_SPARKSHELL_MODEL_INSTRUCTIONS_FILE",
|
|
@@ -129,87 +223,64 @@ fn summary_mode_injects_model_instructions_file_override() {
|
|
|
129
223
|
.arg("printf 'one\ntwo\n'")
|
|
130
224
|
.output()
|
|
131
225
|
.expect("run sparkshell");
|
|
226
|
+
server.join().expect("api server");
|
|
132
227
|
|
|
133
228
|
assert!(output.status.success());
|
|
134
|
-
let
|
|
135
|
-
assert!(
|
|
136
|
-
assert!(
|
|
137
|
-
"model_instructions_file=\"{}\"",
|
|
138
|
-
instructions_file
|
|
139
|
-
.display()
|
|
140
|
-
.to_string()
|
|
141
|
-
.replace('\\', "\\\\")
|
|
142
|
-
.replace('"', "\\\"")
|
|
143
|
-
)));
|
|
229
|
+
let request = request_log.lock().expect("request log");
|
|
230
|
+
assert!(request.contains("\"reasoning\":{\"effort\":\"low\"}"));
|
|
231
|
+
assert!(request.contains("\"instructions\":\"# sparkshell instructions\\n\""));
|
|
144
232
|
|
|
145
233
|
let _ = fs::remove_dir_all(temp);
|
|
146
234
|
}
|
|
147
235
|
|
|
148
236
|
#[test]
|
|
149
237
|
fn summary_failure_falls_back_to_raw_output_with_notice() {
|
|
150
|
-
let
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
&codex,
|
|
154
|
-
"#!/bin/sh\nprintf '%s\n' 'bridge failed' >&2\nexit 9\n",
|
|
155
|
-
);
|
|
238
|
+
let (base_url, server) = start_api_server(1, |_request| {
|
|
239
|
+
(503, "{\"error\":\"bridge failed\"}".to_string())
|
|
240
|
+
});
|
|
156
241
|
|
|
157
|
-
let path = format!(
|
|
158
|
-
"{}:{}",
|
|
159
|
-
temp.display(),
|
|
160
|
-
env::var("PATH").unwrap_or_default()
|
|
161
|
-
);
|
|
162
242
|
let output = Command::new(sparkshell_bin())
|
|
163
|
-
.env("
|
|
243
|
+
.env("OMX_API_BASE_URL", base_url)
|
|
164
244
|
.env("OMX_SPARKSHELL_LINES", "1")
|
|
165
245
|
.arg("/bin/sh")
|
|
166
246
|
.arg("-c")
|
|
167
247
|
.arg("printf 'one\ntwo\n'; printf 'child-err\n' >&2")
|
|
168
248
|
.output()
|
|
169
249
|
.expect("run sparkshell");
|
|
250
|
+
server.join().expect("api server");
|
|
170
251
|
|
|
171
252
|
assert!(output.status.success());
|
|
172
253
|
assert_eq!(String::from_utf8_lossy(&output.stdout), "one\ntwo\n");
|
|
173
254
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
174
255
|
assert!(stderr.contains("child-err"));
|
|
175
256
|
assert!(stderr.contains("summary unavailable"));
|
|
176
|
-
|
|
177
|
-
let _ = fs::remove_dir_all(temp);
|
|
257
|
+
assert!(stderr.contains("showing raw output instead"));
|
|
178
258
|
}
|
|
179
259
|
|
|
180
260
|
#[test]
|
|
181
261
|
fn summary_mode_retries_with_fallback_model_when_spark_is_unavailable() {
|
|
182
|
-
let
|
|
183
|
-
let
|
|
184
|
-
let
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
for
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
",
|
|
202
|
-
args_log.display()
|
|
203
|
-
),
|
|
204
|
-
);
|
|
262
|
+
let request_log = Arc::new(Mutex::new(Vec::new()));
|
|
263
|
+
let request_log_for_server = Arc::clone(&request_log);
|
|
264
|
+
let (base_url, server) = start_api_server(2, move |request| {
|
|
265
|
+
request_log_for_server
|
|
266
|
+
.lock()
|
|
267
|
+
.expect("request log")
|
|
268
|
+
.push(request.clone());
|
|
269
|
+
if request.contains("\"model\":\"spark-test-model\"") {
|
|
270
|
+
(
|
|
271
|
+
429,
|
|
272
|
+
"{\"error\":\"rate limit exceeded for spark model\"}".to_string(),
|
|
273
|
+
)
|
|
274
|
+
} else {
|
|
275
|
+
(
|
|
276
|
+
200,
|
|
277
|
+
response_json("- summary: fallback model recovered summary"),
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
});
|
|
205
281
|
|
|
206
|
-
let path = format!(
|
|
207
|
-
"{}:{}",
|
|
208
|
-
temp.display(),
|
|
209
|
-
env::var("PATH").unwrap_or_default()
|
|
210
|
-
);
|
|
211
282
|
let output = Command::new(sparkshell_bin())
|
|
212
|
-
.env("
|
|
283
|
+
.env("OMX_API_BASE_URL", base_url)
|
|
213
284
|
.env("OMX_SPARKSHELL_LINES", "1")
|
|
214
285
|
.env("OMX_SPARKSHELL_MODEL", "spark-test-model")
|
|
215
286
|
.env("OMX_SPARKSHELL_FALLBACK_MODEL", "frontier-test-model")
|
|
@@ -218,47 +289,36 @@ printf '%s\n' '- summary: fallback model recovered summary'
|
|
|
218
289
|
.arg("printf 'one\ntwo\n'")
|
|
219
290
|
.output()
|
|
220
291
|
.expect("run sparkshell");
|
|
292
|
+
server.join().expect("api server");
|
|
221
293
|
|
|
222
294
|
assert!(output.status.success());
|
|
223
295
|
assert!(String::from_utf8_lossy(&output.stdout).contains("fallback model recovered summary"));
|
|
224
296
|
assert!(String::from_utf8_lossy(&output.stderr).is_empty());
|
|
225
297
|
|
|
226
|
-
let
|
|
227
|
-
|
|
228
|
-
assert!(
|
|
229
|
-
|
|
230
|
-
let _ = fs::remove_dir_all(temp);
|
|
298
|
+
let requests = request_log.lock().expect("request log");
|
|
299
|
+
assert_eq!(requests.len(), 2);
|
|
300
|
+
assert!(requests[0].contains("\"model\":\"spark-test-model\""));
|
|
301
|
+
assert!(requests[1].contains("\"model\":\"frontier-test-model\""));
|
|
231
302
|
}
|
|
232
303
|
|
|
233
304
|
#[test]
|
|
234
305
|
fn summary_mode_reports_both_models_when_fallback_also_fails() {
|
|
235
|
-
let
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
exit 17
|
|
249
|
-
fi
|
|
250
|
-
printf '%s\n' 'fallback backend unavailable' >&2
|
|
251
|
-
exit 29
|
|
252
|
-
",
|
|
253
|
-
);
|
|
306
|
+
let (base_url, server) = start_api_server(2, |request| {
|
|
307
|
+
if request.contains("\"model\":\"spark-test-model\"") {
|
|
308
|
+
(
|
|
309
|
+
429,
|
|
310
|
+
"{\"error\":\"quota exhausted for spark model\"}".to_string(),
|
|
311
|
+
)
|
|
312
|
+
} else {
|
|
313
|
+
(
|
|
314
|
+
503,
|
|
315
|
+
"{\"error\":\"fallback backend unavailable\"}".to_string(),
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
});
|
|
254
319
|
|
|
255
|
-
let path = format!(
|
|
256
|
-
"{}:{}",
|
|
257
|
-
temp.display(),
|
|
258
|
-
env::var("PATH").unwrap_or_default()
|
|
259
|
-
);
|
|
260
320
|
let output = Command::new(sparkshell_bin())
|
|
261
|
-
.env("
|
|
321
|
+
.env("OMX_API_BASE_URL", base_url)
|
|
262
322
|
.env("OMX_SPARKSHELL_LINES", "1")
|
|
263
323
|
.env("OMX_SPARKSHELL_MODEL", "spark-test-model")
|
|
264
324
|
.env("OMX_SPARKSHELL_FALLBACK_MODEL", "frontier-test-model")
|
|
@@ -267,6 +327,7 @@ exit 29
|
|
|
267
327
|
.arg("printf 'one\ntwo\n'; printf 'child-err\n' >&2")
|
|
268
328
|
.output()
|
|
269
329
|
.expect("run sparkshell");
|
|
330
|
+
server.join().expect("api server");
|
|
270
331
|
|
|
271
332
|
assert!(output.status.success());
|
|
272
333
|
assert_eq!(String::from_utf8_lossy(&output.stdout), "one\ntwo\n");
|
|
@@ -274,48 +335,34 @@ exit 29
|
|
|
274
335
|
assert!(stderr.contains("child-err"));
|
|
275
336
|
assert!(stderr.contains("primary model `spark-test-model`"));
|
|
276
337
|
assert!(stderr.contains("fallback model `frontier-test-model`"));
|
|
277
|
-
|
|
278
|
-
let _ = fs::remove_dir_all(temp);
|
|
279
338
|
}
|
|
280
339
|
|
|
281
340
|
#[test]
|
|
282
341
|
fn summary_mode_preserves_child_exit_code() {
|
|
283
|
-
let
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
&codex,
|
|
287
|
-
"#!/bin/sh\nprintf '%s\n' '- failures: command exited non-zero'\n",
|
|
288
|
-
);
|
|
342
|
+
let (base_url, server) = start_api_server(1, |_request| {
|
|
343
|
+
(200, response_json("- failures: command exited non-zero"))
|
|
344
|
+
});
|
|
289
345
|
|
|
290
|
-
let path = format!(
|
|
291
|
-
"{}:{}",
|
|
292
|
-
temp.display(),
|
|
293
|
-
env::var("PATH").unwrap_or_default()
|
|
294
|
-
);
|
|
295
346
|
let output = Command::new(sparkshell_bin())
|
|
296
|
-
.env("
|
|
347
|
+
.env("OMX_API_BASE_URL", base_url)
|
|
297
348
|
.env("OMX_SPARKSHELL_LINES", "1")
|
|
298
349
|
.arg("sh")
|
|
299
350
|
.arg("-c")
|
|
300
351
|
.arg("printf 'one\ntwo\n'; exit 7")
|
|
301
352
|
.output()
|
|
302
353
|
.expect("run sparkshell");
|
|
354
|
+
server.join().expect("api server");
|
|
303
355
|
|
|
304
356
|
assert_eq!(output.status.code(), Some(7));
|
|
305
357
|
assert!(String::from_utf8_lossy(&output.stdout).contains("- failures: command exited non-zero"));
|
|
306
358
|
assert!(String::from_utf8_lossy(&output.stderr).is_empty());
|
|
307
|
-
|
|
308
|
-
let _ = fs::remove_dir_all(temp);
|
|
309
359
|
}
|
|
310
360
|
|
|
311
361
|
#[test]
|
|
312
362
|
fn tmux_pane_mode_captures_large_tail_and_summarizes() {
|
|
313
363
|
let temp = unique_temp_dir("tmux-pane-summary");
|
|
314
364
|
let tmux = temp.join("tmux");
|
|
315
|
-
let codex = temp.join("codex");
|
|
316
365
|
let args_log = temp.join("tmux-args.log");
|
|
317
|
-
let prompt_log = temp.join("pane-prompt.log");
|
|
318
|
-
|
|
319
366
|
write_executable(
|
|
320
367
|
&tmux,
|
|
321
368
|
&format!(
|
|
@@ -323,13 +370,16 @@ fn tmux_pane_mode_captures_large_tail_and_summarizes() {
|
|
|
323
370
|
args_log.display()
|
|
324
371
|
),
|
|
325
372
|
);
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
373
|
+
|
|
374
|
+
let request_log = Arc::new(Mutex::new(String::new()));
|
|
375
|
+
let request_log_for_server = Arc::clone(&request_log);
|
|
376
|
+
let (base_url, server) = start_api_server(1, move |request| {
|
|
377
|
+
*request_log_for_server.lock().expect("request log") = request;
|
|
378
|
+
(
|
|
379
|
+
200,
|
|
380
|
+
response_json("- summary: tmux pane summarized\n- warnings: tail captured"),
|
|
381
|
+
)
|
|
382
|
+
});
|
|
333
383
|
|
|
334
384
|
let path = format!(
|
|
335
385
|
"{}:{}",
|
|
@@ -338,6 +388,7 @@ fn tmux_pane_mode_captures_large_tail_and_summarizes() {
|
|
|
338
388
|
);
|
|
339
389
|
let output = Command::new(sparkshell_bin())
|
|
340
390
|
.env("PATH", path)
|
|
391
|
+
.env("OMX_API_BASE_URL", base_url)
|
|
341
392
|
.env("OMX_SPARKSHELL_LINES", "1")
|
|
342
393
|
.arg("--tmux-pane")
|
|
343
394
|
.arg("%17")
|
|
@@ -345,6 +396,7 @@ fn tmux_pane_mode_captures_large_tail_and_summarizes() {
|
|
|
345
396
|
.arg("400")
|
|
346
397
|
.output()
|
|
347
398
|
.expect("run sparkshell");
|
|
399
|
+
server.join().expect("api server");
|
|
348
400
|
|
|
349
401
|
assert!(output.status.success());
|
|
350
402
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
@@ -356,36 +408,16 @@ fn tmux_pane_mode_captures_large_tail_and_summarizes() {
|
|
|
356
408
|
assert!(tmux_args.contains("%17"));
|
|
357
409
|
assert!(tmux_args.contains("-400"));
|
|
358
410
|
|
|
359
|
-
let
|
|
360
|
-
assert!(
|
|
361
|
-
assert!(
|
|
411
|
+
let request = request_log.lock().expect("request log");
|
|
412
|
+
assert!(request.contains("Command: tmux capture-pane"));
|
|
413
|
+
assert!(request.contains("line-1"));
|
|
362
414
|
|
|
363
415
|
let _ = fs::remove_dir_all(temp);
|
|
364
416
|
}
|
|
365
417
|
|
|
366
418
|
#[test]
|
|
367
419
|
fn raw_mode_keeps_boundary_output_without_summary() {
|
|
368
|
-
let temp = unique_temp_dir("boundary-raw");
|
|
369
|
-
let codex = temp.join("codex");
|
|
370
|
-
let codex_log = temp.join("codex.log");
|
|
371
|
-
write_executable(
|
|
372
|
-
&codex,
|
|
373
|
-
&format!(
|
|
374
|
-
"#!/bin/sh
|
|
375
|
-
printf '%s\n' invoked > '{}'
|
|
376
|
-
exit 0
|
|
377
|
-
",
|
|
378
|
-
codex_log.display()
|
|
379
|
-
),
|
|
380
|
-
);
|
|
381
|
-
|
|
382
|
-
let path = format!(
|
|
383
|
-
"{}:{}",
|
|
384
|
-
temp.display(),
|
|
385
|
-
env::var("PATH").unwrap_or_default()
|
|
386
|
-
);
|
|
387
420
|
let output = Command::new(sparkshell_bin())
|
|
388
|
-
.env("PATH", path)
|
|
389
421
|
.env("OMX_SPARKSHELL_LINES", "2")
|
|
390
422
|
.arg("sh")
|
|
391
423
|
.arg("-c")
|
|
@@ -394,67 +426,47 @@ exit 0
|
|
|
394
426
|
.expect("run sparkshell");
|
|
395
427
|
|
|
396
428
|
assert!(output.status.success());
|
|
397
|
-
assert_eq!(
|
|
398
|
-
String::from_utf8_lossy(&output.stdout),
|
|
399
|
-
"one
|
|
400
|
-
two
|
|
401
|
-
"
|
|
402
|
-
);
|
|
403
|
-
assert!(
|
|
404
|
-
!codex_log.exists(),
|
|
405
|
-
"codex should not run at the raw/summary boundary"
|
|
406
|
-
);
|
|
407
|
-
|
|
408
|
-
let _ = fs::remove_dir_all(temp);
|
|
429
|
+
assert_eq!(String::from_utf8_lossy(&output.stdout), "one\ntwo\n");
|
|
409
430
|
}
|
|
410
431
|
|
|
411
432
|
#[test]
|
|
412
433
|
fn summary_mode_uses_combined_stdout_and_stderr_threshold() {
|
|
413
|
-
let
|
|
414
|
-
let
|
|
415
|
-
let
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
"
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
",
|
|
423
|
-
prompt_log.display()
|
|
424
|
-
),
|
|
425
|
-
);
|
|
434
|
+
let request_log = Arc::new(Mutex::new(String::new()));
|
|
435
|
+
let request_log_for_server = Arc::clone(&request_log);
|
|
436
|
+
let (base_url, server) = start_api_server(1, move |request| {
|
|
437
|
+
*request_log_for_server.lock().expect("request log") = request;
|
|
438
|
+
(
|
|
439
|
+
200,
|
|
440
|
+
response_json("- summary: combined output exceeded threshold"),
|
|
441
|
+
)
|
|
442
|
+
});
|
|
426
443
|
|
|
427
|
-
let path = format!(
|
|
428
|
-
"{}:{}",
|
|
429
|
-
temp.display(),
|
|
430
|
-
env::var("PATH").unwrap_or_default()
|
|
431
|
-
);
|
|
432
444
|
let output = Command::new(sparkshell_bin())
|
|
433
|
-
.env("
|
|
445
|
+
.env("OMX_API_BASE_URL", base_url)
|
|
434
446
|
.env("OMX_SPARKSHELL_LINES", "2")
|
|
435
447
|
.arg("sh")
|
|
436
448
|
.arg("-c")
|
|
437
449
|
.arg("printf 'one\n' && printf 'warn\nextra\n' >&2")
|
|
438
450
|
.output()
|
|
439
451
|
.expect("run sparkshell");
|
|
452
|
+
server.join().expect("api server");
|
|
440
453
|
|
|
441
454
|
assert!(output.status.success());
|
|
442
455
|
assert!(String::from_utf8_lossy(&output.stdout).contains("combined output exceeded threshold"));
|
|
443
|
-
let
|
|
444
|
-
assert!(
|
|
445
|
-
assert!(
|
|
446
|
-
"warn
|
|
447
|
-
extra"
|
|
448
|
-
));
|
|
449
|
-
|
|
450
|
-
let _ = fs::remove_dir_all(temp);
|
|
456
|
+
let request = request_log.lock().expect("request log");
|
|
457
|
+
assert!(request.contains("<<<STDERR"));
|
|
458
|
+
assert!(request.contains("warn\\nextra"));
|
|
451
459
|
}
|
|
452
460
|
|
|
453
461
|
#[test]
|
|
454
|
-
fn
|
|
455
|
-
let
|
|
462
|
+
fn summary_failure_when_api_is_missing_falls_back_to_raw_output() {
|
|
463
|
+
let listener = TcpListener::bind("127.0.0.1:0").expect("reserve port");
|
|
464
|
+
let base_url = format!("http://{}", listener.local_addr().expect("address"));
|
|
465
|
+
drop(listener);
|
|
466
|
+
|
|
456
467
|
let output = Command::new(sparkshell_bin())
|
|
457
|
-
.env("
|
|
468
|
+
.env("OMX_API_BASE_URL", base_url)
|
|
469
|
+
.env("OMX_SPARKSHELL_SUMMARY_TIMEOUT_MS", "500")
|
|
458
470
|
.env("OMX_SPARKSHELL_LINES", "1")
|
|
459
471
|
.arg("/bin/sh")
|
|
460
472
|
.arg("-c")
|
|
@@ -463,41 +475,53 @@ fn summary_failure_when_codex_is_missing_falls_back_to_raw_output() {
|
|
|
463
475
|
.expect("run sparkshell");
|
|
464
476
|
|
|
465
477
|
assert!(output.status.success());
|
|
466
|
-
assert_eq!(
|
|
467
|
-
String::from_utf8_lossy(&output.stdout),
|
|
468
|
-
"one
|
|
469
|
-
two
|
|
470
|
-
"
|
|
471
|
-
);
|
|
478
|
+
assert_eq!(String::from_utf8_lossy(&output.stdout), "one\ntwo\n");
|
|
472
479
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
473
480
|
assert!(stderr.contains("child-err"));
|
|
474
481
|
assert!(stderr.contains("summary unavailable"));
|
|
482
|
+
assert!(stderr.contains("showing raw output instead"));
|
|
483
|
+
}
|
|
475
484
|
|
|
476
|
-
|
|
485
|
+
#[test]
|
|
486
|
+
fn json_summary_failure_reports_raw_output_omitted() {
|
|
487
|
+
let (base_url, server) = start_api_server(1, |_request| {
|
|
488
|
+
(503, "{\"error\":\"bridge failed\"}".to_string())
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
let output = Command::new(sparkshell_bin())
|
|
492
|
+
.env("OMX_API_BASE_URL", base_url)
|
|
493
|
+
.env("OMX_SPARKSHELL_LINES", "1")
|
|
494
|
+
.arg("--json")
|
|
495
|
+
.arg("sh")
|
|
496
|
+
.arg("-c")
|
|
497
|
+
.arg("printf 'one\ntwo\n'")
|
|
498
|
+
.output()
|
|
499
|
+
.expect("run sparkshell");
|
|
500
|
+
server.join().expect("api server");
|
|
501
|
+
|
|
502
|
+
assert!(output.status.success());
|
|
503
|
+
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
504
|
+
assert!(stdout.contains("summary unavailable"));
|
|
505
|
+
assert!(stdout.contains("raw output omitted from JSON report"));
|
|
506
|
+
assert!(!stdout.contains("raw output included"));
|
|
507
|
+
assert!(String::from_utf8_lossy(&output.stderr).is_empty());
|
|
477
508
|
}
|
|
478
509
|
|
|
479
510
|
#[test]
|
|
480
511
|
fn tmux_pane_mode_uses_default_tail_lines_when_not_overridden() {
|
|
481
512
|
let temp = unique_temp_dir("tmux-default-tail");
|
|
482
513
|
let tmux = temp.join("tmux");
|
|
483
|
-
let codex = temp.join("codex");
|
|
484
514
|
let args_log = temp.join("tmux-args.log");
|
|
485
515
|
write_executable(
|
|
486
516
|
&tmux,
|
|
487
517
|
&format!(
|
|
488
|
-
"#!/bin/sh
|
|
489
|
-
printf '%s\n' \"$@\" > '{}'
|
|
490
|
-
printf 'line-1\nline-2\nline-3\n'
|
|
491
|
-
",
|
|
518
|
+
"#!/bin/sh\nprintf '%s\n' \"$@\" > '{}'\nprintf 'line-1\nline-2\nline-3\n'\n",
|
|
492
519
|
args_log.display()
|
|
493
520
|
),
|
|
494
521
|
);
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
printf '%s\n' '- summary: used default tmux tail'
|
|
499
|
-
",
|
|
500
|
-
);
|
|
522
|
+
let (base_url, server) = start_api_server(1, |_request| {
|
|
523
|
+
(200, response_json("- summary: used default tmux tail"))
|
|
524
|
+
});
|
|
501
525
|
|
|
502
526
|
let path = format!(
|
|
503
527
|
"{}:{}",
|
|
@@ -506,11 +530,13 @@ printf '%s\n' '- summary: used default tmux tail'
|
|
|
506
530
|
);
|
|
507
531
|
let output = Command::new(sparkshell_bin())
|
|
508
532
|
.env("PATH", path)
|
|
533
|
+
.env("OMX_API_BASE_URL", base_url)
|
|
509
534
|
.env("OMX_SPARKSHELL_LINES", "1")
|
|
510
535
|
.arg("--tmux-pane")
|
|
511
536
|
.arg("%21")
|
|
512
537
|
.output()
|
|
513
538
|
.expect("run sparkshell");
|
|
539
|
+
server.join().expect("api server");
|
|
514
540
|
|
|
515
541
|
assert!(output.status.success());
|
|
516
542
|
let tmux_args = fs::read_to_string(args_log).expect("tmux args");
|
|
@@ -519,3 +545,442 @@ printf '%s\n' '- summary: used default tmux tail'
|
|
|
519
545
|
|
|
520
546
|
let _ = fs::remove_dir_all(temp);
|
|
521
547
|
}
|
|
548
|
+
|
|
549
|
+
#[test]
|
|
550
|
+
fn summary_module_does_not_shell_out_to_codex() {
|
|
551
|
+
let source =
|
|
552
|
+
fs::read_to_string(Path::new(env!("CARGO_MANIFEST_DIR")).join("src/codex_bridge.rs"))
|
|
553
|
+
.expect("source");
|
|
554
|
+
assert!(!source.contains("Command::new(\"codex\")"));
|
|
555
|
+
assert!(!source.contains(".arg(\"exec\")"));
|
|
556
|
+
assert!(!source.contains("codex exec"));
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
#[test]
|
|
560
|
+
fn json_mode_emits_machine_readable_contract() {
|
|
561
|
+
let output = Command::new(sparkshell_bin())
|
|
562
|
+
.arg("--json")
|
|
563
|
+
.arg("sh")
|
|
564
|
+
.arg("-c")
|
|
565
|
+
.arg("printf 'ok\n'")
|
|
566
|
+
.output()
|
|
567
|
+
.expect("run sparkshell");
|
|
568
|
+
|
|
569
|
+
assert!(output.status.success());
|
|
570
|
+
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
571
|
+
assert!(stdout.contains("\"ok\": true"));
|
|
572
|
+
assert!(stdout.contains("\"mode\": \"command\""));
|
|
573
|
+
assert!(stdout.contains("\"status\": \"ok\""));
|
|
574
|
+
assert!(stdout.contains("\"summary\":"));
|
|
575
|
+
assert!(stdout.contains("\"evidence\":"));
|
|
576
|
+
assert!(stdout.contains("\"raw_hash\":"));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
#[test]
|
|
580
|
+
fn json_mode_reports_failed_command_details() {
|
|
581
|
+
let output = Command::new(sparkshell_bin())
|
|
582
|
+
.arg("--json")
|
|
583
|
+
.arg("sh")
|
|
584
|
+
.arg("-c")
|
|
585
|
+
.arg("printf 'bad\n' >&2; exit 9")
|
|
586
|
+
.output()
|
|
587
|
+
.expect("run sparkshell");
|
|
588
|
+
|
|
589
|
+
assert_eq!(output.status.code(), Some(9));
|
|
590
|
+
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
591
|
+
assert!(stdout.contains("\"ok\": false"));
|
|
592
|
+
assert!(stdout.contains("\"status\": \"failed\""));
|
|
593
|
+
assert!(stdout.contains("\"exit_code\": 9"));
|
|
594
|
+
assert!(stdout.contains("bad"));
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
#[test]
|
|
598
|
+
fn json_mode_classifies_auth_errors() {
|
|
599
|
+
let output = Command::new(sparkshell_bin())
|
|
600
|
+
.arg("--json")
|
|
601
|
+
.arg("sh")
|
|
602
|
+
.arg("-c")
|
|
603
|
+
.arg("printf 'Authorization failed\n' >&2; exit 1")
|
|
604
|
+
.output()
|
|
605
|
+
.expect("run sparkshell");
|
|
606
|
+
|
|
607
|
+
assert_eq!(output.status.code(), Some(1));
|
|
608
|
+
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
609
|
+
assert!(stdout.contains("\"classification\": \"auth_error\""));
|
|
610
|
+
assert!(stdout.contains("authentication-like error"));
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
#[test]
|
|
614
|
+
fn direct_command_preserves_child_json_flag() {
|
|
615
|
+
let temp = unique_temp_dir("child-json-flag");
|
|
616
|
+
let script = temp.join("echo-argv");
|
|
617
|
+
write_executable(
|
|
618
|
+
&script,
|
|
619
|
+
r#"#!/usr/bin/env bash
|
|
620
|
+
printf '%s\n' "$@"
|
|
621
|
+
"#,
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
let output = Command::new(sparkshell_bin())
|
|
625
|
+
.arg(script)
|
|
626
|
+
.arg("--json")
|
|
627
|
+
.arg("value")
|
|
628
|
+
.output()
|
|
629
|
+
.expect("run sparkshell");
|
|
630
|
+
|
|
631
|
+
assert!(output.status.success());
|
|
632
|
+
assert_eq!(String::from_utf8_lossy(&output.stdout), "--json\nvalue\n");
|
|
633
|
+
let _ = fs::remove_dir_all(temp);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
#[test]
|
|
637
|
+
fn team_diagnostics_reads_last_turn_at_heartbeat() {
|
|
638
|
+
let temp = unique_temp_dir("last-turn-heartbeat");
|
|
639
|
+
let worker_dir = temp.join("team/demo/workers/worker-1");
|
|
640
|
+
fs::create_dir_all(&worker_dir).expect("worker dir");
|
|
641
|
+
fs::write(
|
|
642
|
+
worker_dir.join("heartbeat.json"),
|
|
643
|
+
r#"{"last_turn_at":"1970-01-01T00:00:00.000Z"}"#,
|
|
644
|
+
)
|
|
645
|
+
.expect("heartbeat");
|
|
646
|
+
fs::write(
|
|
647
|
+
worker_dir.join("status.json"),
|
|
648
|
+
r#"{"state":"working","current_task_id":"1","updated_at":"2026-05-17T20:00:00.000Z"}"#,
|
|
649
|
+
)
|
|
650
|
+
.expect("status");
|
|
651
|
+
|
|
652
|
+
let output = Command::new(sparkshell_bin())
|
|
653
|
+
.env("OMX_TEAM_STATE_ROOT", temp.display().to_string())
|
|
654
|
+
.arg("--json")
|
|
655
|
+
.arg("--team")
|
|
656
|
+
.arg("demo")
|
|
657
|
+
.arg("--worker")
|
|
658
|
+
.arg("worker-1")
|
|
659
|
+
.arg("printf")
|
|
660
|
+
.arg("ok\n")
|
|
661
|
+
.output()
|
|
662
|
+
.expect("run sparkshell");
|
|
663
|
+
|
|
664
|
+
assert!(output.status.success());
|
|
665
|
+
assert!(
|
|
666
|
+
String::from_utf8_lossy(&output.stdout).contains("\"classification\": \"stale_heartbeat\"")
|
|
667
|
+
);
|
|
668
|
+
let _ = fs::remove_dir_all(temp);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
fn run_team_status_diagnostics(status_json: &str) -> String {
|
|
672
|
+
let temp = unique_temp_dir("team-status");
|
|
673
|
+
let worker_dir = temp.join("team/demo/workers/worker-1");
|
|
674
|
+
fs::create_dir_all(&worker_dir).expect("worker dir");
|
|
675
|
+
fs::write(worker_dir.join("status.json"), status_json).expect("status");
|
|
676
|
+
|
|
677
|
+
let output = Command::new(sparkshell_bin())
|
|
678
|
+
.env("OMX_TEAM_STATE_ROOT", temp.display().to_string())
|
|
679
|
+
.arg("--json")
|
|
680
|
+
.arg("--team")
|
|
681
|
+
.arg("demo")
|
|
682
|
+
.arg("--worker")
|
|
683
|
+
.arg("worker-1")
|
|
684
|
+
.arg("sh")
|
|
685
|
+
.arg("-c")
|
|
686
|
+
.arg("printf 'quiet\n'")
|
|
687
|
+
.output()
|
|
688
|
+
.expect("run sparkshell");
|
|
689
|
+
|
|
690
|
+
let _ = fs::remove_dir_all(temp);
|
|
691
|
+
assert!(output.status.success());
|
|
692
|
+
String::from_utf8_lossy(&output.stdout).into_owned()
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
#[test]
|
|
696
|
+
fn json_mode_treats_worker_status_working_as_busy() {
|
|
697
|
+
let stdout = run_team_status_diagnostics(
|
|
698
|
+
r#"{"state":"working","current_task_id":"1","updated_at":"2026-05-17T20:00:00.000Z"}"#,
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
assert!(stdout.contains("\"classification\": \"busy_processing\""));
|
|
702
|
+
assert!(stdout.contains("\"next_action\": \"wait\""));
|
|
703
|
+
assert!(stdout.contains("do not shutdown yet"));
|
|
704
|
+
assert!(!stdout.contains("\"classification\": \"unknown\""));
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
#[test]
|
|
708
|
+
fn json_mode_reports_blocked_worker_status() {
|
|
709
|
+
let stdout = run_team_status_diagnostics(
|
|
710
|
+
r#"{"state":"blocked","updated_at":"2026-05-17T20:00:00.000Z"}"#,
|
|
711
|
+
);
|
|
712
|
+
|
|
713
|
+
assert!(stdout.contains("\"classification\": \"waiting_for_input\""));
|
|
714
|
+
assert!(stdout.contains("\"next_action\": \"inspect raw pane\""));
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
#[test]
|
|
718
|
+
fn json_mode_reports_failed_worker_status() {
|
|
719
|
+
let stdout = run_team_status_diagnostics(
|
|
720
|
+
r#"{"state":"failed","updated_at":"2026-05-17T20:00:00.000Z"}"#,
|
|
721
|
+
);
|
|
722
|
+
|
|
723
|
+
assert!(stdout.contains("\"classification\": \"test_failure\""));
|
|
724
|
+
assert!(stdout.contains("worker status is failed"));
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
#[test]
|
|
728
|
+
fn json_mode_leaves_inactive_worker_statuses_to_output_heuristics() {
|
|
729
|
+
for state in ["idle", "done", "draining", "unknown"] {
|
|
730
|
+
let stdout = run_team_status_diagnostics(&format!(
|
|
731
|
+
r#"{{"state":"{state}","updated_at":"2026-05-17T20:00:00.000Z"}}"#
|
|
732
|
+
));
|
|
733
|
+
|
|
734
|
+
assert!(stdout.contains("\"classification\": \"unknown\""));
|
|
735
|
+
assert!(!stdout.contains("do not shutdown yet"));
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
#[test]
|
|
740
|
+
fn json_mode_reads_team_state_from_env_root() {
|
|
741
|
+
let temp = unique_temp_dir("team-state");
|
|
742
|
+
let worker_dir = temp.join("team/demo/workers/worker-1");
|
|
743
|
+
fs::create_dir_all(&worker_dir).expect("worker dir");
|
|
744
|
+
fs::write(
|
|
745
|
+
worker_dir.join("status.json"),
|
|
746
|
+
r#"{"state":"busy","task":"in_progress"}"#,
|
|
747
|
+
)
|
|
748
|
+
.expect("status");
|
|
749
|
+
|
|
750
|
+
let output = Command::new(sparkshell_bin())
|
|
751
|
+
.env("OMX_TEAM_STATE_ROOT", temp.display().to_string())
|
|
752
|
+
.arg("--json")
|
|
753
|
+
.arg("--team")
|
|
754
|
+
.arg("demo")
|
|
755
|
+
.arg("--worker")
|
|
756
|
+
.arg("worker-1")
|
|
757
|
+
.arg("sh")
|
|
758
|
+
.arg("-c")
|
|
759
|
+
.arg("printf 'quiet\n'")
|
|
760
|
+
.output()
|
|
761
|
+
.expect("run sparkshell");
|
|
762
|
+
|
|
763
|
+
assert!(output.status.success());
|
|
764
|
+
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
765
|
+
assert!(stdout.contains("\"classification\": \"busy_processing\""));
|
|
766
|
+
assert!(stdout.contains("do not shutdown yet"));
|
|
767
|
+
|
|
768
|
+
let _ = fs::remove_dir_all(temp);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
#[test]
|
|
772
|
+
fn pane_json_cache_reports_hits_and_since_last_changes() {
|
|
773
|
+
let temp = unique_temp_dir("pane-cache");
|
|
774
|
+
let tmux = temp.join("tmux");
|
|
775
|
+
let cache = temp.join("cache");
|
|
776
|
+
let pane = temp.join("pane.txt");
|
|
777
|
+
fs::write(&pane, "line-1\nline-2\n").expect("pane");
|
|
778
|
+
write_executable(&tmux, &format!("#!/bin/sh\ncat {}\n", pane.display()));
|
|
779
|
+
let path = format!(
|
|
780
|
+
"{}:{}",
|
|
781
|
+
temp.display(),
|
|
782
|
+
env::var("PATH").unwrap_or_default()
|
|
783
|
+
);
|
|
784
|
+
|
|
785
|
+
let first = Command::new(sparkshell_bin())
|
|
786
|
+
.env("PATH", &path)
|
|
787
|
+
.env("OMX_SPARKSHELL_CACHE_DIR", cache.display().to_string())
|
|
788
|
+
.arg("--json")
|
|
789
|
+
.arg("--tmux-pane")
|
|
790
|
+
.arg("%31")
|
|
791
|
+
.output()
|
|
792
|
+
.expect("first");
|
|
793
|
+
assert!(first.status.success());
|
|
794
|
+
assert!(String::from_utf8_lossy(&first.stdout).contains("\"cache_hit\":false"));
|
|
795
|
+
|
|
796
|
+
let second = Command::new(sparkshell_bin())
|
|
797
|
+
.env("PATH", &path)
|
|
798
|
+
.env("OMX_SPARKSHELL_CACHE_DIR", cache.display().to_string())
|
|
799
|
+
.arg("--json")
|
|
800
|
+
.arg("--tmux-pane")
|
|
801
|
+
.arg("%31")
|
|
802
|
+
.output()
|
|
803
|
+
.expect("second");
|
|
804
|
+
assert!(second.status.success());
|
|
805
|
+
assert!(String::from_utf8_lossy(&second.stdout).contains("\"cache_hit\":true"));
|
|
806
|
+
|
|
807
|
+
fs::write(&pane, "line-1\nline-2\nline-3\n").expect("pane update");
|
|
808
|
+
let third = Command::new(sparkshell_bin())
|
|
809
|
+
.env("PATH", &path)
|
|
810
|
+
.env("OMX_SPARKSHELL_CACHE_DIR", cache.display().to_string())
|
|
811
|
+
.arg("--json")
|
|
812
|
+
.arg("--since-last")
|
|
813
|
+
.arg("--tmux-pane")
|
|
814
|
+
.arg("%31")
|
|
815
|
+
.output()
|
|
816
|
+
.expect("third");
|
|
817
|
+
assert!(third.status.success());
|
|
818
|
+
let stdout = String::from_utf8_lossy(&third.stdout);
|
|
819
|
+
assert!(stdout.contains("\"changed_line_ranges\":[\"3-3\"]"));
|
|
820
|
+
assert!(stdout.contains("new findings since last observation"));
|
|
821
|
+
assert!(stdout.contains("line-3"));
|
|
822
|
+
|
|
823
|
+
let _ = fs::remove_dir_all(temp);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
#[test]
|
|
827
|
+
fn pane_cache_key_does_not_escape_cache_dir_for_path_like_pane_ids() {
|
|
828
|
+
let temp = unique_temp_dir("pane-cache-traversal");
|
|
829
|
+
let tmux = temp.join("tmux");
|
|
830
|
+
let cache = temp.join("cache");
|
|
831
|
+
let intermediate = cache.join("pane-..");
|
|
832
|
+
let outside = temp.join("outside-pr2371.txt");
|
|
833
|
+
|
|
834
|
+
fs::create_dir_all(&intermediate).expect("intermediate cache dir");
|
|
835
|
+
write_executable(
|
|
836
|
+
&tmux,
|
|
837
|
+
"#!/bin/sh
|
|
838
|
+
printf 'safe pane output
|
|
839
|
+
'
|
|
840
|
+
",
|
|
841
|
+
);
|
|
842
|
+
let path = format!(
|
|
843
|
+
"{}:{}",
|
|
844
|
+
temp.display(),
|
|
845
|
+
env::var("PATH").unwrap_or_default()
|
|
846
|
+
);
|
|
847
|
+
|
|
848
|
+
let output = Command::new(sparkshell_bin())
|
|
849
|
+
.env("PATH", &path)
|
|
850
|
+
.env("OMX_SPARKSHELL_CACHE_DIR", cache.display().to_string())
|
|
851
|
+
.arg("--json")
|
|
852
|
+
.arg("--tmux-pane")
|
|
853
|
+
.arg("../../../outside-pr2371")
|
|
854
|
+
.output()
|
|
855
|
+
.expect("run sparkshell");
|
|
856
|
+
|
|
857
|
+
assert!(
|
|
858
|
+
output.status.success(),
|
|
859
|
+
"sparkshell failed: stdout={} stderr={}",
|
|
860
|
+
String::from_utf8_lossy(&output.stdout),
|
|
861
|
+
String::from_utf8_lossy(&output.stderr)
|
|
862
|
+
);
|
|
863
|
+
assert!(
|
|
864
|
+
!outside.exists(),
|
|
865
|
+
"path-like pane id wrote outside cache dir at {}",
|
|
866
|
+
outside.display()
|
|
867
|
+
);
|
|
868
|
+
|
|
869
|
+
let cache_files = fs::read_dir(&cache)
|
|
870
|
+
.expect("cache dir")
|
|
871
|
+
.map(|entry| entry.expect("cache entry").path())
|
|
872
|
+
.collect::<Vec<_>>();
|
|
873
|
+
assert!(
|
|
874
|
+
cache_files.iter().any(|path| {
|
|
875
|
+
path.is_file()
|
|
876
|
+
&& path.parent() == Some(cache.as_path())
|
|
877
|
+
&& path
|
|
878
|
+
.file_name()
|
|
879
|
+
.and_then(|name| name.to_str())
|
|
880
|
+
.is_some_and(|name| name.starts_with("pane-h") && name.ends_with(".txt"))
|
|
881
|
+
}),
|
|
882
|
+
"expected sanitized pane cache file directly under cache dir, got {cache_files:?}"
|
|
883
|
+
);
|
|
884
|
+
|
|
885
|
+
let _ = fs::remove_dir_all(temp);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
#[test]
|
|
889
|
+
fn pane_cache_does_not_persist_raw_secret_like_text() {
|
|
890
|
+
let temp = unique_temp_dir("pane-cache-secret");
|
|
891
|
+
let tmux = temp.join("tmux");
|
|
892
|
+
let cache = temp.join("cache");
|
|
893
|
+
let pane = temp.join("pane.txt");
|
|
894
|
+
let secret_line = "OPENAI_API_KEY=sk-test-secret";
|
|
895
|
+
fs::write(
|
|
896
|
+
&pane,
|
|
897
|
+
format!(
|
|
898
|
+
"starting
|
|
899
|
+
{secret_line}
|
|
900
|
+
finished
|
|
901
|
+
"
|
|
902
|
+
),
|
|
903
|
+
)
|
|
904
|
+
.expect("pane");
|
|
905
|
+
write_executable(
|
|
906
|
+
&tmux,
|
|
907
|
+
&format!(
|
|
908
|
+
"#!/bin/sh
|
|
909
|
+
cat {}
|
|
910
|
+
",
|
|
911
|
+
pane.display()
|
|
912
|
+
),
|
|
913
|
+
);
|
|
914
|
+
let path = format!(
|
|
915
|
+
"{}:{}",
|
|
916
|
+
temp.display(),
|
|
917
|
+
env::var("PATH").unwrap_or_default()
|
|
918
|
+
);
|
|
919
|
+
|
|
920
|
+
let output = Command::new(sparkshell_bin())
|
|
921
|
+
.env("PATH", &path)
|
|
922
|
+
.env("OMX_SPARKSHELL_CACHE_DIR", cache.display().to_string())
|
|
923
|
+
.arg("--json")
|
|
924
|
+
.arg("--tmux-pane")
|
|
925
|
+
.arg("%99")
|
|
926
|
+
.output()
|
|
927
|
+
.expect("run sparkshell");
|
|
928
|
+
|
|
929
|
+
assert!(output.status.success());
|
|
930
|
+
let cache_file = cache.join("pane-pct99.txt");
|
|
931
|
+
let cached = fs::read_to_string(&cache_file).expect("cache file");
|
|
932
|
+
assert!(
|
|
933
|
+
!cached.contains(secret_line),
|
|
934
|
+
"cache file persisted raw secret-like pane text: {cached}"
|
|
935
|
+
);
|
|
936
|
+
assert!(
|
|
937
|
+
!cached.contains("sk-test-secret"),
|
|
938
|
+
"cache file persisted raw token value: {cached}"
|
|
939
|
+
);
|
|
940
|
+
assert!(
|
|
941
|
+
!cached.contains("OPENAI_API_KEY"),
|
|
942
|
+
"cache file persisted raw secret variable name: {cached}"
|
|
943
|
+
);
|
|
944
|
+
assert!(cached.contains("omx-sparkshell-cache-v2"));
|
|
945
|
+
assert!(cached.contains("lines=3"));
|
|
946
|
+
|
|
947
|
+
let _ = fs::remove_dir_all(temp);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
#[test]
|
|
951
|
+
fn raw_mode_preserves_non_utf8_bytes() {
|
|
952
|
+
let temp = unique_temp_dir("raw-non-utf8");
|
|
953
|
+
let script = temp.join("raw-bytes");
|
|
954
|
+
write_executable(
|
|
955
|
+
&script,
|
|
956
|
+
r#"#!/usr/bin/env bash
|
|
957
|
+
printf '\xff\xfe\n'
|
|
958
|
+
"#,
|
|
959
|
+
);
|
|
960
|
+
|
|
961
|
+
let output = Command::new(sparkshell_bin())
|
|
962
|
+
.arg(script)
|
|
963
|
+
.output()
|
|
964
|
+
.expect("run sparkshell");
|
|
965
|
+
|
|
966
|
+
assert!(output.status.success());
|
|
967
|
+
assert_eq!(output.stdout, vec![0xff, 0xfe, b'\n']);
|
|
968
|
+
let _ = fs::remove_dir_all(temp);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
#[test]
|
|
972
|
+
fn shell_mode_executes_explicit_shell_and_redacts_json_output() {
|
|
973
|
+
let output = Command::new(sparkshell_bin())
|
|
974
|
+
.arg("--json")
|
|
975
|
+
.arg("--shell")
|
|
976
|
+
.arg("printf 'left && right\n'; printf 'Authorization: Bearer secret-token\n' >&2")
|
|
977
|
+
.output()
|
|
978
|
+
.expect("run sparkshell");
|
|
979
|
+
|
|
980
|
+
assert!(output.status.success());
|
|
981
|
+
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
982
|
+
assert!(stdout.contains("\"mode\": \"shell\""));
|
|
983
|
+
assert!(stdout.contains("left && right"));
|
|
984
|
+
assert!(stdout.contains("Authorization: Bearer [REDACTED]"));
|
|
985
|
+
assert!(stdout.contains(r#""redactions": {"count": 1}"#));
|
|
986
|
+
}
|