oh-my-codex 0.18.6 → 0.18.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +6 -6
- package/Cargo.toml +1 -1
- package/README.md +56 -7
- package/dist/agents/__tests__/definitions.test.js +11 -0
- package/dist/agents/__tests__/definitions.test.js.map +1 -1
- package/dist/agents/__tests__/native-config.test.js +14 -5
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/agents/definitions.d.ts +2 -0
- package/dist/agents/definitions.d.ts.map +1 -1
- package/dist/agents/definitions.js +4 -1
- package/dist/agents/definitions.js.map +1 -1
- package/dist/agents/native-config.js +2 -2
- package/dist/agents/native-config.js.map +1 -1
- package/dist/autopilot/__tests__/fsm.test.d.ts +2 -0
- package/dist/autopilot/__tests__/fsm.test.d.ts.map +1 -0
- package/dist/autopilot/__tests__/fsm.test.js +75 -0
- package/dist/autopilot/__tests__/fsm.test.js.map +1 -0
- package/dist/autopilot/__tests__/ralplan-gate.test.d.ts +2 -0
- package/dist/autopilot/__tests__/ralplan-gate.test.d.ts.map +1 -0
- package/dist/autopilot/__tests__/ralplan-gate.test.js +79 -0
- package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -0
- package/dist/autopilot/deep-interview-gate.d.ts +18 -0
- package/dist/autopilot/deep-interview-gate.d.ts.map +1 -0
- package/dist/autopilot/deep-interview-gate.js +256 -0
- package/dist/autopilot/deep-interview-gate.js.map +1 -0
- package/dist/autopilot/fsm.d.ts +13 -0
- package/dist/autopilot/fsm.d.ts.map +1 -0
- package/dist/autopilot/fsm.js +70 -0
- package/dist/autopilot/fsm.js.map +1 -0
- package/dist/autopilot/ralplan-gate.d.ts +17 -0
- package/dist/autopilot/ralplan-gate.d.ts.map +1 -0
- package/dist/autopilot/ralplan-gate.js +61 -0
- package/dist/autopilot/ralplan-gate.js.map +1 -0
- package/dist/cli/__tests__/index.test.js +24 -4
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +175 -6
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/question.test.js +100 -0
- package/dist/cli/__tests__/question.test.js.map +1 -1
- package/dist/cli/__tests__/setup-refresh.test.js +18 -0
- package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +2 -2
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/index.d.ts +3 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +191 -36
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/question.d.ts.map +1 -1
- package/dist/cli/question.js +36 -5
- package/dist/cli/question.js.map +1 -1
- package/dist/config/__tests__/deep-interview.test.js +7 -6
- package/dist/config/__tests__/deep-interview.test.js.map +1 -1
- package/dist/config/deep-interview.d.ts.map +1 -1
- package/dist/config/deep-interview.js +14 -4
- package/dist/config/deep-interview.js.map +1 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +8 -0
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/deep-interview-contract.test.js +10 -0
- package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +649 -11
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +63 -0
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/session.test.js +25 -0
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/deep-interview-config-instruction.js +1 -1
- package/dist/hooks/deep-interview-config-instruction.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts +1 -0
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +171 -21
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/keyword-registry.d.ts.map +1 -1
- package/dist/hooks/keyword-registry.js +1 -0
- package/dist/hooks/keyword-registry.js.map +1 -1
- package/dist/hooks/session.d.ts +2 -0
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +13 -5
- package/dist/hooks/session.js.map +1 -1
- package/dist/hud/__tests__/authority.test.js +35 -0
- package/dist/hud/__tests__/authority.test.js.map +1 -1
- package/dist/hud/__tests__/index.test.js +168 -2
- package/dist/hud/__tests__/index.test.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +67 -13
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/__tests__/state.test.js +80 -0
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/__tests__/tmux.test.js +134 -1
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/authority.d.ts.map +1 -1
- package/dist/hud/authority.js +13 -2
- package/dist/hud/authority.js.map +1 -1
- package/dist/hud/index.d.ts +17 -0
- package/dist/hud/index.d.ts.map +1 -1
- package/dist/hud/index.js +64 -10
- package/dist/hud/index.js.map +1 -1
- package/dist/hud/reconcile.js +1 -1
- package/dist/hud/reconcile.js.map +1 -1
- package/dist/hud/state.d.ts.map +1 -1
- package/dist/hud/state.js +16 -1
- package/dist/hud/state.js.map +1 -1
- package/dist/hud/tmux.d.ts +2 -0
- package/dist/hud/tmux.d.ts.map +1 -1
- package/dist/hud/tmux.js +39 -2
- package/dist/hud/tmux.js.map +1 -1
- package/dist/mcp/__tests__/hermes-bridge.test.js +203 -7
- package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
- package/dist/mcp/__tests__/state-server.test.js +13 -1
- package/dist/mcp/__tests__/state-server.test.js.map +1 -1
- package/dist/mcp/hermes-bridge.d.ts +12 -2
- package/dist/mcp/hermes-bridge.d.ts.map +1 -1
- package/dist/mcp/hermes-bridge.js +83 -9
- package/dist/mcp/hermes-bridge.js.map +1 -1
- package/dist/modes/__tests__/base-autoresearch-contract.test.js +7 -1
- package/dist/modes/__tests__/base-autoresearch-contract.test.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +130 -0
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/orchestrator.js +1 -1
- package/dist/pipeline/orchestrator.js.map +1 -1
- package/dist/pipeline/stages/ralplan.d.ts +1 -0
- package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
- package/dist/pipeline/stages/ralplan.js +14 -5
- package/dist/pipeline/stages/ralplan.js.map +1 -1
- package/dist/question/__tests__/deep-interview.test.js +160 -2
- package/dist/question/__tests__/deep-interview.test.js.map +1 -1
- package/dist/question/__tests__/policy.test.js +63 -3
- package/dist/question/__tests__/policy.test.js.map +1 -1
- package/dist/question/__tests__/renderer.test.js +191 -2
- package/dist/question/__tests__/renderer.test.js.map +1 -1
- package/dist/question/__tests__/state.test.js +94 -3
- package/dist/question/__tests__/state.test.js.map +1 -1
- package/dist/question/__tests__/ui.test.js +4 -0
- package/dist/question/__tests__/ui.test.js.map +1 -1
- package/dist/question/autopilot-wait.d.ts +12 -2
- package/dist/question/autopilot-wait.d.ts.map +1 -1
- package/dist/question/autopilot-wait.js +158 -47
- package/dist/question/autopilot-wait.js.map +1 -1
- package/dist/question/deep-interview.d.ts.map +1 -1
- package/dist/question/deep-interview.js +22 -6
- package/dist/question/deep-interview.js.map +1 -1
- package/dist/question/policy.d.ts.map +1 -1
- package/dist/question/policy.js +2 -5
- package/dist/question/policy.js.map +1 -1
- package/dist/question/renderer.d.ts +12 -0
- package/dist/question/renderer.d.ts.map +1 -1
- package/dist/question/renderer.js +87 -3
- package/dist/question/renderer.js.map +1 -1
- package/dist/question/state.d.ts +8 -1
- package/dist/question/state.d.ts.map +1 -1
- package/dist/question/state.js +54 -14
- package/dist/question/state.js.map +1 -1
- package/dist/question/types.d.ts +1 -1
- package/dist/question/types.d.ts.map +1 -1
- package/dist/question/ui.d.ts +1 -0
- package/dist/question/ui.d.ts.map +1 -1
- package/dist/question/ui.js +1 -0
- package/dist/question/ui.js.map +1 -1
- package/dist/ralplan/__tests__/runtime.test.js +191 -0
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/consensus-gate.d.ts +9 -1
- package/dist/ralplan/consensus-gate.d.ts.map +1 -1
- package/dist/ralplan/consensus-gate.js +84 -2
- package/dist/ralplan/consensus-gate.js.map +1 -1
- package/dist/ralplan/runtime.d.ts +9 -0
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +32 -11
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +1487 -34
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +356 -38
- 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 +79 -1
- package/dist/scripts/codex-native-pre-post.js.map +1 -1
- package/dist/scripts/hook-payload-guard.d.ts +9 -0
- package/dist/scripts/hook-payload-guard.d.ts.map +1 -0
- package/dist/scripts/hook-payload-guard.js +111 -0
- package/dist/scripts/hook-payload-guard.js.map +1 -0
- package/dist/scripts/notify-fallback-watcher.js +8 -1
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/__tests__/payload-guard.test.d.ts +2 -0
- package/dist/scripts/notify-hook/__tests__/payload-guard.test.d.ts.map +1 -0
- package/dist/scripts/notify-hook/__tests__/payload-guard.test.js +39 -0
- package/dist/scripts/notify-hook/__tests__/payload-guard.test.js.map +1 -0
- package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.js +234 -86
- package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
- package/dist/scripts/notify-hook.js +11 -2
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +1012 -1
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/skill-active.test.js +59 -1
- package/dist/state/__tests__/skill-active.test.js.map +1 -1
- package/dist/state/__tests__/workflow-transition.test.js +73 -7
- package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +102 -0
- package/dist/state/operations.js.map +1 -1
- package/dist/state/skill-active.d.ts.map +1 -1
- package/dist/state/skill-active.js +33 -3
- package/dist/state/skill-active.js.map +1 -1
- package/dist/state/workflow-transition-reconcile.d.ts +6 -0
- package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
- package/dist/state/workflow-transition-reconcile.js +28 -1
- package/dist/state/workflow-transition-reconcile.js.map +1 -1
- package/dist/state/workflow-transition.d.ts.map +1 -1
- package/dist/state/workflow-transition.js +10 -3
- package/dist/state/workflow-transition.js.map +1 -1
- package/dist/subagents/__tests__/tracker.test.js +139 -0
- package/dist/subagents/__tests__/tracker.test.js.map +1 -1
- package/dist/subagents/tracker.d.ts +3 -0
- package/dist/subagents/tracker.d.ts.map +1 -1
- package/dist/subagents/tracker.js +41 -4
- package/dist/subagents/tracker.js.map +1 -1
- package/dist/team/__tests__/coordination-protocol.test.d.ts +2 -0
- package/dist/team/__tests__/coordination-protocol.test.d.ts.map +1 -0
- package/dist/team/__tests__/coordination-protocol.test.js +173 -0
- package/dist/team/__tests__/coordination-protocol.test.js.map +1 -0
- package/dist/team/__tests__/runtime.test.js +51 -2
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/state.test.js +83 -0
- package/dist/team/__tests__/state.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +45 -0
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/worker-bootstrap.test.js +84 -0
- package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
- package/dist/team/coordination-protocol.d.ts +14 -0
- package/dist/team/coordination-protocol.d.ts.map +1 -0
- package/dist/team/coordination-protocol.js +244 -0
- package/dist/team/coordination-protocol.js.map +1 -0
- package/dist/team/runtime.d.ts +1 -0
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +19 -3
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/state/tasks.d.ts.map +1 -1
- package/dist/team/state/tasks.js +24 -0
- package/dist/team/state/tasks.js.map +1 -1
- package/dist/team/state/types.d.ts +21 -1
- package/dist/team/state/types.d.ts.map +1 -1
- package/dist/team/state/types.js.map +1 -1
- package/dist/team/state.d.ts +17 -1
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +12 -5
- package/dist/team/state.js.map +1 -1
- package/dist/team/team-ops.d.ts +1 -1
- package/dist/team/team-ops.d.ts.map +1 -1
- package/dist/team/team-ops.js.map +1 -1
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +19 -1
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worker-bootstrap.d.ts.map +1 -1
- package/dist/team/worker-bootstrap.js +63 -0
- package/dist/team/worker-bootstrap.js.map +1 -1
- package/dist/utils/__tests__/agents-model-table.test.js +4 -2
- package/dist/utils/__tests__/agents-model-table.test.js.map +1 -1
- package/dist/utils/agents-model-table.d.ts.map +1 -1
- package/dist/utils/agents-model-table.js +3 -0
- package/dist/utils/agents-model-table.js.map +1 -1
- package/package.json +1 -1
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/skills/autopilot/SKILL.md +10 -5
- package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +9 -4
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +12 -0
- package/plugins/oh-my-codex/skills/team/SKILL.md +16 -0
- package/plugins/oh-my-codex/skills/worker/SKILL.md +14 -0
- package/skills/autopilot/SKILL.md +10 -5
- package/skills/deep-interview/SKILL.md +9 -4
- package/skills/ralplan/SKILL.md +12 -0
- package/skills/team/SKILL.md +16 -0
- package/skills/worker/SKILL.md +14 -0
- package/src/scripts/__tests__/codex-native-hook.test.ts +2202 -523
- package/src/scripts/codex-native-hook.ts +444 -36
- package/src/scripts/codex-native-pre-post.ts +80 -0
- package/src/scripts/hook-payload-guard.ts +113 -0
- package/src/scripts/notify-fallback-watcher.ts +8 -1
- package/src/scripts/notify-hook/__tests__/payload-guard.test.ts +41 -0
- package/src/scripts/notify-hook/team-worker-stop.ts +193 -52
- package/src/scripts/notify-hook.ts +14 -2
|
@@ -22,6 +22,8 @@ import { getLegacyWikiDir, serializePage, writePage } from "../../wiki/storage.j
|
|
|
22
22
|
import { WIKI_SCHEMA_VERSION } from "../../wiki/types.js";
|
|
23
23
|
import { createUltragoalPlan, readUltragoalPlan } from "../../ultragoal/artifacts.js";
|
|
24
24
|
import { getBaseStateDir } from "../../state/paths.js";
|
|
25
|
+
import { maybeNudgeLeaderForAllowedWorkerStop } from "../notify-hook/team-worker-stop.js";
|
|
26
|
+
import { MAX_NATIVE_STDIN_JSON_BYTES } from "../hook-payload-guard.js";
|
|
25
27
|
function nativeHookScriptPath() {
|
|
26
28
|
return join(process.cwd(), "dist", "scripts", "codex-native-hook.js");
|
|
27
29
|
}
|
|
@@ -104,6 +106,12 @@ async function withLoreGuardConfig(value, prefix, run) {
|
|
|
104
106
|
}
|
|
105
107
|
}
|
|
106
108
|
function buildWorkerStopFakeTmux(tmuxLogPath, options = {}) {
|
|
109
|
+
const rawCaptureText = options.captureText ?? (options.busyLeader ? "• Working… (esc to interrupt)" : "› ready");
|
|
110
|
+
const captureText = `'${rawCaptureText.replace(/'/g, "'\"'\"'")}'`;
|
|
111
|
+
const currentCommand = `'${(options.currentCommand ?? "codex").replace(/'/g, "'\"'\"'")}'`;
|
|
112
|
+
const sendDelaySeconds = Math.max(0, options.sendDelayMs ?? 0) / 1000;
|
|
113
|
+
const removePathOnSend = options.removePathOnSend ? `'${options.removePathOnSend.replace(/'/g, "'\"'\"'")}'` : "";
|
|
114
|
+
const removePathOnCapture = options.removePathOnCapture ? `'${options.removePathOnCapture.replace(/'/g, "'\"'\"'")}'` : "";
|
|
107
115
|
return `#!/usr/bin/env bash
|
|
108
116
|
set -eu
|
|
109
117
|
echo "$@" >> "${tmuxLogPath}"
|
|
@@ -124,17 +132,20 @@ if [[ "$cmd" == "display-message" ]]; then
|
|
|
124
132
|
"#{pane_id}") echo "%42" ;;
|
|
125
133
|
"#{pane_current_path}") pwd ;;
|
|
126
134
|
"#{pane_start_command}") echo "codex" ;;
|
|
127
|
-
"#{pane_current_command}")
|
|
135
|
+
"#{pane_current_command}") printf '%s\\n' ${currentCommand} ;;
|
|
128
136
|
"#S") echo "omx-team-worker-stop" ;;
|
|
129
137
|
*) ;;
|
|
130
138
|
esac
|
|
131
139
|
exit 0
|
|
132
140
|
fi
|
|
133
141
|
if [[ "$cmd" == "capture-pane" ]]; then
|
|
134
|
-
${
|
|
142
|
+
${removePathOnCapture ? `rm -rf ${removePathOnCapture}` : ""}
|
|
143
|
+
printf '%s\\n' ${captureText}
|
|
135
144
|
exit 0
|
|
136
145
|
fi
|
|
137
146
|
if [[ "$cmd" == "send-keys" ]]; then
|
|
147
|
+
${sendDelaySeconds > 0 ? `sleep ${sendDelaySeconds}` : ""}
|
|
148
|
+
${removePathOnSend ? `rm -rf ${removePathOnSend}` : ""}
|
|
138
149
|
${options.failSend ? "exit 1" : "exit 0"}
|
|
139
150
|
fi
|
|
140
151
|
exit 0
|
|
@@ -281,13 +292,78 @@ describe("codex native hook dispatch", () => {
|
|
|
281
292
|
it("does not treat a different module url as the main module", () => {
|
|
282
293
|
assert.equal(isCodexNativeHookMainModule(pathToFileURL("/tmp/omx native/other-script.js").href, "/tmp/omx native/codex-native-hook.js"), false);
|
|
283
294
|
});
|
|
284
|
-
it("emits
|
|
295
|
+
it("emits schema-safe JSON stdout when CLI stdin is malformed", () => {
|
|
285
296
|
const stdout = runNativeHookCli("{");
|
|
286
297
|
const output = parseSingleJsonStdout(stdout);
|
|
298
|
+
assert.equal(output.continue, false);
|
|
299
|
+
assert.equal(output.stopReason, "native_hook_stdin_parse_error");
|
|
300
|
+
assert.equal(output.hookSpecificOutput, undefined);
|
|
301
|
+
assert.match(String(output.systemMessage ?? ""), /stdin JSON parsing failed inside codex-native-hook:/);
|
|
302
|
+
});
|
|
303
|
+
it("redacts unterminated prompt-like malformed stdin fields", async () => {
|
|
304
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-malformed-unterminated-"));
|
|
305
|
+
try {
|
|
306
|
+
const privatePrompt = "PRIVATE_UNTERMINATED_PROMPT";
|
|
307
|
+
const malformed = `{hook_event_name:"PostToolUse", prompt:"${privatePrompt}`;
|
|
308
|
+
const result = spawnSync(process.execPath, [nativeHookScriptPath()], {
|
|
309
|
+
cwd,
|
|
310
|
+
input: malformed,
|
|
311
|
+
encoding: "utf-8",
|
|
312
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
313
|
+
});
|
|
314
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
315
|
+
assert.equal(result.stderr, "");
|
|
316
|
+
const output = parseSingleJsonStdout(result.stdout);
|
|
317
|
+
assert.equal(output.stopReason, "native_hook_stdin_parse_error");
|
|
318
|
+
const log = await readFile(join(cwd, ".omx", "logs", `native-hook-${new Date().toISOString().split("T")[0]}.jsonl`), "utf-8");
|
|
319
|
+
const entry = JSON.parse(log.trim());
|
|
320
|
+
const prefix = String(entry.raw_input_prefix ?? "");
|
|
321
|
+
assert.doesNotMatch(prefix, new RegExp(privatePrompt));
|
|
322
|
+
assert.match(prefix, /prompt:"\[REDACTED\]"/);
|
|
323
|
+
}
|
|
324
|
+
finally {
|
|
325
|
+
await rm(cwd, { recursive: true, force: true });
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
it("logs a bounded redacted raw stdin prefix when CLI stdin is malformed", async () => {
|
|
329
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-malformed-log-prefix-"));
|
|
330
|
+
try {
|
|
331
|
+
const secret = "sk-test-secret123456";
|
|
332
|
+
const promptText = "summarize private launch notes";
|
|
333
|
+
const malformed = `{hook_event_name:"PostToolUse", access_token:"${secret}", prompt:"${promptText}", text:"${promptText}", bad:"${"x".repeat(400)}"}${String.fromCharCode(10, 0, 7)}`;
|
|
334
|
+
const result = spawnSync(process.execPath, [nativeHookScriptPath()], {
|
|
335
|
+
cwd,
|
|
336
|
+
input: malformed,
|
|
337
|
+
encoding: "utf-8",
|
|
338
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
339
|
+
});
|
|
340
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
341
|
+
assert.equal(result.stderr, "");
|
|
342
|
+
const output = parseSingleJsonStdout(result.stdout);
|
|
343
|
+
assert.equal(output.stopReason, "native_hook_stdin_parse_error");
|
|
344
|
+
const log = await readFile(join(cwd, ".omx", "logs", `native-hook-${new Date().toISOString().split("T")[0]}.jsonl`), "utf-8");
|
|
345
|
+
const entry = JSON.parse(log.trim());
|
|
346
|
+
const prefix = String(entry.raw_input_prefix ?? "");
|
|
347
|
+
assert.equal(entry.type, "native_hook_stdin_parse_error");
|
|
348
|
+
assert.equal(entry.raw_input_length, Buffer.byteLength(malformed, "utf-8"));
|
|
349
|
+
assert.ok(prefix.length <= 240, `prefix should be bounded, got ${prefix.length}`);
|
|
350
|
+
assert.doesNotMatch(prefix, /[\u0000-\u001f\u007f-\u009f]/);
|
|
351
|
+
assert.doesNotMatch(prefix, new RegExp(secret));
|
|
352
|
+
assert.doesNotMatch(prefix, new RegExp(promptText));
|
|
353
|
+
assert.match(prefix, /\[REDACTED\]/);
|
|
354
|
+
}
|
|
355
|
+
finally {
|
|
356
|
+
await rm(cwd, { recursive: true, force: true });
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
it("emits Stop-schema-safe block JSON when malformed stdin still identifies Stop", () => {
|
|
360
|
+
const stdout = runNativeHookCli('{hook_event_name:"Stop",');
|
|
361
|
+
const output = parseSingleJsonStdout(stdout);
|
|
287
362
|
assert.equal(output.decision, "block");
|
|
288
363
|
assert.equal(output.reason, "OMX native hook received malformed JSON input. Preserve runtime state, inspect the emitting hook payload yourself, and retry with valid JSON.");
|
|
289
|
-
assert.equal(output.
|
|
290
|
-
assert.
|
|
364
|
+
assert.equal(output.stopReason, "native_hook_stdin_parse_error");
|
|
365
|
+
assert.equal(output.hookSpecificOutput, undefined);
|
|
366
|
+
assert.match(String(output.systemMessage ?? ""), /stdin JSON parsing failed inside codex-native-hook:/);
|
|
291
367
|
});
|
|
292
368
|
it("emits parseable no-op JSON stdout for inactive Stop CLI runs", async () => {
|
|
293
369
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-noop-json-"));
|
|
@@ -301,6 +377,112 @@ describe("codex native hook dispatch", () => {
|
|
|
301
377
|
}, { cwd });
|
|
302
378
|
const output = parseSingleJsonStdout(stdout);
|
|
303
379
|
assert.deepEqual(output, {});
|
|
380
|
+
assert.equal(existsSync(join(cwd, ".omx", "state")), false);
|
|
381
|
+
}
|
|
382
|
+
finally {
|
|
383
|
+
await rm(cwd, { recursive: true, force: true });
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
it("returns empty JSON for oversized Stop stdin without parsing or creating inactive state", async () => {
|
|
387
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-"));
|
|
388
|
+
try {
|
|
389
|
+
const oversizedStop = JSON.stringify({
|
|
390
|
+
hook_event_name: "Stop",
|
|
391
|
+
cwd,
|
|
392
|
+
session_id: "sess-cli-stop-oversized",
|
|
393
|
+
transcript: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
|
|
394
|
+
});
|
|
395
|
+
const stdout = runNativeHookCli(oversizedStop, { cwd });
|
|
396
|
+
assert.deepEqual(parseSingleJsonStdout(stdout), {});
|
|
397
|
+
assert.equal(existsSync(join(cwd, ".omx", "state")), false);
|
|
398
|
+
}
|
|
399
|
+
finally {
|
|
400
|
+
await rm(cwd, { recursive: true, force: true });
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
it("blocks oversized Stop stdin when current session autopilot is active", async () => {
|
|
404
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-active-"));
|
|
405
|
+
try {
|
|
406
|
+
await writeActiveAutopilotSession(cwd, "sess-cli-stop-oversized-active");
|
|
407
|
+
const oversizedStop = JSON.stringify({
|
|
408
|
+
hook_event_name: "Stop",
|
|
409
|
+
cwd,
|
|
410
|
+
session_id: "native-session-hidden-by-oversized-payload",
|
|
411
|
+
transcript: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
|
|
412
|
+
});
|
|
413
|
+
const output = parseSingleJsonStdout(runNativeHookCli(oversizedStop, { cwd }));
|
|
414
|
+
assert.equal(output.decision, "block");
|
|
415
|
+
assert.equal(output.stopReason, "native_stop_stdin_oversized_active_workflow");
|
|
416
|
+
assert.match(String(output.systemMessage ?? ""), /active current-session workflow state/);
|
|
417
|
+
assert.equal(existsSync(join(cwd, ".omx", "logs")), false);
|
|
418
|
+
}
|
|
419
|
+
finally {
|
|
420
|
+
await rm(cwd, { recursive: true, force: true });
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
it("does not block oversized Stop stdin for unrelated root autopilot state", async () => {
|
|
424
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-stale-root-"));
|
|
425
|
+
try {
|
|
426
|
+
await writeJson(join(cwd, ".omx", "state", "session.json"), {
|
|
427
|
+
session_id: "sess-current-without-active-autopilot",
|
|
428
|
+
cwd,
|
|
429
|
+
});
|
|
430
|
+
await writeJson(join(cwd, ".omx", "state", "autopilot-state.json"), {
|
|
431
|
+
active: true,
|
|
432
|
+
current_phase: "execution",
|
|
433
|
+
});
|
|
434
|
+
const oversizedStop = JSON.stringify({
|
|
435
|
+
hook_event_name: "Stop",
|
|
436
|
+
cwd,
|
|
437
|
+
transcript: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
|
|
438
|
+
});
|
|
439
|
+
assert.deepEqual(parseSingleJsonStdout(runNativeHookCli(oversizedStop, { cwd })), {});
|
|
440
|
+
assert.equal(existsSync(join(cwd, ".omx", "logs")), false);
|
|
441
|
+
}
|
|
442
|
+
finally {
|
|
443
|
+
await rm(cwd, { recursive: true, force: true });
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
it("does not block oversized Stop stdin when terminal run-state shadows stale autopilot state", async () => {
|
|
447
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-terminal-run-"));
|
|
448
|
+
try {
|
|
449
|
+
const sessionId = "sess-cli-stop-oversized-terminal-run";
|
|
450
|
+
await writeActiveAutopilotSession(cwd, sessionId);
|
|
451
|
+
await writeJson(join(cwd, ".omx", "state", "sessions", sessionId, "run-state.json"), {
|
|
452
|
+
version: 1,
|
|
453
|
+
active: false,
|
|
454
|
+
mode: "autopilot",
|
|
455
|
+
outcome: "finish",
|
|
456
|
+
lifecycle_outcome: "finished",
|
|
457
|
+
current_phase: "complete",
|
|
458
|
+
completed_at: "2026-05-20T11:00:00.000Z",
|
|
459
|
+
updated_at: "2026-05-20T11:00:00.000Z",
|
|
460
|
+
});
|
|
461
|
+
const oversizedStop = JSON.stringify({
|
|
462
|
+
hook_event_name: "Stop",
|
|
463
|
+
cwd,
|
|
464
|
+
transcript: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
|
|
465
|
+
});
|
|
466
|
+
assert.deepEqual(parseSingleJsonStdout(runNativeHookCli(oversizedStop, { cwd })), {});
|
|
467
|
+
assert.equal(existsSync(join(cwd, ".omx", "logs")), false);
|
|
468
|
+
}
|
|
469
|
+
finally {
|
|
470
|
+
await rm(cwd, { recursive: true, force: true });
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
it("fails closed for oversized non-Stop stdin before parsing", async () => {
|
|
474
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-nonstop-oversized-"));
|
|
475
|
+
try {
|
|
476
|
+
const oversizedPrompt = JSON.stringify({
|
|
477
|
+
hook_event_name: "UserPromptSubmit",
|
|
478
|
+
cwd,
|
|
479
|
+
session_id: "sess-cli-prompt-oversized",
|
|
480
|
+
prompt: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
|
|
481
|
+
});
|
|
482
|
+
const output = parseSingleJsonStdout(runNativeHookCli(oversizedPrompt, { cwd }));
|
|
483
|
+
assert.equal(output.continue, false);
|
|
484
|
+
assert.equal(output.stopReason, "native_hook_stdin_oversized");
|
|
485
|
+
assert.match(String(output.systemMessage ?? ""), /rejected oversized stdin JSON before parsing/);
|
|
304
486
|
}
|
|
305
487
|
finally {
|
|
306
488
|
await rm(cwd, { recursive: true, force: true });
|
|
@@ -915,6 +1097,50 @@ describe("codex native hook dispatch", () => {
|
|
|
915
1097
|
await rm(cwd, { recursive: true, force: true });
|
|
916
1098
|
}
|
|
917
1099
|
});
|
|
1100
|
+
it("keeps a self-parented native role thread as subagent evidence", async () => {
|
|
1101
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-self-parented-subagent-"));
|
|
1102
|
+
try {
|
|
1103
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
1104
|
+
const canonicalSessionId = "omx-autopilot-session";
|
|
1105
|
+
const nativeRoleThreadId = "codex-architect-thread";
|
|
1106
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
1107
|
+
await writeSessionStart(cwd, canonicalSessionId, {
|
|
1108
|
+
nativeSessionId: nativeRoleThreadId,
|
|
1109
|
+
});
|
|
1110
|
+
const transcriptPath = join(cwd, "architect-subagent-rollout.jsonl");
|
|
1111
|
+
await writeFile(transcriptPath, `${JSON.stringify({
|
|
1112
|
+
type: "session_meta",
|
|
1113
|
+
payload: {
|
|
1114
|
+
id: nativeRoleThreadId,
|
|
1115
|
+
source: {
|
|
1116
|
+
subagent: {
|
|
1117
|
+
thread_spawn: {
|
|
1118
|
+
parent_thread_id: nativeRoleThreadId,
|
|
1119
|
+
depth: 1,
|
|
1120
|
+
agent_nickname: "Architect",
|
|
1121
|
+
agent_role: "architect",
|
|
1122
|
+
},
|
|
1123
|
+
},
|
|
1124
|
+
},
|
|
1125
|
+
agent_nickname: "Architect",
|
|
1126
|
+
agent_role: "architect",
|
|
1127
|
+
},
|
|
1128
|
+
})}\n`);
|
|
1129
|
+
await dispatchCodexNativeHook({
|
|
1130
|
+
hook_event_name: "SessionStart",
|
|
1131
|
+
cwd,
|
|
1132
|
+
session_id: nativeRoleThreadId,
|
|
1133
|
+
transcript_path: transcriptPath,
|
|
1134
|
+
}, { cwd, sessionOwnerPid: process.pid });
|
|
1135
|
+
const tracking = JSON.parse(await readFile(join(stateDir, "subagent-tracking.json"), "utf-8"));
|
|
1136
|
+
assert.equal(tracking.sessions?.[canonicalSessionId]?.leader_thread_id, undefined);
|
|
1137
|
+
assert.equal(tracking.sessions?.[canonicalSessionId]?.threads?.[nativeRoleThreadId]?.kind, "subagent");
|
|
1138
|
+
assert.equal(tracking.sessions?.[canonicalSessionId]?.threads?.[nativeRoleThreadId]?.mode, "architect");
|
|
1139
|
+
}
|
|
1140
|
+
finally {
|
|
1141
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
918
1144
|
it("does not attach a subagent SessionStart to an unrelated canonical leader", async () => {
|
|
919
1145
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-start-mismatch-"));
|
|
920
1146
|
try {
|
|
@@ -1018,6 +1244,89 @@ describe("codex native hook dispatch", () => {
|
|
|
1018
1244
|
await rm(cwd, { recursive: true, force: true });
|
|
1019
1245
|
}
|
|
1020
1246
|
});
|
|
1247
|
+
it("prefers the OMX owner session id when a native new session revives HUD", async () => {
|
|
1248
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-owner-session-revive-"));
|
|
1249
|
+
try {
|
|
1250
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
1251
|
+
const ownerSessionId = "omx-launch-owner-hud";
|
|
1252
|
+
const oldNativeSessionId = "codex-native-hud-old";
|
|
1253
|
+
const nativeSessionId = "codex-native-hud-new";
|
|
1254
|
+
await mkdir(stateDir, { recursive: true });
|
|
1255
|
+
await writeSessionStart(cwd, ownerSessionId, {
|
|
1256
|
+
nativeSessionId: oldNativeSessionId,
|
|
1257
|
+
pid: process.pid,
|
|
1258
|
+
});
|
|
1259
|
+
await dispatchCodexNativeHook({
|
|
1260
|
+
hook_event_name: "SessionStart",
|
|
1261
|
+
cwd,
|
|
1262
|
+
session_id: nativeSessionId,
|
|
1263
|
+
}, {
|
|
1264
|
+
cwd,
|
|
1265
|
+
sessionOwnerPid: process.pid,
|
|
1266
|
+
});
|
|
1267
|
+
const sessionState = JSON.parse(await readFile(join(stateDir, "session.json"), "utf-8"));
|
|
1268
|
+
assert.equal(sessionState.session_id, nativeSessionId);
|
|
1269
|
+
assert.equal(sessionState.native_session_id, nativeSessionId);
|
|
1270
|
+
assert.equal(sessionState.previous_native_session_id, oldNativeSessionId);
|
|
1271
|
+
assert.equal(sessionState.owner_omx_session_id, ownerSessionId);
|
|
1272
|
+
let reconcileCall = null;
|
|
1273
|
+
const promptResult = await dispatchCodexNativeHook({
|
|
1274
|
+
hook_event_name: "UserPromptSubmit",
|
|
1275
|
+
cwd,
|
|
1276
|
+
session_id: nativeSessionId,
|
|
1277
|
+
thread_id: "thread-hud-owner",
|
|
1278
|
+
turn_id: "turn-hud-owner",
|
|
1279
|
+
prompt: "$ralplan fix native new hud owner handoff",
|
|
1280
|
+
}, {
|
|
1281
|
+
cwd,
|
|
1282
|
+
reconcileHudForPromptSubmitFn: async (hookCwd, deps = {}) => {
|
|
1283
|
+
reconcileCall = { cwd: hookCwd, sessionId: deps.sessionId };
|
|
1284
|
+
return { status: "recreated", paneId: "%9", desiredHeight: 3, duplicateCount: 0 };
|
|
1285
|
+
},
|
|
1286
|
+
});
|
|
1287
|
+
assert.equal(promptResult.omxEventName, "keyword-detector");
|
|
1288
|
+
assert.deepEqual(reconcileCall, { cwd, sessionId: ownerSessionId });
|
|
1289
|
+
}
|
|
1290
|
+
finally {
|
|
1291
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
it("falls back to the canonical session id for malformed HUD owner ids", async () => {
|
|
1295
|
+
for (const [index, invalidOwnerSessionId] of ["codex-native-hud-owner", "omx-../../stale"].entries()) {
|
|
1296
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-invalid-owner-revive-"));
|
|
1297
|
+
try {
|
|
1298
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
1299
|
+
const canonicalSessionId = "omx-launch-hud-safe";
|
|
1300
|
+
const nativeSessionId = "codex-native-hud-safe";
|
|
1301
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
1302
|
+
await writeSessionStart(cwd, canonicalSessionId);
|
|
1303
|
+
const sessionStatePath = join(stateDir, "session.json");
|
|
1304
|
+
const sessionState = JSON.parse(await readFile(sessionStatePath, "utf-8"));
|
|
1305
|
+
sessionState.owner_omx_session_id = invalidOwnerSessionId;
|
|
1306
|
+
await writeJson(sessionStatePath, sessionState);
|
|
1307
|
+
let reconcileCall = null;
|
|
1308
|
+
const promptResult = await dispatchCodexNativeHook({
|
|
1309
|
+
hook_event_name: "UserPromptSubmit",
|
|
1310
|
+
cwd,
|
|
1311
|
+
session_id: nativeSessionId,
|
|
1312
|
+
thread_id: `thread-hud-invalid-owner-${index}`,
|
|
1313
|
+
turn_id: "turn-hud-invalid-owner",
|
|
1314
|
+
prompt: "$ralplan fix malformed hud owner handoff",
|
|
1315
|
+
}, {
|
|
1316
|
+
cwd,
|
|
1317
|
+
reconcileHudForPromptSubmitFn: async (hookCwd, deps = {}) => {
|
|
1318
|
+
reconcileCall = { cwd: hookCwd, sessionId: deps.sessionId };
|
|
1319
|
+
return { status: "recreated", paneId: "%9", desiredHeight: 3, duplicateCount: 0 };
|
|
1320
|
+
},
|
|
1321
|
+
});
|
|
1322
|
+
assert.equal(promptResult.omxEventName, "keyword-detector");
|
|
1323
|
+
assert.deepEqual(reconcileCall, { cwd, sessionId: canonicalSessionId });
|
|
1324
|
+
}
|
|
1325
|
+
finally {
|
|
1326
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
});
|
|
1021
1330
|
it("passes the canonical OMX session id when UserPromptSubmit revives HUD", async () => {
|
|
1022
1331
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-session-revive-"));
|
|
1023
1332
|
try {
|
|
@@ -2208,46 +2517,340 @@ standardMaxRounds = 15
|
|
|
2208
2517
|
await rm(cwd, { recursive: true, force: true });
|
|
2209
2518
|
}
|
|
2210
2519
|
});
|
|
2211
|
-
it("
|
|
2212
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
2520
|
+
it("does not treat a corrupt leader kind=subagent tracker entry as native subagent prompt scope", async () => {
|
|
2521
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-corrupt-leader-subagent-"));
|
|
2213
2522
|
try {
|
|
2214
|
-
|
|
2523
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
2524
|
+
const canonicalSessionId = "sess-corrupt-leader";
|
|
2525
|
+
const leaderNativeSessionId = "native-corrupt-leader";
|
|
2526
|
+
const nowIso = new Date().toISOString();
|
|
2527
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
2528
|
+
session_id: canonicalSessionId,
|
|
2529
|
+
native_session_id: leaderNativeSessionId,
|
|
2530
|
+
});
|
|
2531
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
2532
|
+
schemaVersion: 1,
|
|
2533
|
+
sessions: {
|
|
2534
|
+
[canonicalSessionId]: {
|
|
2535
|
+
session_id: canonicalSessionId,
|
|
2536
|
+
leader_thread_id: leaderNativeSessionId,
|
|
2537
|
+
updated_at: nowIso,
|
|
2538
|
+
threads: {
|
|
2539
|
+
[leaderNativeSessionId]: {
|
|
2540
|
+
thread_id: leaderNativeSessionId,
|
|
2541
|
+
kind: "subagent",
|
|
2542
|
+
first_seen_at: nowIso,
|
|
2543
|
+
last_seen_at: nowIso,
|
|
2544
|
+
turn_count: 2,
|
|
2545
|
+
},
|
|
2546
|
+
},
|
|
2547
|
+
},
|
|
2548
|
+
},
|
|
2549
|
+
});
|
|
2215
2550
|
const result = await dispatchCodexNativeHook({
|
|
2216
2551
|
hook_event_name: "UserPromptSubmit",
|
|
2217
2552
|
cwd,
|
|
2218
|
-
session_id:
|
|
2219
|
-
thread_id:
|
|
2220
|
-
turn_id: "turn-
|
|
2221
|
-
prompt: "$
|
|
2553
|
+
session_id: leaderNativeSessionId,
|
|
2554
|
+
thread_id: leaderNativeSessionId,
|
|
2555
|
+
turn_id: "turn-corrupt-leader",
|
|
2556
|
+
prompt: "$autopilot continue this review blocker fix",
|
|
2222
2557
|
}, { cwd });
|
|
2223
2558
|
assert.equal(result.omxEventName, "keyword-detector");
|
|
2224
|
-
assert.equal(result.skillState?.skill, "
|
|
2225
|
-
|
|
2226
|
-
assert.match(message, /\$oh-my-codex:ralplan" -> ralplan/);
|
|
2227
|
-
assert.match(message, /use CLI-first state updates via `omx state write\/read\/clear --input '<json>' --json`/);
|
|
2228
|
-
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-plugin-1", "ralplan-state.json")), true);
|
|
2559
|
+
assert.equal(result.skillState?.skill, "autopilot");
|
|
2560
|
+
assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")), true);
|
|
2229
2561
|
}
|
|
2230
2562
|
finally {
|
|
2231
2563
|
await rm(cwd, { recursive: true, force: true });
|
|
2232
2564
|
}
|
|
2233
2565
|
});
|
|
2234
|
-
it("
|
|
2235
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
2566
|
+
it("lets the current canonical leader boundary beat stale global subagent tracking with a distinct prompt thread id", async () => {
|
|
2567
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-current-leader-stale-global-"));
|
|
2236
2568
|
try {
|
|
2237
|
-
|
|
2569
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
2570
|
+
const canonicalSessionId = "sess-current-leader";
|
|
2571
|
+
const leaderNativeSessionId = "native-current-leader";
|
|
2572
|
+
const staleSessionId = "sess-stale-subagent";
|
|
2573
|
+
const staleLeaderNativeSessionId = "native-stale-leader";
|
|
2574
|
+
const nowIso = new Date().toISOString();
|
|
2575
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
2576
|
+
session_id: canonicalSessionId,
|
|
2577
|
+
native_session_id: leaderNativeSessionId,
|
|
2578
|
+
});
|
|
2579
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
2580
|
+
schemaVersion: 1,
|
|
2581
|
+
sessions: {
|
|
2582
|
+
[canonicalSessionId]: {
|
|
2583
|
+
session_id: canonicalSessionId,
|
|
2584
|
+
leader_thread_id: leaderNativeSessionId,
|
|
2585
|
+
updated_at: nowIso,
|
|
2586
|
+
threads: {
|
|
2587
|
+
[leaderNativeSessionId]: {
|
|
2588
|
+
thread_id: leaderNativeSessionId,
|
|
2589
|
+
kind: "leader",
|
|
2590
|
+
first_seen_at: nowIso,
|
|
2591
|
+
last_seen_at: nowIso,
|
|
2592
|
+
turn_count: 1,
|
|
2593
|
+
},
|
|
2594
|
+
},
|
|
2595
|
+
},
|
|
2596
|
+
[staleSessionId]: {
|
|
2597
|
+
session_id: staleSessionId,
|
|
2598
|
+
leader_thread_id: staleLeaderNativeSessionId,
|
|
2599
|
+
updated_at: nowIso,
|
|
2600
|
+
threads: {
|
|
2601
|
+
[staleLeaderNativeSessionId]: {
|
|
2602
|
+
thread_id: staleLeaderNativeSessionId,
|
|
2603
|
+
kind: "leader",
|
|
2604
|
+
first_seen_at: nowIso,
|
|
2605
|
+
last_seen_at: nowIso,
|
|
2606
|
+
turn_count: 1,
|
|
2607
|
+
},
|
|
2608
|
+
[leaderNativeSessionId]: {
|
|
2609
|
+
thread_id: leaderNativeSessionId,
|
|
2610
|
+
kind: "subagent",
|
|
2611
|
+
first_seen_at: nowIso,
|
|
2612
|
+
last_seen_at: nowIso,
|
|
2613
|
+
turn_count: 1,
|
|
2614
|
+
mode: "architect",
|
|
2615
|
+
},
|
|
2616
|
+
},
|
|
2617
|
+
},
|
|
2618
|
+
},
|
|
2619
|
+
});
|
|
2238
2620
|
const result = await dispatchCodexNativeHook({
|
|
2239
2621
|
hook_event_name: "UserPromptSubmit",
|
|
2240
2622
|
cwd,
|
|
2241
|
-
session_id:
|
|
2242
|
-
thread_id: "thread-
|
|
2243
|
-
turn_id: "turn-
|
|
2244
|
-
prompt: "$autopilot
|
|
2623
|
+
session_id: leaderNativeSessionId,
|
|
2624
|
+
thread_id: "thread-current-turn-not-native-session",
|
|
2625
|
+
turn_id: "turn-current-leader",
|
|
2626
|
+
prompt: "$autopilot continue",
|
|
2245
2627
|
}, { cwd });
|
|
2246
2628
|
assert.equal(result.omxEventName, "keyword-detector");
|
|
2247
2629
|
assert.equal(result.skillState?.skill, "autopilot");
|
|
2248
|
-
|
|
2249
|
-
assert.
|
|
2250
|
-
|
|
2630
|
+
assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")), true);
|
|
2631
|
+
assert.equal(existsSync(join(stateDir, "sessions", staleSessionId, "autopilot-state.json")), false);
|
|
2632
|
+
}
|
|
2633
|
+
finally {
|
|
2634
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2635
|
+
}
|
|
2636
|
+
});
|
|
2637
|
+
it("lets the current session native leader beat stale global subagent tracking without a canonical summary and with a distinct prompt thread id", async () => {
|
|
2638
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-current-native-leader-stale-global-"));
|
|
2639
|
+
try {
|
|
2640
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
2641
|
+
const canonicalSessionId = "sess-current-native-leader";
|
|
2642
|
+
const leaderNativeSessionId = "native-current-leader-no-summary";
|
|
2643
|
+
const staleSessionId = "sess-stale-native-subagent";
|
|
2644
|
+
const staleLeaderNativeSessionId = "native-stale-parent";
|
|
2645
|
+
const nowIso = new Date().toISOString();
|
|
2646
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
2647
|
+
session_id: canonicalSessionId,
|
|
2648
|
+
native_session_id: leaderNativeSessionId,
|
|
2649
|
+
});
|
|
2650
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
2651
|
+
schemaVersion: 1,
|
|
2652
|
+
sessions: {
|
|
2653
|
+
[staleSessionId]: {
|
|
2654
|
+
session_id: staleSessionId,
|
|
2655
|
+
leader_thread_id: staleLeaderNativeSessionId,
|
|
2656
|
+
updated_at: nowIso,
|
|
2657
|
+
threads: {
|
|
2658
|
+
[staleLeaderNativeSessionId]: {
|
|
2659
|
+
thread_id: staleLeaderNativeSessionId,
|
|
2660
|
+
kind: "leader",
|
|
2661
|
+
first_seen_at: nowIso,
|
|
2662
|
+
last_seen_at: nowIso,
|
|
2663
|
+
turn_count: 1,
|
|
2664
|
+
},
|
|
2665
|
+
[leaderNativeSessionId]: {
|
|
2666
|
+
thread_id: leaderNativeSessionId,
|
|
2667
|
+
kind: "subagent",
|
|
2668
|
+
first_seen_at: nowIso,
|
|
2669
|
+
last_seen_at: nowIso,
|
|
2670
|
+
turn_count: 1,
|
|
2671
|
+
mode: "critic",
|
|
2672
|
+
},
|
|
2673
|
+
},
|
|
2674
|
+
},
|
|
2675
|
+
},
|
|
2676
|
+
});
|
|
2677
|
+
const result = await dispatchCodexNativeHook({
|
|
2678
|
+
hook_event_name: "UserPromptSubmit",
|
|
2679
|
+
cwd,
|
|
2680
|
+
session_id: leaderNativeSessionId,
|
|
2681
|
+
thread_id: "thread-current-turn-not-native-session",
|
|
2682
|
+
turn_id: "turn-current-native-leader",
|
|
2683
|
+
prompt: "$autopilot continue",
|
|
2684
|
+
}, { cwd });
|
|
2685
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
2686
|
+
assert.equal(result.skillState?.skill, "autopilot");
|
|
2687
|
+
assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")), true);
|
|
2688
|
+
assert.equal(existsSync(join(stateDir, "sessions", staleSessionId, "autopilot-state.json")), false);
|
|
2689
|
+
}
|
|
2690
|
+
finally {
|
|
2691
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2692
|
+
}
|
|
2693
|
+
});
|
|
2694
|
+
it("lets the current session native leader beat a malformed canonical subagent entry", async () => {
|
|
2695
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-current-native-leader-malformed-canonical-"));
|
|
2696
|
+
try {
|
|
2697
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
2698
|
+
const canonicalSessionId = "sess-current-native-leader-malformed";
|
|
2699
|
+
const leaderNativeSessionId = "native-current-leader-malformed";
|
|
2700
|
+
const nowIso = new Date().toISOString();
|
|
2701
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
2702
|
+
session_id: canonicalSessionId,
|
|
2703
|
+
native_session_id: leaderNativeSessionId,
|
|
2704
|
+
});
|
|
2705
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
2706
|
+
schemaVersion: 1,
|
|
2707
|
+
sessions: {
|
|
2708
|
+
[canonicalSessionId]: {
|
|
2709
|
+
session_id: canonicalSessionId,
|
|
2710
|
+
updated_at: nowIso,
|
|
2711
|
+
threads: {
|
|
2712
|
+
[leaderNativeSessionId]: {
|
|
2713
|
+
thread_id: leaderNativeSessionId,
|
|
2714
|
+
kind: "subagent",
|
|
2715
|
+
first_seen_at: nowIso,
|
|
2716
|
+
last_seen_at: nowIso,
|
|
2717
|
+
turn_count: 1,
|
|
2718
|
+
mode: "architect",
|
|
2719
|
+
},
|
|
2720
|
+
},
|
|
2721
|
+
},
|
|
2722
|
+
},
|
|
2723
|
+
});
|
|
2724
|
+
const result = await dispatchCodexNativeHook({
|
|
2725
|
+
hook_event_name: "UserPromptSubmit",
|
|
2726
|
+
cwd,
|
|
2727
|
+
session_id: leaderNativeSessionId,
|
|
2728
|
+
thread_id: leaderNativeSessionId,
|
|
2729
|
+
turn_id: "turn-current-native-leader-malformed",
|
|
2730
|
+
prompt: "$autopilot continue",
|
|
2731
|
+
}, { cwd });
|
|
2732
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
2733
|
+
assert.equal(result.skillState?.skill, "autopilot");
|
|
2734
|
+
assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")), true);
|
|
2735
|
+
}
|
|
2736
|
+
finally {
|
|
2737
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2738
|
+
}
|
|
2739
|
+
});
|
|
2740
|
+
it("still treats mixed child and leader payload identities as native subagent scope", async () => {
|
|
2741
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-mixed-child-leader-identity-"));
|
|
2742
|
+
try {
|
|
2743
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
2744
|
+
const canonicalSessionId = "sess-mixed-child-leader";
|
|
2745
|
+
const leaderNativeSessionId = "native-mixed-leader";
|
|
2746
|
+
const childNativeSessionId = "native-mixed-child";
|
|
2747
|
+
const nowIso = new Date().toISOString();
|
|
2748
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
2749
|
+
session_id: canonicalSessionId,
|
|
2750
|
+
native_session_id: leaderNativeSessionId,
|
|
2751
|
+
});
|
|
2752
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
2753
|
+
schemaVersion: 1,
|
|
2754
|
+
sessions: {
|
|
2755
|
+
[canonicalSessionId]: {
|
|
2756
|
+
session_id: canonicalSessionId,
|
|
2757
|
+
leader_thread_id: leaderNativeSessionId,
|
|
2758
|
+
updated_at: nowIso,
|
|
2759
|
+
threads: {
|
|
2760
|
+
[leaderNativeSessionId]: {
|
|
2761
|
+
thread_id: leaderNativeSessionId,
|
|
2762
|
+
kind: "leader",
|
|
2763
|
+
first_seen_at: nowIso,
|
|
2764
|
+
last_seen_at: nowIso,
|
|
2765
|
+
turn_count: 1,
|
|
2766
|
+
},
|
|
2767
|
+
[childNativeSessionId]: {
|
|
2768
|
+
thread_id: childNativeSessionId,
|
|
2769
|
+
kind: "subagent",
|
|
2770
|
+
first_seen_at: nowIso,
|
|
2771
|
+
last_seen_at: nowIso,
|
|
2772
|
+
turn_count: 1,
|
|
2773
|
+
mode: "critic",
|
|
2774
|
+
},
|
|
2775
|
+
},
|
|
2776
|
+
},
|
|
2777
|
+
},
|
|
2778
|
+
});
|
|
2779
|
+
const result = await dispatchCodexNativeHook({
|
|
2780
|
+
hook_event_name: "UserPromptSubmit",
|
|
2781
|
+
cwd,
|
|
2782
|
+
session_id: childNativeSessionId,
|
|
2783
|
+
thread_id: leaderNativeSessionId,
|
|
2784
|
+
turn_id: "turn-mixed-child-leader",
|
|
2785
|
+
prompt: "$ralplan review this as delegated text",
|
|
2786
|
+
}, { cwd });
|
|
2787
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
2788
|
+
assert.equal(result.skillState, null);
|
|
2789
|
+
assert.equal(result.outputJson, null);
|
|
2790
|
+
assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "ralplan-state.json")), false);
|
|
2791
|
+
assert.equal(existsSync(join(stateDir, "sessions", childNativeSessionId, "ralplan-state.json")), false);
|
|
2792
|
+
const reversedResult = await dispatchCodexNativeHook({
|
|
2793
|
+
hook_event_name: "UserPromptSubmit",
|
|
2794
|
+
cwd,
|
|
2795
|
+
session_id: leaderNativeSessionId,
|
|
2796
|
+
thread_id: childNativeSessionId,
|
|
2797
|
+
turn_id: "turn-mixed-leader-child",
|
|
2798
|
+
prompt: "$autopilot review this as delegated text",
|
|
2799
|
+
}, { cwd });
|
|
2800
|
+
assert.equal(reversedResult.omxEventName, "keyword-detector");
|
|
2801
|
+
assert.equal(reversedResult.skillState, null);
|
|
2802
|
+
assert.equal(reversedResult.outputJson, null);
|
|
2803
|
+
assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")), false);
|
|
2804
|
+
assert.equal(existsSync(join(stateDir, "sessions", childNativeSessionId, "autopilot-state.json")), false);
|
|
2805
|
+
}
|
|
2806
|
+
finally {
|
|
2807
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2808
|
+
}
|
|
2809
|
+
});
|
|
2810
|
+
it("records plugin-prefixed keyword activation from UserPromptSubmit payloads", async () => {
|
|
2811
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-plugin-prefixed-"));
|
|
2812
|
+
try {
|
|
2813
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
2814
|
+
const result = await dispatchCodexNativeHook({
|
|
2815
|
+
hook_event_name: "UserPromptSubmit",
|
|
2816
|
+
cwd,
|
|
2817
|
+
session_id: "sess-plugin-1",
|
|
2818
|
+
thread_id: "thread-plugin-1",
|
|
2819
|
+
turn_id: "turn-plugin-1",
|
|
2820
|
+
prompt: "$oh-my-codex:ralplan implement issue #1307",
|
|
2821
|
+
}, { cwd });
|
|
2822
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
2823
|
+
assert.equal(result.skillState?.skill, "ralplan");
|
|
2824
|
+
const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
|
|
2825
|
+
assert.match(message, /\$oh-my-codex:ralplan" -> ralplan/);
|
|
2826
|
+
assert.match(message, /use CLI-first state updates via `omx state write\/read\/clear --input '<json>' --json`/);
|
|
2827
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-plugin-1", "ralplan-state.json")), true);
|
|
2828
|
+
}
|
|
2829
|
+
finally {
|
|
2830
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2831
|
+
}
|
|
2832
|
+
});
|
|
2833
|
+
it("injects autopilot ralplan consensus gate guidance on prompt activation", async () => {
|
|
2834
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-gate-"));
|
|
2835
|
+
try {
|
|
2836
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
2837
|
+
const result = await dispatchCodexNativeHook({
|
|
2838
|
+
hook_event_name: "UserPromptSubmit",
|
|
2839
|
+
cwd,
|
|
2840
|
+
session_id: "sess-autopilot-ralplan-gate",
|
|
2841
|
+
thread_id: "thread-autopilot-ralplan-gate",
|
|
2842
|
+
turn_id: "turn-autopilot-ralplan-gate",
|
|
2843
|
+
prompt: "$autopilot implement issue #2430",
|
|
2844
|
+
}, { cwd });
|
|
2845
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
2846
|
+
assert.equal(result.skillState?.skill, "autopilot");
|
|
2847
|
+
const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
|
|
2848
|
+
assert.match(message, /Autopilot protocol:/);
|
|
2849
|
+
assert.match(message, /deep-interview -> ralplan -> ultragoal -> code-review -> ultraqa/);
|
|
2850
|
+
assert.match(message, /structured question chain, not a one-question gate/);
|
|
2851
|
+
assert.match(message, /re-score ambiguity against the active threshold/);
|
|
2852
|
+
assert.match(message, /max_rounds as a cap/);
|
|
2853
|
+
assert.match(message, /Do not advance from deep-interview to ralplan merely because the first question was answered/);
|
|
2251
2854
|
assert.match(message, /Planner output has been reviewed sequentially by Architect and then Critic/);
|
|
2252
2855
|
assert.match(message, /do not hand off to Ultragoal or implementation until .*ralplan_architect_review.*ralplan_critic_review/);
|
|
2253
2856
|
}
|
|
@@ -2705,6 +3308,11 @@ ${JSON.stringify({
|
|
|
2705
3308
|
assert.equal(result.skillState?.skill, "autopilot");
|
|
2706
3309
|
const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
|
|
2707
3310
|
assert.match(message, /"keep going" -> ralph/);
|
|
3311
|
+
assert.match(message, /Autopilot protocol:/);
|
|
3312
|
+
assert.match(message, /structured question chain, not a one-question gate/);
|
|
3313
|
+
assert.match(message, /re-score ambiguity against the active threshold/);
|
|
3314
|
+
assert.match(message, /max_rounds as a cap/);
|
|
3315
|
+
assert.match(message, /Do not advance from deep-interview to ralplan merely because the first question was answered/);
|
|
2708
3316
|
assert.doesNotMatch(message, /denied workflow keyword/i);
|
|
2709
3317
|
assert.doesNotMatch(message, /Unsupported workflow overlap: autopilot \+ ralph\./);
|
|
2710
3318
|
assert.doesNotMatch(message, /Prompt-side `\$ralph` activation/);
|
|
@@ -2714,6 +3322,107 @@ ${JSON.stringify({
|
|
|
2714
3322
|
await rm(cwd, { recursive: true, force: true });
|
|
2715
3323
|
}
|
|
2716
3324
|
});
|
|
3325
|
+
it("keeps omx question answers on the active autopilot skill so the interview chain guidance is injected", async () => {
|
|
3326
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-question-answer-continuation-"));
|
|
3327
|
+
try {
|
|
3328
|
+
const sessionId = "sess-autopilot-question-answer";
|
|
3329
|
+
const sessionDir = join(cwd, ".omx", "state", "sessions", sessionId);
|
|
3330
|
+
await mkdir(sessionDir, { recursive: true });
|
|
3331
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
3332
|
+
version: 1,
|
|
3333
|
+
active: true,
|
|
3334
|
+
skill: "autopilot",
|
|
3335
|
+
keyword: "$autopilot",
|
|
3336
|
+
phase: "deep-interview",
|
|
3337
|
+
initialized_mode: "autopilot",
|
|
3338
|
+
initialized_state_path: `.omx/state/sessions/${sessionId}/autopilot-state.json`,
|
|
3339
|
+
session_id: sessionId,
|
|
3340
|
+
active_skills: [
|
|
3341
|
+
{ skill: "autopilot", phase: "deep-interview", active: true, session_id: sessionId },
|
|
3342
|
+
],
|
|
3343
|
+
});
|
|
3344
|
+
await writeJson(join(sessionDir, "autopilot-state.json"), {
|
|
3345
|
+
active: true,
|
|
3346
|
+
mode: "autopilot",
|
|
3347
|
+
current_phase: "deep-interview",
|
|
3348
|
+
started_at: "2026-04-19T00:00:00.000Z",
|
|
3349
|
+
updated_at: "2026-04-19T00:10:00.000Z",
|
|
3350
|
+
session_id: sessionId,
|
|
3351
|
+
});
|
|
3352
|
+
const result = await dispatchCodexNativeHook({
|
|
3353
|
+
hook_event_name: "UserPromptSubmit",
|
|
3354
|
+
cwd,
|
|
3355
|
+
session_id: sessionId,
|
|
3356
|
+
thread_id: "thread-autopilot-question-answer",
|
|
3357
|
+
turn_id: "turn-autopilot-question-answer",
|
|
3358
|
+
prompt: "[omx question answered] semantic_marker_expansion $ralplan",
|
|
3359
|
+
}, { cwd });
|
|
3360
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
3361
|
+
assert.equal(result.skillState?.skill, "autopilot");
|
|
3362
|
+
const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
|
|
3363
|
+
assert.match(message, /continued active workflow skill "autopilot"/);
|
|
3364
|
+
assert.match(message, /Autopilot protocol:/);
|
|
3365
|
+
assert.match(message, /structured question chain, not a one-question gate/);
|
|
3366
|
+
assert.match(message, /This turn is a marked omx question answer/);
|
|
3367
|
+
assert.match(message, /then re-score/);
|
|
3368
|
+
assert.match(message, /write interview_complete evidence and hand off/);
|
|
3369
|
+
assert.match(message, /readiness gate remains unresolved and the answer would materially change execution/);
|
|
3370
|
+
assert.match(message, /Do not advance from deep-interview to ralplan merely because the first question was answered/);
|
|
3371
|
+
assert.doesNotMatch(message, /denied workflow keyword/i);
|
|
3372
|
+
assert.equal(existsSync(join(sessionDir, "ralplan-state.json")), false);
|
|
3373
|
+
}
|
|
3374
|
+
finally {
|
|
3375
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3376
|
+
}
|
|
3377
|
+
});
|
|
3378
|
+
it("keeps deep-interview bridge guidance on marked question answers with workflow-like tokens", async () => {
|
|
3379
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-question-answer-continuation-"));
|
|
3380
|
+
try {
|
|
3381
|
+
const sessionId = "sess-deep-interview-question-answer";
|
|
3382
|
+
const sessionDir = join(cwd, ".omx", "state", "sessions", sessionId);
|
|
3383
|
+
await mkdir(sessionDir, { recursive: true });
|
|
3384
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
3385
|
+
version: 1,
|
|
3386
|
+
active: true,
|
|
3387
|
+
skill: "deep-interview",
|
|
3388
|
+
keyword: "$deep-interview",
|
|
3389
|
+
phase: "planning",
|
|
3390
|
+
initialized_mode: "deep-interview",
|
|
3391
|
+
initialized_state_path: `.omx/state/sessions/${sessionId}/deep-interview-state.json`,
|
|
3392
|
+
session_id: sessionId,
|
|
3393
|
+
active_skills: [
|
|
3394
|
+
{ skill: "deep-interview", phase: "planning", active: true, session_id: sessionId },
|
|
3395
|
+
],
|
|
3396
|
+
});
|
|
3397
|
+
await writeJson(join(sessionDir, "deep-interview-state.json"), {
|
|
3398
|
+
active: true,
|
|
3399
|
+
mode: "deep-interview",
|
|
3400
|
+
current_phase: "intent-first",
|
|
3401
|
+
started_at: "2026-04-21T10:00:00.000Z",
|
|
3402
|
+
updated_at: "2026-04-21T10:00:00.000Z",
|
|
3403
|
+
});
|
|
3404
|
+
const result = await dispatchCodexNativeHook({
|
|
3405
|
+
hook_event_name: "UserPromptSubmit",
|
|
3406
|
+
cwd,
|
|
3407
|
+
session_id: sessionId,
|
|
3408
|
+
thread_id: "thread-deep-interview-question-answer",
|
|
3409
|
+
turn_id: "turn-deep-interview-question-answer",
|
|
3410
|
+
prompt: "[omx question answered] answer text $ralplan",
|
|
3411
|
+
}, { cwd });
|
|
3412
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
3413
|
+
assert.equal(result.skillState?.skill, "deep-interview");
|
|
3414
|
+
const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
|
|
3415
|
+
assert.match(message, /continued active workflow skill "deep-interview"/);
|
|
3416
|
+
assert.match(message, /workflow-like tokens inside the marked omx question answer are treated as answer text/);
|
|
3417
|
+
assert.match(message, /Deep-interview is active, but this session is not attached to tmux/);
|
|
3418
|
+
assert.match(message, /native structured question tool when available/);
|
|
3419
|
+
assert.doesNotMatch(message, /detected workflow keyword "\$ralplan" -> ralplan/);
|
|
3420
|
+
assert.equal(existsSync(join(sessionDir, "ralplan-state.json")), false);
|
|
3421
|
+
}
|
|
3422
|
+
finally {
|
|
3423
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3424
|
+
}
|
|
3425
|
+
});
|
|
2717
3426
|
it("clarifies outside-tmux prompt-side deep-interview activation without pretending omx question is directly available", async () => {
|
|
2718
3427
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-routing-"));
|
|
2719
3428
|
try {
|
|
@@ -3140,6 +3849,10 @@ export async function onHookEvent(event) {
|
|
|
3140
3849
|
active: true,
|
|
3141
3850
|
mode: "deep-interview",
|
|
3142
3851
|
current_phase: "intent-first",
|
|
3852
|
+
deep_interview_gate: {
|
|
3853
|
+
status: "complete",
|
|
3854
|
+
rationale: "Requirements are clarified and ready for ralplan consensus.",
|
|
3855
|
+
},
|
|
3143
3856
|
});
|
|
3144
3857
|
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
3145
3858
|
active: true,
|
|
@@ -5758,20 +6471,56 @@ exit 0
|
|
|
5758
6471
|
await rm(cwd, { recursive: true, force: true });
|
|
5759
6472
|
}
|
|
5760
6473
|
});
|
|
5761
|
-
it("
|
|
5762
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-
|
|
6474
|
+
it("does not block ordinary non-zero grep output in PostToolUse", async () => {
|
|
6475
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-grep-nonzero-"));
|
|
5763
6476
|
try {
|
|
5764
6477
|
const result = await dispatchCodexNativeHook({
|
|
5765
6478
|
hook_event_name: "PostToolUse",
|
|
5766
6479
|
cwd,
|
|
5767
6480
|
tool_name: "Bash",
|
|
5768
|
-
tool_use_id: "tool-
|
|
5769
|
-
tool_input: { command: "
|
|
5770
|
-
tool_response: "{\"exit_code\":
|
|
6481
|
+
tool_use_id: "tool-grep-nonzero",
|
|
6482
|
+
tool_input: { command: "grep -R missing-pattern src | head -20" },
|
|
6483
|
+
tool_response: "{\"exit_code\":1,\"stdout\":\"src/example.ts:TODO\",\"stderr\":\"\"}",
|
|
5771
6484
|
}, { cwd });
|
|
5772
6485
|
assert.equal(result.omxEventName, "post-tool-use");
|
|
5773
|
-
assert.
|
|
5774
|
-
|
|
6486
|
+
assert.equal(result.outputJson, null);
|
|
6487
|
+
}
|
|
6488
|
+
finally {
|
|
6489
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6490
|
+
}
|
|
6491
|
+
});
|
|
6492
|
+
it("does not block ordinary non-zero diagnostic output in PostToolUse", async () => {
|
|
6493
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-diagnostic-nonzero-"));
|
|
6494
|
+
try {
|
|
6495
|
+
const result = await dispatchCodexNativeHook({
|
|
6496
|
+
hook_event_name: "PostToolUse",
|
|
6497
|
+
cwd,
|
|
6498
|
+
tool_name: "Bash",
|
|
6499
|
+
tool_use_id: "tool-diagnostic-nonzero",
|
|
6500
|
+
tool_input: { command: "find src -name nope -print" },
|
|
6501
|
+
tool_response: "{\"exit_code\":1,\"stdout\":\"searched 10 files\",\"stderr\":\"\"}",
|
|
6502
|
+
}, { cwd });
|
|
6503
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
6504
|
+
assert.equal(result.outputJson, null);
|
|
6505
|
+
}
|
|
6506
|
+
finally {
|
|
6507
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6508
|
+
}
|
|
6509
|
+
});
|
|
6510
|
+
it("treats stderr-only informative non-zero output as reviewable instead of a generic failure", async () => {
|
|
6511
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-informative-stderr-"));
|
|
6512
|
+
try {
|
|
6513
|
+
const result = await dispatchCodexNativeHook({
|
|
6514
|
+
hook_event_name: "PostToolUse",
|
|
6515
|
+
cwd,
|
|
6516
|
+
tool_name: "Bash",
|
|
6517
|
+
tool_use_id: "tool-useful-stderr",
|
|
6518
|
+
tool_input: { command: "gh pr checks" },
|
|
6519
|
+
tool_response: "{\"exit_code\":8,\"stdout\":\"\",\"stderr\":\"build pending\\nlint pass\"}",
|
|
6520
|
+
}, { cwd });
|
|
6521
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
6522
|
+
assert.deepEqual(result.outputJson, {
|
|
6523
|
+
decision: "block",
|
|
5775
6524
|
reason: "The Bash command returned a non-zero exit code but produced useful output that should be reviewed before retrying.",
|
|
5776
6525
|
hookSpecificOutput: {
|
|
5777
6526
|
hookEventName: "PostToolUse",
|
|
@@ -5808,6 +6557,72 @@ exit 0
|
|
|
5808
6557
|
await rm(cwd, { recursive: true, force: true });
|
|
5809
6558
|
}
|
|
5810
6559
|
});
|
|
6560
|
+
it("treats wrapped gh pr checks output as reviewable", async () => {
|
|
6561
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-gh-wrapped-"));
|
|
6562
|
+
try {
|
|
6563
|
+
for (const command of [
|
|
6564
|
+
"GH_PAGER=cat gh pr checks",
|
|
6565
|
+
"env GH_TOKEN=ghp_testtoken gh pr checks",
|
|
6566
|
+
"/usr/bin/env gh pr checks",
|
|
6567
|
+
"env -- gh pr checks",
|
|
6568
|
+
"env -C repo gh pr checks",
|
|
6569
|
+
"/usr/bin/gh pr checks",
|
|
6570
|
+
"gh --repo owner/repo pr checks",
|
|
6571
|
+
"echo a; gh pr checks",
|
|
6572
|
+
"cd repo && gh pr checks",
|
|
6573
|
+
]) {
|
|
6574
|
+
const result = await dispatchCodexNativeHook({
|
|
6575
|
+
hook_event_name: "PostToolUse",
|
|
6576
|
+
cwd,
|
|
6577
|
+
tool_name: "Bash",
|
|
6578
|
+
tool_use_id: `tool-useful-${command}`,
|
|
6579
|
+
tool_input: { command },
|
|
6580
|
+
tool_response: "{\"exit_code\":8,\"stdout\":\"build pending\",\"stderr\":\"\"}",
|
|
6581
|
+
}, { cwd });
|
|
6582
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
6583
|
+
assert.equal(result.outputJson?.decision, "block", command);
|
|
6584
|
+
}
|
|
6585
|
+
}
|
|
6586
|
+
finally {
|
|
6587
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6588
|
+
}
|
|
6589
|
+
});
|
|
6590
|
+
it("does not treat heredoc gh pr checks text as a reviewable command", async () => {
|
|
6591
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-gh-heredoc-"));
|
|
6592
|
+
try {
|
|
6593
|
+
const result = await dispatchCodexNativeHook({
|
|
6594
|
+
hook_event_name: "PostToolUse",
|
|
6595
|
+
cwd,
|
|
6596
|
+
tool_name: "Bash",
|
|
6597
|
+
tool_use_id: "tool-heredoc-gh-checks",
|
|
6598
|
+
tool_input: { command: "cat <<'EOF'\ngh pr checks\nEOF\nfalse" },
|
|
6599
|
+
tool_response: "{\"exit_code\":1,\"stdout\":\"gh pr checks\",\"stderr\":\"\"}",
|
|
6600
|
+
}, { cwd });
|
|
6601
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
6602
|
+
assert.equal(result.outputJson, null);
|
|
6603
|
+
}
|
|
6604
|
+
finally {
|
|
6605
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6606
|
+
}
|
|
6607
|
+
});
|
|
6608
|
+
it("does not treat echoed gh pr checks text as a reviewable command", async () => {
|
|
6609
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-gh-echo-"));
|
|
6610
|
+
try {
|
|
6611
|
+
const result = await dispatchCodexNativeHook({
|
|
6612
|
+
hook_event_name: "PostToolUse",
|
|
6613
|
+
cwd,
|
|
6614
|
+
tool_name: "Bash",
|
|
6615
|
+
tool_use_id: "tool-echo-gh-checks",
|
|
6616
|
+
tool_input: { command: "echo gh pr checks" },
|
|
6617
|
+
tool_response: "{\"exit_code\":1,\"stdout\":\"gh pr checks\",\"stderr\":\"\"}",
|
|
6618
|
+
}, { cwd });
|
|
6619
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
6620
|
+
assert.equal(result.outputJson, null);
|
|
6621
|
+
}
|
|
6622
|
+
finally {
|
|
6623
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6624
|
+
}
|
|
6625
|
+
});
|
|
5811
6626
|
it("returns MCP transport-death guidance and preserves failed team state", async () => {
|
|
5812
6627
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-dead-"));
|
|
5813
6628
|
try {
|
|
@@ -6649,6 +7464,307 @@ exit 0
|
|
|
6649
7464
|
await rm(cwd, { recursive: true, force: true });
|
|
6650
7465
|
}
|
|
6651
7466
|
});
|
|
7467
|
+
it("dedupes allowed worker Stop leader nudges across workers in the same team window", async () => {
|
|
7468
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-team-dedupe-"));
|
|
7469
|
+
const prevPath = process.env.PATH;
|
|
7470
|
+
try {
|
|
7471
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7472
|
+
const logsDir = join(cwd, ".omx", "logs");
|
|
7473
|
+
const teamName = "worker-stop-team-dedupe";
|
|
7474
|
+
const teamDir = join(stateDir, "team", teamName);
|
|
7475
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
7476
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
7477
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
7478
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
|
|
7479
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
7480
|
+
await writeJson(join(teamDir, "manifest.v2.json"), {
|
|
7481
|
+
name: teamName,
|
|
7482
|
+
tmux_session: "omx-team-worker-stop",
|
|
7483
|
+
leader_pane_id: "%42",
|
|
7484
|
+
workers: [
|
|
7485
|
+
{ name: "worker-1", index: 1, pane_id: "%10" },
|
|
7486
|
+
{ name: "worker-2", index: 2, pane_id: "%11" },
|
|
7487
|
+
],
|
|
7488
|
+
});
|
|
7489
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
7490
|
+
const first = await maybeNudgeLeaderForAllowedWorkerStop({
|
|
7491
|
+
stateDir,
|
|
7492
|
+
logsDir,
|
|
7493
|
+
workerContext: { teamName, workerName: "worker-1" },
|
|
7494
|
+
});
|
|
7495
|
+
const second = await maybeNudgeLeaderForAllowedWorkerStop({
|
|
7496
|
+
stateDir,
|
|
7497
|
+
logsDir,
|
|
7498
|
+
workerContext: { teamName, workerName: "worker-2" },
|
|
7499
|
+
});
|
|
7500
|
+
assert.equal(first.result, "sent");
|
|
7501
|
+
assert.equal(second.result, "suppressed_team_cooldown");
|
|
7502
|
+
const tmuxLog = await readFile(tmuxLogPath, "utf-8");
|
|
7503
|
+
const stopNudges = tmuxLog.match(/send-keys -t %42 -l \[OMX\] worker-\d+ native Stop allowed/g) || [];
|
|
7504
|
+
assert.equal(stopNudges.length, 1, "same-team workers should share one leader nudge cooldown window");
|
|
7505
|
+
const teamNudgeState = JSON.parse(await readFile(join(teamDir, "worker-stop-nudge.json"), "utf-8"));
|
|
7506
|
+
assert.equal(teamNudgeState.worker, "worker-1");
|
|
7507
|
+
assert.equal(teamNudgeState.delivery, "sent");
|
|
7508
|
+
}
|
|
7509
|
+
finally {
|
|
7510
|
+
if (typeof prevPath === "string")
|
|
7511
|
+
process.env.PATH = prevPath;
|
|
7512
|
+
else
|
|
7513
|
+
delete process.env.PATH;
|
|
7514
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7515
|
+
}
|
|
7516
|
+
});
|
|
7517
|
+
it("serializes concurrent allowed worker Stop leader nudges with a team lock", async () => {
|
|
7518
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-concurrent-dedupe-"));
|
|
7519
|
+
const prevPath = process.env.PATH;
|
|
7520
|
+
try {
|
|
7521
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7522
|
+
const logsDir = join(cwd, ".omx", "logs");
|
|
7523
|
+
const teamName = "worker-stop-concurrent";
|
|
7524
|
+
const teamDir = join(stateDir, "team", teamName);
|
|
7525
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
7526
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
7527
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
7528
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath, { sendDelayMs: 100 }));
|
|
7529
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
7530
|
+
await writeJson(join(teamDir, "manifest.v2.json"), {
|
|
7531
|
+
name: teamName,
|
|
7532
|
+
tmux_session: "omx-team-worker-stop",
|
|
7533
|
+
leader_pane_id: "%42",
|
|
7534
|
+
workers: [
|
|
7535
|
+
{ name: "worker-1", index: 1, pane_id: "%10" },
|
|
7536
|
+
{ name: "worker-2", index: 2, pane_id: "%11" },
|
|
7537
|
+
],
|
|
7538
|
+
});
|
|
7539
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
7540
|
+
const results = await Promise.all([
|
|
7541
|
+
maybeNudgeLeaderForAllowedWorkerStop({
|
|
7542
|
+
stateDir,
|
|
7543
|
+
logsDir,
|
|
7544
|
+
workerContext: { teamName, workerName: "worker-1" },
|
|
7545
|
+
}),
|
|
7546
|
+
maybeNudgeLeaderForAllowedWorkerStop({
|
|
7547
|
+
stateDir,
|
|
7548
|
+
logsDir,
|
|
7549
|
+
workerContext: { teamName, workerName: "worker-2" },
|
|
7550
|
+
}),
|
|
7551
|
+
]);
|
|
7552
|
+
assert.equal(results.filter((result) => result.result === "sent").length, 1);
|
|
7553
|
+
assert.equal(results.filter((result) => result.result === "suppressed_team_lock_held").length, 1);
|
|
7554
|
+
const tmuxLog = await readFile(tmuxLogPath, "utf-8");
|
|
7555
|
+
const stopNudges = tmuxLog.match(/send-keys -t %42 -l \[OMX\] worker-\d+ native Stop allowed/g) || [];
|
|
7556
|
+
assert.equal(stopNudges.length, 1, "concurrent same-team workers should emit only one leader nudge");
|
|
7557
|
+
assert.equal(existsSync(join(teamDir, "worker-stop-nudge.lock")), false);
|
|
7558
|
+
}
|
|
7559
|
+
finally {
|
|
7560
|
+
if (typeof prevPath === "string")
|
|
7561
|
+
process.env.PATH = prevPath;
|
|
7562
|
+
else
|
|
7563
|
+
delete process.env.PATH;
|
|
7564
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7565
|
+
}
|
|
7566
|
+
});
|
|
7567
|
+
it("skips worker Stop leader nudge when team state is missing or shut down", async () => {
|
|
7568
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-missing-team-"));
|
|
7569
|
+
try {
|
|
7570
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7571
|
+
const logsDir = join(cwd, ".omx", "logs");
|
|
7572
|
+
const result = await maybeNudgeLeaderForAllowedWorkerStop({
|
|
7573
|
+
stateDir,
|
|
7574
|
+
logsDir,
|
|
7575
|
+
workerContext: { teamName: "removed-team", workerName: "worker-1" },
|
|
7576
|
+
});
|
|
7577
|
+
assert.equal(result.result, "team_state_gone_or_shutdown");
|
|
7578
|
+
assert.equal(existsSync(join(stateDir, "team", "removed-team", "worker-stop-nudge.json")), false);
|
|
7579
|
+
await writeJson(join(stateDir, "team", "shutdown-team", "shutdown.json"), {
|
|
7580
|
+
started_at: new Date().toISOString(),
|
|
7581
|
+
});
|
|
7582
|
+
const shutdownResult = await maybeNudgeLeaderForAllowedWorkerStop({
|
|
7583
|
+
stateDir,
|
|
7584
|
+
logsDir,
|
|
7585
|
+
workerContext: { teamName: "shutdown-team", workerName: "worker-1" },
|
|
7586
|
+
});
|
|
7587
|
+
assert.equal(shutdownResult.result, "team_state_gone_or_shutdown");
|
|
7588
|
+
assert.equal(existsSync(join(stateDir, "team", "shutdown-team", "worker-stop-nudge.json")), false);
|
|
7589
|
+
}
|
|
7590
|
+
finally {
|
|
7591
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7592
|
+
}
|
|
7593
|
+
});
|
|
7594
|
+
it("does not treat old visible worker Stop transcript as pending queue state", async () => {
|
|
7595
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-queue-dedupe-"));
|
|
7596
|
+
const prevPath = process.env.PATH;
|
|
7597
|
+
try {
|
|
7598
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7599
|
+
const logsDir = join(cwd, ".omx", "logs");
|
|
7600
|
+
const teamName = "queued-stop-dedupe";
|
|
7601
|
+
const teamDir = join(stateDir, "team", teamName);
|
|
7602
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
7603
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
7604
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
7605
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath, {
|
|
7606
|
+
busyLeader: true,
|
|
7607
|
+
captureText: `[OMX] worker-1 native Stop allowed. Run \`omx team status ${teamName}\`, read worker messages/results, then assign next task, reconcile completion, or shut down. [OMX_TMUX_INJECT]\n`
|
|
7608
|
+
+ "• Working… (esc to interrupt)",
|
|
7609
|
+
}));
|
|
7610
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
7611
|
+
await writeJson(join(teamDir, "manifest.v2.json"), {
|
|
7612
|
+
name: teamName,
|
|
7613
|
+
tmux_session: "omx-team-worker-stop",
|
|
7614
|
+
leader_pane_id: "%42",
|
|
7615
|
+
workers: [{ name: "worker-2", index: 2, pane_id: "%11" }],
|
|
7616
|
+
});
|
|
7617
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
7618
|
+
const result = await maybeNudgeLeaderForAllowedWorkerStop({
|
|
7619
|
+
stateDir,
|
|
7620
|
+
logsDir,
|
|
7621
|
+
workerContext: { teamName, workerName: "worker-2" },
|
|
7622
|
+
});
|
|
7623
|
+
assert.equal(result.result, "queued");
|
|
7624
|
+
const tmuxLog = await readFile(tmuxLogPath, "utf-8");
|
|
7625
|
+
assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-2 native Stop allowed/);
|
|
7626
|
+
assert.match(tmuxLog, /send-keys -t %42 Tab/);
|
|
7627
|
+
const teamNudgeState = JSON.parse(await readFile(join(teamDir, "worker-stop-nudge.json"), "utf-8"));
|
|
7628
|
+
assert.equal(teamNudgeState.worker, "worker-2");
|
|
7629
|
+
assert.equal(teamNudgeState.delivery, "queued");
|
|
7630
|
+
}
|
|
7631
|
+
finally {
|
|
7632
|
+
if (typeof prevPath === "string")
|
|
7633
|
+
process.env.PATH = prevPath;
|
|
7634
|
+
else
|
|
7635
|
+
delete process.env.PATH;
|
|
7636
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7637
|
+
}
|
|
7638
|
+
});
|
|
7639
|
+
it("reports deferred when non-teardown persistence failure prevents worker Stop nudge cooldown state", async () => {
|
|
7640
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-persist-fail-"));
|
|
7641
|
+
const prevPath = process.env.PATH;
|
|
7642
|
+
try {
|
|
7643
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7644
|
+
const logsDir = join(cwd, ".omx", "logs");
|
|
7645
|
+
const teamName = "worker-stop-persist-fail";
|
|
7646
|
+
const teamDir = join(stateDir, "team", teamName);
|
|
7647
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
7648
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
7649
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
7650
|
+
await writeJson(join(teamDir, "manifest.v2.json"), {
|
|
7651
|
+
name: teamName,
|
|
7652
|
+
tmux_session: "omx-team-worker-stop",
|
|
7653
|
+
leader_pane_id: "%42",
|
|
7654
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
7655
|
+
});
|
|
7656
|
+
await writeFile(join(teamDir, "workers"), "not a directory");
|
|
7657
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
|
|
7658
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
7659
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
7660
|
+
const result = await maybeNudgeLeaderForAllowedWorkerStop({
|
|
7661
|
+
stateDir,
|
|
7662
|
+
logsDir,
|
|
7663
|
+
workerContext: { teamName, workerName: "worker-1" },
|
|
7664
|
+
});
|
|
7665
|
+
assert.equal(result.result, "deferred");
|
|
7666
|
+
assert.equal(existsSync(join(teamDir, "worker-stop-nudge.json")), false);
|
|
7667
|
+
assert.equal(existsSync(join(teamDir, "workers", "worker-1", "worker-stop-nudge.json")), false);
|
|
7668
|
+
const tmuxLog = await readFile(tmuxLogPath, "utf-8");
|
|
7669
|
+
assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
|
|
7670
|
+
const deliveryLogPath = join(logsDir, `team-delivery-${new Date().toISOString().split("T")[0]}.jsonl`);
|
|
7671
|
+
const deliveryEvents = (await readFile(deliveryLogPath, "utf-8"))
|
|
7672
|
+
.trim()
|
|
7673
|
+
.split("\n")
|
|
7674
|
+
.map((line) => JSON.parse(line));
|
|
7675
|
+
const deferredEvent = deliveryEvents.find((event) => event.event === "nudge_triggered" && event.result === "deferred");
|
|
7676
|
+
assert.equal(deferredEvent?.team, teamName);
|
|
7677
|
+
assert.equal(deferredEvent?.from_worker, "worker-1");
|
|
7678
|
+
assert.match(String(deferredEvent?.reason || ""), /EEXIST|ENOTDIR|not a directory|file already exists/);
|
|
7679
|
+
}
|
|
7680
|
+
finally {
|
|
7681
|
+
if (typeof prevPath === "string")
|
|
7682
|
+
process.env.PATH = prevPath;
|
|
7683
|
+
else
|
|
7684
|
+
delete process.env.PATH;
|
|
7685
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7686
|
+
}
|
|
7687
|
+
});
|
|
7688
|
+
it("does not recreate team state when teardown removes it during worker Stop delivery", async () => {
|
|
7689
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-teardown-race-"));
|
|
7690
|
+
const prevPath = process.env.PATH;
|
|
7691
|
+
try {
|
|
7692
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7693
|
+
const logsDir = join(cwd, ".omx", "logs");
|
|
7694
|
+
const teamName = "worker-stop-teardown-race";
|
|
7695
|
+
const teamDir = join(stateDir, "team", teamName);
|
|
7696
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
7697
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
7698
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
7699
|
+
await writeJson(join(teamDir, "manifest.v2.json"), {
|
|
7700
|
+
name: teamName,
|
|
7701
|
+
tmux_session: "omx-team-worker-stop",
|
|
7702
|
+
leader_pane_id: "%42",
|
|
7703
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
7704
|
+
});
|
|
7705
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath, { removePathOnSend: teamDir }));
|
|
7706
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
7707
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
7708
|
+
const result = await maybeNudgeLeaderForAllowedWorkerStop({
|
|
7709
|
+
stateDir,
|
|
7710
|
+
logsDir,
|
|
7711
|
+
workerContext: { teamName, workerName: "worker-1" },
|
|
7712
|
+
});
|
|
7713
|
+
assert.equal(result.result, "sent");
|
|
7714
|
+
assert.equal(existsSync(teamDir), false, "worker Stop delivery must not recreate removed team state");
|
|
7715
|
+
const tmuxLog = await readFile(tmuxLogPath, "utf-8");
|
|
7716
|
+
assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
|
|
7717
|
+
}
|
|
7718
|
+
finally {
|
|
7719
|
+
if (typeof prevPath === "string")
|
|
7720
|
+
process.env.PATH = prevPath;
|
|
7721
|
+
else
|
|
7722
|
+
delete process.env.PATH;
|
|
7723
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7724
|
+
}
|
|
7725
|
+
});
|
|
7726
|
+
it("does not recreate team state when teardown removes it before deferred worker Stop recording", async () => {
|
|
7727
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-deferred-teardown-"));
|
|
7728
|
+
const prevPath = process.env.PATH;
|
|
7729
|
+
try {
|
|
7730
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7731
|
+
const logsDir = join(cwd, ".omx", "logs");
|
|
7732
|
+
const teamName = "worker-stop-deferred-teardown";
|
|
7733
|
+
const teamDir = join(stateDir, "team", teamName);
|
|
7734
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
7735
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
7736
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
7737
|
+
await writeJson(join(teamDir, "manifest.v2.json"), {
|
|
7738
|
+
name: teamName,
|
|
7739
|
+
tmux_session: "omx-team-worker-stop",
|
|
7740
|
+
leader_pane_id: "%42",
|
|
7741
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
7742
|
+
});
|
|
7743
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath, {
|
|
7744
|
+
currentCommand: "bash",
|
|
7745
|
+
captureText: "$ ",
|
|
7746
|
+
removePathOnCapture: teamDir,
|
|
7747
|
+
}));
|
|
7748
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
7749
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
7750
|
+
const result = await maybeNudgeLeaderForAllowedWorkerStop({
|
|
7751
|
+
stateDir,
|
|
7752
|
+
logsDir,
|
|
7753
|
+
workerContext: { teamName, workerName: "worker-1" },
|
|
7754
|
+
});
|
|
7755
|
+
assert.equal(result.result, "team_state_gone_or_shutdown");
|
|
7756
|
+
assert.equal(existsSync(teamDir), false, "deferred worker Stop recording must not recreate removed team state");
|
|
7757
|
+
const tmuxLog = await readFile(tmuxLogPath, "utf-8");
|
|
7758
|
+
assert.doesNotMatch(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
|
|
7759
|
+
}
|
|
7760
|
+
finally {
|
|
7761
|
+
if (typeof prevPath === "string")
|
|
7762
|
+
process.env.PATH = prevPath;
|
|
7763
|
+
else
|
|
7764
|
+
delete process.env.PATH;
|
|
7765
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7766
|
+
}
|
|
7767
|
+
});
|
|
6652
7768
|
it("allows worker Stop when the Stop nudge helper cannot deliver", async () => {
|
|
6653
7769
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-helper-fail-"));
|
|
6654
7770
|
const prevTeamWorker = process.env.OMX_TEAM_WORKER;
|
|
@@ -10337,6 +11453,315 @@ exit 0
|
|
|
10337
11453
|
await rm(cwd, { recursive: true, force: true });
|
|
10338
11454
|
}
|
|
10339
11455
|
});
|
|
11456
|
+
it("blocks implementation writes while ralplan is active without execution handoff", async () => {
|
|
11457
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-block-"));
|
|
11458
|
+
try {
|
|
11459
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
11460
|
+
const sessionId = "sess-ralplan-pretool-block";
|
|
11461
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
11462
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
11463
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
11464
|
+
active: true,
|
|
11465
|
+
skill: "ralplan",
|
|
11466
|
+
phase: "planning",
|
|
11467
|
+
session_id: sessionId,
|
|
11468
|
+
active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
|
|
11469
|
+
});
|
|
11470
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
11471
|
+
active: true,
|
|
11472
|
+
mode: "ralplan",
|
|
11473
|
+
current_phase: "critic-review",
|
|
11474
|
+
session_id: sessionId,
|
|
11475
|
+
});
|
|
11476
|
+
const result = await dispatchCodexNativeHook({
|
|
11477
|
+
hook_event_name: "PreToolUse",
|
|
11478
|
+
cwd,
|
|
11479
|
+
session_id: sessionId,
|
|
11480
|
+
thread_id: "thread-ralplan-pretool-block",
|
|
11481
|
+
tool_name: "Edit",
|
|
11482
|
+
tool_input: { file_path: "src/runtime.ts" },
|
|
11483
|
+
}, { cwd });
|
|
11484
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
11485
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
11486
|
+
assert.match(String(result.outputJson?.reason ?? ""), /Ralplan is active .*implementation\/write tools are blocked/i);
|
|
11487
|
+
assert.match(String(result.outputJson?.hookSpecificOutput?.additionalContext ?? ""), /\$ultragoal.*\$team.*\$ralph/i);
|
|
11488
|
+
}
|
|
11489
|
+
finally {
|
|
11490
|
+
await rm(cwd, { recursive: true, force: true });
|
|
11491
|
+
}
|
|
11492
|
+
});
|
|
11493
|
+
it("blocks implementation writes while Autopilot is supervising ralplan without handoff", async () => {
|
|
11494
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-pretool-block-"));
|
|
11495
|
+
try {
|
|
11496
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
11497
|
+
const sessionId = "sess-autopilot-ralplan-pretool-block";
|
|
11498
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
11499
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
11500
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
11501
|
+
active: true,
|
|
11502
|
+
skill: "autopilot",
|
|
11503
|
+
phase: "ralplan",
|
|
11504
|
+
session_id: sessionId,
|
|
11505
|
+
active_skills: [{ skill: "autopilot", phase: "ralplan", active: true, session_id: sessionId }],
|
|
11506
|
+
});
|
|
11507
|
+
await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
|
|
11508
|
+
active: true,
|
|
11509
|
+
mode: "autopilot",
|
|
11510
|
+
current_phase: "ralplan",
|
|
11511
|
+
session_id: sessionId,
|
|
11512
|
+
state: {
|
|
11513
|
+
handoff_artifacts: {
|
|
11514
|
+
ralplan_consensus_gate: { required: true, complete: false },
|
|
11515
|
+
},
|
|
11516
|
+
},
|
|
11517
|
+
});
|
|
11518
|
+
const result = await dispatchCodexNativeHook({
|
|
11519
|
+
hook_event_name: "PreToolUse",
|
|
11520
|
+
cwd,
|
|
11521
|
+
session_id: sessionId,
|
|
11522
|
+
thread_id: "thread-autopilot-ralplan-pretool-block",
|
|
11523
|
+
tool_name: "Edit",
|
|
11524
|
+
tool_input: { file_path: "src/runtime.ts" },
|
|
11525
|
+
}, { cwd });
|
|
11526
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
11527
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
11528
|
+
assert.match(String(result.outputJson?.reason ?? ""), /Ralplan is active .*implementation\/write tools are blocked/i);
|
|
11529
|
+
}
|
|
11530
|
+
finally {
|
|
11531
|
+
await rm(cwd, { recursive: true, force: true });
|
|
11532
|
+
}
|
|
11533
|
+
});
|
|
11534
|
+
it("allows implementation writes when terminal Autopilot run-state shadows stale supervised ralplan state", async () => {
|
|
11535
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-terminal-pretool-"));
|
|
11536
|
+
try {
|
|
11537
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
11538
|
+
const sessionId = "sess-autopilot-ralplan-terminal-pretool";
|
|
11539
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
11540
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
11541
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
11542
|
+
active: true,
|
|
11543
|
+
skill: "autopilot",
|
|
11544
|
+
phase: "ralplan",
|
|
11545
|
+
session_id: sessionId,
|
|
11546
|
+
active_skills: [{ skill: "autopilot", phase: "ralplan", active: true, session_id: sessionId }],
|
|
11547
|
+
});
|
|
11548
|
+
await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
|
|
11549
|
+
active: true,
|
|
11550
|
+
mode: "autopilot",
|
|
11551
|
+
current_phase: "ralplan",
|
|
11552
|
+
session_id: sessionId,
|
|
11553
|
+
});
|
|
11554
|
+
await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
|
|
11555
|
+
version: 1,
|
|
11556
|
+
active: false,
|
|
11557
|
+
mode: "autopilot",
|
|
11558
|
+
outcome: "finish",
|
|
11559
|
+
lifecycle_outcome: "finished",
|
|
11560
|
+
current_phase: "complete",
|
|
11561
|
+
completed_at: "2026-05-30T00:00:00.000Z",
|
|
11562
|
+
updated_at: "2026-05-30T00:00:00.000Z",
|
|
11563
|
+
});
|
|
11564
|
+
const result = await dispatchCodexNativeHook({
|
|
11565
|
+
hook_event_name: "PreToolUse",
|
|
11566
|
+
cwd,
|
|
11567
|
+
session_id: sessionId,
|
|
11568
|
+
thread_id: "thread-autopilot-ralplan-terminal-pretool",
|
|
11569
|
+
tool_name: "Edit",
|
|
11570
|
+
tool_input: { file_path: "src/runtime.ts" },
|
|
11571
|
+
}, { cwd });
|
|
11572
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
11573
|
+
assert.equal(result.outputJson, null);
|
|
11574
|
+
}
|
|
11575
|
+
finally {
|
|
11576
|
+
await rm(cwd, { recursive: true, force: true });
|
|
11577
|
+
}
|
|
11578
|
+
});
|
|
11579
|
+
it("blocks bash implementation writes while Autopilot is supervising ralplan without handoff", async () => {
|
|
11580
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-pretool-bash-block-"));
|
|
11581
|
+
try {
|
|
11582
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
11583
|
+
const sessionId = "sess-autopilot-ralplan-pretool-bash-block";
|
|
11584
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
11585
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
11586
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
11587
|
+
active: true,
|
|
11588
|
+
skill: "autopilot",
|
|
11589
|
+
phase: "ralplan",
|
|
11590
|
+
session_id: sessionId,
|
|
11591
|
+
active_skills: [{ skill: "autopilot", phase: "ralplan", active: true, session_id: sessionId }],
|
|
11592
|
+
});
|
|
11593
|
+
await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
|
|
11594
|
+
active: true,
|
|
11595
|
+
mode: "autopilot",
|
|
11596
|
+
current_phase: "ralplan",
|
|
11597
|
+
session_id: sessionId,
|
|
11598
|
+
});
|
|
11599
|
+
const result = await dispatchCodexNativeHook({
|
|
11600
|
+
hook_event_name: "PreToolUse",
|
|
11601
|
+
cwd,
|
|
11602
|
+
session_id: sessionId,
|
|
11603
|
+
thread_id: "thread-autopilot-ralplan-pretool-bash-block",
|
|
11604
|
+
tool_name: "Bash",
|
|
11605
|
+
tool_input: { command: "cat <<'EOF' > src/runtime.ts\nimplementation\nEOF" },
|
|
11606
|
+
}, { cwd });
|
|
11607
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
11608
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
11609
|
+
assert.match(String(result.outputJson?.reason ?? ""), /Ralplan is active .*implementation\/write tools are blocked/i);
|
|
11610
|
+
}
|
|
11611
|
+
finally {
|
|
11612
|
+
await rm(cwd, { recursive: true, force: true });
|
|
11613
|
+
}
|
|
11614
|
+
});
|
|
11615
|
+
it("allows ralplan planning artifact writes without execution handoff", async () => {
|
|
11616
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-artifact-"));
|
|
11617
|
+
try {
|
|
11618
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
11619
|
+
const sessionId = "sess-ralplan-pretool-artifact";
|
|
11620
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
11621
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
11622
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
11623
|
+
active: true,
|
|
11624
|
+
skill: "ralplan",
|
|
11625
|
+
phase: "planning",
|
|
11626
|
+
session_id: sessionId,
|
|
11627
|
+
active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
|
|
11628
|
+
});
|
|
11629
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
11630
|
+
active: true,
|
|
11631
|
+
mode: "ralplan",
|
|
11632
|
+
current_phase: "planning",
|
|
11633
|
+
session_id: sessionId,
|
|
11634
|
+
});
|
|
11635
|
+
const result = await dispatchCodexNativeHook({
|
|
11636
|
+
hook_event_name: "PreToolUse",
|
|
11637
|
+
cwd,
|
|
11638
|
+
session_id: sessionId,
|
|
11639
|
+
thread_id: "thread-ralplan-pretool-artifact",
|
|
11640
|
+
tool_name: "Write",
|
|
11641
|
+
tool_input: { file_path: ".omx/plans/prd-issue-2603.md" },
|
|
11642
|
+
}, { cwd });
|
|
11643
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
11644
|
+
assert.equal(result.outputJson, null);
|
|
11645
|
+
}
|
|
11646
|
+
finally {
|
|
11647
|
+
await rm(cwd, { recursive: true, force: true });
|
|
11648
|
+
}
|
|
11649
|
+
});
|
|
11650
|
+
it("blocks bash implementation writes while ralplan is active without execution handoff", async () => {
|
|
11651
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-bash-block-"));
|
|
11652
|
+
try {
|
|
11653
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
11654
|
+
const sessionId = "sess-ralplan-pretool-bash-block";
|
|
11655
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
11656
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
11657
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
11658
|
+
active: true,
|
|
11659
|
+
skill: "ralplan",
|
|
11660
|
+
phase: "planning",
|
|
11661
|
+
session_id: sessionId,
|
|
11662
|
+
active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
|
|
11663
|
+
});
|
|
11664
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
11665
|
+
active: true,
|
|
11666
|
+
mode: "ralplan",
|
|
11667
|
+
current_phase: "planning",
|
|
11668
|
+
session_id: sessionId,
|
|
11669
|
+
});
|
|
11670
|
+
const result = await dispatchCodexNativeHook({
|
|
11671
|
+
hook_event_name: "PreToolUse",
|
|
11672
|
+
cwd,
|
|
11673
|
+
session_id: sessionId,
|
|
11674
|
+
thread_id: "thread-ralplan-pretool-bash-block",
|
|
11675
|
+
tool_name: "Bash",
|
|
11676
|
+
tool_input: { command: "cat <<'EOF' > src/runtime.ts\nimplementation\nEOF" },
|
|
11677
|
+
}, { cwd });
|
|
11678
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
11679
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
11680
|
+
assert.match(String(result.outputJson?.reason ?? ""), /Ralplan is active .*implementation\/write tools are blocked/i);
|
|
11681
|
+
}
|
|
11682
|
+
finally {
|
|
11683
|
+
await rm(cwd, { recursive: true, force: true });
|
|
11684
|
+
}
|
|
11685
|
+
});
|
|
11686
|
+
it("allows bash planning artifact writes while ralplan is active without execution handoff", async () => {
|
|
11687
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-bash-artifact-"));
|
|
11688
|
+
try {
|
|
11689
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
11690
|
+
const sessionId = "sess-ralplan-pretool-bash-artifact";
|
|
11691
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
11692
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
11693
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
11694
|
+
active: true,
|
|
11695
|
+
skill: "ralplan",
|
|
11696
|
+
phase: "planning",
|
|
11697
|
+
session_id: sessionId,
|
|
11698
|
+
active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
|
|
11699
|
+
});
|
|
11700
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
11701
|
+
active: true,
|
|
11702
|
+
mode: "ralplan",
|
|
11703
|
+
current_phase: "planning",
|
|
11704
|
+
session_id: sessionId,
|
|
11705
|
+
});
|
|
11706
|
+
const result = await dispatchCodexNativeHook({
|
|
11707
|
+
hook_event_name: "PreToolUse",
|
|
11708
|
+
cwd,
|
|
11709
|
+
session_id: sessionId,
|
|
11710
|
+
thread_id: "thread-ralplan-pretool-bash-artifact",
|
|
11711
|
+
tool_name: "Bash",
|
|
11712
|
+
tool_input: { command: "cat <<'EOF' > .omx/plans/prd-issue-2603.md\nplanning\nEOF" },
|
|
11713
|
+
}, { cwd });
|
|
11714
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
11715
|
+
assert.equal(result.outputJson, null);
|
|
11716
|
+
}
|
|
11717
|
+
finally {
|
|
11718
|
+
await rm(cwd, { recursive: true, force: true });
|
|
11719
|
+
}
|
|
11720
|
+
});
|
|
11721
|
+
it("allows implementation writes when an explicit execution handoff is active", async () => {
|
|
11722
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-handoff-"));
|
|
11723
|
+
try {
|
|
11724
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
11725
|
+
const sessionId = "sess-ralplan-pretool-handoff";
|
|
11726
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
11727
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
11728
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
11729
|
+
active: true,
|
|
11730
|
+
skill: "ultragoal",
|
|
11731
|
+
phase: "planning",
|
|
11732
|
+
session_id: sessionId,
|
|
11733
|
+
active_skills: [
|
|
11734
|
+
{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId },
|
|
11735
|
+
{ skill: "ultragoal", phase: "planning", active: true, session_id: sessionId },
|
|
11736
|
+
],
|
|
11737
|
+
});
|
|
11738
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
11739
|
+
active: true,
|
|
11740
|
+
mode: "ralplan",
|
|
11741
|
+
current_phase: "complete",
|
|
11742
|
+
session_id: sessionId,
|
|
11743
|
+
});
|
|
11744
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
|
|
11745
|
+
active: true,
|
|
11746
|
+
mode: "ultragoal",
|
|
11747
|
+
current_phase: "planning",
|
|
11748
|
+
session_id: sessionId,
|
|
11749
|
+
});
|
|
11750
|
+
const result = await dispatchCodexNativeHook({
|
|
11751
|
+
hook_event_name: "PreToolUse",
|
|
11752
|
+
cwd,
|
|
11753
|
+
session_id: sessionId,
|
|
11754
|
+
thread_id: "thread-ralplan-pretool-handoff",
|
|
11755
|
+
tool_name: "Edit",
|
|
11756
|
+
tool_input: { file_path: "src/runtime.ts" },
|
|
11757
|
+
}, { cwd });
|
|
11758
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
11759
|
+
assert.equal(result.outputJson, null);
|
|
11760
|
+
}
|
|
11761
|
+
finally {
|
|
11762
|
+
await rm(cwd, { recursive: true, force: true });
|
|
11763
|
+
}
|
|
11764
|
+
});
|
|
10340
11765
|
it("does not block Stop from root team state without team_name when no session is known", async () => {
|
|
10341
11766
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-root-team-no-session-no-name-"));
|
|
10342
11767
|
try {
|
|
@@ -10866,6 +12291,34 @@ describe("codex native hook triage integration", () => {
|
|
|
10866
12291
|
await rm(cwd, { recursive: true, force: true });
|
|
10867
12292
|
}
|
|
10868
12293
|
});
|
|
12294
|
+
it("makes bare autopilot command activation observable in state and prompt guidance", async () => {
|
|
12295
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-autopilot-bare-observable-"));
|
|
12296
|
+
try {
|
|
12297
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
12298
|
+
await writeSessionStart(cwd, "sess-autopilot-bare-observable");
|
|
12299
|
+
const result = await dispatchCodexNativeHook({
|
|
12300
|
+
hook_event_name: "UserPromptSubmit",
|
|
12301
|
+
cwd,
|
|
12302
|
+
session_id: "sess-autopilot-bare-observable",
|
|
12303
|
+
thread_id: "thread-autopilot-bare-observable",
|
|
12304
|
+
turn_id: "turn-autopilot-bare-observable",
|
|
12305
|
+
prompt: "run autopilot",
|
|
12306
|
+
}, { cwd });
|
|
12307
|
+
assert.equal(result.skillState?.skill, "autopilot");
|
|
12308
|
+
assert.equal(result.skillState?.phase, "deep-interview");
|
|
12309
|
+
assert.equal(result.skillState?.initialized_state_path, ".omx/state/sessions/sess-autopilot-bare-observable/autopilot-state.json");
|
|
12310
|
+
const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
|
|
12311
|
+
assert.match(additionalContext, /detected workflow keyword "autopilot" -> autopilot/);
|
|
12312
|
+
assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
|
|
12313
|
+
const statePath = join(cwd, ".omx", "state", "sessions", "sess-autopilot-bare-observable", "autopilot-state.json");
|
|
12314
|
+
const modeState = JSON.parse(await readFile(statePath, "utf-8"));
|
|
12315
|
+
assert.equal(modeState.active, true);
|
|
12316
|
+
assert.equal(modeState.current_phase, "deep-interview");
|
|
12317
|
+
}
|
|
12318
|
+
finally {
|
|
12319
|
+
await rm(cwd, { recursive: true, force: true });
|
|
12320
|
+
}
|
|
12321
|
+
});
|
|
10869
12322
|
// ── Group 2: HEAVY injection ─────────────────────────────────────────────
|
|
10870
12323
|
it("injects HEAVY advisory and writes prompt-routing-state for a multi-step goal prompt", async () => {
|
|
10871
12324
|
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-heavy-"));
|