oh-my-codex 0.17.2 → 0.18.0
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 +1 -0
- package/crates/omx-api/Cargo.toml +19 -0
- package/crates/omx-api/src/lib.rs +2940 -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 +4 -0
- package/crates/omx-sparkshell/src/main.rs +738 -29
- 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 +479 -238
- 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__/doctor-warning-copy.test.js +51 -0
- 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 +123 -5
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +76 -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 +45 -22
- package/dist/cli/__tests__/question.test.js.map +1 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +2 -0
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +138 -0
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/setup-scope.test.js +8 -2
- package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js +5 -0
- package/dist/cli/__tests__/sparkshell-cli.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/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +39 -4
- 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 +10 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +128 -10
- 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/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +6 -1
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/sparkshell.d.ts.map +1 -1
- package/dist/cli/sparkshell.js +20 -3
- package/dist/cli/sparkshell.js.map +1 -1
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +90 -0
- package/dist/config/generator.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__/keyword-detector.test.js +11 -0
- 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__/prompt-guidance-wave-two.test.js +4 -0
- package/dist/hooks/__tests__/prompt-guidance-wave-two.test.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/hud/__tests__/reconcile.test.js +2 -2
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/__tests__/tmux.test.js +23 -18
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/tmux.d.ts.map +1 -1
- package/dist/hud/tmux.js +7 -6
- 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/scripts/__tests__/codex-native-hook.test.js +737 -26
- 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 +4 -1
- package/dist/scripts/__tests__/smoke-packed-install.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/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +208 -8
- 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 +89 -24
- 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/team-dispatch.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.js +27 -9
- 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 +1 -0
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.js +38 -0
- 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 +1 -1
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +2 -0
- package/dist/scripts/smoke-packed-install.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 +153 -25
- 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 +55 -10
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/utils/__tests__/agents-md.test.js +45 -1
- package/dist/utils/__tests__/agents-md.test.js.map +1 -1
- package/dist/utils/agents-md.d.ts +2 -0
- package/dist/utils/agents-md.d.ts.map +1 -1
- package/dist/utils/agents-md.js +19 -0
- package/dist/utils/agents-md.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 +4 -3
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/skills/best-practice-research/SKILL.md +83 -0
- package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +1 -0
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +1 -1
- package/prompts/researcher.md +15 -10
- package/skills/best-practice-research/SKILL.md +83 -0
- package/skills/deep-interview/SKILL.md +1 -0
- package/skills/ralplan/SKILL.md +1 -1
- package/src/scripts/__tests__/codex-native-hook.test.ts +810 -4
- package/src/scripts/__tests__/notify-dispatcher.test.ts +223 -1
- package/src/scripts/__tests__/smoke-packed-install.test.ts +8 -2
- package/src/scripts/build-api.ts +48 -0
- package/src/scripts/codex-native-hook.ts +262 -10
- package/src/scripts/codex-native-pre-post.ts +103 -24
- package/src/scripts/notify-dispatcher.ts +97 -0
- package/src/scripts/notify-hook/team-dispatch.ts +27 -8
- package/src/scripts/notify-hook/team-leader-nudge.ts +25 -11
- package/src/scripts/notify-hook/team-tmux-guard.ts +42 -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 +2 -0
- package/templates/catalog-manifest.json +7 -0
|
@@ -62,7 +62,10 @@ async function writeJson(path: string, value: unknown): Promise<void> {
|
|
|
62
62
|
await writeFile(path, JSON.stringify(value, null, 2));
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
function buildWorkerStopFakeTmux(
|
|
65
|
+
function buildWorkerStopFakeTmux(
|
|
66
|
+
tmuxLogPath: string,
|
|
67
|
+
options: { failSend?: boolean; busyLeader?: boolean } = {},
|
|
68
|
+
): string {
|
|
66
69
|
return `#!/usr/bin/env bash
|
|
67
70
|
set -eu
|
|
68
71
|
echo "$@" >> "${tmuxLogPath}"
|
|
@@ -90,7 +93,7 @@ if [[ "$cmd" == "display-message" ]]; then
|
|
|
90
93
|
exit 0
|
|
91
94
|
fi
|
|
92
95
|
if [[ "$cmd" == "capture-pane" ]]; then
|
|
93
|
-
echo "› ready"
|
|
96
|
+
${options.busyLeader ? 'echo "• Working… (esc to interrupt)"' : 'echo "› ready"'}
|
|
94
97
|
exit 0
|
|
95
98
|
fi
|
|
96
99
|
if [[ "$cmd" == "send-keys" ]]; then
|
|
@@ -1701,6 +1704,71 @@ describe("codex native hook dispatch", () => {
|
|
|
1701
1704
|
}
|
|
1702
1705
|
});
|
|
1703
1706
|
|
|
1707
|
+
it("does not repeat Stop block when the last autoresearch-goal completion attempt reported objective mismatch", async () => {
|
|
1708
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autoresearch-mismatch-reported-stop-"));
|
|
1709
|
+
try {
|
|
1710
|
+
await writeJson(join(cwd, ".omx", "goals", "autoresearch", "mismatched-mission", "mission.json"), {
|
|
1711
|
+
version: 1,
|
|
1712
|
+
workflow: "autoresearch-goal",
|
|
1713
|
+
slug: "mismatched-mission",
|
|
1714
|
+
topic: "Passing research bound to another Codex goal",
|
|
1715
|
+
status: "passed",
|
|
1716
|
+
});
|
|
1717
|
+
await writeJson(join(cwd, ".omx", "goals", "autoresearch", "mismatched-mission", "completion.json"), {
|
|
1718
|
+
verdict: "pass",
|
|
1719
|
+
passed: true,
|
|
1720
|
+
});
|
|
1721
|
+
|
|
1722
|
+
const result = await dispatchCodexNativeHook({
|
|
1723
|
+
hook_event_name: "Stop",
|
|
1724
|
+
cwd,
|
|
1725
|
+
session_id: "sess-autoresearch-mismatch-reported-stop",
|
|
1726
|
+
thread_id: "thread-autoresearch-mismatch-reported-stop",
|
|
1727
|
+
last_assistant_message: [
|
|
1728
|
+
"I called get_goal and ran omx autoresearch-goal complete --slug mismatched-mission --codex-goal-json /tmp/snapshot.json.",
|
|
1729
|
+
"The autoresearch-goal completion failed with Codex goal objective mismatch, so I will not repeat the same complete command blindly in this thread.",
|
|
1730
|
+
].join("\n"),
|
|
1731
|
+
}, { cwd });
|
|
1732
|
+
|
|
1733
|
+
assert.notEqual(result.outputJson?.decision, "block");
|
|
1734
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /autoresearch-goal complete --slug mismatched-mission/);
|
|
1735
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /get_goal snapshot reconciliation/);
|
|
1736
|
+
} finally {
|
|
1737
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1738
|
+
}
|
|
1739
|
+
});
|
|
1740
|
+
|
|
1741
|
+
it("still blocks later autoresearch-goal completion claims after an objective mismatch if no mismatch is reported in the final answer", async () => {
|
|
1742
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autoresearch-mismatch-later-retry-stop-"));
|
|
1743
|
+
try {
|
|
1744
|
+
await writeJson(join(cwd, ".omx", "goals", "autoresearch", "retryable-mission", "mission.json"), {
|
|
1745
|
+
version: 1,
|
|
1746
|
+
workflow: "autoresearch-goal",
|
|
1747
|
+
slug: "retryable-mission",
|
|
1748
|
+
topic: "Passing research that can still retry with the correct snapshot",
|
|
1749
|
+
status: "passed",
|
|
1750
|
+
});
|
|
1751
|
+
await writeJson(join(cwd, ".omx", "goals", "autoresearch", "retryable-mission", "completion.json"), {
|
|
1752
|
+
verdict: "pass",
|
|
1753
|
+
passed: true,
|
|
1754
|
+
});
|
|
1755
|
+
|
|
1756
|
+
const result = await dispatchCodexNativeHook({
|
|
1757
|
+
hook_event_name: "Stop",
|
|
1758
|
+
cwd,
|
|
1759
|
+
session_id: "sess-autoresearch-mismatch-later-retry-stop",
|
|
1760
|
+
thread_id: "thread-autoresearch-mismatch-later-retry-stop",
|
|
1761
|
+
last_assistant_message: "Autoresearch goal complete; next call update_goal({status: \"complete\"}).",
|
|
1762
|
+
}, { cwd });
|
|
1763
|
+
|
|
1764
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
1765
|
+
assert.match(JSON.stringify(result.outputJson), /get_goal snapshot reconciliation/);
|
|
1766
|
+
assert.match(JSON.stringify(result.outputJson), /omx autoresearch-goal complete --slug retryable-mission/);
|
|
1767
|
+
} finally {
|
|
1768
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1769
|
+
}
|
|
1770
|
+
});
|
|
1771
|
+
|
|
1704
1772
|
it("treats workflow keywords in native subagent prompt text as literal delegation text", async () => {
|
|
1705
1773
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-keyword-literal-"));
|
|
1706
1774
|
try {
|
|
@@ -2863,6 +2931,52 @@ exit 0
|
|
|
2863
2931
|
}
|
|
2864
2932
|
});
|
|
2865
2933
|
|
|
2934
|
+
it("does not block Bash commands that only mention omx question in quoted arguments", async () => {
|
|
2935
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-quoted-mention-"));
|
|
2936
|
+
try {
|
|
2937
|
+
const result = await dispatchCodexNativeHook(
|
|
2938
|
+
{
|
|
2939
|
+
hook_event_name: "PreToolUse",
|
|
2940
|
+
cwd,
|
|
2941
|
+
tool_name: "Bash",
|
|
2942
|
+
tool_use_id: "tool-question-quoted-mention",
|
|
2943
|
+
tool_input: {
|
|
2944
|
+
command: `omx ultragoal create-goals --brief "Deep interview says omx question failed in tmux"`,
|
|
2945
|
+
},
|
|
2946
|
+
},
|
|
2947
|
+
{ cwd },
|
|
2948
|
+
);
|
|
2949
|
+
|
|
2950
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2951
|
+
assert.equal(result.outputJson, null);
|
|
2952
|
+
} finally {
|
|
2953
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2954
|
+
}
|
|
2955
|
+
});
|
|
2956
|
+
|
|
2957
|
+
it("does not block Bash heredocs that only document omx question text", async () => {
|
|
2958
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-heredoc-mention-"));
|
|
2959
|
+
try {
|
|
2960
|
+
const result = await dispatchCodexNativeHook(
|
|
2961
|
+
{
|
|
2962
|
+
hook_event_name: "PreToolUse",
|
|
2963
|
+
cwd,
|
|
2964
|
+
tool_name: "Bash",
|
|
2965
|
+
tool_use_id: "tool-question-heredoc-mention",
|
|
2966
|
+
tool_input: {
|
|
2967
|
+
command: `cat > issue-notes.md <<'EOF'\nomx question failed in the attached tmux pane\nEOF`,
|
|
2968
|
+
},
|
|
2969
|
+
},
|
|
2970
|
+
{ cwd },
|
|
2971
|
+
);
|
|
2972
|
+
|
|
2973
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2974
|
+
assert.equal(result.outputJson, null);
|
|
2975
|
+
} finally {
|
|
2976
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2977
|
+
}
|
|
2978
|
+
});
|
|
2979
|
+
|
|
2866
2980
|
it("allows Bash omx question when the command preserves the leader-pane return hint", async () => {
|
|
2867
2981
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-allow-"));
|
|
2868
2982
|
try {
|
|
@@ -4866,6 +4980,99 @@ exit 0
|
|
|
4866
4980
|
}
|
|
4867
4981
|
});
|
|
4868
4982
|
|
|
4983
|
+
it("does not treat non-MCP source output containing detector constants as MCP transport death", async () => {
|
|
4984
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-read-mcp-source-"));
|
|
4985
|
+
try {
|
|
4986
|
+
const result = await dispatchCodexNativeHook(
|
|
4987
|
+
{
|
|
4988
|
+
hook_event_name: "PostToolUse",
|
|
4989
|
+
cwd,
|
|
4990
|
+
tool_name: "Read",
|
|
4991
|
+
tool_use_id: "tool-read-mcp-source",
|
|
4992
|
+
tool_input: { file_path: "src/scripts/codex-native-pre-post.ts" },
|
|
4993
|
+
tool_response:
|
|
4994
|
+
"const MCP_TRANSPORT_FAILURE_PATTERNS = [/transport closed/i, /server disconnected/i];\nconst context = /\\bmcp\\b/i;",
|
|
4995
|
+
},
|
|
4996
|
+
{ cwd },
|
|
4997
|
+
);
|
|
4998
|
+
|
|
4999
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
5000
|
+
assert.equal(result.outputJson, null);
|
|
5001
|
+
} finally {
|
|
5002
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5003
|
+
}
|
|
5004
|
+
});
|
|
5005
|
+
|
|
5006
|
+
it("does not treat non-MCP docs stdout mentioning closed MCP transport as transport death", async () => {
|
|
5007
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-docs-mcp-log-"));
|
|
5008
|
+
try {
|
|
5009
|
+
const result = await dispatchCodexNativeHook(
|
|
5010
|
+
{
|
|
5011
|
+
hook_event_name: "PostToolUse",
|
|
5012
|
+
cwd,
|
|
5013
|
+
tool_name: "ShellOutput",
|
|
5014
|
+
tool_use_id: "tool-docs-mcp-log",
|
|
5015
|
+
tool_response: JSON.stringify({
|
|
5016
|
+
stdout: "Troubleshooting note: MCP transport closed after the server disconnected in an old log.",
|
|
5017
|
+
stderr: "",
|
|
5018
|
+
}),
|
|
5019
|
+
},
|
|
5020
|
+
{ cwd },
|
|
5021
|
+
);
|
|
5022
|
+
|
|
5023
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
5024
|
+
assert.equal(result.outputJson, null);
|
|
5025
|
+
} finally {
|
|
5026
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5027
|
+
}
|
|
5028
|
+
});
|
|
5029
|
+
|
|
5030
|
+
it("does not MCP-block non-MCP command output with unrelated stderr and MCP transport stdout", async () => {
|
|
5031
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-nonmcp-mixed-output-"));
|
|
5032
|
+
try {
|
|
5033
|
+
const result = await dispatchCodexNativeHook(
|
|
5034
|
+
{
|
|
5035
|
+
hook_event_name: "PostToolUse",
|
|
5036
|
+
cwd,
|
|
5037
|
+
tool_name: "ShellOutput",
|
|
5038
|
+
tool_use_id: "tool-nonmcp-mixed-output",
|
|
5039
|
+
tool_response: JSON.stringify({
|
|
5040
|
+
stdout: "captured log line: MCP transport closed before response",
|
|
5041
|
+
stderr: "grep: fixture.txt: No such file or directory",
|
|
5042
|
+
}),
|
|
5043
|
+
},
|
|
5044
|
+
{ cwd },
|
|
5045
|
+
);
|
|
5046
|
+
|
|
5047
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
5048
|
+
assert.equal(result.outputJson, null);
|
|
5049
|
+
} finally {
|
|
5050
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5051
|
+
}
|
|
5052
|
+
});
|
|
5053
|
+
|
|
5054
|
+
it("still blocks MCP-like raw transport failures", async () => {
|
|
5055
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-raw-transport-"));
|
|
5056
|
+
try {
|
|
5057
|
+
const result = await dispatchCodexNativeHook(
|
|
5058
|
+
{
|
|
5059
|
+
hook_event_name: "PostToolUse",
|
|
5060
|
+
cwd,
|
|
5061
|
+
tool_name: "mcp__omx_state__state_write",
|
|
5062
|
+
tool_use_id: "tool-mcp-raw-transport",
|
|
5063
|
+
tool_response: "transport closed after server disconnected",
|
|
5064
|
+
},
|
|
5065
|
+
{ cwd },
|
|
5066
|
+
);
|
|
5067
|
+
|
|
5068
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
5069
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
5070
|
+
assert.match(String(result.outputJson?.reason || ""), /lost its transport\/server connection/);
|
|
5071
|
+
} finally {
|
|
5072
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5073
|
+
}
|
|
5074
|
+
});
|
|
5075
|
+
|
|
4869
5076
|
it("returns PostToolUse MCP transport fallback guidance for clear MCP transport death", async () => {
|
|
4870
5077
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-transport-"));
|
|
4871
5078
|
try {
|
|
@@ -5313,6 +5520,61 @@ exit 0
|
|
|
5313
5520
|
}
|
|
5314
5521
|
});
|
|
5315
5522
|
|
|
5523
|
+
it("requires Autopilot code review after a compact-boundary Stop exemption", async () => {
|
|
5524
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-review-compact-"));
|
|
5525
|
+
try {
|
|
5526
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
5527
|
+
const sessionId = "sess-stop-autopilot-review-compact";
|
|
5528
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
5529
|
+
await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
|
|
5530
|
+
active: true,
|
|
5531
|
+
mode: "autopilot",
|
|
5532
|
+
current_phase: "code-review",
|
|
5533
|
+
state: {
|
|
5534
|
+
phase_cycle: ["ralplan", "ralph", "code-review"],
|
|
5535
|
+
handoff_artifacts: {
|
|
5536
|
+
ralplan: ".omx/plans/prd-issue-2366.md",
|
|
5537
|
+
ralph: { verification: ["npm test"] },
|
|
5538
|
+
code_review: null,
|
|
5539
|
+
},
|
|
5540
|
+
review_verdict: null,
|
|
5541
|
+
},
|
|
5542
|
+
});
|
|
5543
|
+
|
|
5544
|
+
const compactBoundary = await dispatchCodexNativeHook(
|
|
5545
|
+
{
|
|
5546
|
+
hook_event_name: "Stop",
|
|
5547
|
+
cwd,
|
|
5548
|
+
session_id: sessionId,
|
|
5549
|
+
stop_reason: "context compact",
|
|
5550
|
+
},
|
|
5551
|
+
{ cwd },
|
|
5552
|
+
);
|
|
5553
|
+
const resumedStop = await dispatchCodexNativeHook(
|
|
5554
|
+
{
|
|
5555
|
+
hook_event_name: "Stop",
|
|
5556
|
+
cwd,
|
|
5557
|
+
session_id: sessionId,
|
|
5558
|
+
},
|
|
5559
|
+
{ cwd },
|
|
5560
|
+
);
|
|
5561
|
+
|
|
5562
|
+
assert.equal(compactBoundary.omxEventName, "stop");
|
|
5563
|
+
assert.equal(compactBoundary.outputJson, null);
|
|
5564
|
+
assert.equal(resumedStop.omxEventName, "stop");
|
|
5565
|
+
assert.deepEqual(resumedStop.outputJson, {
|
|
5566
|
+
decision: "block",
|
|
5567
|
+
reason:
|
|
5568
|
+
"OMX autopilot is still active (phase: code-review); continue the task and gather fresh verification evidence before stopping.",
|
|
5569
|
+
stopReason: "autopilot_code-review",
|
|
5570
|
+
systemMessage:
|
|
5571
|
+
"OMX autopilot is still active (phase: code-review). Run the required $code-review step before completing or clearing Autopilot state.",
|
|
5572
|
+
});
|
|
5573
|
+
} finally {
|
|
5574
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5575
|
+
}
|
|
5576
|
+
});
|
|
5577
|
+
|
|
5316
5578
|
it("suppresses duplicate Autopilot planning Stop replays so stale planning state cannot loop indefinitely", async () => {
|
|
5317
5579
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-planning-replay-"));
|
|
5318
5580
|
try {
|
|
@@ -5913,6 +6175,98 @@ exit 0
|
|
|
5913
6175
|
}
|
|
5914
6176
|
});
|
|
5915
6177
|
|
|
6178
|
+
it("queues worker Stop leader nudge with Tab and submit when leader pane is busy", async () => {
|
|
6179
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-busy-leader-"));
|
|
6180
|
+
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
6181
|
+
const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
6182
|
+
const prevPath = process.env.PATH;
|
|
6183
|
+
try {
|
|
6184
|
+
await initTeamState(
|
|
6185
|
+
"worker-stop-team-busy-leader",
|
|
6186
|
+
"worker stop busy leader",
|
|
6187
|
+
"executor",
|
|
6188
|
+
1,
|
|
6189
|
+
cwd,
|
|
6190
|
+
undefined,
|
|
6191
|
+
{ ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-busy-leader" },
|
|
6192
|
+
);
|
|
6193
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
6194
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
6195
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
6196
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath, { busyLeader: true }));
|
|
6197
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
6198
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
6199
|
+
const teamDir = join(stateDir, "team", "worker-stop-team-busy-leader");
|
|
6200
|
+
const workerDir = join(teamDir, "workers", "worker-1");
|
|
6201
|
+
await writeJson(join(teamDir, "config.json"), {
|
|
6202
|
+
name: "worker-stop-team-busy-leader",
|
|
6203
|
+
tmux_session: "omx-team-worker-stop",
|
|
6204
|
+
leader_pane_id: "%42",
|
|
6205
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
6206
|
+
});
|
|
6207
|
+
await writeJson(join(teamDir, "manifest.v2.json"), {
|
|
6208
|
+
name: "worker-stop-team-busy-leader",
|
|
6209
|
+
tmux_session: "omx-team-worker-stop",
|
|
6210
|
+
leader_pane_id: "%42",
|
|
6211
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
6212
|
+
});
|
|
6213
|
+
await writeJson(join(workerDir, "identity.json"), {
|
|
6214
|
+
name: "worker-1",
|
|
6215
|
+
index: 1,
|
|
6216
|
+
role: "executor",
|
|
6217
|
+
assigned_tasks: ["1"],
|
|
6218
|
+
worktree_path: cwd,
|
|
6219
|
+
team_state_root: stateDir,
|
|
6220
|
+
});
|
|
6221
|
+
await writeJson(join(workerDir, "status.json"), {
|
|
6222
|
+
state: "done",
|
|
6223
|
+
current_task_id: "1",
|
|
6224
|
+
updated_at: new Date().toISOString(),
|
|
6225
|
+
});
|
|
6226
|
+
await writeJson(join(teamDir, "tasks", "task-1.json"), {
|
|
6227
|
+
id: "1",
|
|
6228
|
+
subject: "hook task",
|
|
6229
|
+
description: "finish hook task",
|
|
6230
|
+
status: "completed",
|
|
6231
|
+
owner: "worker-1",
|
|
6232
|
+
created_at: new Date().toISOString(),
|
|
6233
|
+
});
|
|
6234
|
+
|
|
6235
|
+
process.env.OMX_TEAM_WORKER = "worker-stop-team-busy-leader/worker-1";
|
|
6236
|
+
process.env.OMX_TEAM_STATE_ROOT = stateDir;
|
|
6237
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
6238
|
+
|
|
6239
|
+
const result = await dispatchCodexNativeHook(
|
|
6240
|
+
{
|
|
6241
|
+
hook_event_name: "Stop",
|
|
6242
|
+
cwd,
|
|
6243
|
+
session_id: "sess-stop-team-worker-busy-leader",
|
|
6244
|
+
},
|
|
6245
|
+
{ cwd },
|
|
6246
|
+
);
|
|
6247
|
+
|
|
6248
|
+
assert.equal(result.outputJson, null);
|
|
6249
|
+
const tmuxLog = await readFile(tmuxLogPath, "utf-8");
|
|
6250
|
+
assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
|
|
6251
|
+
assert.match(tmuxLog, /send-keys -t %42 Tab/);
|
|
6252
|
+
assert.match(tmuxLog, /send-keys -t %42 C-m/);
|
|
6253
|
+
assert.ok(
|
|
6254
|
+
tmuxLog.indexOf("send-keys -t %42 Tab") < tmuxLog.indexOf("send-keys -t %42 C-m"),
|
|
6255
|
+
"busy worker-stop nudge should press Tab before C-m",
|
|
6256
|
+
);
|
|
6257
|
+
const nudgeState = JSON.parse(await readFile(join(workerDir, "worker-stop-nudge.json"), "utf-8"));
|
|
6258
|
+
assert.equal(nudgeState.delivery, "queued");
|
|
6259
|
+
} finally {
|
|
6260
|
+
if (typeof prevTeamWorker === "string") process.env.OMX_TEAM_WORKER = prevTeamWorker;
|
|
6261
|
+
else delete process.env.OMX_TEAM_WORKER;
|
|
6262
|
+
if (typeof prevTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
|
|
6263
|
+
else delete process.env.OMX_TEAM_STATE_ROOT;
|
|
6264
|
+
if (typeof prevPath === "string") process.env.PATH = prevPath;
|
|
6265
|
+
else delete process.env.PATH;
|
|
6266
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6267
|
+
}
|
|
6268
|
+
});
|
|
6269
|
+
|
|
5916
6270
|
it("allows worker Stop when the Stop nudge helper cannot deliver", async () => {
|
|
5917
6271
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-helper-fail-"));
|
|
5918
6272
|
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
@@ -6843,8 +7197,287 @@ exit 0
|
|
|
6843
7197
|
}
|
|
6844
7198
|
});
|
|
6845
7199
|
|
|
6846
|
-
it("does not block
|
|
6847
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
7200
|
+
it("does not block when canonical root ralplan state is inactive but session ralplan state is stale active", async () => {
|
|
7201
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-session-ralplan-root-inactive-"));
|
|
7202
|
+
try {
|
|
7203
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7204
|
+
const sessionId = "sess-stop-stale-session-ralplan";
|
|
7205
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
7206
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
7207
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
7208
|
+
active: false,
|
|
7209
|
+
skill: "ralplan",
|
|
7210
|
+
phase: "reviewing",
|
|
7211
|
+
active_skills: [],
|
|
7212
|
+
});
|
|
7213
|
+
await writeJson(join(stateDir, "ralplan-state.json"), {
|
|
7214
|
+
active: false,
|
|
7215
|
+
mode: "ralplan",
|
|
7216
|
+
current_phase: "complete",
|
|
7217
|
+
session_id: sessionId,
|
|
7218
|
+
});
|
|
7219
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
7220
|
+
active: true,
|
|
7221
|
+
skill: "ralplan",
|
|
7222
|
+
phase: "planning",
|
|
7223
|
+
session_id: sessionId,
|
|
7224
|
+
active_skills: [{
|
|
7225
|
+
skill: "ralplan",
|
|
7226
|
+
phase: "planning",
|
|
7227
|
+
active: true,
|
|
7228
|
+
session_id: sessionId,
|
|
7229
|
+
}],
|
|
7230
|
+
});
|
|
7231
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
7232
|
+
active: true,
|
|
7233
|
+
mode: "ralplan",
|
|
7234
|
+
current_phase: "planning",
|
|
7235
|
+
session_id: sessionId,
|
|
7236
|
+
});
|
|
7237
|
+
|
|
7238
|
+
const result = await dispatchCodexNativeHook(
|
|
7239
|
+
{
|
|
7240
|
+
hook_event_name: "Stop",
|
|
7241
|
+
cwd,
|
|
7242
|
+
session_id: sessionId,
|
|
7243
|
+
},
|
|
7244
|
+
{ cwd },
|
|
7245
|
+
);
|
|
7246
|
+
|
|
7247
|
+
assert.equal(result.omxEventName, "stop");
|
|
7248
|
+
assert.equal(result.outputJson, null);
|
|
7249
|
+
} finally {
|
|
7250
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7251
|
+
}
|
|
7252
|
+
});
|
|
7253
|
+
|
|
7254
|
+
it("keeps blocking current session ralplan when root inactive ralplan state belongs to another session", async () => {
|
|
7255
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-other-session-"));
|
|
7256
|
+
try {
|
|
7257
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7258
|
+
const sessionId = "sess-stop-current-active-ralplan";
|
|
7259
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
7260
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
7261
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
7262
|
+
active: false,
|
|
7263
|
+
skill: "ralplan",
|
|
7264
|
+
phase: "complete",
|
|
7265
|
+
session_id: "sess-stop-old-ralplan",
|
|
7266
|
+
active_skills: [],
|
|
7267
|
+
});
|
|
7268
|
+
await writeJson(join(stateDir, "ralplan-state.json"), {
|
|
7269
|
+
active: false,
|
|
7270
|
+
mode: "ralplan",
|
|
7271
|
+
current_phase: "complete",
|
|
7272
|
+
session_id: "sess-stop-old-ralplan",
|
|
7273
|
+
});
|
|
7274
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
7275
|
+
active: true,
|
|
7276
|
+
skill: "ralplan",
|
|
7277
|
+
phase: "planning",
|
|
7278
|
+
session_id: sessionId,
|
|
7279
|
+
active_skills: [{
|
|
7280
|
+
skill: "ralplan",
|
|
7281
|
+
phase: "planning",
|
|
7282
|
+
active: true,
|
|
7283
|
+
session_id: sessionId,
|
|
7284
|
+
}],
|
|
7285
|
+
});
|
|
7286
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
7287
|
+
active: true,
|
|
7288
|
+
mode: "ralplan",
|
|
7289
|
+
current_phase: "planning",
|
|
7290
|
+
session_id: sessionId,
|
|
7291
|
+
});
|
|
7292
|
+
|
|
7293
|
+
const result = await dispatchCodexNativeHook(
|
|
7294
|
+
{
|
|
7295
|
+
hook_event_name: "Stop",
|
|
7296
|
+
cwd,
|
|
7297
|
+
session_id: sessionId,
|
|
7298
|
+
},
|
|
7299
|
+
{ cwd },
|
|
7300
|
+
);
|
|
7301
|
+
|
|
7302
|
+
assert.equal(result.omxEventName, "stop");
|
|
7303
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
7304
|
+
assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
|
|
7305
|
+
} finally {
|
|
7306
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7307
|
+
}
|
|
7308
|
+
});
|
|
7309
|
+
|
|
7310
|
+
it("keeps blocking current session ralplan when root inactive ralplan state is unscoped", async () => {
|
|
7311
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-unscoped-"));
|
|
7312
|
+
try {
|
|
7313
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7314
|
+
const sessionId = "sess-stop-unscoped-root-current-active";
|
|
7315
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
7316
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
7317
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
7318
|
+
active: false,
|
|
7319
|
+
skill: "ralplan",
|
|
7320
|
+
phase: "complete",
|
|
7321
|
+
active_skills: [],
|
|
7322
|
+
});
|
|
7323
|
+
await writeJson(join(stateDir, "ralplan-state.json"), {
|
|
7324
|
+
active: false,
|
|
7325
|
+
mode: "ralplan",
|
|
7326
|
+
current_phase: "complete",
|
|
7327
|
+
});
|
|
7328
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
7329
|
+
active: true,
|
|
7330
|
+
skill: "ralplan",
|
|
7331
|
+
phase: "planning",
|
|
7332
|
+
session_id: sessionId,
|
|
7333
|
+
active_skills: [{
|
|
7334
|
+
skill: "ralplan",
|
|
7335
|
+
phase: "planning",
|
|
7336
|
+
active: true,
|
|
7337
|
+
session_id: sessionId,
|
|
7338
|
+
}],
|
|
7339
|
+
});
|
|
7340
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
7341
|
+
active: true,
|
|
7342
|
+
mode: "ralplan",
|
|
7343
|
+
current_phase: "planning",
|
|
7344
|
+
session_id: sessionId,
|
|
7345
|
+
});
|
|
7346
|
+
|
|
7347
|
+
const result = await dispatchCodexNativeHook(
|
|
7348
|
+
{
|
|
7349
|
+
hook_event_name: "Stop",
|
|
7350
|
+
cwd,
|
|
7351
|
+
session_id: sessionId,
|
|
7352
|
+
},
|
|
7353
|
+
{ cwd },
|
|
7354
|
+
);
|
|
7355
|
+
|
|
7356
|
+
assert.equal(result.omxEventName, "stop");
|
|
7357
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
7358
|
+
assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
|
|
7359
|
+
} finally {
|
|
7360
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7361
|
+
}
|
|
7362
|
+
});
|
|
7363
|
+
|
|
7364
|
+
it("does not block stale session ralplan when root ralplan is terminal and another root skill is active", async () => {
|
|
7365
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-ralplan-other-root-skill-"));
|
|
7366
|
+
try {
|
|
7367
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7368
|
+
const sessionId = "sess-stop-stale-ralplan-other-root-skill";
|
|
7369
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
7370
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
7371
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
7372
|
+
active: true,
|
|
7373
|
+
skill: "deep-interview",
|
|
7374
|
+
phase: "intent-first",
|
|
7375
|
+
session_id: sessionId,
|
|
7376
|
+
active_skills: [{
|
|
7377
|
+
skill: "deep-interview",
|
|
7378
|
+
phase: "intent-first",
|
|
7379
|
+
active: true,
|
|
7380
|
+
session_id: sessionId,
|
|
7381
|
+
}],
|
|
7382
|
+
});
|
|
7383
|
+
await writeJson(join(stateDir, "ralplan-state.json"), {
|
|
7384
|
+
active: false,
|
|
7385
|
+
mode: "ralplan",
|
|
7386
|
+
current_phase: "complete",
|
|
7387
|
+
session_id: sessionId,
|
|
7388
|
+
});
|
|
7389
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
7390
|
+
active: true,
|
|
7391
|
+
skill: "ralplan",
|
|
7392
|
+
phase: "planning",
|
|
7393
|
+
session_id: sessionId,
|
|
7394
|
+
active_skills: [{
|
|
7395
|
+
skill: "ralplan",
|
|
7396
|
+
phase: "planning",
|
|
7397
|
+
active: true,
|
|
7398
|
+
session_id: sessionId,
|
|
7399
|
+
}],
|
|
7400
|
+
});
|
|
7401
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
7402
|
+
active: true,
|
|
7403
|
+
mode: "ralplan",
|
|
7404
|
+
current_phase: "planning",
|
|
7405
|
+
session_id: sessionId,
|
|
7406
|
+
});
|
|
7407
|
+
|
|
7408
|
+
const result = await dispatchCodexNativeHook(
|
|
7409
|
+
{
|
|
7410
|
+
hook_event_name: "Stop",
|
|
7411
|
+
cwd,
|
|
7412
|
+
session_id: sessionId,
|
|
7413
|
+
},
|
|
7414
|
+
{ cwd },
|
|
7415
|
+
);
|
|
7416
|
+
|
|
7417
|
+
assert.equal(result.omxEventName, "stop");
|
|
7418
|
+
assert.equal(result.outputJson, null);
|
|
7419
|
+
} finally {
|
|
7420
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7421
|
+
}
|
|
7422
|
+
});
|
|
7423
|
+
|
|
7424
|
+
it("keeps blocking session ralplan when canonical root state is not inactive", async () => {
|
|
7425
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-active-"));
|
|
7426
|
+
try {
|
|
7427
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7428
|
+
const sessionId = "sess-stop-session-ralplan-root-active";
|
|
7429
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
7430
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
7431
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
7432
|
+
active: true,
|
|
7433
|
+
skill: "ralplan",
|
|
7434
|
+
phase: "planning",
|
|
7435
|
+
session_id: sessionId,
|
|
7436
|
+
active_skills: [{
|
|
7437
|
+
skill: "ralplan",
|
|
7438
|
+
phase: "planning",
|
|
7439
|
+
active: true,
|
|
7440
|
+
session_id: sessionId,
|
|
7441
|
+
}],
|
|
7442
|
+
});
|
|
7443
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
7444
|
+
active: true,
|
|
7445
|
+
skill: "ralplan",
|
|
7446
|
+
phase: "planning",
|
|
7447
|
+
session_id: sessionId,
|
|
7448
|
+
active_skills: [{
|
|
7449
|
+
skill: "ralplan",
|
|
7450
|
+
phase: "planning",
|
|
7451
|
+
active: true,
|
|
7452
|
+
session_id: sessionId,
|
|
7453
|
+
}],
|
|
7454
|
+
});
|
|
7455
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
7456
|
+
active: true,
|
|
7457
|
+
mode: "ralplan",
|
|
7458
|
+
current_phase: "planning",
|
|
7459
|
+
session_id: sessionId,
|
|
7460
|
+
});
|
|
7461
|
+
|
|
7462
|
+
const result = await dispatchCodexNativeHook(
|
|
7463
|
+
{
|
|
7464
|
+
hook_event_name: "Stop",
|
|
7465
|
+
cwd,
|
|
7466
|
+
session_id: sessionId,
|
|
7467
|
+
},
|
|
7468
|
+
{ cwd },
|
|
7469
|
+
);
|
|
7470
|
+
|
|
7471
|
+
assert.equal(result.omxEventName, "stop");
|
|
7472
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
7473
|
+
assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
|
|
7474
|
+
} finally {
|
|
7475
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7476
|
+
}
|
|
7477
|
+
});
|
|
7478
|
+
|
|
7479
|
+
it("does not block on stale ralplan skill-active when canonical run-state is terminal", async () => {
|
|
7480
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-terminal-ralplan-run-"));
|
|
6848
7481
|
try {
|
|
6849
7482
|
const stateDir = join(cwd, ".omx", "state");
|
|
6850
7483
|
const sessionId = "sess-stop-terminal-ralplan";
|
|
@@ -7985,6 +8618,47 @@ exit 0
|
|
|
7985
8618
|
}
|
|
7986
8619
|
});
|
|
7987
8620
|
|
|
8621
|
+
it("allows Stop from stale orphaned session-scoped Ralph starting iteration zero state", async () => {
|
|
8622
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-orphan-starting-ralph-"));
|
|
8623
|
+
try {
|
|
8624
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
8625
|
+
const sessionId = "sess-stale-orphan-ralph";
|
|
8626
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
8627
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: sessionId, cwd });
|
|
8628
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
8629
|
+
active: true,
|
|
8630
|
+
mode: "ralph",
|
|
8631
|
+
current_phase: "starting",
|
|
8632
|
+
iteration: 0,
|
|
8633
|
+
session_id: sessionId,
|
|
8634
|
+
updated_at: "2000-01-01T00:00:00.000Z",
|
|
8635
|
+
});
|
|
8636
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
8637
|
+
active: true,
|
|
8638
|
+
skill: "ralph",
|
|
8639
|
+
phase: "starting",
|
|
8640
|
+
session_id: sessionId,
|
|
8641
|
+
active_skills: [{ skill: "ralph", phase: "starting", active: true, session_id: sessionId }],
|
|
8642
|
+
});
|
|
8643
|
+
|
|
8644
|
+
const result = await dispatchCodexNativeHook(
|
|
8645
|
+
{
|
|
8646
|
+
hook_event_name: "Stop",
|
|
8647
|
+
cwd,
|
|
8648
|
+
session_id: sessionId,
|
|
8649
|
+
thread_id: "thread-verifier-terminal",
|
|
8650
|
+
last_assistant_message: "APPROVE: read-only verifier evidence is fresh and sufficient.",
|
|
8651
|
+
},
|
|
8652
|
+
{ cwd },
|
|
8653
|
+
);
|
|
8654
|
+
|
|
8655
|
+
assert.equal(result.omxEventName, "stop");
|
|
8656
|
+
assert.equal(result.outputJson, null);
|
|
8657
|
+
} finally {
|
|
8658
|
+
await rm(cwd, { recursive: true, force: true });
|
|
8659
|
+
}
|
|
8660
|
+
});
|
|
8661
|
+
|
|
7988
8662
|
it("blocks Stop on visible active session-scoped Ralph starting state and reports its path", async () => {
|
|
7989
8663
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-visible-starting-ralph-"));
|
|
7990
8664
|
try {
|
|
@@ -8027,6 +8701,138 @@ exit 0
|
|
|
8027
8701
|
}
|
|
8028
8702
|
});
|
|
8029
8703
|
|
|
8704
|
+
it("retires prompt-seeded Ralph starting state when canonical Ralph already completed with audit", async () => {
|
|
8705
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-shadowed-starting-"));
|
|
8706
|
+
try {
|
|
8707
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
8708
|
+
const nativeSessionId = "native-hook-seed";
|
|
8709
|
+
const canonicalSessionId = "omx-runtime-session";
|
|
8710
|
+
await mkdir(join(stateDir, "sessions", nativeSessionId), { recursive: true });
|
|
8711
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
8712
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
8713
|
+
session_id: canonicalSessionId,
|
|
8714
|
+
cwd,
|
|
8715
|
+
});
|
|
8716
|
+
await writeJson(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), {
|
|
8717
|
+
active: true,
|
|
8718
|
+
mode: "ralph",
|
|
8719
|
+
current_phase: "starting",
|
|
8720
|
+
session_id: nativeSessionId,
|
|
8721
|
+
iteration: 0,
|
|
8722
|
+
task_slug: "mvp-h-local-method-preflight-execution",
|
|
8723
|
+
started_at: "2026-05-14T07:00:00.000Z",
|
|
8724
|
+
});
|
|
8725
|
+
await writeJson(join(stateDir, "sessions", nativeSessionId, "skill-active-state.json"), {
|
|
8726
|
+
active: true,
|
|
8727
|
+
skill: "ralph",
|
|
8728
|
+
phase: "starting",
|
|
8729
|
+
session_id: nativeSessionId,
|
|
8730
|
+
active_skills: [{ skill: "ralph", phase: "starting", active: true, session_id: nativeSessionId }],
|
|
8731
|
+
});
|
|
8732
|
+
await writeJson(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), {
|
|
8733
|
+
active: false,
|
|
8734
|
+
mode: "ralph",
|
|
8735
|
+
current_phase: "complete",
|
|
8736
|
+
session_id: canonicalSessionId,
|
|
8737
|
+
completed_at: "2026-05-14T07:30:00.000Z",
|
|
8738
|
+
completion_audit: {
|
|
8739
|
+
passed: true,
|
|
8740
|
+
prompt_to_artifact_checklist: ["task evidence mapped"],
|
|
8741
|
+
verification_evidence: ["fresh verification evidence recorded"],
|
|
8742
|
+
},
|
|
8743
|
+
});
|
|
8744
|
+
|
|
8745
|
+
const result = await dispatchCodexNativeHook(
|
|
8746
|
+
{
|
|
8747
|
+
hook_event_name: "Stop",
|
|
8748
|
+
cwd,
|
|
8749
|
+
session_id: nativeSessionId,
|
|
8750
|
+
},
|
|
8751
|
+
{ cwd },
|
|
8752
|
+
);
|
|
8753
|
+
|
|
8754
|
+
assert.equal(result.omxEventName, "stop");
|
|
8755
|
+
assert.equal(result.outputJson, null);
|
|
8756
|
+
const retiredState = JSON.parse(await readFile(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), "utf-8"));
|
|
8757
|
+
assert.equal(retiredState.active, false);
|
|
8758
|
+
assert.equal(retiredState.current_phase, "complete");
|
|
8759
|
+
assert.equal(retiredState.stop_reason, "shadowed_by_completed_canonical_ralph");
|
|
8760
|
+
assert.equal(retiredState.shadowed_by_completed_canonical_ralph.session_id, canonicalSessionId);
|
|
8761
|
+
} finally {
|
|
8762
|
+
await rm(cwd, { recursive: true, force: true });
|
|
8763
|
+
}
|
|
8764
|
+
});
|
|
8765
|
+
|
|
8766
|
+
it("does not retire prompt-seeded Ralph starting state from a completed canonical Ralph owned by another thread", async () => {
|
|
8767
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-shadowed-thread-mismatch-"));
|
|
8768
|
+
try {
|
|
8769
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
8770
|
+
const nativeSessionId = "native-hook-seed";
|
|
8771
|
+
const canonicalSessionId = "omx-runtime-session";
|
|
8772
|
+
await mkdir(join(stateDir, "sessions", nativeSessionId), { recursive: true });
|
|
8773
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
8774
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
8775
|
+
session_id: canonicalSessionId,
|
|
8776
|
+
cwd,
|
|
8777
|
+
});
|
|
8778
|
+
await writeJson(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), {
|
|
8779
|
+
active: true,
|
|
8780
|
+
mode: "ralph",
|
|
8781
|
+
current_phase: "starting",
|
|
8782
|
+
session_id: nativeSessionId,
|
|
8783
|
+
iteration: 0,
|
|
8784
|
+
task_slug: "mvp-h-local-method-preflight-execution",
|
|
8785
|
+
started_at: "2026-05-14T07:00:00.000Z",
|
|
8786
|
+
});
|
|
8787
|
+
await writeJson(join(stateDir, "sessions", nativeSessionId, "skill-active-state.json"), {
|
|
8788
|
+
active: true,
|
|
8789
|
+
skill: "ralph",
|
|
8790
|
+
phase: "starting",
|
|
8791
|
+
session_id: nativeSessionId,
|
|
8792
|
+
active_skills: [{ skill: "ralph", phase: "starting", active: true, session_id: nativeSessionId }],
|
|
8793
|
+
});
|
|
8794
|
+
await writeJson(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), {
|
|
8795
|
+
active: false,
|
|
8796
|
+
mode: "ralph",
|
|
8797
|
+
current_phase: "complete",
|
|
8798
|
+
session_id: canonicalSessionId,
|
|
8799
|
+
owner_codex_thread_id: "thread-A",
|
|
8800
|
+
completed_at: "2026-05-14T07:30:00.000Z",
|
|
8801
|
+
completion_audit: {
|
|
8802
|
+
passed: true,
|
|
8803
|
+
prompt_to_artifact_checklist: ["task evidence mapped"],
|
|
8804
|
+
verification_evidence: ["fresh verification evidence recorded"],
|
|
8805
|
+
},
|
|
8806
|
+
});
|
|
8807
|
+
|
|
8808
|
+
const result = await dispatchCodexNativeHook(
|
|
8809
|
+
{
|
|
8810
|
+
hook_event_name: "Stop",
|
|
8811
|
+
cwd,
|
|
8812
|
+
session_id: nativeSessionId,
|
|
8813
|
+
thread_id: "thread-B",
|
|
8814
|
+
},
|
|
8815
|
+
{ cwd },
|
|
8816
|
+
);
|
|
8817
|
+
|
|
8818
|
+
assert.equal(result.omxEventName, "stop");
|
|
8819
|
+
assert.deepEqual(result.outputJson, {
|
|
8820
|
+
decision: "block",
|
|
8821
|
+
reason:
|
|
8822
|
+
"OMX Ralph is still active (phase: starting; state: .omx/state/sessions/native-hook-seed/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
|
|
8823
|
+
stopReason: "ralph_starting",
|
|
8824
|
+
systemMessage:
|
|
8825
|
+
"OMX Ralph is still active (phase: starting; state: .omx/state/sessions/native-hook-seed/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
|
|
8826
|
+
});
|
|
8827
|
+
const preservedState = JSON.parse(await readFile(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), "utf-8"));
|
|
8828
|
+
assert.equal(preservedState.active, true);
|
|
8829
|
+
assert.equal(preservedState.current_phase, "starting");
|
|
8830
|
+
assert.equal(preservedState.stop_reason, undefined);
|
|
8831
|
+
} finally {
|
|
8832
|
+
await rm(cwd, { recursive: true, force: true });
|
|
8833
|
+
}
|
|
8834
|
+
});
|
|
8835
|
+
|
|
8030
8836
|
it("does not block Stop from another session-scoped Ralph state when an explicit session_id has no active Ralph state", async () => {
|
|
8031
8837
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-explicit-session-ralph-"));
|
|
8032
8838
|
try {
|