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
|
@@ -67,7 +67,7 @@ if [[ "$cmd" == "display-message" ]]; then
|
|
|
67
67
|
exit 0
|
|
68
68
|
fi
|
|
69
69
|
if [[ "$cmd" == "capture-pane" ]]; then
|
|
70
|
-
echo "› ready"
|
|
70
|
+
${options.busyLeader ? 'echo "• Working… (esc to interrupt)"' : 'echo "› ready"'}
|
|
71
71
|
exit 0
|
|
72
72
|
fi
|
|
73
73
|
if [[ "$cmd" == "send-keys" ]]; then
|
|
@@ -1409,6 +1409,67 @@ describe("codex native hook dispatch", () => {
|
|
|
1409
1409
|
await rm(cwd, { recursive: true, force: true });
|
|
1410
1410
|
}
|
|
1411
1411
|
});
|
|
1412
|
+
it("does not repeat Stop block when the last autoresearch-goal completion attempt reported objective mismatch", async () => {
|
|
1413
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autoresearch-mismatch-reported-stop-"));
|
|
1414
|
+
try {
|
|
1415
|
+
await writeJson(join(cwd, ".omx", "goals", "autoresearch", "mismatched-mission", "mission.json"), {
|
|
1416
|
+
version: 1,
|
|
1417
|
+
workflow: "autoresearch-goal",
|
|
1418
|
+
slug: "mismatched-mission",
|
|
1419
|
+
topic: "Passing research bound to another Codex goal",
|
|
1420
|
+
status: "passed",
|
|
1421
|
+
});
|
|
1422
|
+
await writeJson(join(cwd, ".omx", "goals", "autoresearch", "mismatched-mission", "completion.json"), {
|
|
1423
|
+
verdict: "pass",
|
|
1424
|
+
passed: true,
|
|
1425
|
+
});
|
|
1426
|
+
const result = await dispatchCodexNativeHook({
|
|
1427
|
+
hook_event_name: "Stop",
|
|
1428
|
+
cwd,
|
|
1429
|
+
session_id: "sess-autoresearch-mismatch-reported-stop",
|
|
1430
|
+
thread_id: "thread-autoresearch-mismatch-reported-stop",
|
|
1431
|
+
last_assistant_message: [
|
|
1432
|
+
"I called get_goal and ran omx autoresearch-goal complete --slug mismatched-mission --codex-goal-json /tmp/snapshot.json.",
|
|
1433
|
+
"The autoresearch-goal completion failed with Codex goal objective mismatch, so I will not repeat the same complete command blindly in this thread.",
|
|
1434
|
+
].join("\n"),
|
|
1435
|
+
}, { cwd });
|
|
1436
|
+
assert.notEqual(result.outputJson?.decision, "block");
|
|
1437
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /autoresearch-goal complete --slug mismatched-mission/);
|
|
1438
|
+
assert.doesNotMatch(JSON.stringify(result.outputJson), /get_goal snapshot reconciliation/);
|
|
1439
|
+
}
|
|
1440
|
+
finally {
|
|
1441
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1442
|
+
}
|
|
1443
|
+
});
|
|
1444
|
+
it("still blocks later autoresearch-goal completion claims after an objective mismatch if no mismatch is reported in the final answer", async () => {
|
|
1445
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autoresearch-mismatch-later-retry-stop-"));
|
|
1446
|
+
try {
|
|
1447
|
+
await writeJson(join(cwd, ".omx", "goals", "autoresearch", "retryable-mission", "mission.json"), {
|
|
1448
|
+
version: 1,
|
|
1449
|
+
workflow: "autoresearch-goal",
|
|
1450
|
+
slug: "retryable-mission",
|
|
1451
|
+
topic: "Passing research that can still retry with the correct snapshot",
|
|
1452
|
+
status: "passed",
|
|
1453
|
+
});
|
|
1454
|
+
await writeJson(join(cwd, ".omx", "goals", "autoresearch", "retryable-mission", "completion.json"), {
|
|
1455
|
+
verdict: "pass",
|
|
1456
|
+
passed: true,
|
|
1457
|
+
});
|
|
1458
|
+
const result = await dispatchCodexNativeHook({
|
|
1459
|
+
hook_event_name: "Stop",
|
|
1460
|
+
cwd,
|
|
1461
|
+
session_id: "sess-autoresearch-mismatch-later-retry-stop",
|
|
1462
|
+
thread_id: "thread-autoresearch-mismatch-later-retry-stop",
|
|
1463
|
+
last_assistant_message: "Autoresearch goal complete; next call update_goal({status: \"complete\"}).",
|
|
1464
|
+
}, { cwd });
|
|
1465
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
1466
|
+
assert.match(JSON.stringify(result.outputJson), /get_goal snapshot reconciliation/);
|
|
1467
|
+
assert.match(JSON.stringify(result.outputJson), /omx autoresearch-goal complete --slug retryable-mission/);
|
|
1468
|
+
}
|
|
1469
|
+
finally {
|
|
1470
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1412
1473
|
it("treats workflow keywords in native subagent prompt text as literal delegation text", async () => {
|
|
1413
1474
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-keyword-literal-"));
|
|
1414
1475
|
try {
|
|
@@ -2370,6 +2431,44 @@ exit 0
|
|
|
2370
2431
|
await rm(cwd, { recursive: true, force: true });
|
|
2371
2432
|
}
|
|
2372
2433
|
});
|
|
2434
|
+
it("does not block Bash commands that only mention omx question in quoted arguments", async () => {
|
|
2435
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-quoted-mention-"));
|
|
2436
|
+
try {
|
|
2437
|
+
const result = await dispatchCodexNativeHook({
|
|
2438
|
+
hook_event_name: "PreToolUse",
|
|
2439
|
+
cwd,
|
|
2440
|
+
tool_name: "Bash",
|
|
2441
|
+
tool_use_id: "tool-question-quoted-mention",
|
|
2442
|
+
tool_input: {
|
|
2443
|
+
command: `omx ultragoal create-goals --brief "Deep interview says omx question failed in tmux"`,
|
|
2444
|
+
},
|
|
2445
|
+
}, { cwd });
|
|
2446
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2447
|
+
assert.equal(result.outputJson, null);
|
|
2448
|
+
}
|
|
2449
|
+
finally {
|
|
2450
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2451
|
+
}
|
|
2452
|
+
});
|
|
2453
|
+
it("does not block Bash heredocs that only document omx question text", async () => {
|
|
2454
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-heredoc-mention-"));
|
|
2455
|
+
try {
|
|
2456
|
+
const result = await dispatchCodexNativeHook({
|
|
2457
|
+
hook_event_name: "PreToolUse",
|
|
2458
|
+
cwd,
|
|
2459
|
+
tool_name: "Bash",
|
|
2460
|
+
tool_use_id: "tool-question-heredoc-mention",
|
|
2461
|
+
tool_input: {
|
|
2462
|
+
command: `cat > issue-notes.md <<'EOF'\nomx question failed in the attached tmux pane\nEOF`,
|
|
2463
|
+
},
|
|
2464
|
+
}, { cwd });
|
|
2465
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
2466
|
+
assert.equal(result.outputJson, null);
|
|
2467
|
+
}
|
|
2468
|
+
finally {
|
|
2469
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2470
|
+
}
|
|
2471
|
+
});
|
|
2373
2472
|
it("allows Bash omx question when the command preserves the leader-pane return hint", async () => {
|
|
2374
2473
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-allow-"));
|
|
2375
2474
|
try {
|
|
@@ -4052,6 +4151,82 @@ exit 0
|
|
|
4052
4151
|
await rm(cwd, { recursive: true, force: true });
|
|
4053
4152
|
}
|
|
4054
4153
|
});
|
|
4154
|
+
it("does not treat non-MCP source output containing detector constants as MCP transport death", async () => {
|
|
4155
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-read-mcp-source-"));
|
|
4156
|
+
try {
|
|
4157
|
+
const result = await dispatchCodexNativeHook({
|
|
4158
|
+
hook_event_name: "PostToolUse",
|
|
4159
|
+
cwd,
|
|
4160
|
+
tool_name: "Read",
|
|
4161
|
+
tool_use_id: "tool-read-mcp-source",
|
|
4162
|
+
tool_input: { file_path: "src/scripts/codex-native-pre-post.ts" },
|
|
4163
|
+
tool_response: "const MCP_TRANSPORT_FAILURE_PATTERNS = [/transport closed/i, /server disconnected/i];\nconst context = /\\bmcp\\b/i;",
|
|
4164
|
+
}, { cwd });
|
|
4165
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
4166
|
+
assert.equal(result.outputJson, null);
|
|
4167
|
+
}
|
|
4168
|
+
finally {
|
|
4169
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4170
|
+
}
|
|
4171
|
+
});
|
|
4172
|
+
it("does not treat non-MCP docs stdout mentioning closed MCP transport as transport death", async () => {
|
|
4173
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-docs-mcp-log-"));
|
|
4174
|
+
try {
|
|
4175
|
+
const result = await dispatchCodexNativeHook({
|
|
4176
|
+
hook_event_name: "PostToolUse",
|
|
4177
|
+
cwd,
|
|
4178
|
+
tool_name: "ShellOutput",
|
|
4179
|
+
tool_use_id: "tool-docs-mcp-log",
|
|
4180
|
+
tool_response: JSON.stringify({
|
|
4181
|
+
stdout: "Troubleshooting note: MCP transport closed after the server disconnected in an old log.",
|
|
4182
|
+
stderr: "",
|
|
4183
|
+
}),
|
|
4184
|
+
}, { cwd });
|
|
4185
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
4186
|
+
assert.equal(result.outputJson, null);
|
|
4187
|
+
}
|
|
4188
|
+
finally {
|
|
4189
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4190
|
+
}
|
|
4191
|
+
});
|
|
4192
|
+
it("does not MCP-block non-MCP command output with unrelated stderr and MCP transport stdout", async () => {
|
|
4193
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-nonmcp-mixed-output-"));
|
|
4194
|
+
try {
|
|
4195
|
+
const result = await dispatchCodexNativeHook({
|
|
4196
|
+
hook_event_name: "PostToolUse",
|
|
4197
|
+
cwd,
|
|
4198
|
+
tool_name: "ShellOutput",
|
|
4199
|
+
tool_use_id: "tool-nonmcp-mixed-output",
|
|
4200
|
+
tool_response: JSON.stringify({
|
|
4201
|
+
stdout: "captured log line: MCP transport closed before response",
|
|
4202
|
+
stderr: "grep: fixture.txt: No such file or directory",
|
|
4203
|
+
}),
|
|
4204
|
+
}, { cwd });
|
|
4205
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
4206
|
+
assert.equal(result.outputJson, null);
|
|
4207
|
+
}
|
|
4208
|
+
finally {
|
|
4209
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4210
|
+
}
|
|
4211
|
+
});
|
|
4212
|
+
it("still blocks MCP-like raw transport failures", async () => {
|
|
4213
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-raw-transport-"));
|
|
4214
|
+
try {
|
|
4215
|
+
const result = await dispatchCodexNativeHook({
|
|
4216
|
+
hook_event_name: "PostToolUse",
|
|
4217
|
+
cwd,
|
|
4218
|
+
tool_name: "mcp__omx_state__state_write",
|
|
4219
|
+
tool_use_id: "tool-mcp-raw-transport",
|
|
4220
|
+
tool_response: "transport closed after server disconnected",
|
|
4221
|
+
}, { cwd });
|
|
4222
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
4223
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
4224
|
+
assert.match(String(result.outputJson?.reason || ""), /lost its transport\/server connection/);
|
|
4225
|
+
}
|
|
4226
|
+
finally {
|
|
4227
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4228
|
+
}
|
|
4229
|
+
});
|
|
4055
4230
|
it("returns PostToolUse MCP transport fallback guidance for clear MCP transport death", async () => {
|
|
4056
4231
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-transport-"));
|
|
4057
4232
|
try {
|
|
@@ -4366,6 +4541,51 @@ exit 0
|
|
|
4366
4541
|
await rm(cwd, { recursive: true, force: true });
|
|
4367
4542
|
}
|
|
4368
4543
|
});
|
|
4544
|
+
it("requires Autopilot code review after a compact-boundary Stop exemption", async () => {
|
|
4545
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-review-compact-"));
|
|
4546
|
+
try {
|
|
4547
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4548
|
+
const sessionId = "sess-stop-autopilot-review-compact";
|
|
4549
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
4550
|
+
await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
|
|
4551
|
+
active: true,
|
|
4552
|
+
mode: "autopilot",
|
|
4553
|
+
current_phase: "code-review",
|
|
4554
|
+
state: {
|
|
4555
|
+
phase_cycle: ["ralplan", "ralph", "code-review"],
|
|
4556
|
+
handoff_artifacts: {
|
|
4557
|
+
ralplan: ".omx/plans/prd-issue-2366.md",
|
|
4558
|
+
ralph: { verification: ["npm test"] },
|
|
4559
|
+
code_review: null,
|
|
4560
|
+
},
|
|
4561
|
+
review_verdict: null,
|
|
4562
|
+
},
|
|
4563
|
+
});
|
|
4564
|
+
const compactBoundary = await dispatchCodexNativeHook({
|
|
4565
|
+
hook_event_name: "Stop",
|
|
4566
|
+
cwd,
|
|
4567
|
+
session_id: sessionId,
|
|
4568
|
+
stop_reason: "context compact",
|
|
4569
|
+
}, { cwd });
|
|
4570
|
+
const resumedStop = await dispatchCodexNativeHook({
|
|
4571
|
+
hook_event_name: "Stop",
|
|
4572
|
+
cwd,
|
|
4573
|
+
session_id: sessionId,
|
|
4574
|
+
}, { cwd });
|
|
4575
|
+
assert.equal(compactBoundary.omxEventName, "stop");
|
|
4576
|
+
assert.equal(compactBoundary.outputJson, null);
|
|
4577
|
+
assert.equal(resumedStop.omxEventName, "stop");
|
|
4578
|
+
assert.deepEqual(resumedStop.outputJson, {
|
|
4579
|
+
decision: "block",
|
|
4580
|
+
reason: "OMX autopilot is still active (phase: code-review); continue the task and gather fresh verification evidence before stopping.",
|
|
4581
|
+
stopReason: "autopilot_code-review",
|
|
4582
|
+
systemMessage: "OMX autopilot is still active (phase: code-review). Run the required $code-review step before completing or clearing Autopilot state.",
|
|
4583
|
+
});
|
|
4584
|
+
}
|
|
4585
|
+
finally {
|
|
4586
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4587
|
+
}
|
|
4588
|
+
});
|
|
4369
4589
|
it("suppresses duplicate Autopilot planning Stop replays so stale planning state cannot loop indefinitely", async () => {
|
|
4370
4590
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-planning-replay-"));
|
|
4371
4591
|
try {
|
|
@@ -4858,6 +5078,87 @@ exit 0
|
|
|
4858
5078
|
await rm(cwd, { recursive: true, force: true });
|
|
4859
5079
|
}
|
|
4860
5080
|
});
|
|
5081
|
+
it("queues worker Stop leader nudge with Tab and submit when leader pane is busy", async () => {
|
|
5082
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-busy-leader-"));
|
|
5083
|
+
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
5084
|
+
const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
5085
|
+
const prevPath = process.env.PATH;
|
|
5086
|
+
try {
|
|
5087
|
+
await initTeamState("worker-stop-team-busy-leader", "worker stop busy leader", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-busy-leader" });
|
|
5088
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
5089
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
5090
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
5091
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath, { busyLeader: true }));
|
|
5092
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
5093
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
5094
|
+
const teamDir = join(stateDir, "team", "worker-stop-team-busy-leader");
|
|
5095
|
+
const workerDir = join(teamDir, "workers", "worker-1");
|
|
5096
|
+
await writeJson(join(teamDir, "config.json"), {
|
|
5097
|
+
name: "worker-stop-team-busy-leader",
|
|
5098
|
+
tmux_session: "omx-team-worker-stop",
|
|
5099
|
+
leader_pane_id: "%42",
|
|
5100
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
5101
|
+
});
|
|
5102
|
+
await writeJson(join(teamDir, "manifest.v2.json"), {
|
|
5103
|
+
name: "worker-stop-team-busy-leader",
|
|
5104
|
+
tmux_session: "omx-team-worker-stop",
|
|
5105
|
+
leader_pane_id: "%42",
|
|
5106
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
5107
|
+
});
|
|
5108
|
+
await writeJson(join(workerDir, "identity.json"), {
|
|
5109
|
+
name: "worker-1",
|
|
5110
|
+
index: 1,
|
|
5111
|
+
role: "executor",
|
|
5112
|
+
assigned_tasks: ["1"],
|
|
5113
|
+
worktree_path: cwd,
|
|
5114
|
+
team_state_root: stateDir,
|
|
5115
|
+
});
|
|
5116
|
+
await writeJson(join(workerDir, "status.json"), {
|
|
5117
|
+
state: "done",
|
|
5118
|
+
current_task_id: "1",
|
|
5119
|
+
updated_at: new Date().toISOString(),
|
|
5120
|
+
});
|
|
5121
|
+
await writeJson(join(teamDir, "tasks", "task-1.json"), {
|
|
5122
|
+
id: "1",
|
|
5123
|
+
subject: "hook task",
|
|
5124
|
+
description: "finish hook task",
|
|
5125
|
+
status: "completed",
|
|
5126
|
+
owner: "worker-1",
|
|
5127
|
+
created_at: new Date().toISOString(),
|
|
5128
|
+
});
|
|
5129
|
+
process.env.OMX_TEAM_WORKER = "worker-stop-team-busy-leader/worker-1";
|
|
5130
|
+
process.env.OMX_TEAM_STATE_ROOT = stateDir;
|
|
5131
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
5132
|
+
const result = await dispatchCodexNativeHook({
|
|
5133
|
+
hook_event_name: "Stop",
|
|
5134
|
+
cwd,
|
|
5135
|
+
session_id: "sess-stop-team-worker-busy-leader",
|
|
5136
|
+
}, { cwd });
|
|
5137
|
+
assert.equal(result.outputJson, null);
|
|
5138
|
+
const tmuxLog = await readFile(tmuxLogPath, "utf-8");
|
|
5139
|
+
assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
|
|
5140
|
+
assert.match(tmuxLog, /send-keys -t %42 Tab/);
|
|
5141
|
+
assert.match(tmuxLog, /send-keys -t %42 C-m/);
|
|
5142
|
+
assert.ok(tmuxLog.indexOf("send-keys -t %42 Tab") < tmuxLog.indexOf("send-keys -t %42 C-m"), "busy worker-stop nudge should press Tab before C-m");
|
|
5143
|
+
const nudgeState = JSON.parse(await readFile(join(workerDir, "worker-stop-nudge.json"), "utf-8"));
|
|
5144
|
+
assert.equal(nudgeState.delivery, "queued");
|
|
5145
|
+
}
|
|
5146
|
+
finally {
|
|
5147
|
+
if (typeof prevTeamWorker === "string")
|
|
5148
|
+
process.env.OMX_TEAM_WORKER = prevTeamWorker;
|
|
5149
|
+
else
|
|
5150
|
+
delete process.env.OMX_TEAM_WORKER;
|
|
5151
|
+
if (typeof prevTeamStateRoot === "string")
|
|
5152
|
+
process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
|
|
5153
|
+
else
|
|
5154
|
+
delete process.env.OMX_TEAM_STATE_ROOT;
|
|
5155
|
+
if (typeof prevPath === "string")
|
|
5156
|
+
process.env.PATH = prevPath;
|
|
5157
|
+
else
|
|
5158
|
+
delete process.env.PATH;
|
|
5159
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5160
|
+
}
|
|
5161
|
+
});
|
|
4861
5162
|
it("allows worker Stop when the Stop nudge helper cannot deliver", async () => {
|
|
4862
5163
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-helper-fail-"));
|
|
4863
5164
|
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
@@ -5622,13 +5923,25 @@ exit 0
|
|
|
5622
5923
|
await rm(cwd, { recursive: true, force: true });
|
|
5623
5924
|
}
|
|
5624
5925
|
});
|
|
5625
|
-
it("does not block
|
|
5626
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
5926
|
+
it("does not block when canonical root ralplan state is inactive but session ralplan state is stale active", async () => {
|
|
5927
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-session-ralplan-root-inactive-"));
|
|
5627
5928
|
try {
|
|
5628
5929
|
const stateDir = join(cwd, ".omx", "state");
|
|
5629
|
-
const sessionId = "sess-stop-
|
|
5930
|
+
const sessionId = "sess-stop-stale-session-ralplan";
|
|
5630
5931
|
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
5631
5932
|
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
5933
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
5934
|
+
active: false,
|
|
5935
|
+
skill: "ralplan",
|
|
5936
|
+
phase: "reviewing",
|
|
5937
|
+
active_skills: [],
|
|
5938
|
+
});
|
|
5939
|
+
await writeJson(join(stateDir, "ralplan-state.json"), {
|
|
5940
|
+
active: false,
|
|
5941
|
+
mode: "ralplan",
|
|
5942
|
+
current_phase: "complete",
|
|
5943
|
+
session_id: sessionId,
|
|
5944
|
+
});
|
|
5632
5945
|
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
5633
5946
|
active: true,
|
|
5634
5947
|
skill: "ralplan",
|
|
@@ -5647,16 +5960,6 @@ exit 0
|
|
|
5647
5960
|
current_phase: "planning",
|
|
5648
5961
|
session_id: sessionId,
|
|
5649
5962
|
});
|
|
5650
|
-
await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
|
|
5651
|
-
version: 1,
|
|
5652
|
-
mode: "ralplan",
|
|
5653
|
-
active: false,
|
|
5654
|
-
outcome: "finish",
|
|
5655
|
-
lifecycle_outcome: "finished",
|
|
5656
|
-
current_phase: "complete",
|
|
5657
|
-
completed_at: "2026-05-01T00:00:00.000Z",
|
|
5658
|
-
updated_at: "2026-05-01T00:00:00.000Z",
|
|
5659
|
-
});
|
|
5660
5963
|
const result = await dispatchCodexNativeHook({
|
|
5661
5964
|
hook_event_name: "Stop",
|
|
5662
5965
|
cwd,
|
|
@@ -5669,13 +5972,26 @@ exit 0
|
|
|
5669
5972
|
await rm(cwd, { recursive: true, force: true });
|
|
5670
5973
|
}
|
|
5671
5974
|
});
|
|
5672
|
-
it("
|
|
5673
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
5975
|
+
it("keeps blocking current session ralplan when root inactive ralplan state belongs to another session", async () => {
|
|
5976
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-other-session-"));
|
|
5674
5977
|
try {
|
|
5675
5978
|
const stateDir = join(cwd, ".omx", "state");
|
|
5676
|
-
const sessionId = "sess-stop-current-ralplan";
|
|
5979
|
+
const sessionId = "sess-stop-current-active-ralplan";
|
|
5677
5980
|
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
5678
5981
|
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
5982
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
5983
|
+
active: false,
|
|
5984
|
+
skill: "ralplan",
|
|
5985
|
+
phase: "complete",
|
|
5986
|
+
session_id: "sess-stop-old-ralplan",
|
|
5987
|
+
active_skills: [],
|
|
5988
|
+
});
|
|
5989
|
+
await writeJson(join(stateDir, "ralplan-state.json"), {
|
|
5990
|
+
active: false,
|
|
5991
|
+
mode: "ralplan",
|
|
5992
|
+
current_phase: "complete",
|
|
5993
|
+
session_id: "sess-stop-old-ralplan",
|
|
5994
|
+
});
|
|
5679
5995
|
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
5680
5996
|
active: true,
|
|
5681
5997
|
skill: "ralplan",
|
|
@@ -5692,7 +6008,7 @@ exit 0
|
|
|
5692
6008
|
active: true,
|
|
5693
6009
|
mode: "ralplan",
|
|
5694
6010
|
current_phase: "planning",
|
|
5695
|
-
session_id:
|
|
6011
|
+
session_id: sessionId,
|
|
5696
6012
|
});
|
|
5697
6013
|
const result = await dispatchCodexNativeHook({
|
|
5698
6014
|
hook_event_name: "Stop",
|
|
@@ -5700,21 +6016,260 @@ exit 0
|
|
|
5700
6016
|
session_id: sessionId,
|
|
5701
6017
|
}, { cwd });
|
|
5702
6018
|
assert.equal(result.omxEventName, "stop");
|
|
5703
|
-
assert.equal(result.outputJson,
|
|
6019
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
6020
|
+
assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
|
|
5704
6021
|
}
|
|
5705
6022
|
finally {
|
|
5706
6023
|
await rm(cwd, { recursive: true, force: true });
|
|
5707
6024
|
}
|
|
5708
6025
|
});
|
|
5709
|
-
it("
|
|
5710
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
6026
|
+
it("keeps blocking current session ralplan when root inactive ralplan state is unscoped", async () => {
|
|
6027
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-unscoped-"));
|
|
5711
6028
|
try {
|
|
5712
6029
|
const stateDir = join(cwd, ".omx", "state");
|
|
5713
|
-
|
|
5714
|
-
await
|
|
5715
|
-
await writeJson(join(stateDir, "
|
|
5716
|
-
|
|
5717
|
-
|
|
6030
|
+
const sessionId = "sess-stop-unscoped-root-current-active";
|
|
6031
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
6032
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
6033
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
6034
|
+
active: false,
|
|
6035
|
+
skill: "ralplan",
|
|
6036
|
+
phase: "complete",
|
|
6037
|
+
active_skills: [],
|
|
6038
|
+
});
|
|
6039
|
+
await writeJson(join(stateDir, "ralplan-state.json"), {
|
|
6040
|
+
active: false,
|
|
6041
|
+
mode: "ralplan",
|
|
6042
|
+
current_phase: "complete",
|
|
6043
|
+
});
|
|
6044
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
6045
|
+
active: true,
|
|
6046
|
+
skill: "ralplan",
|
|
6047
|
+
phase: "planning",
|
|
6048
|
+
session_id: sessionId,
|
|
6049
|
+
active_skills: [{
|
|
6050
|
+
skill: "ralplan",
|
|
6051
|
+
phase: "planning",
|
|
6052
|
+
active: true,
|
|
6053
|
+
session_id: sessionId,
|
|
6054
|
+
}],
|
|
6055
|
+
});
|
|
6056
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
6057
|
+
active: true,
|
|
6058
|
+
mode: "ralplan",
|
|
6059
|
+
current_phase: "planning",
|
|
6060
|
+
session_id: sessionId,
|
|
6061
|
+
});
|
|
6062
|
+
const result = await dispatchCodexNativeHook({
|
|
6063
|
+
hook_event_name: "Stop",
|
|
6064
|
+
cwd,
|
|
6065
|
+
session_id: sessionId,
|
|
6066
|
+
}, { cwd });
|
|
6067
|
+
assert.equal(result.omxEventName, "stop");
|
|
6068
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
6069
|
+
assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
|
|
6070
|
+
}
|
|
6071
|
+
finally {
|
|
6072
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6073
|
+
}
|
|
6074
|
+
});
|
|
6075
|
+
it("does not block stale session ralplan when root ralplan is terminal and another root skill is active", async () => {
|
|
6076
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-ralplan-other-root-skill-"));
|
|
6077
|
+
try {
|
|
6078
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
6079
|
+
const sessionId = "sess-stop-stale-ralplan-other-root-skill";
|
|
6080
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
6081
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
6082
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
6083
|
+
active: true,
|
|
6084
|
+
skill: "deep-interview",
|
|
6085
|
+
phase: "intent-first",
|
|
6086
|
+
session_id: sessionId,
|
|
6087
|
+
active_skills: [{
|
|
6088
|
+
skill: "deep-interview",
|
|
6089
|
+
phase: "intent-first",
|
|
6090
|
+
active: true,
|
|
6091
|
+
session_id: sessionId,
|
|
6092
|
+
}],
|
|
6093
|
+
});
|
|
6094
|
+
await writeJson(join(stateDir, "ralplan-state.json"), {
|
|
6095
|
+
active: false,
|
|
6096
|
+
mode: "ralplan",
|
|
6097
|
+
current_phase: "complete",
|
|
6098
|
+
session_id: sessionId,
|
|
6099
|
+
});
|
|
6100
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
6101
|
+
active: true,
|
|
6102
|
+
skill: "ralplan",
|
|
6103
|
+
phase: "planning",
|
|
6104
|
+
session_id: sessionId,
|
|
6105
|
+
active_skills: [{
|
|
6106
|
+
skill: "ralplan",
|
|
6107
|
+
phase: "planning",
|
|
6108
|
+
active: true,
|
|
6109
|
+
session_id: sessionId,
|
|
6110
|
+
}],
|
|
6111
|
+
});
|
|
6112
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
6113
|
+
active: true,
|
|
6114
|
+
mode: "ralplan",
|
|
6115
|
+
current_phase: "planning",
|
|
6116
|
+
session_id: sessionId,
|
|
6117
|
+
});
|
|
6118
|
+
const result = await dispatchCodexNativeHook({
|
|
6119
|
+
hook_event_name: "Stop",
|
|
6120
|
+
cwd,
|
|
6121
|
+
session_id: sessionId,
|
|
6122
|
+
}, { cwd });
|
|
6123
|
+
assert.equal(result.omxEventName, "stop");
|
|
6124
|
+
assert.equal(result.outputJson, null);
|
|
6125
|
+
}
|
|
6126
|
+
finally {
|
|
6127
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6128
|
+
}
|
|
6129
|
+
});
|
|
6130
|
+
it("keeps blocking session ralplan when canonical root state is not inactive", async () => {
|
|
6131
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-active-"));
|
|
6132
|
+
try {
|
|
6133
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
6134
|
+
const sessionId = "sess-stop-session-ralplan-root-active";
|
|
6135
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
6136
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
6137
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
6138
|
+
active: true,
|
|
6139
|
+
skill: "ralplan",
|
|
6140
|
+
phase: "planning",
|
|
6141
|
+
session_id: sessionId,
|
|
6142
|
+
active_skills: [{
|
|
6143
|
+
skill: "ralplan",
|
|
6144
|
+
phase: "planning",
|
|
6145
|
+
active: true,
|
|
6146
|
+
session_id: sessionId,
|
|
6147
|
+
}],
|
|
6148
|
+
});
|
|
6149
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
6150
|
+
active: true,
|
|
6151
|
+
skill: "ralplan",
|
|
6152
|
+
phase: "planning",
|
|
6153
|
+
session_id: sessionId,
|
|
6154
|
+
active_skills: [{
|
|
6155
|
+
skill: "ralplan",
|
|
6156
|
+
phase: "planning",
|
|
6157
|
+
active: true,
|
|
6158
|
+
session_id: sessionId,
|
|
6159
|
+
}],
|
|
6160
|
+
});
|
|
6161
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
6162
|
+
active: true,
|
|
6163
|
+
mode: "ralplan",
|
|
6164
|
+
current_phase: "planning",
|
|
6165
|
+
session_id: sessionId,
|
|
6166
|
+
});
|
|
6167
|
+
const result = await dispatchCodexNativeHook({
|
|
6168
|
+
hook_event_name: "Stop",
|
|
6169
|
+
cwd,
|
|
6170
|
+
session_id: sessionId,
|
|
6171
|
+
}, { cwd });
|
|
6172
|
+
assert.equal(result.omxEventName, "stop");
|
|
6173
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
6174
|
+
assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
|
|
6175
|
+
}
|
|
6176
|
+
finally {
|
|
6177
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6178
|
+
}
|
|
6179
|
+
});
|
|
6180
|
+
it("does not block on stale ralplan skill-active when canonical run-state is terminal", async () => {
|
|
6181
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-terminal-ralplan-run-"));
|
|
6182
|
+
try {
|
|
6183
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
6184
|
+
const sessionId = "sess-stop-terminal-ralplan";
|
|
6185
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
6186
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
6187
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
6188
|
+
active: true,
|
|
6189
|
+
skill: "ralplan",
|
|
6190
|
+
phase: "planning",
|
|
6191
|
+
session_id: sessionId,
|
|
6192
|
+
active_skills: [{
|
|
6193
|
+
skill: "ralplan",
|
|
6194
|
+
phase: "planning",
|
|
6195
|
+
active: true,
|
|
6196
|
+
session_id: sessionId,
|
|
6197
|
+
}],
|
|
6198
|
+
});
|
|
6199
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
6200
|
+
active: true,
|
|
6201
|
+
mode: "ralplan",
|
|
6202
|
+
current_phase: "planning",
|
|
6203
|
+
session_id: sessionId,
|
|
6204
|
+
});
|
|
6205
|
+
await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
|
|
6206
|
+
version: 1,
|
|
6207
|
+
mode: "ralplan",
|
|
6208
|
+
active: false,
|
|
6209
|
+
outcome: "finish",
|
|
6210
|
+
lifecycle_outcome: "finished",
|
|
6211
|
+
current_phase: "complete",
|
|
6212
|
+
completed_at: "2026-05-01T00:00:00.000Z",
|
|
6213
|
+
updated_at: "2026-05-01T00:00:00.000Z",
|
|
6214
|
+
});
|
|
6215
|
+
const result = await dispatchCodexNativeHook({
|
|
6216
|
+
hook_event_name: "Stop",
|
|
6217
|
+
cwd,
|
|
6218
|
+
session_id: sessionId,
|
|
6219
|
+
}, { cwd });
|
|
6220
|
+
assert.equal(result.omxEventName, "stop");
|
|
6221
|
+
assert.equal(result.outputJson, null);
|
|
6222
|
+
}
|
|
6223
|
+
finally {
|
|
6224
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6225
|
+
}
|
|
6226
|
+
});
|
|
6227
|
+
it("does not block on stale ralplan skill-active when pinned mode state belongs to another session", async () => {
|
|
6228
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-foreign-ralplan-"));
|
|
6229
|
+
try {
|
|
6230
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
6231
|
+
const sessionId = "sess-stop-current-ralplan";
|
|
6232
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
6233
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
6234
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
6235
|
+
active: true,
|
|
6236
|
+
skill: "ralplan",
|
|
6237
|
+
phase: "planning",
|
|
6238
|
+
session_id: sessionId,
|
|
6239
|
+
active_skills: [{
|
|
6240
|
+
skill: "ralplan",
|
|
6241
|
+
phase: "planning",
|
|
6242
|
+
active: true,
|
|
6243
|
+
session_id: sessionId,
|
|
6244
|
+
}],
|
|
6245
|
+
});
|
|
6246
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
6247
|
+
active: true,
|
|
6248
|
+
mode: "ralplan",
|
|
6249
|
+
current_phase: "planning",
|
|
6250
|
+
session_id: "sess-other-ralplan",
|
|
6251
|
+
});
|
|
6252
|
+
const result = await dispatchCodexNativeHook({
|
|
6253
|
+
hook_event_name: "Stop",
|
|
6254
|
+
cwd,
|
|
6255
|
+
session_id: sessionId,
|
|
6256
|
+
}, { cwd });
|
|
6257
|
+
assert.equal(result.omxEventName, "stop");
|
|
6258
|
+
assert.equal(result.outputJson, null);
|
|
6259
|
+
}
|
|
6260
|
+
finally {
|
|
6261
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6262
|
+
}
|
|
6263
|
+
});
|
|
6264
|
+
it("returns an explicit ralplan waiting status while subagents are still active", async () => {
|
|
6265
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-subagent-"));
|
|
6266
|
+
try {
|
|
6267
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
6268
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-skill-subagent"), { recursive: true });
|
|
6269
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-skill-subagent" });
|
|
6270
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-skill-subagent", "skill-active-state.json"), {
|
|
6271
|
+
active: true,
|
|
6272
|
+
skill: "ralplan",
|
|
5718
6273
|
phase: "planning",
|
|
5719
6274
|
});
|
|
5720
6275
|
await writeJson(join(stateDir, "sessions", "sess-stop-skill-subagent", "ralplan-state.json"), {
|
|
@@ -6605,6 +7160,42 @@ exit 0
|
|
|
6605
7160
|
await rm(cwd, { recursive: true, force: true });
|
|
6606
7161
|
}
|
|
6607
7162
|
});
|
|
7163
|
+
it("allows Stop from stale orphaned session-scoped Ralph starting iteration zero state", async () => {
|
|
7164
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-orphan-starting-ralph-"));
|
|
7165
|
+
try {
|
|
7166
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7167
|
+
const sessionId = "sess-stale-orphan-ralph";
|
|
7168
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
7169
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: sessionId, cwd });
|
|
7170
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
7171
|
+
active: true,
|
|
7172
|
+
mode: "ralph",
|
|
7173
|
+
current_phase: "starting",
|
|
7174
|
+
iteration: 0,
|
|
7175
|
+
session_id: sessionId,
|
|
7176
|
+
updated_at: "2000-01-01T00:00:00.000Z",
|
|
7177
|
+
});
|
|
7178
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
7179
|
+
active: true,
|
|
7180
|
+
skill: "ralph",
|
|
7181
|
+
phase: "starting",
|
|
7182
|
+
session_id: sessionId,
|
|
7183
|
+
active_skills: [{ skill: "ralph", phase: "starting", active: true, session_id: sessionId }],
|
|
7184
|
+
});
|
|
7185
|
+
const result = await dispatchCodexNativeHook({
|
|
7186
|
+
hook_event_name: "Stop",
|
|
7187
|
+
cwd,
|
|
7188
|
+
session_id: sessionId,
|
|
7189
|
+
thread_id: "thread-verifier-terminal",
|
|
7190
|
+
last_assistant_message: "APPROVE: read-only verifier evidence is fresh and sufficient.",
|
|
7191
|
+
}, { cwd });
|
|
7192
|
+
assert.equal(result.omxEventName, "stop");
|
|
7193
|
+
assert.equal(result.outputJson, null);
|
|
7194
|
+
}
|
|
7195
|
+
finally {
|
|
7196
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7197
|
+
}
|
|
7198
|
+
});
|
|
6608
7199
|
it("blocks Stop on visible active session-scoped Ralph starting state and reports its path", async () => {
|
|
6609
7200
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-visible-starting-ralph-"));
|
|
6610
7201
|
try {
|
|
@@ -6640,6 +7231,126 @@ exit 0
|
|
|
6640
7231
|
await rm(cwd, { recursive: true, force: true });
|
|
6641
7232
|
}
|
|
6642
7233
|
});
|
|
7234
|
+
it("retires prompt-seeded Ralph starting state when canonical Ralph already completed with audit", async () => {
|
|
7235
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-shadowed-starting-"));
|
|
7236
|
+
try {
|
|
7237
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7238
|
+
const nativeSessionId = "native-hook-seed";
|
|
7239
|
+
const canonicalSessionId = "omx-runtime-session";
|
|
7240
|
+
await mkdir(join(stateDir, "sessions", nativeSessionId), { recursive: true });
|
|
7241
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
7242
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
7243
|
+
session_id: canonicalSessionId,
|
|
7244
|
+
cwd,
|
|
7245
|
+
});
|
|
7246
|
+
await writeJson(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), {
|
|
7247
|
+
active: true,
|
|
7248
|
+
mode: "ralph",
|
|
7249
|
+
current_phase: "starting",
|
|
7250
|
+
session_id: nativeSessionId,
|
|
7251
|
+
iteration: 0,
|
|
7252
|
+
task_slug: "mvp-h-local-method-preflight-execution",
|
|
7253
|
+
started_at: "2026-05-14T07:00:00.000Z",
|
|
7254
|
+
});
|
|
7255
|
+
await writeJson(join(stateDir, "sessions", nativeSessionId, "skill-active-state.json"), {
|
|
7256
|
+
active: true,
|
|
7257
|
+
skill: "ralph",
|
|
7258
|
+
phase: "starting",
|
|
7259
|
+
session_id: nativeSessionId,
|
|
7260
|
+
active_skills: [{ skill: "ralph", phase: "starting", active: true, session_id: nativeSessionId }],
|
|
7261
|
+
});
|
|
7262
|
+
await writeJson(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), {
|
|
7263
|
+
active: false,
|
|
7264
|
+
mode: "ralph",
|
|
7265
|
+
current_phase: "complete",
|
|
7266
|
+
session_id: canonicalSessionId,
|
|
7267
|
+
completed_at: "2026-05-14T07:30:00.000Z",
|
|
7268
|
+
completion_audit: {
|
|
7269
|
+
passed: true,
|
|
7270
|
+
prompt_to_artifact_checklist: ["task evidence mapped"],
|
|
7271
|
+
verification_evidence: ["fresh verification evidence recorded"],
|
|
7272
|
+
},
|
|
7273
|
+
});
|
|
7274
|
+
const result = await dispatchCodexNativeHook({
|
|
7275
|
+
hook_event_name: "Stop",
|
|
7276
|
+
cwd,
|
|
7277
|
+
session_id: nativeSessionId,
|
|
7278
|
+
}, { cwd });
|
|
7279
|
+
assert.equal(result.omxEventName, "stop");
|
|
7280
|
+
assert.equal(result.outputJson, null);
|
|
7281
|
+
const retiredState = JSON.parse(await readFile(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), "utf-8"));
|
|
7282
|
+
assert.equal(retiredState.active, false);
|
|
7283
|
+
assert.equal(retiredState.current_phase, "complete");
|
|
7284
|
+
assert.equal(retiredState.stop_reason, "shadowed_by_completed_canonical_ralph");
|
|
7285
|
+
assert.equal(retiredState.shadowed_by_completed_canonical_ralph.session_id, canonicalSessionId);
|
|
7286
|
+
}
|
|
7287
|
+
finally {
|
|
7288
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7289
|
+
}
|
|
7290
|
+
});
|
|
7291
|
+
it("does not retire prompt-seeded Ralph starting state from a completed canonical Ralph owned by another thread", async () => {
|
|
7292
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-shadowed-thread-mismatch-"));
|
|
7293
|
+
try {
|
|
7294
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7295
|
+
const nativeSessionId = "native-hook-seed";
|
|
7296
|
+
const canonicalSessionId = "omx-runtime-session";
|
|
7297
|
+
await mkdir(join(stateDir, "sessions", nativeSessionId), { recursive: true });
|
|
7298
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
7299
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
7300
|
+
session_id: canonicalSessionId,
|
|
7301
|
+
cwd,
|
|
7302
|
+
});
|
|
7303
|
+
await writeJson(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), {
|
|
7304
|
+
active: true,
|
|
7305
|
+
mode: "ralph",
|
|
7306
|
+
current_phase: "starting",
|
|
7307
|
+
session_id: nativeSessionId,
|
|
7308
|
+
iteration: 0,
|
|
7309
|
+
task_slug: "mvp-h-local-method-preflight-execution",
|
|
7310
|
+
started_at: "2026-05-14T07:00:00.000Z",
|
|
7311
|
+
});
|
|
7312
|
+
await writeJson(join(stateDir, "sessions", nativeSessionId, "skill-active-state.json"), {
|
|
7313
|
+
active: true,
|
|
7314
|
+
skill: "ralph",
|
|
7315
|
+
phase: "starting",
|
|
7316
|
+
session_id: nativeSessionId,
|
|
7317
|
+
active_skills: [{ skill: "ralph", phase: "starting", active: true, session_id: nativeSessionId }],
|
|
7318
|
+
});
|
|
7319
|
+
await writeJson(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), {
|
|
7320
|
+
active: false,
|
|
7321
|
+
mode: "ralph",
|
|
7322
|
+
current_phase: "complete",
|
|
7323
|
+
session_id: canonicalSessionId,
|
|
7324
|
+
owner_codex_thread_id: "thread-A",
|
|
7325
|
+
completed_at: "2026-05-14T07:30:00.000Z",
|
|
7326
|
+
completion_audit: {
|
|
7327
|
+
passed: true,
|
|
7328
|
+
prompt_to_artifact_checklist: ["task evidence mapped"],
|
|
7329
|
+
verification_evidence: ["fresh verification evidence recorded"],
|
|
7330
|
+
},
|
|
7331
|
+
});
|
|
7332
|
+
const result = await dispatchCodexNativeHook({
|
|
7333
|
+
hook_event_name: "Stop",
|
|
7334
|
+
cwd,
|
|
7335
|
+
session_id: nativeSessionId,
|
|
7336
|
+
thread_id: "thread-B",
|
|
7337
|
+
}, { cwd });
|
|
7338
|
+
assert.equal(result.omxEventName, "stop");
|
|
7339
|
+
assert.deepEqual(result.outputJson, {
|
|
7340
|
+
decision: "block",
|
|
7341
|
+
reason: "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.",
|
|
7342
|
+
stopReason: "ralph_starting",
|
|
7343
|
+
systemMessage: "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.",
|
|
7344
|
+
});
|
|
7345
|
+
const preservedState = JSON.parse(await readFile(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), "utf-8"));
|
|
7346
|
+
assert.equal(preservedState.active, true);
|
|
7347
|
+
assert.equal(preservedState.current_phase, "starting");
|
|
7348
|
+
assert.equal(preservedState.stop_reason, undefined);
|
|
7349
|
+
}
|
|
7350
|
+
finally {
|
|
7351
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7352
|
+
}
|
|
7353
|
+
});
|
|
6643
7354
|
it("does not block Stop from another session-scoped Ralph state when an explicit session_id has no active Ralph state", async () => {
|
|
6644
7355
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-explicit-session-ralph-"));
|
|
6645
7356
|
try {
|