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
|
@@ -33,6 +33,8 @@ import { getLegacyWikiDir, serializePage, writePage } from "../../wiki/storage.j
|
|
|
33
33
|
import { WIKI_SCHEMA_VERSION } from "../../wiki/types.js";
|
|
34
34
|
import { createUltragoalPlan, readUltragoalPlan } from "../../ultragoal/artifacts.js";
|
|
35
35
|
import { getBaseStateDir } from "../../state/paths.js";
|
|
36
|
+
import { maybeNudgeLeaderForAllowedWorkerStop } from "../notify-hook/team-worker-stop.js";
|
|
37
|
+
import { MAX_NATIVE_STDIN_JSON_BYTES } from "../hook-payload-guard.js";
|
|
36
38
|
|
|
37
39
|
function nativeHookScriptPath(): string {
|
|
38
40
|
return join(process.cwd(), "dist", "scripts", "codex-native-hook.js");
|
|
@@ -136,8 +138,22 @@ async function withLoreGuardConfig<T>(
|
|
|
136
138
|
|
|
137
139
|
function buildWorkerStopFakeTmux(
|
|
138
140
|
tmuxLogPath: string,
|
|
139
|
-
options: {
|
|
141
|
+
options: {
|
|
142
|
+
failSend?: boolean;
|
|
143
|
+
busyLeader?: boolean;
|
|
144
|
+
captureText?: string;
|
|
145
|
+
currentCommand?: string;
|
|
146
|
+
sendDelayMs?: number;
|
|
147
|
+
removePathOnSend?: string;
|
|
148
|
+
removePathOnCapture?: string;
|
|
149
|
+
} = {},
|
|
140
150
|
): string {
|
|
151
|
+
const rawCaptureText = options.captureText ?? (options.busyLeader ? "• Working… (esc to interrupt)" : "› ready");
|
|
152
|
+
const captureText = `'${rawCaptureText.replace(/'/g, "'\"'\"'")}'`;
|
|
153
|
+
const currentCommand = `'${(options.currentCommand ?? "codex").replace(/'/g, "'\"'\"'")}'`;
|
|
154
|
+
const sendDelaySeconds = Math.max(0, options.sendDelayMs ?? 0) / 1000;
|
|
155
|
+
const removePathOnSend = options.removePathOnSend ? `'${options.removePathOnSend.replace(/'/g, "'\"'\"'")}'` : "";
|
|
156
|
+
const removePathOnCapture = options.removePathOnCapture ? `'${options.removePathOnCapture.replace(/'/g, "'\"'\"'")}'` : "";
|
|
141
157
|
return `#!/usr/bin/env bash
|
|
142
158
|
set -eu
|
|
143
159
|
echo "$@" >> "${tmuxLogPath}"
|
|
@@ -158,17 +174,20 @@ if [[ "$cmd" == "display-message" ]]; then
|
|
|
158
174
|
"#{pane_id}") echo "%42" ;;
|
|
159
175
|
"#{pane_current_path}") pwd ;;
|
|
160
176
|
"#{pane_start_command}") echo "codex" ;;
|
|
161
|
-
"#{pane_current_command}")
|
|
177
|
+
"#{pane_current_command}") printf '%s\\n' ${currentCommand} ;;
|
|
162
178
|
"#S") echo "omx-team-worker-stop" ;;
|
|
163
179
|
*) ;;
|
|
164
180
|
esac
|
|
165
181
|
exit 0
|
|
166
182
|
fi
|
|
167
183
|
if [[ "$cmd" == "capture-pane" ]]; then
|
|
168
|
-
${
|
|
184
|
+
${removePathOnCapture ? `rm -rf ${removePathOnCapture}` : ""}
|
|
185
|
+
printf '%s\\n' ${captureText}
|
|
169
186
|
exit 0
|
|
170
187
|
fi
|
|
171
188
|
if [[ "$cmd" == "send-keys" ]]; then
|
|
189
|
+
${sendDelaySeconds > 0 ? `sleep ${sendDelaySeconds}` : ""}
|
|
190
|
+
${removePathOnSend ? `rm -rf ${removePathOnSend}` : ""}
|
|
172
191
|
${options.failSend ? "exit 1" : "exit 0"}
|
|
173
192
|
fi
|
|
174
193
|
exit 0
|
|
@@ -393,13 +412,94 @@ describe("codex native hook dispatch", () => {
|
|
|
393
412
|
);
|
|
394
413
|
});
|
|
395
414
|
|
|
396
|
-
it("emits
|
|
415
|
+
it("emits schema-safe JSON stdout when CLI stdin is malformed", () => {
|
|
397
416
|
const stdout = runNativeHookCli("{");
|
|
398
417
|
|
|
418
|
+
const output = parseSingleJsonStdout(stdout) as {
|
|
419
|
+
continue?: boolean;
|
|
420
|
+
stopReason?: string;
|
|
421
|
+
systemMessage?: string;
|
|
422
|
+
hookSpecificOutput?: unknown;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
assert.equal(output.continue, false);
|
|
426
|
+
assert.equal(output.stopReason, "native_hook_stdin_parse_error");
|
|
427
|
+
assert.equal(output.hookSpecificOutput, undefined);
|
|
428
|
+
assert.match(
|
|
429
|
+
String(output.systemMessage ?? ""),
|
|
430
|
+
/stdin JSON parsing failed inside codex-native-hook:/,
|
|
431
|
+
);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("redacts unterminated prompt-like malformed stdin fields", async () => {
|
|
435
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-malformed-unterminated-"));
|
|
436
|
+
try {
|
|
437
|
+
const privatePrompt = "PRIVATE_UNTERMINATED_PROMPT";
|
|
438
|
+
const malformed = `{hook_event_name:"PostToolUse", prompt:"${privatePrompt}`;
|
|
439
|
+
const result = spawnSync(process.execPath, [nativeHookScriptPath()], {
|
|
440
|
+
cwd,
|
|
441
|
+
input: malformed,
|
|
442
|
+
encoding: "utf-8",
|
|
443
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
447
|
+
assert.equal(result.stderr, "");
|
|
448
|
+
const output = parseSingleJsonStdout(result.stdout);
|
|
449
|
+
assert.equal(output.stopReason, "native_hook_stdin_parse_error");
|
|
450
|
+
|
|
451
|
+
const log = await readFile(join(cwd, ".omx", "logs", `native-hook-${new Date().toISOString().split("T")[0]}.jsonl`), "utf-8");
|
|
452
|
+
const entry = JSON.parse(log.trim()) as Record<string, unknown>;
|
|
453
|
+
const prefix = String(entry.raw_input_prefix ?? "");
|
|
454
|
+
assert.doesNotMatch(prefix, new RegExp(privatePrompt));
|
|
455
|
+
assert.match(prefix, /prompt:"\[REDACTED\]"/);
|
|
456
|
+
} finally {
|
|
457
|
+
await rm(cwd, { recursive: true, force: true });
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("logs a bounded redacted raw stdin prefix when CLI stdin is malformed", async () => {
|
|
462
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-malformed-log-prefix-"));
|
|
463
|
+
try {
|
|
464
|
+
const secret = "sk-test-secret123456";
|
|
465
|
+
const promptText = "summarize private launch notes";
|
|
466
|
+
const malformed = `{hook_event_name:"PostToolUse", access_token:"${secret}", prompt:"${promptText}", text:"${promptText}", bad:"${"x".repeat(400)}"}${String.fromCharCode(10, 0, 7)}`;
|
|
467
|
+
const result = spawnSync(process.execPath, [nativeHookScriptPath()], {
|
|
468
|
+
cwd,
|
|
469
|
+
input: malformed,
|
|
470
|
+
encoding: "utf-8",
|
|
471
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
475
|
+
assert.equal(result.stderr, "");
|
|
476
|
+
const output = parseSingleJsonStdout(result.stdout);
|
|
477
|
+
assert.equal(output.stopReason, "native_hook_stdin_parse_error");
|
|
478
|
+
|
|
479
|
+
const log = await readFile(join(cwd, ".omx", "logs", `native-hook-${new Date().toISOString().split("T")[0]}.jsonl`), "utf-8");
|
|
480
|
+
const entry = JSON.parse(log.trim()) as Record<string, unknown>;
|
|
481
|
+
const prefix = String(entry.raw_input_prefix ?? "");
|
|
482
|
+
assert.equal(entry.type, "native_hook_stdin_parse_error");
|
|
483
|
+
assert.equal(entry.raw_input_length, Buffer.byteLength(malformed, "utf-8"));
|
|
484
|
+
assert.ok(prefix.length <= 240, `prefix should be bounded, got ${prefix.length}`);
|
|
485
|
+
assert.doesNotMatch(prefix, /[\u0000-\u001f\u007f-\u009f]/);
|
|
486
|
+
assert.doesNotMatch(prefix, new RegExp(secret));
|
|
487
|
+
assert.doesNotMatch(prefix, new RegExp(promptText));
|
|
488
|
+
assert.match(prefix, /\[REDACTED\]/);
|
|
489
|
+
} finally {
|
|
490
|
+
await rm(cwd, { recursive: true, force: true });
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it("emits Stop-schema-safe block JSON when malformed stdin still identifies Stop", () => {
|
|
495
|
+
const stdout = runNativeHookCli('{hook_event_name:"Stop",');
|
|
496
|
+
|
|
399
497
|
const output = parseSingleJsonStdout(stdout) as {
|
|
400
498
|
decision?: string;
|
|
401
499
|
reason?: string;
|
|
402
|
-
|
|
500
|
+
stopReason?: string;
|
|
501
|
+
systemMessage?: string;
|
|
502
|
+
hookSpecificOutput?: unknown;
|
|
403
503
|
};
|
|
404
504
|
|
|
405
505
|
assert.equal(output.decision, "block");
|
|
@@ -407,9 +507,10 @@ describe("codex native hook dispatch", () => {
|
|
|
407
507
|
output.reason,
|
|
408
508
|
"OMX native hook received malformed JSON input. Preserve runtime state, inspect the emitting hook payload yourself, and retry with valid JSON.",
|
|
409
509
|
);
|
|
410
|
-
assert.equal(output.
|
|
510
|
+
assert.equal(output.stopReason, "native_hook_stdin_parse_error");
|
|
511
|
+
assert.equal(output.hookSpecificOutput, undefined);
|
|
411
512
|
assert.match(
|
|
412
|
-
String(output.
|
|
513
|
+
String(output.systemMessage ?? ""),
|
|
413
514
|
/stdin JSON parsing failed inside codex-native-hook:/,
|
|
414
515
|
);
|
|
415
516
|
});
|
|
@@ -427,6 +528,125 @@ describe("codex native hook dispatch", () => {
|
|
|
427
528
|
const output = parseSingleJsonStdout(stdout);
|
|
428
529
|
|
|
429
530
|
assert.deepEqual(output, {});
|
|
531
|
+
assert.equal(existsSync(join(cwd, ".omx", "state")), false);
|
|
532
|
+
} finally {
|
|
533
|
+
await rm(cwd, { recursive: true, force: true });
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it("returns empty JSON for oversized Stop stdin without parsing or creating inactive state", async () => {
|
|
538
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-"));
|
|
539
|
+
try {
|
|
540
|
+
const oversizedStop = JSON.stringify({
|
|
541
|
+
hook_event_name: "Stop",
|
|
542
|
+
cwd,
|
|
543
|
+
session_id: "sess-cli-stop-oversized",
|
|
544
|
+
transcript: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
const stdout = runNativeHookCli(oversizedStop, { cwd });
|
|
548
|
+
assert.deepEqual(parseSingleJsonStdout(stdout), {});
|
|
549
|
+
assert.equal(existsSync(join(cwd, ".omx", "state")), false);
|
|
550
|
+
} finally {
|
|
551
|
+
await rm(cwd, { recursive: true, force: true });
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it("blocks oversized Stop stdin when current session autopilot is active", async () => {
|
|
556
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-active-"));
|
|
557
|
+
try {
|
|
558
|
+
await writeActiveAutopilotSession(cwd, "sess-cli-stop-oversized-active");
|
|
559
|
+
const oversizedStop = JSON.stringify({
|
|
560
|
+
hook_event_name: "Stop",
|
|
561
|
+
cwd,
|
|
562
|
+
session_id: "native-session-hidden-by-oversized-payload",
|
|
563
|
+
transcript: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
const output = parseSingleJsonStdout(runNativeHookCli(oversizedStop, { cwd })) as {
|
|
567
|
+
decision?: string;
|
|
568
|
+
stopReason?: string;
|
|
569
|
+
systemMessage?: string;
|
|
570
|
+
};
|
|
571
|
+
assert.equal(output.decision, "block");
|
|
572
|
+
assert.equal(output.stopReason, "native_stop_stdin_oversized_active_workflow");
|
|
573
|
+
assert.match(String(output.systemMessage ?? ""), /active current-session workflow state/);
|
|
574
|
+
assert.equal(existsSync(join(cwd, ".omx", "logs")), false);
|
|
575
|
+
} finally {
|
|
576
|
+
await rm(cwd, { recursive: true, force: true });
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it("does not block oversized Stop stdin for unrelated root autopilot state", async () => {
|
|
581
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-stale-root-"));
|
|
582
|
+
try {
|
|
583
|
+
await writeJson(join(cwd, ".omx", "state", "session.json"), {
|
|
584
|
+
session_id: "sess-current-without-active-autopilot",
|
|
585
|
+
cwd,
|
|
586
|
+
});
|
|
587
|
+
await writeJson(join(cwd, ".omx", "state", "autopilot-state.json"), {
|
|
588
|
+
active: true,
|
|
589
|
+
current_phase: "execution",
|
|
590
|
+
});
|
|
591
|
+
const oversizedStop = JSON.stringify({
|
|
592
|
+
hook_event_name: "Stop",
|
|
593
|
+
cwd,
|
|
594
|
+
transcript: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
assert.deepEqual(parseSingleJsonStdout(runNativeHookCli(oversizedStop, { cwd })), {});
|
|
598
|
+
assert.equal(existsSync(join(cwd, ".omx", "logs")), false);
|
|
599
|
+
} finally {
|
|
600
|
+
await rm(cwd, { recursive: true, force: true });
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it("does not block oversized Stop stdin when terminal run-state shadows stale autopilot state", async () => {
|
|
605
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-terminal-run-"));
|
|
606
|
+
try {
|
|
607
|
+
const sessionId = "sess-cli-stop-oversized-terminal-run";
|
|
608
|
+
await writeActiveAutopilotSession(cwd, sessionId);
|
|
609
|
+
await writeJson(join(cwd, ".omx", "state", "sessions", sessionId, "run-state.json"), {
|
|
610
|
+
version: 1,
|
|
611
|
+
active: false,
|
|
612
|
+
mode: "autopilot",
|
|
613
|
+
outcome: "finish",
|
|
614
|
+
lifecycle_outcome: "finished",
|
|
615
|
+
current_phase: "complete",
|
|
616
|
+
completed_at: "2026-05-20T11:00:00.000Z",
|
|
617
|
+
updated_at: "2026-05-20T11:00:00.000Z",
|
|
618
|
+
});
|
|
619
|
+
const oversizedStop = JSON.stringify({
|
|
620
|
+
hook_event_name: "Stop",
|
|
621
|
+
cwd,
|
|
622
|
+
transcript: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
assert.deepEqual(parseSingleJsonStdout(runNativeHookCli(oversizedStop, { cwd })), {});
|
|
626
|
+
assert.equal(existsSync(join(cwd, ".omx", "logs")), false);
|
|
627
|
+
} finally {
|
|
628
|
+
await rm(cwd, { recursive: true, force: true });
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it("fails closed for oversized non-Stop stdin before parsing", async () => {
|
|
633
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-nonstop-oversized-"));
|
|
634
|
+
try {
|
|
635
|
+
const oversizedPrompt = JSON.stringify({
|
|
636
|
+
hook_event_name: "UserPromptSubmit",
|
|
637
|
+
cwd,
|
|
638
|
+
session_id: "sess-cli-prompt-oversized",
|
|
639
|
+
prompt: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
const output = parseSingleJsonStdout(runNativeHookCli(oversizedPrompt, { cwd })) as {
|
|
643
|
+
continue?: boolean;
|
|
644
|
+
stopReason?: string;
|
|
645
|
+
systemMessage?: string;
|
|
646
|
+
};
|
|
647
|
+
assert.equal(output.continue, false);
|
|
648
|
+
assert.equal(output.stopReason, "native_hook_stdin_oversized");
|
|
649
|
+
assert.match(String(output.systemMessage ?? ""), /rejected oversized stdin JSON before parsing/);
|
|
430
650
|
} finally {
|
|
431
651
|
await rm(cwd, { recursive: true, force: true });
|
|
432
652
|
}
|
|
@@ -1161,6 +1381,66 @@ describe("codex native hook dispatch", () => {
|
|
|
1161
1381
|
}
|
|
1162
1382
|
});
|
|
1163
1383
|
|
|
1384
|
+
it("keeps a self-parented native role thread as subagent evidence", async () => {
|
|
1385
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-self-parented-subagent-"));
|
|
1386
|
+
try {
|
|
1387
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
1388
|
+
const canonicalSessionId = "omx-autopilot-session";
|
|
1389
|
+
const nativeRoleThreadId = "codex-architect-thread";
|
|
1390
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
1391
|
+
await writeSessionStart(cwd, canonicalSessionId, {
|
|
1392
|
+
nativeSessionId: nativeRoleThreadId,
|
|
1393
|
+
});
|
|
1394
|
+
|
|
1395
|
+
const transcriptPath = join(cwd, "architect-subagent-rollout.jsonl");
|
|
1396
|
+
await writeFile(
|
|
1397
|
+
transcriptPath,
|
|
1398
|
+
`${JSON.stringify({
|
|
1399
|
+
type: "session_meta",
|
|
1400
|
+
payload: {
|
|
1401
|
+
id: nativeRoleThreadId,
|
|
1402
|
+
source: {
|
|
1403
|
+
subagent: {
|
|
1404
|
+
thread_spawn: {
|
|
1405
|
+
parent_thread_id: nativeRoleThreadId,
|
|
1406
|
+
depth: 1,
|
|
1407
|
+
agent_nickname: "Architect",
|
|
1408
|
+
agent_role: "architect",
|
|
1409
|
+
},
|
|
1410
|
+
},
|
|
1411
|
+
},
|
|
1412
|
+
agent_nickname: "Architect",
|
|
1413
|
+
agent_role: "architect",
|
|
1414
|
+
},
|
|
1415
|
+
})}\n`,
|
|
1416
|
+
);
|
|
1417
|
+
|
|
1418
|
+
await dispatchCodexNativeHook(
|
|
1419
|
+
{
|
|
1420
|
+
hook_event_name: "SessionStart",
|
|
1421
|
+
cwd,
|
|
1422
|
+
session_id: nativeRoleThreadId,
|
|
1423
|
+
transcript_path: transcriptPath,
|
|
1424
|
+
},
|
|
1425
|
+
{ cwd, sessionOwnerPid: process.pid },
|
|
1426
|
+
);
|
|
1427
|
+
|
|
1428
|
+
const tracking = JSON.parse(
|
|
1429
|
+
await readFile(join(stateDir, "subagent-tracking.json"), "utf-8"),
|
|
1430
|
+
) as {
|
|
1431
|
+
sessions?: Record<string, {
|
|
1432
|
+
leader_thread_id?: string;
|
|
1433
|
+
threads?: Record<string, { kind?: string; mode?: string }>;
|
|
1434
|
+
}>;
|
|
1435
|
+
};
|
|
1436
|
+
assert.equal(tracking.sessions?.[canonicalSessionId]?.leader_thread_id, undefined);
|
|
1437
|
+
assert.equal(tracking.sessions?.[canonicalSessionId]?.threads?.[nativeRoleThreadId]?.kind, "subagent");
|
|
1438
|
+
assert.equal(tracking.sessions?.[canonicalSessionId]?.threads?.[nativeRoleThreadId]?.mode, "architect");
|
|
1439
|
+
} finally {
|
|
1440
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1441
|
+
}
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1164
1444
|
it("does not attach a subagent SessionStart to an unrelated canonical leader", async () => {
|
|
1165
1445
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-start-mismatch-"));
|
|
1166
1446
|
try {
|
|
@@ -1289,6 +1569,109 @@ describe("codex native hook dispatch", () => {
|
|
|
1289
1569
|
}
|
|
1290
1570
|
});
|
|
1291
1571
|
|
|
1572
|
+
it("prefers the OMX owner session id when a native new session revives HUD", async () => {
|
|
1573
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-owner-session-revive-"));
|
|
1574
|
+
try {
|
|
1575
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
1576
|
+
const ownerSessionId = "omx-launch-owner-hud";
|
|
1577
|
+
const oldNativeSessionId = "codex-native-hud-old";
|
|
1578
|
+
const nativeSessionId = "codex-native-hud-new";
|
|
1579
|
+
await mkdir(stateDir, { recursive: true });
|
|
1580
|
+
await writeSessionStart(cwd, ownerSessionId, {
|
|
1581
|
+
nativeSessionId: oldNativeSessionId,
|
|
1582
|
+
pid: process.pid,
|
|
1583
|
+
});
|
|
1584
|
+
await dispatchCodexNativeHook(
|
|
1585
|
+
{
|
|
1586
|
+
hook_event_name: "SessionStart",
|
|
1587
|
+
cwd,
|
|
1588
|
+
session_id: nativeSessionId,
|
|
1589
|
+
},
|
|
1590
|
+
{
|
|
1591
|
+
cwd,
|
|
1592
|
+
sessionOwnerPid: process.pid,
|
|
1593
|
+
},
|
|
1594
|
+
);
|
|
1595
|
+
|
|
1596
|
+
const sessionState = JSON.parse(await readFile(join(stateDir, "session.json"), "utf-8")) as {
|
|
1597
|
+
session_id?: string;
|
|
1598
|
+
native_session_id?: string;
|
|
1599
|
+
previous_native_session_id?: string;
|
|
1600
|
+
owner_omx_session_id?: string;
|
|
1601
|
+
};
|
|
1602
|
+
assert.equal(sessionState.session_id, nativeSessionId);
|
|
1603
|
+
assert.equal(sessionState.native_session_id, nativeSessionId);
|
|
1604
|
+
assert.equal(sessionState.previous_native_session_id, oldNativeSessionId);
|
|
1605
|
+
assert.equal(sessionState.owner_omx_session_id, ownerSessionId);
|
|
1606
|
+
|
|
1607
|
+
let reconcileCall: { cwd: string; sessionId?: string } | null = null;
|
|
1608
|
+
const promptResult = await dispatchCodexNativeHook(
|
|
1609
|
+
{
|
|
1610
|
+
hook_event_name: "UserPromptSubmit",
|
|
1611
|
+
cwd,
|
|
1612
|
+
session_id: nativeSessionId,
|
|
1613
|
+
thread_id: "thread-hud-owner",
|
|
1614
|
+
turn_id: "turn-hud-owner",
|
|
1615
|
+
prompt: "$ralplan fix native new hud owner handoff",
|
|
1616
|
+
},
|
|
1617
|
+
{
|
|
1618
|
+
cwd,
|
|
1619
|
+
reconcileHudForPromptSubmitFn: async (hookCwd, deps = {}) => {
|
|
1620
|
+
reconcileCall = { cwd: hookCwd, sessionId: deps.sessionId };
|
|
1621
|
+
return { status: "recreated", paneId: "%9", desiredHeight: 3, duplicateCount: 0 };
|
|
1622
|
+
},
|
|
1623
|
+
},
|
|
1624
|
+
);
|
|
1625
|
+
|
|
1626
|
+
assert.equal(promptResult.omxEventName, "keyword-detector");
|
|
1627
|
+
assert.deepEqual(reconcileCall, { cwd, sessionId: ownerSessionId });
|
|
1628
|
+
} finally {
|
|
1629
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1630
|
+
}
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
it("falls back to the canonical session id for malformed HUD owner ids", async () => {
|
|
1634
|
+
for (const [index, invalidOwnerSessionId] of ["codex-native-hud-owner", "omx-../../stale"].entries()) {
|
|
1635
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-invalid-owner-revive-"));
|
|
1636
|
+
try {
|
|
1637
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
1638
|
+
const canonicalSessionId = "omx-launch-hud-safe";
|
|
1639
|
+
const nativeSessionId = "codex-native-hud-safe";
|
|
1640
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
1641
|
+
await writeSessionStart(cwd, canonicalSessionId);
|
|
1642
|
+
|
|
1643
|
+
const sessionStatePath = join(stateDir, "session.json");
|
|
1644
|
+
const sessionState = JSON.parse(await readFile(sessionStatePath, "utf-8")) as Record<string, unknown>;
|
|
1645
|
+
sessionState.owner_omx_session_id = invalidOwnerSessionId;
|
|
1646
|
+
await writeJson(sessionStatePath, sessionState);
|
|
1647
|
+
|
|
1648
|
+
let reconcileCall: { cwd: string; sessionId?: string } | null = null;
|
|
1649
|
+
const promptResult = await dispatchCodexNativeHook(
|
|
1650
|
+
{
|
|
1651
|
+
hook_event_name: "UserPromptSubmit",
|
|
1652
|
+
cwd,
|
|
1653
|
+
session_id: nativeSessionId,
|
|
1654
|
+
thread_id: `thread-hud-invalid-owner-${index}`,
|
|
1655
|
+
turn_id: "turn-hud-invalid-owner",
|
|
1656
|
+
prompt: "$ralplan fix malformed hud owner handoff",
|
|
1657
|
+
},
|
|
1658
|
+
{
|
|
1659
|
+
cwd,
|
|
1660
|
+
reconcileHudForPromptSubmitFn: async (hookCwd, deps = {}) => {
|
|
1661
|
+
reconcileCall = { cwd: hookCwd, sessionId: deps.sessionId };
|
|
1662
|
+
return { status: "recreated", paneId: "%9", desiredHeight: 3, duplicateCount: 0 };
|
|
1663
|
+
},
|
|
1664
|
+
},
|
|
1665
|
+
);
|
|
1666
|
+
|
|
1667
|
+
assert.equal(promptResult.omxEventName, "keyword-detector");
|
|
1668
|
+
assert.deepEqual(reconcileCall, { cwd, sessionId: canonicalSessionId });
|
|
1669
|
+
} finally {
|
|
1670
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
});
|
|
1674
|
+
|
|
1292
1675
|
it("passes the canonical OMX session id when UserPromptSubmit revives HUD", async () => {
|
|
1293
1676
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-session-revive-"));
|
|
1294
1677
|
try {
|
|
@@ -2676,30 +3059,373 @@ standardMaxRounds = 15
|
|
|
2676
3059
|
}
|
|
2677
3060
|
});
|
|
2678
3061
|
|
|
2679
|
-
it("
|
|
2680
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
3062
|
+
it("does not treat a corrupt leader kind=subagent tracker entry as native subagent prompt scope", async () => {
|
|
3063
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-corrupt-leader-subagent-"));
|
|
2681
3064
|
try {
|
|
2682
|
-
|
|
3065
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
3066
|
+
const canonicalSessionId = "sess-corrupt-leader";
|
|
3067
|
+
const leaderNativeSessionId = "native-corrupt-leader";
|
|
3068
|
+
const nowIso = new Date().toISOString();
|
|
3069
|
+
|
|
3070
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
3071
|
+
session_id: canonicalSessionId,
|
|
3072
|
+
native_session_id: leaderNativeSessionId,
|
|
3073
|
+
});
|
|
3074
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
3075
|
+
schemaVersion: 1,
|
|
3076
|
+
sessions: {
|
|
3077
|
+
[canonicalSessionId]: {
|
|
3078
|
+
session_id: canonicalSessionId,
|
|
3079
|
+
leader_thread_id: leaderNativeSessionId,
|
|
3080
|
+
updated_at: nowIso,
|
|
3081
|
+
threads: {
|
|
3082
|
+
[leaderNativeSessionId]: {
|
|
3083
|
+
thread_id: leaderNativeSessionId,
|
|
3084
|
+
kind: "subagent",
|
|
3085
|
+
first_seen_at: nowIso,
|
|
3086
|
+
last_seen_at: nowIso,
|
|
3087
|
+
turn_count: 2,
|
|
3088
|
+
},
|
|
3089
|
+
},
|
|
3090
|
+
},
|
|
3091
|
+
},
|
|
3092
|
+
});
|
|
3093
|
+
|
|
2683
3094
|
const result = await dispatchCodexNativeHook(
|
|
2684
3095
|
{
|
|
2685
3096
|
hook_event_name: "UserPromptSubmit",
|
|
2686
3097
|
cwd,
|
|
2687
|
-
session_id:
|
|
2688
|
-
thread_id:
|
|
2689
|
-
turn_id: "turn-
|
|
2690
|
-
prompt: "$
|
|
3098
|
+
session_id: leaderNativeSessionId,
|
|
3099
|
+
thread_id: leaderNativeSessionId,
|
|
3100
|
+
turn_id: "turn-corrupt-leader",
|
|
3101
|
+
prompt: "$autopilot continue this review blocker fix",
|
|
2691
3102
|
},
|
|
2692
3103
|
{ cwd },
|
|
2693
3104
|
);
|
|
2694
3105
|
|
|
2695
3106
|
assert.equal(result.omxEventName, "keyword-detector");
|
|
2696
|
-
assert.equal(result.skillState?.skill, "
|
|
2697
|
-
|
|
2698
|
-
(
|
|
3107
|
+
assert.equal(result.skillState?.skill, "autopilot");
|
|
3108
|
+
assert.equal(
|
|
3109
|
+
existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")),
|
|
3110
|
+
true,
|
|
2699
3111
|
);
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
3112
|
+
} finally {
|
|
3113
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3114
|
+
}
|
|
3115
|
+
});
|
|
3116
|
+
|
|
3117
|
+
it("lets the current canonical leader boundary beat stale global subagent tracking with a distinct prompt thread id", async () => {
|
|
3118
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-current-leader-stale-global-"));
|
|
3119
|
+
try {
|
|
3120
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
3121
|
+
const canonicalSessionId = "sess-current-leader";
|
|
3122
|
+
const leaderNativeSessionId = "native-current-leader";
|
|
3123
|
+
const staleSessionId = "sess-stale-subagent";
|
|
3124
|
+
const staleLeaderNativeSessionId = "native-stale-leader";
|
|
3125
|
+
const nowIso = new Date().toISOString();
|
|
3126
|
+
|
|
3127
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
3128
|
+
session_id: canonicalSessionId,
|
|
3129
|
+
native_session_id: leaderNativeSessionId,
|
|
3130
|
+
});
|
|
3131
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
3132
|
+
schemaVersion: 1,
|
|
3133
|
+
sessions: {
|
|
3134
|
+
[canonicalSessionId]: {
|
|
3135
|
+
session_id: canonicalSessionId,
|
|
3136
|
+
leader_thread_id: leaderNativeSessionId,
|
|
3137
|
+
updated_at: nowIso,
|
|
3138
|
+
threads: {
|
|
3139
|
+
[leaderNativeSessionId]: {
|
|
3140
|
+
thread_id: leaderNativeSessionId,
|
|
3141
|
+
kind: "leader",
|
|
3142
|
+
first_seen_at: nowIso,
|
|
3143
|
+
last_seen_at: nowIso,
|
|
3144
|
+
turn_count: 1,
|
|
3145
|
+
},
|
|
3146
|
+
},
|
|
3147
|
+
},
|
|
3148
|
+
[staleSessionId]: {
|
|
3149
|
+
session_id: staleSessionId,
|
|
3150
|
+
leader_thread_id: staleLeaderNativeSessionId,
|
|
3151
|
+
updated_at: nowIso,
|
|
3152
|
+
threads: {
|
|
3153
|
+
[staleLeaderNativeSessionId]: {
|
|
3154
|
+
thread_id: staleLeaderNativeSessionId,
|
|
3155
|
+
kind: "leader",
|
|
3156
|
+
first_seen_at: nowIso,
|
|
3157
|
+
last_seen_at: nowIso,
|
|
3158
|
+
turn_count: 1,
|
|
3159
|
+
},
|
|
3160
|
+
[leaderNativeSessionId]: {
|
|
3161
|
+
thread_id: leaderNativeSessionId,
|
|
3162
|
+
kind: "subagent",
|
|
3163
|
+
first_seen_at: nowIso,
|
|
3164
|
+
last_seen_at: nowIso,
|
|
3165
|
+
turn_count: 1,
|
|
3166
|
+
mode: "architect",
|
|
3167
|
+
},
|
|
3168
|
+
},
|
|
3169
|
+
},
|
|
3170
|
+
},
|
|
3171
|
+
});
|
|
3172
|
+
|
|
3173
|
+
const result = await dispatchCodexNativeHook(
|
|
3174
|
+
{
|
|
3175
|
+
hook_event_name: "UserPromptSubmit",
|
|
3176
|
+
cwd,
|
|
3177
|
+
session_id: leaderNativeSessionId,
|
|
3178
|
+
thread_id: "thread-current-turn-not-native-session",
|
|
3179
|
+
turn_id: "turn-current-leader",
|
|
3180
|
+
prompt: "$autopilot continue",
|
|
3181
|
+
},
|
|
3182
|
+
{ cwd },
|
|
3183
|
+
);
|
|
3184
|
+
|
|
3185
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
3186
|
+
assert.equal(result.skillState?.skill, "autopilot");
|
|
3187
|
+
assert.equal(
|
|
3188
|
+
existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")),
|
|
3189
|
+
true,
|
|
3190
|
+
);
|
|
3191
|
+
assert.equal(
|
|
3192
|
+
existsSync(join(stateDir, "sessions", staleSessionId, "autopilot-state.json")),
|
|
3193
|
+
false,
|
|
3194
|
+
);
|
|
3195
|
+
} finally {
|
|
3196
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3197
|
+
}
|
|
3198
|
+
});
|
|
3199
|
+
|
|
3200
|
+
it("lets the current session native leader beat stale global subagent tracking without a canonical summary and with a distinct prompt thread id", async () => {
|
|
3201
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-current-native-leader-stale-global-"));
|
|
3202
|
+
try {
|
|
3203
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
3204
|
+
const canonicalSessionId = "sess-current-native-leader";
|
|
3205
|
+
const leaderNativeSessionId = "native-current-leader-no-summary";
|
|
3206
|
+
const staleSessionId = "sess-stale-native-subagent";
|
|
3207
|
+
const staleLeaderNativeSessionId = "native-stale-parent";
|
|
3208
|
+
const nowIso = new Date().toISOString();
|
|
3209
|
+
|
|
3210
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
3211
|
+
session_id: canonicalSessionId,
|
|
3212
|
+
native_session_id: leaderNativeSessionId,
|
|
3213
|
+
});
|
|
3214
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
3215
|
+
schemaVersion: 1,
|
|
3216
|
+
sessions: {
|
|
3217
|
+
[staleSessionId]: {
|
|
3218
|
+
session_id: staleSessionId,
|
|
3219
|
+
leader_thread_id: staleLeaderNativeSessionId,
|
|
3220
|
+
updated_at: nowIso,
|
|
3221
|
+
threads: {
|
|
3222
|
+
[staleLeaderNativeSessionId]: {
|
|
3223
|
+
thread_id: staleLeaderNativeSessionId,
|
|
3224
|
+
kind: "leader",
|
|
3225
|
+
first_seen_at: nowIso,
|
|
3226
|
+
last_seen_at: nowIso,
|
|
3227
|
+
turn_count: 1,
|
|
3228
|
+
},
|
|
3229
|
+
[leaderNativeSessionId]: {
|
|
3230
|
+
thread_id: leaderNativeSessionId,
|
|
3231
|
+
kind: "subagent",
|
|
3232
|
+
first_seen_at: nowIso,
|
|
3233
|
+
last_seen_at: nowIso,
|
|
3234
|
+
turn_count: 1,
|
|
3235
|
+
mode: "critic",
|
|
3236
|
+
},
|
|
3237
|
+
},
|
|
3238
|
+
},
|
|
3239
|
+
},
|
|
3240
|
+
});
|
|
3241
|
+
|
|
3242
|
+
const result = await dispatchCodexNativeHook(
|
|
3243
|
+
{
|
|
3244
|
+
hook_event_name: "UserPromptSubmit",
|
|
3245
|
+
cwd,
|
|
3246
|
+
session_id: leaderNativeSessionId,
|
|
3247
|
+
thread_id: "thread-current-turn-not-native-session",
|
|
3248
|
+
turn_id: "turn-current-native-leader",
|
|
3249
|
+
prompt: "$autopilot continue",
|
|
3250
|
+
},
|
|
3251
|
+
{ cwd },
|
|
3252
|
+
);
|
|
3253
|
+
|
|
3254
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
3255
|
+
assert.equal(result.skillState?.skill, "autopilot");
|
|
3256
|
+
assert.equal(
|
|
3257
|
+
existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")),
|
|
3258
|
+
true,
|
|
3259
|
+
);
|
|
3260
|
+
assert.equal(
|
|
3261
|
+
existsSync(join(stateDir, "sessions", staleSessionId, "autopilot-state.json")),
|
|
3262
|
+
false,
|
|
3263
|
+
);
|
|
3264
|
+
} finally {
|
|
3265
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3266
|
+
}
|
|
3267
|
+
});
|
|
3268
|
+
|
|
3269
|
+
it("lets the current session native leader beat a malformed canonical subagent entry", async () => {
|
|
3270
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-current-native-leader-malformed-canonical-"));
|
|
3271
|
+
try {
|
|
3272
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
3273
|
+
const canonicalSessionId = "sess-current-native-leader-malformed";
|
|
3274
|
+
const leaderNativeSessionId = "native-current-leader-malformed";
|
|
3275
|
+
const nowIso = new Date().toISOString();
|
|
3276
|
+
|
|
3277
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
3278
|
+
session_id: canonicalSessionId,
|
|
3279
|
+
native_session_id: leaderNativeSessionId,
|
|
3280
|
+
});
|
|
3281
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
3282
|
+
schemaVersion: 1,
|
|
3283
|
+
sessions: {
|
|
3284
|
+
[canonicalSessionId]: {
|
|
3285
|
+
session_id: canonicalSessionId,
|
|
3286
|
+
updated_at: nowIso,
|
|
3287
|
+
threads: {
|
|
3288
|
+
[leaderNativeSessionId]: {
|
|
3289
|
+
thread_id: leaderNativeSessionId,
|
|
3290
|
+
kind: "subagent",
|
|
3291
|
+
first_seen_at: nowIso,
|
|
3292
|
+
last_seen_at: nowIso,
|
|
3293
|
+
turn_count: 1,
|
|
3294
|
+
mode: "architect",
|
|
3295
|
+
},
|
|
3296
|
+
},
|
|
3297
|
+
},
|
|
3298
|
+
},
|
|
3299
|
+
});
|
|
3300
|
+
|
|
3301
|
+
const result = await dispatchCodexNativeHook(
|
|
3302
|
+
{
|
|
3303
|
+
hook_event_name: "UserPromptSubmit",
|
|
3304
|
+
cwd,
|
|
3305
|
+
session_id: leaderNativeSessionId,
|
|
3306
|
+
thread_id: leaderNativeSessionId,
|
|
3307
|
+
turn_id: "turn-current-native-leader-malformed",
|
|
3308
|
+
prompt: "$autopilot continue",
|
|
3309
|
+
},
|
|
3310
|
+
{ cwd },
|
|
3311
|
+
);
|
|
3312
|
+
|
|
3313
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
3314
|
+
assert.equal(result.skillState?.skill, "autopilot");
|
|
3315
|
+
assert.equal(
|
|
3316
|
+
existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")),
|
|
3317
|
+
true,
|
|
3318
|
+
);
|
|
3319
|
+
} finally {
|
|
3320
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3321
|
+
}
|
|
3322
|
+
});
|
|
3323
|
+
|
|
3324
|
+
it("still treats mixed child and leader payload identities as native subagent scope", async () => {
|
|
3325
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-mixed-child-leader-identity-"));
|
|
3326
|
+
try {
|
|
3327
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
3328
|
+
const canonicalSessionId = "sess-mixed-child-leader";
|
|
3329
|
+
const leaderNativeSessionId = "native-mixed-leader";
|
|
3330
|
+
const childNativeSessionId = "native-mixed-child";
|
|
3331
|
+
const nowIso = new Date().toISOString();
|
|
3332
|
+
|
|
3333
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
3334
|
+
session_id: canonicalSessionId,
|
|
3335
|
+
native_session_id: leaderNativeSessionId,
|
|
3336
|
+
});
|
|
3337
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
3338
|
+
schemaVersion: 1,
|
|
3339
|
+
sessions: {
|
|
3340
|
+
[canonicalSessionId]: {
|
|
3341
|
+
session_id: canonicalSessionId,
|
|
3342
|
+
leader_thread_id: leaderNativeSessionId,
|
|
3343
|
+
updated_at: nowIso,
|
|
3344
|
+
threads: {
|
|
3345
|
+
[leaderNativeSessionId]: {
|
|
3346
|
+
thread_id: leaderNativeSessionId,
|
|
3347
|
+
kind: "leader",
|
|
3348
|
+
first_seen_at: nowIso,
|
|
3349
|
+
last_seen_at: nowIso,
|
|
3350
|
+
turn_count: 1,
|
|
3351
|
+
},
|
|
3352
|
+
[childNativeSessionId]: {
|
|
3353
|
+
thread_id: childNativeSessionId,
|
|
3354
|
+
kind: "subagent",
|
|
3355
|
+
first_seen_at: nowIso,
|
|
3356
|
+
last_seen_at: nowIso,
|
|
3357
|
+
turn_count: 1,
|
|
3358
|
+
mode: "critic",
|
|
3359
|
+
},
|
|
3360
|
+
},
|
|
3361
|
+
},
|
|
3362
|
+
},
|
|
3363
|
+
});
|
|
3364
|
+
|
|
3365
|
+
const result = await dispatchCodexNativeHook(
|
|
3366
|
+
{
|
|
3367
|
+
hook_event_name: "UserPromptSubmit",
|
|
3368
|
+
cwd,
|
|
3369
|
+
session_id: childNativeSessionId,
|
|
3370
|
+
thread_id: leaderNativeSessionId,
|
|
3371
|
+
turn_id: "turn-mixed-child-leader",
|
|
3372
|
+
prompt: "$ralplan review this as delegated text",
|
|
3373
|
+
},
|
|
3374
|
+
{ cwd },
|
|
3375
|
+
);
|
|
3376
|
+
|
|
3377
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
3378
|
+
assert.equal(result.skillState, null);
|
|
3379
|
+
assert.equal(result.outputJson, null);
|
|
3380
|
+
assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "ralplan-state.json")), false);
|
|
3381
|
+
assert.equal(existsSync(join(stateDir, "sessions", childNativeSessionId, "ralplan-state.json")), false);
|
|
3382
|
+
|
|
3383
|
+
const reversedResult = await dispatchCodexNativeHook(
|
|
3384
|
+
{
|
|
3385
|
+
hook_event_name: "UserPromptSubmit",
|
|
3386
|
+
cwd,
|
|
3387
|
+
session_id: leaderNativeSessionId,
|
|
3388
|
+
thread_id: childNativeSessionId,
|
|
3389
|
+
turn_id: "turn-mixed-leader-child",
|
|
3390
|
+
prompt: "$autopilot review this as delegated text",
|
|
3391
|
+
},
|
|
3392
|
+
{ cwd },
|
|
3393
|
+
);
|
|
3394
|
+
|
|
3395
|
+
assert.equal(reversedResult.omxEventName, "keyword-detector");
|
|
3396
|
+
assert.equal(reversedResult.skillState, null);
|
|
3397
|
+
assert.equal(reversedResult.outputJson, null);
|
|
3398
|
+
assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")), false);
|
|
3399
|
+
assert.equal(existsSync(join(stateDir, "sessions", childNativeSessionId, "autopilot-state.json")), false);
|
|
3400
|
+
} finally {
|
|
3401
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3402
|
+
}
|
|
3403
|
+
});
|
|
3404
|
+
|
|
3405
|
+
it("records plugin-prefixed keyword activation from UserPromptSubmit payloads", async () => {
|
|
3406
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-plugin-prefixed-"));
|
|
3407
|
+
try {
|
|
3408
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
3409
|
+
const result = await dispatchCodexNativeHook(
|
|
3410
|
+
{
|
|
3411
|
+
hook_event_name: "UserPromptSubmit",
|
|
3412
|
+
cwd,
|
|
3413
|
+
session_id: "sess-plugin-1",
|
|
3414
|
+
thread_id: "thread-plugin-1",
|
|
3415
|
+
turn_id: "turn-plugin-1",
|
|
3416
|
+
prompt: "$oh-my-codex:ralplan implement issue #1307",
|
|
3417
|
+
},
|
|
3418
|
+
{ cwd },
|
|
3419
|
+
);
|
|
3420
|
+
|
|
3421
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
3422
|
+
assert.equal(result.skillState?.skill, "ralplan");
|
|
3423
|
+
const message = String(
|
|
3424
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
3425
|
+
);
|
|
3426
|
+
assert.match(message, /\$oh-my-codex:ralplan" -> ralplan/);
|
|
3427
|
+
assert.match(message, /use CLI-first state updates via `omx state write\/read\/clear --input '<json>' --json`/);
|
|
3428
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-plugin-1", "ralplan-state.json")), true);
|
|
2703
3429
|
} finally {
|
|
2704
3430
|
await rm(cwd, { recursive: true, force: true });
|
|
2705
3431
|
}
|
|
@@ -2728,6 +3454,10 @@ standardMaxRounds = 15
|
|
|
2728
3454
|
);
|
|
2729
3455
|
assert.match(message, /Autopilot protocol:/);
|
|
2730
3456
|
assert.match(message, /deep-interview -> ralplan -> ultragoal -> code-review -> ultraqa/);
|
|
3457
|
+
assert.match(message, /structured question chain, not a one-question gate/);
|
|
3458
|
+
assert.match(message, /re-score ambiguity against the active threshold/);
|
|
3459
|
+
assert.match(message, /max_rounds as a cap/);
|
|
3460
|
+
assert.match(message, /Do not advance from deep-interview to ralplan merely because the first question was answered/);
|
|
2731
3461
|
assert.match(message, /Planner output has been reviewed sequentially by Architect and then Critic/);
|
|
2732
3462
|
assert.match(message, /do not hand off to Ultragoal or implementation until .*ralplan_architect_review.*ralplan_critic_review/);
|
|
2733
3463
|
} finally {
|
|
@@ -3280,6 +4010,11 @@ ${JSON.stringify({
|
|
|
3280
4010
|
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
3281
4011
|
);
|
|
3282
4012
|
assert.match(message, /"keep going" -> ralph/);
|
|
4013
|
+
assert.match(message, /Autopilot protocol:/);
|
|
4014
|
+
assert.match(message, /structured question chain, not a one-question gate/);
|
|
4015
|
+
assert.match(message, /re-score ambiguity against the active threshold/);
|
|
4016
|
+
assert.match(message, /max_rounds as a cap/);
|
|
4017
|
+
assert.match(message, /Do not advance from deep-interview to ralplan merely because the first question was answered/);
|
|
3283
4018
|
assert.doesNotMatch(message, /denied workflow keyword/i);
|
|
3284
4019
|
assert.doesNotMatch(message, /Unsupported workflow overlap: autopilot \+ ralph\./);
|
|
3285
4020
|
assert.doesNotMatch(message, /Prompt-side `\$ralph` activation/);
|
|
@@ -3289,6 +4024,122 @@ ${JSON.stringify({
|
|
|
3289
4024
|
}
|
|
3290
4025
|
});
|
|
3291
4026
|
|
|
4027
|
+
|
|
4028
|
+
it("keeps omx question answers on the active autopilot skill so the interview chain guidance is injected", async () => {
|
|
4029
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-question-answer-continuation-"));
|
|
4030
|
+
try {
|
|
4031
|
+
const sessionId = "sess-autopilot-question-answer";
|
|
4032
|
+
const sessionDir = join(cwd, ".omx", "state", "sessions", sessionId);
|
|
4033
|
+
await mkdir(sessionDir, { recursive: true });
|
|
4034
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
4035
|
+
version: 1,
|
|
4036
|
+
active: true,
|
|
4037
|
+
skill: "autopilot",
|
|
4038
|
+
keyword: "$autopilot",
|
|
4039
|
+
phase: "deep-interview",
|
|
4040
|
+
initialized_mode: "autopilot",
|
|
4041
|
+
initialized_state_path: `.omx/state/sessions/${sessionId}/autopilot-state.json`,
|
|
4042
|
+
session_id: sessionId,
|
|
4043
|
+
active_skills: [
|
|
4044
|
+
{ skill: "autopilot", phase: "deep-interview", active: true, session_id: sessionId },
|
|
4045
|
+
],
|
|
4046
|
+
});
|
|
4047
|
+
await writeJson(join(sessionDir, "autopilot-state.json"), {
|
|
4048
|
+
active: true,
|
|
4049
|
+
mode: "autopilot",
|
|
4050
|
+
current_phase: "deep-interview",
|
|
4051
|
+
started_at: "2026-04-19T00:00:00.000Z",
|
|
4052
|
+
updated_at: "2026-04-19T00:10:00.000Z",
|
|
4053
|
+
session_id: sessionId,
|
|
4054
|
+
});
|
|
4055
|
+
|
|
4056
|
+
const result = await dispatchCodexNativeHook(
|
|
4057
|
+
{
|
|
4058
|
+
hook_event_name: "UserPromptSubmit",
|
|
4059
|
+
cwd,
|
|
4060
|
+
session_id: sessionId,
|
|
4061
|
+
thread_id: "thread-autopilot-question-answer",
|
|
4062
|
+
turn_id: "turn-autopilot-question-answer",
|
|
4063
|
+
prompt: "[omx question answered] semantic_marker_expansion $ralplan",
|
|
4064
|
+
},
|
|
4065
|
+
{ cwd },
|
|
4066
|
+
);
|
|
4067
|
+
|
|
4068
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
4069
|
+
assert.equal(result.skillState?.skill, "autopilot");
|
|
4070
|
+
const message = String(
|
|
4071
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
4072
|
+
);
|
|
4073
|
+
assert.match(message, /continued active workflow skill "autopilot"/);
|
|
4074
|
+
assert.match(message, /Autopilot protocol:/);
|
|
4075
|
+
assert.match(message, /structured question chain, not a one-question gate/);
|
|
4076
|
+
assert.match(message, /This turn is a marked omx question answer/);
|
|
4077
|
+
assert.match(message, /then re-score/);
|
|
4078
|
+
assert.match(message, /write interview_complete evidence and hand off/);
|
|
4079
|
+
assert.match(message, /readiness gate remains unresolved and the answer would materially change execution/);
|
|
4080
|
+
assert.match(message, /Do not advance from deep-interview to ralplan merely because the first question was answered/);
|
|
4081
|
+
assert.doesNotMatch(message, /denied workflow keyword/i);
|
|
4082
|
+
assert.equal(existsSync(join(sessionDir, "ralplan-state.json")), false);
|
|
4083
|
+
} finally {
|
|
4084
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4085
|
+
}
|
|
4086
|
+
});
|
|
4087
|
+
|
|
4088
|
+
it("keeps deep-interview bridge guidance on marked question answers with workflow-like tokens", async () => {
|
|
4089
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-question-answer-continuation-"));
|
|
4090
|
+
try {
|
|
4091
|
+
const sessionId = "sess-deep-interview-question-answer";
|
|
4092
|
+
const sessionDir = join(cwd, ".omx", "state", "sessions", sessionId);
|
|
4093
|
+
await mkdir(sessionDir, { recursive: true });
|
|
4094
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
4095
|
+
version: 1,
|
|
4096
|
+
active: true,
|
|
4097
|
+
skill: "deep-interview",
|
|
4098
|
+
keyword: "$deep-interview",
|
|
4099
|
+
phase: "planning",
|
|
4100
|
+
initialized_mode: "deep-interview",
|
|
4101
|
+
initialized_state_path: `.omx/state/sessions/${sessionId}/deep-interview-state.json`,
|
|
4102
|
+
session_id: sessionId,
|
|
4103
|
+
active_skills: [
|
|
4104
|
+
{ skill: "deep-interview", phase: "planning", active: true, session_id: sessionId },
|
|
4105
|
+
],
|
|
4106
|
+
});
|
|
4107
|
+
await writeJson(join(sessionDir, "deep-interview-state.json"), {
|
|
4108
|
+
active: true,
|
|
4109
|
+
mode: "deep-interview",
|
|
4110
|
+
current_phase: "intent-first",
|
|
4111
|
+
started_at: "2026-04-21T10:00:00.000Z",
|
|
4112
|
+
updated_at: "2026-04-21T10:00:00.000Z",
|
|
4113
|
+
});
|
|
4114
|
+
|
|
4115
|
+
const result = await dispatchCodexNativeHook(
|
|
4116
|
+
{
|
|
4117
|
+
hook_event_name: "UserPromptSubmit",
|
|
4118
|
+
cwd,
|
|
4119
|
+
session_id: sessionId,
|
|
4120
|
+
thread_id: "thread-deep-interview-question-answer",
|
|
4121
|
+
turn_id: "turn-deep-interview-question-answer",
|
|
4122
|
+
prompt: "[omx question answered] answer text $ralplan",
|
|
4123
|
+
},
|
|
4124
|
+
{ cwd },
|
|
4125
|
+
);
|
|
4126
|
+
|
|
4127
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
4128
|
+
assert.equal(result.skillState?.skill, "deep-interview");
|
|
4129
|
+
const message = String(
|
|
4130
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
4131
|
+
);
|
|
4132
|
+
assert.match(message, /continued active workflow skill "deep-interview"/);
|
|
4133
|
+
assert.match(message, /workflow-like tokens inside the marked omx question answer are treated as answer text/);
|
|
4134
|
+
assert.match(message, /Deep-interview is active, but this session is not attached to tmux/);
|
|
4135
|
+
assert.match(message, /native structured question tool when available/);
|
|
4136
|
+
assert.doesNotMatch(message, /detected workflow keyword "\$ralplan" -> ralplan/);
|
|
4137
|
+
assert.equal(existsSync(join(sessionDir, "ralplan-state.json")), false);
|
|
4138
|
+
} finally {
|
|
4139
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4140
|
+
}
|
|
4141
|
+
});
|
|
4142
|
+
|
|
3292
4143
|
it("clarifies outside-tmux prompt-side deep-interview activation without pretending omx question is directly available", async () => {
|
|
3293
4144
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-routing-"));
|
|
3294
4145
|
try {
|
|
@@ -3810,6 +4661,10 @@ export async function onHookEvent(event) {
|
|
|
3810
4661
|
active: true,
|
|
3811
4662
|
mode: "deep-interview",
|
|
3812
4663
|
current_phase: "intent-first",
|
|
4664
|
+
deep_interview_gate: {
|
|
4665
|
+
status: "complete",
|
|
4666
|
+
rationale: "Requirements are clarified and ready for ralplan consensus.",
|
|
4667
|
+
},
|
|
3813
4668
|
});
|
|
3814
4669
|
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
3815
4670
|
active: true,
|
|
@@ -6936,27 +7791,71 @@ exit 0
|
|
|
6936
7791
|
current_phase: "team-exec",
|
|
6937
7792
|
});
|
|
6938
7793
|
|
|
6939
|
-
await dispatchCodexNativeHook(
|
|
7794
|
+
await dispatchCodexNativeHook(
|
|
7795
|
+
{
|
|
7796
|
+
hook_event_name: "PostToolUse",
|
|
7797
|
+
cwd,
|
|
7798
|
+
session_id: nativeSessionId,
|
|
7799
|
+
tool_name: "mcp__omx_state__state_write",
|
|
7800
|
+
tool_use_id: "tool-mcp-transport-team-native",
|
|
7801
|
+
tool_input: { mode: "team", active: true },
|
|
7802
|
+
tool_response: "{\"error\":\"MCP transport closed\",\"details\":\"stdio pipe closed before response\"}",
|
|
7803
|
+
},
|
|
7804
|
+
{ cwd },
|
|
7805
|
+
);
|
|
7806
|
+
|
|
7807
|
+
const phase = await readTeamPhase("transport-team", cwd);
|
|
7808
|
+
const attention = await readTeamLeaderAttention("transport-team", cwd);
|
|
7809
|
+
assert.equal(phase?.current_phase, "failed");
|
|
7810
|
+
assert.equal(attention?.leader_attention_reason, "mcp_transport_dead");
|
|
7811
|
+
assert.equal(attention?.leader_attention_pending, true);
|
|
7812
|
+
assert.equal(attention?.leader_session_id, canonicalSessionId);
|
|
7813
|
+
} finally {
|
|
7814
|
+
process.chdir(previousCwd);
|
|
7815
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7816
|
+
}
|
|
7817
|
+
});
|
|
7818
|
+
|
|
7819
|
+
it("does not block ordinary non-zero grep output in PostToolUse", async () => {
|
|
7820
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-grep-nonzero-"));
|
|
7821
|
+
try {
|
|
7822
|
+
const result = await dispatchCodexNativeHook(
|
|
7823
|
+
{
|
|
7824
|
+
hook_event_name: "PostToolUse",
|
|
7825
|
+
cwd,
|
|
7826
|
+
tool_name: "Bash",
|
|
7827
|
+
tool_use_id: "tool-grep-nonzero",
|
|
7828
|
+
tool_input: { command: "grep -R missing-pattern src | head -20" },
|
|
7829
|
+
tool_response: "{\"exit_code\":1,\"stdout\":\"src/example.ts:TODO\",\"stderr\":\"\"}",
|
|
7830
|
+
},
|
|
7831
|
+
{ cwd },
|
|
7832
|
+
);
|
|
7833
|
+
|
|
7834
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
7835
|
+
assert.equal(result.outputJson, null);
|
|
7836
|
+
} finally {
|
|
7837
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7838
|
+
}
|
|
7839
|
+
});
|
|
7840
|
+
|
|
7841
|
+
it("does not block ordinary non-zero diagnostic output in PostToolUse", async () => {
|
|
7842
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-diagnostic-nonzero-"));
|
|
7843
|
+
try {
|
|
7844
|
+
const result = await dispatchCodexNativeHook(
|
|
6940
7845
|
{
|
|
6941
7846
|
hook_event_name: "PostToolUse",
|
|
6942
7847
|
cwd,
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
tool_response: "{\"error\":\"MCP transport closed\",\"details\":\"stdio pipe closed before response\"}",
|
|
7848
|
+
tool_name: "Bash",
|
|
7849
|
+
tool_use_id: "tool-diagnostic-nonzero",
|
|
7850
|
+
tool_input: { command: "find src -name nope -print" },
|
|
7851
|
+
tool_response: "{\"exit_code\":1,\"stdout\":\"searched 10 files\",\"stderr\":\"\"}",
|
|
6948
7852
|
},
|
|
6949
7853
|
{ cwd },
|
|
6950
7854
|
);
|
|
6951
7855
|
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
assert.equal(phase?.current_phase, "failed");
|
|
6955
|
-
assert.equal(attention?.leader_attention_reason, "mcp_transport_dead");
|
|
6956
|
-
assert.equal(attention?.leader_attention_pending, true);
|
|
6957
|
-
assert.equal(attention?.leader_session_id, canonicalSessionId);
|
|
7856
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
7857
|
+
assert.equal(result.outputJson, null);
|
|
6958
7858
|
} finally {
|
|
6959
|
-
process.chdir(previousCwd);
|
|
6960
7859
|
await rm(cwd, { recursive: true, force: true });
|
|
6961
7860
|
}
|
|
6962
7861
|
});
|
|
@@ -7021,6 +7920,84 @@ exit 0
|
|
|
7021
7920
|
}
|
|
7022
7921
|
});
|
|
7023
7922
|
|
|
7923
|
+
it("treats wrapped gh pr checks output as reviewable", async () => {
|
|
7924
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-gh-wrapped-"));
|
|
7925
|
+
try {
|
|
7926
|
+
for (const command of [
|
|
7927
|
+
"GH_PAGER=cat gh pr checks",
|
|
7928
|
+
"env GH_TOKEN=ghp_testtoken gh pr checks",
|
|
7929
|
+
"/usr/bin/env gh pr checks",
|
|
7930
|
+
"env -- gh pr checks",
|
|
7931
|
+
"env -C repo gh pr checks",
|
|
7932
|
+
"/usr/bin/gh pr checks",
|
|
7933
|
+
"gh --repo owner/repo pr checks",
|
|
7934
|
+
"echo a; gh pr checks",
|
|
7935
|
+
"cd repo && gh pr checks",
|
|
7936
|
+
]) {
|
|
7937
|
+
const result = await dispatchCodexNativeHook(
|
|
7938
|
+
{
|
|
7939
|
+
hook_event_name: "PostToolUse",
|
|
7940
|
+
cwd,
|
|
7941
|
+
tool_name: "Bash",
|
|
7942
|
+
tool_use_id: `tool-useful-${command}`,
|
|
7943
|
+
tool_input: { command },
|
|
7944
|
+
tool_response: "{\"exit_code\":8,\"stdout\":\"build pending\",\"stderr\":\"\"}",
|
|
7945
|
+
},
|
|
7946
|
+
{ cwd },
|
|
7947
|
+
);
|
|
7948
|
+
|
|
7949
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
7950
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block", command);
|
|
7951
|
+
}
|
|
7952
|
+
} finally {
|
|
7953
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7954
|
+
}
|
|
7955
|
+
});
|
|
7956
|
+
|
|
7957
|
+
it("does not treat heredoc gh pr checks text as a reviewable command", async () => {
|
|
7958
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-gh-heredoc-"));
|
|
7959
|
+
try {
|
|
7960
|
+
const result = await dispatchCodexNativeHook(
|
|
7961
|
+
{
|
|
7962
|
+
hook_event_name: "PostToolUse",
|
|
7963
|
+
cwd,
|
|
7964
|
+
tool_name: "Bash",
|
|
7965
|
+
tool_use_id: "tool-heredoc-gh-checks",
|
|
7966
|
+
tool_input: { command: "cat <<'EOF'\ngh pr checks\nEOF\nfalse" },
|
|
7967
|
+
tool_response: "{\"exit_code\":1,\"stdout\":\"gh pr checks\",\"stderr\":\"\"}",
|
|
7968
|
+
},
|
|
7969
|
+
{ cwd },
|
|
7970
|
+
);
|
|
7971
|
+
|
|
7972
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
7973
|
+
assert.equal(result.outputJson, null);
|
|
7974
|
+
} finally {
|
|
7975
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7976
|
+
}
|
|
7977
|
+
});
|
|
7978
|
+
|
|
7979
|
+
it("does not treat echoed gh pr checks text as a reviewable command", async () => {
|
|
7980
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-gh-echo-"));
|
|
7981
|
+
try {
|
|
7982
|
+
const result = await dispatchCodexNativeHook(
|
|
7983
|
+
{
|
|
7984
|
+
hook_event_name: "PostToolUse",
|
|
7985
|
+
cwd,
|
|
7986
|
+
tool_name: "Bash",
|
|
7987
|
+
tool_use_id: "tool-echo-gh-checks",
|
|
7988
|
+
tool_input: { command: "echo gh pr checks" },
|
|
7989
|
+
tool_response: "{\"exit_code\":1,\"stdout\":\"gh pr checks\",\"stderr\":\"\"}",
|
|
7990
|
+
},
|
|
7991
|
+
{ cwd },
|
|
7992
|
+
);
|
|
7993
|
+
|
|
7994
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
7995
|
+
assert.equal(result.outputJson, null);
|
|
7996
|
+
} finally {
|
|
7997
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7998
|
+
}
|
|
7999
|
+
});
|
|
8000
|
+
|
|
7024
8001
|
it("returns MCP transport-death guidance and preserves failed team state", async () => {
|
|
7025
8002
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-dead-"));
|
|
7026
8003
|
try {
|
|
@@ -8029,31 +9006,341 @@ exit 0
|
|
|
8029
9006
|
process.env.OMX_TEAM_STATE_ROOT = stateDir;
|
|
8030
9007
|
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
8031
9008
|
|
|
8032
|
-
const result = await dispatchCodexNativeHook(
|
|
8033
|
-
{
|
|
8034
|
-
hook_event_name: "Stop",
|
|
8035
|
-
cwd,
|
|
8036
|
-
session_id: "sess-stop-team-worker-busy-leader",
|
|
8037
|
-
},
|
|
8038
|
-
{ cwd },
|
|
8039
|
-
);
|
|
9009
|
+
const result = await dispatchCodexNativeHook(
|
|
9010
|
+
{
|
|
9011
|
+
hook_event_name: "Stop",
|
|
9012
|
+
cwd,
|
|
9013
|
+
session_id: "sess-stop-team-worker-busy-leader",
|
|
9014
|
+
},
|
|
9015
|
+
{ cwd },
|
|
9016
|
+
);
|
|
9017
|
+
|
|
9018
|
+
assert.equal(result.outputJson, null);
|
|
9019
|
+
const tmuxLog = await readFile(tmuxLogPath, "utf-8");
|
|
9020
|
+
assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
|
|
9021
|
+
assert.match(tmuxLog, /send-keys -t %42 Tab/);
|
|
9022
|
+
assert.match(tmuxLog, /send-keys -t %42 C-m/);
|
|
9023
|
+
assert.ok(
|
|
9024
|
+
tmuxLog.indexOf("send-keys -t %42 Tab") < tmuxLog.indexOf("send-keys -t %42 C-m"),
|
|
9025
|
+
"busy worker-stop nudge should press Tab before C-m",
|
|
9026
|
+
);
|
|
9027
|
+
const nudgeState = JSON.parse(await readFile(join(workerDir, "worker-stop-nudge.json"), "utf-8"));
|
|
9028
|
+
assert.equal(nudgeState.delivery, "queued");
|
|
9029
|
+
} finally {
|
|
9030
|
+
if (typeof prevTeamWorker === "string") process.env.OMX_TEAM_WORKER = prevTeamWorker;
|
|
9031
|
+
else delete process.env.OMX_TEAM_WORKER;
|
|
9032
|
+
if (typeof prevTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
|
|
9033
|
+
else delete process.env.OMX_TEAM_STATE_ROOT;
|
|
9034
|
+
if (typeof prevPath === "string") process.env.PATH = prevPath;
|
|
9035
|
+
else delete process.env.PATH;
|
|
9036
|
+
await rm(cwd, { recursive: true, force: true });
|
|
9037
|
+
}
|
|
9038
|
+
});
|
|
9039
|
+
|
|
9040
|
+
it("dedupes allowed worker Stop leader nudges across workers in the same team window", async () => {
|
|
9041
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-team-dedupe-"));
|
|
9042
|
+
const prevPath = process.env.PATH;
|
|
9043
|
+
try {
|
|
9044
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
9045
|
+
const logsDir = join(cwd, ".omx", "logs");
|
|
9046
|
+
const teamName = "worker-stop-team-dedupe";
|
|
9047
|
+
const teamDir = join(stateDir, "team", teamName);
|
|
9048
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
9049
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
9050
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
9051
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
|
|
9052
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
9053
|
+
await writeJson(join(teamDir, "manifest.v2.json"), {
|
|
9054
|
+
name: teamName,
|
|
9055
|
+
tmux_session: "omx-team-worker-stop",
|
|
9056
|
+
leader_pane_id: "%42",
|
|
9057
|
+
workers: [
|
|
9058
|
+
{ name: "worker-1", index: 1, pane_id: "%10" },
|
|
9059
|
+
{ name: "worker-2", index: 2, pane_id: "%11" },
|
|
9060
|
+
],
|
|
9061
|
+
});
|
|
9062
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
9063
|
+
|
|
9064
|
+
const first = await maybeNudgeLeaderForAllowedWorkerStop({
|
|
9065
|
+
stateDir,
|
|
9066
|
+
logsDir,
|
|
9067
|
+
workerContext: { teamName, workerName: "worker-1" },
|
|
9068
|
+
});
|
|
9069
|
+
const second = await maybeNudgeLeaderForAllowedWorkerStop({
|
|
9070
|
+
stateDir,
|
|
9071
|
+
logsDir,
|
|
9072
|
+
workerContext: { teamName, workerName: "worker-2" },
|
|
9073
|
+
});
|
|
9074
|
+
|
|
9075
|
+
assert.equal(first.result, "sent");
|
|
9076
|
+
assert.equal(second.result, "suppressed_team_cooldown");
|
|
9077
|
+
const tmuxLog = await readFile(tmuxLogPath, "utf-8");
|
|
9078
|
+
const stopNudges = tmuxLog.match(/send-keys -t %42 -l \[OMX\] worker-\d+ native Stop allowed/g) || [];
|
|
9079
|
+
assert.equal(stopNudges.length, 1, "same-team workers should share one leader nudge cooldown window");
|
|
9080
|
+
const teamNudgeState = JSON.parse(await readFile(join(teamDir, "worker-stop-nudge.json"), "utf-8"));
|
|
9081
|
+
assert.equal(teamNudgeState.worker, "worker-1");
|
|
9082
|
+
assert.equal(teamNudgeState.delivery, "sent");
|
|
9083
|
+
} finally {
|
|
9084
|
+
if (typeof prevPath === "string") process.env.PATH = prevPath;
|
|
9085
|
+
else delete process.env.PATH;
|
|
9086
|
+
await rm(cwd, { recursive: true, force: true });
|
|
9087
|
+
}
|
|
9088
|
+
});
|
|
9089
|
+
|
|
9090
|
+
it("serializes concurrent allowed worker Stop leader nudges with a team lock", async () => {
|
|
9091
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-concurrent-dedupe-"));
|
|
9092
|
+
const prevPath = process.env.PATH;
|
|
9093
|
+
try {
|
|
9094
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
9095
|
+
const logsDir = join(cwd, ".omx", "logs");
|
|
9096
|
+
const teamName = "worker-stop-concurrent";
|
|
9097
|
+
const teamDir = join(stateDir, "team", teamName);
|
|
9098
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
9099
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
9100
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
9101
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath, { sendDelayMs: 100 }));
|
|
9102
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
9103
|
+
await writeJson(join(teamDir, "manifest.v2.json"), {
|
|
9104
|
+
name: teamName,
|
|
9105
|
+
tmux_session: "omx-team-worker-stop",
|
|
9106
|
+
leader_pane_id: "%42",
|
|
9107
|
+
workers: [
|
|
9108
|
+
{ name: "worker-1", index: 1, pane_id: "%10" },
|
|
9109
|
+
{ name: "worker-2", index: 2, pane_id: "%11" },
|
|
9110
|
+
],
|
|
9111
|
+
});
|
|
9112
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
9113
|
+
|
|
9114
|
+
const results = await Promise.all([
|
|
9115
|
+
maybeNudgeLeaderForAllowedWorkerStop({
|
|
9116
|
+
stateDir,
|
|
9117
|
+
logsDir,
|
|
9118
|
+
workerContext: { teamName, workerName: "worker-1" },
|
|
9119
|
+
}),
|
|
9120
|
+
maybeNudgeLeaderForAllowedWorkerStop({
|
|
9121
|
+
stateDir,
|
|
9122
|
+
logsDir,
|
|
9123
|
+
workerContext: { teamName, workerName: "worker-2" },
|
|
9124
|
+
}),
|
|
9125
|
+
]);
|
|
9126
|
+
|
|
9127
|
+
assert.equal(results.filter((result) => result.result === "sent").length, 1);
|
|
9128
|
+
assert.equal(results.filter((result) => result.result === "suppressed_team_lock_held").length, 1);
|
|
9129
|
+
const tmuxLog = await readFile(tmuxLogPath, "utf-8");
|
|
9130
|
+
const stopNudges = tmuxLog.match(/send-keys -t %42 -l \[OMX\] worker-\d+ native Stop allowed/g) || [];
|
|
9131
|
+
assert.equal(stopNudges.length, 1, "concurrent same-team workers should emit only one leader nudge");
|
|
9132
|
+
assert.equal(existsSync(join(teamDir, "worker-stop-nudge.lock")), false);
|
|
9133
|
+
} finally {
|
|
9134
|
+
if (typeof prevPath === "string") process.env.PATH = prevPath;
|
|
9135
|
+
else delete process.env.PATH;
|
|
9136
|
+
await rm(cwd, { recursive: true, force: true });
|
|
9137
|
+
}
|
|
9138
|
+
});
|
|
9139
|
+
|
|
9140
|
+
it("skips worker Stop leader nudge when team state is missing or shut down", async () => {
|
|
9141
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-missing-team-"));
|
|
9142
|
+
try {
|
|
9143
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
9144
|
+
const logsDir = join(cwd, ".omx", "logs");
|
|
9145
|
+
const result = await maybeNudgeLeaderForAllowedWorkerStop({
|
|
9146
|
+
stateDir,
|
|
9147
|
+
logsDir,
|
|
9148
|
+
workerContext: { teamName: "removed-team", workerName: "worker-1" },
|
|
9149
|
+
});
|
|
9150
|
+
|
|
9151
|
+
assert.equal(result.result, "team_state_gone_or_shutdown");
|
|
9152
|
+
assert.equal(existsSync(join(stateDir, "team", "removed-team", "worker-stop-nudge.json")), false);
|
|
9153
|
+
|
|
9154
|
+
await writeJson(join(stateDir, "team", "shutdown-team", "shutdown.json"), {
|
|
9155
|
+
started_at: new Date().toISOString(),
|
|
9156
|
+
});
|
|
9157
|
+
const shutdownResult = await maybeNudgeLeaderForAllowedWorkerStop({
|
|
9158
|
+
stateDir,
|
|
9159
|
+
logsDir,
|
|
9160
|
+
workerContext: { teamName: "shutdown-team", workerName: "worker-1" },
|
|
9161
|
+
});
|
|
9162
|
+
assert.equal(shutdownResult.result, "team_state_gone_or_shutdown");
|
|
9163
|
+
assert.equal(existsSync(join(stateDir, "team", "shutdown-team", "worker-stop-nudge.json")), false);
|
|
9164
|
+
} finally {
|
|
9165
|
+
await rm(cwd, { recursive: true, force: true });
|
|
9166
|
+
}
|
|
9167
|
+
});
|
|
9168
|
+
|
|
9169
|
+
it("does not treat old visible worker Stop transcript as pending queue state", async () => {
|
|
9170
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-queue-dedupe-"));
|
|
9171
|
+
const prevPath = process.env.PATH;
|
|
9172
|
+
try {
|
|
9173
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
9174
|
+
const logsDir = join(cwd, ".omx", "logs");
|
|
9175
|
+
const teamName = "queued-stop-dedupe";
|
|
9176
|
+
const teamDir = join(stateDir, "team", teamName);
|
|
9177
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
9178
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
9179
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
9180
|
+
await writeFile(
|
|
9181
|
+
join(fakeBinDir, "tmux"),
|
|
9182
|
+
buildWorkerStopFakeTmux(tmuxLogPath, {
|
|
9183
|
+
busyLeader: true,
|
|
9184
|
+
captureText:
|
|
9185
|
+
`[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`
|
|
9186
|
+
+ "• Working… (esc to interrupt)",
|
|
9187
|
+
}),
|
|
9188
|
+
);
|
|
9189
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
9190
|
+
await writeJson(join(teamDir, "manifest.v2.json"), {
|
|
9191
|
+
name: teamName,
|
|
9192
|
+
tmux_session: "omx-team-worker-stop",
|
|
9193
|
+
leader_pane_id: "%42",
|
|
9194
|
+
workers: [{ name: "worker-2", index: 2, pane_id: "%11" }],
|
|
9195
|
+
});
|
|
9196
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
9197
|
+
|
|
9198
|
+
const result = await maybeNudgeLeaderForAllowedWorkerStop({
|
|
9199
|
+
stateDir,
|
|
9200
|
+
logsDir,
|
|
9201
|
+
workerContext: { teamName, workerName: "worker-2" },
|
|
9202
|
+
});
|
|
9203
|
+
|
|
9204
|
+
assert.equal(result.result, "queued");
|
|
9205
|
+
const tmuxLog = await readFile(tmuxLogPath, "utf-8");
|
|
9206
|
+
assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-2 native Stop allowed/);
|
|
9207
|
+
assert.match(tmuxLog, /send-keys -t %42 Tab/);
|
|
9208
|
+
const teamNudgeState = JSON.parse(await readFile(join(teamDir, "worker-stop-nudge.json"), "utf-8"));
|
|
9209
|
+
assert.equal(teamNudgeState.worker, "worker-2");
|
|
9210
|
+
assert.equal(teamNudgeState.delivery, "queued");
|
|
9211
|
+
} finally {
|
|
9212
|
+
if (typeof prevPath === "string") process.env.PATH = prevPath;
|
|
9213
|
+
else delete process.env.PATH;
|
|
9214
|
+
await rm(cwd, { recursive: true, force: true });
|
|
9215
|
+
}
|
|
9216
|
+
});
|
|
9217
|
+
|
|
9218
|
+
it("reports deferred when non-teardown persistence failure prevents worker Stop nudge cooldown state", async () => {
|
|
9219
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-persist-fail-"));
|
|
9220
|
+
const prevPath = process.env.PATH;
|
|
9221
|
+
try {
|
|
9222
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
9223
|
+
const logsDir = join(cwd, ".omx", "logs");
|
|
9224
|
+
const teamName = "worker-stop-persist-fail";
|
|
9225
|
+
const teamDir = join(stateDir, "team", teamName);
|
|
9226
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
9227
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
9228
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
9229
|
+
await writeJson(join(teamDir, "manifest.v2.json"), {
|
|
9230
|
+
name: teamName,
|
|
9231
|
+
tmux_session: "omx-team-worker-stop",
|
|
9232
|
+
leader_pane_id: "%42",
|
|
9233
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
9234
|
+
});
|
|
9235
|
+
await writeFile(join(teamDir, "workers"), "not a directory");
|
|
9236
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
|
|
9237
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
9238
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
9239
|
+
|
|
9240
|
+
const result = await maybeNudgeLeaderForAllowedWorkerStop({
|
|
9241
|
+
stateDir,
|
|
9242
|
+
logsDir,
|
|
9243
|
+
workerContext: { teamName, workerName: "worker-1" },
|
|
9244
|
+
});
|
|
9245
|
+
|
|
9246
|
+
assert.equal(result.result, "deferred");
|
|
9247
|
+
assert.equal(existsSync(join(teamDir, "worker-stop-nudge.json")), false);
|
|
9248
|
+
assert.equal(existsSync(join(teamDir, "workers", "worker-1", "worker-stop-nudge.json")), false);
|
|
9249
|
+
const tmuxLog = await readFile(tmuxLogPath, "utf-8");
|
|
9250
|
+
assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
|
|
9251
|
+
const deliveryLogPath = join(logsDir, `team-delivery-${new Date().toISOString().split("T")[0]}.jsonl`);
|
|
9252
|
+
const deliveryEvents = (await readFile(deliveryLogPath, "utf-8"))
|
|
9253
|
+
.trim()
|
|
9254
|
+
.split("\n")
|
|
9255
|
+
.map((line) => JSON.parse(line));
|
|
9256
|
+
const deferredEvent = deliveryEvents.find((event) => event.event === "nudge_triggered" && event.result === "deferred");
|
|
9257
|
+
assert.equal(deferredEvent?.team, teamName);
|
|
9258
|
+
assert.equal(deferredEvent?.from_worker, "worker-1");
|
|
9259
|
+
assert.match(String(deferredEvent?.reason || ""), /EEXIST|ENOTDIR|not a directory|file already exists/);
|
|
9260
|
+
} finally {
|
|
9261
|
+
if (typeof prevPath === "string") process.env.PATH = prevPath;
|
|
9262
|
+
else delete process.env.PATH;
|
|
9263
|
+
await rm(cwd, { recursive: true, force: true });
|
|
9264
|
+
}
|
|
9265
|
+
});
|
|
9266
|
+
|
|
9267
|
+
it("does not recreate team state when teardown removes it during worker Stop delivery", async () => {
|
|
9268
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-teardown-race-"));
|
|
9269
|
+
const prevPath = process.env.PATH;
|
|
9270
|
+
try {
|
|
9271
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
9272
|
+
const logsDir = join(cwd, ".omx", "logs");
|
|
9273
|
+
const teamName = "worker-stop-teardown-race";
|
|
9274
|
+
const teamDir = join(stateDir, "team", teamName);
|
|
9275
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
9276
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
9277
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
9278
|
+
await writeJson(join(teamDir, "manifest.v2.json"), {
|
|
9279
|
+
name: teamName,
|
|
9280
|
+
tmux_session: "omx-team-worker-stop",
|
|
9281
|
+
leader_pane_id: "%42",
|
|
9282
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
9283
|
+
});
|
|
9284
|
+
await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath, { removePathOnSend: teamDir }));
|
|
9285
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
9286
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
9287
|
+
|
|
9288
|
+
const result = await maybeNudgeLeaderForAllowedWorkerStop({
|
|
9289
|
+
stateDir,
|
|
9290
|
+
logsDir,
|
|
9291
|
+
workerContext: { teamName, workerName: "worker-1" },
|
|
9292
|
+
});
|
|
9293
|
+
|
|
9294
|
+
assert.equal(result.result, "sent");
|
|
9295
|
+
assert.equal(existsSync(teamDir), false, "worker Stop delivery must not recreate removed team state");
|
|
9296
|
+
const tmuxLog = await readFile(tmuxLogPath, "utf-8");
|
|
9297
|
+
assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
|
|
9298
|
+
} finally {
|
|
9299
|
+
if (typeof prevPath === "string") process.env.PATH = prevPath;
|
|
9300
|
+
else delete process.env.PATH;
|
|
9301
|
+
await rm(cwd, { recursive: true, force: true });
|
|
9302
|
+
}
|
|
9303
|
+
});
|
|
9304
|
+
|
|
9305
|
+
it("does not recreate team state when teardown removes it before deferred worker Stop recording", async () => {
|
|
9306
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-deferred-teardown-"));
|
|
9307
|
+
const prevPath = process.env.PATH;
|
|
9308
|
+
try {
|
|
9309
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
9310
|
+
const logsDir = join(cwd, ".omx", "logs");
|
|
9311
|
+
const teamName = "worker-stop-deferred-teardown";
|
|
9312
|
+
const teamDir = join(stateDir, "team", teamName);
|
|
9313
|
+
const fakeBinDir = join(cwd, "fake-bin");
|
|
9314
|
+
const tmuxLogPath = join(cwd, "tmux.log");
|
|
9315
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
9316
|
+
await writeJson(join(teamDir, "manifest.v2.json"), {
|
|
9317
|
+
name: teamName,
|
|
9318
|
+
tmux_session: "omx-team-worker-stop",
|
|
9319
|
+
leader_pane_id: "%42",
|
|
9320
|
+
workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
|
|
9321
|
+
});
|
|
9322
|
+
await writeFile(
|
|
9323
|
+
join(fakeBinDir, "tmux"),
|
|
9324
|
+
buildWorkerStopFakeTmux(tmuxLogPath, {
|
|
9325
|
+
currentCommand: "bash",
|
|
9326
|
+
captureText: "$ ",
|
|
9327
|
+
removePathOnCapture: teamDir,
|
|
9328
|
+
}),
|
|
9329
|
+
);
|
|
9330
|
+
await chmod(join(fakeBinDir, "tmux"), 0o755);
|
|
9331
|
+
process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
|
|
9332
|
+
|
|
9333
|
+
const result = await maybeNudgeLeaderForAllowedWorkerStop({
|
|
9334
|
+
stateDir,
|
|
9335
|
+
logsDir,
|
|
9336
|
+
workerContext: { teamName, workerName: "worker-1" },
|
|
9337
|
+
});
|
|
8040
9338
|
|
|
8041
|
-
assert.equal(result.
|
|
9339
|
+
assert.equal(result.result, "team_state_gone_or_shutdown");
|
|
9340
|
+
assert.equal(existsSync(teamDir), false, "deferred worker Stop recording must not recreate removed team state");
|
|
8042
9341
|
const tmuxLog = await readFile(tmuxLogPath, "utf-8");
|
|
8043
|
-
assert.
|
|
8044
|
-
assert.match(tmuxLog, /send-keys -t %42 Tab/);
|
|
8045
|
-
assert.match(tmuxLog, /send-keys -t %42 C-m/);
|
|
8046
|
-
assert.ok(
|
|
8047
|
-
tmuxLog.indexOf("send-keys -t %42 Tab") < tmuxLog.indexOf("send-keys -t %42 C-m"),
|
|
8048
|
-
"busy worker-stop nudge should press Tab before C-m",
|
|
8049
|
-
);
|
|
8050
|
-
const nudgeState = JSON.parse(await readFile(join(workerDir, "worker-stop-nudge.json"), "utf-8"));
|
|
8051
|
-
assert.equal(nudgeState.delivery, "queued");
|
|
9342
|
+
assert.doesNotMatch(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
|
|
8052
9343
|
} finally {
|
|
8053
|
-
if (typeof prevTeamWorker === "string") process.env.OMX_TEAM_WORKER = prevTeamWorker;
|
|
8054
|
-
else delete process.env.OMX_TEAM_WORKER;
|
|
8055
|
-
if (typeof prevTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
|
|
8056
|
-
else delete process.env.OMX_TEAM_STATE_ROOT;
|
|
8057
9344
|
if (typeof prevPath === "string") process.env.PATH = prevPath;
|
|
8058
9345
|
else delete process.env.PATH;
|
|
8059
9346
|
await rm(cwd, { recursive: true, force: true });
|
|
@@ -11618,19 +12905,259 @@ exit 0
|
|
|
11618
12905
|
await rm(cwd, { recursive: true, force: true });
|
|
11619
12906
|
}
|
|
11620
12907
|
});
|
|
11621
|
-
|
|
11622
|
-
it("auto-continues native Stop on permission-seeking prompts", async () => {
|
|
11623
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-permission-"));
|
|
12908
|
+
|
|
12909
|
+
it("auto-continues native Stop on permission-seeking prompts", async () => {
|
|
12910
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-permission-"));
|
|
12911
|
+
try {
|
|
12912
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
12913
|
+
process.env.OMX_SESSION_ID = "sess-stop-auto-permission";
|
|
12914
|
+
|
|
12915
|
+
const result = await dispatchCodexNativeHook(
|
|
12916
|
+
{
|
|
12917
|
+
hook_event_name: "Stop",
|
|
12918
|
+
cwd,
|
|
12919
|
+
session_id: "sess-stop-auto-permission",
|
|
12920
|
+
last_assistant_message: "Would you like me to continue with the cleanup?",
|
|
12921
|
+
},
|
|
12922
|
+
{ cwd },
|
|
12923
|
+
);
|
|
12924
|
+
|
|
12925
|
+
assert.equal(result.omxEventName, "stop");
|
|
12926
|
+
assert.deepEqual(result.outputJson, {
|
|
12927
|
+
decision: "block",
|
|
12928
|
+
reason: DEFAULT_AUTO_NUDGE_RESPONSE,
|
|
12929
|
+
stopReason: "auto_nudge",
|
|
12930
|
+
systemMessage:
|
|
12931
|
+
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
12932
|
+
});
|
|
12933
|
+
} finally {
|
|
12934
|
+
await rm(cwd, { recursive: true, force: true });
|
|
12935
|
+
}
|
|
12936
|
+
});
|
|
12937
|
+
|
|
12938
|
+
it("auto-continues native Stop on \"if you want\" permission-seeking prompts", async () => {
|
|
12939
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-if-you-want-"));
|
|
12940
|
+
try {
|
|
12941
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
12942
|
+
process.env.OMX_SESSION_ID = "sess-stop-auto-if-you-want";
|
|
12943
|
+
|
|
12944
|
+
const result = await dispatchCodexNativeHook(
|
|
12945
|
+
{
|
|
12946
|
+
hook_event_name: "Stop",
|
|
12947
|
+
cwd,
|
|
12948
|
+
session_id: "sess-stop-auto-if-you-want",
|
|
12949
|
+
last_assistant_message: "If you want, I can continue with the cleanup from here.",
|
|
12950
|
+
},
|
|
12951
|
+
{ cwd },
|
|
12952
|
+
);
|
|
12953
|
+
|
|
12954
|
+
assert.equal(result.omxEventName, "stop");
|
|
12955
|
+
assert.deepEqual(result.outputJson, {
|
|
12956
|
+
decision: "block",
|
|
12957
|
+
reason: DEFAULT_AUTO_NUDGE_RESPONSE,
|
|
12958
|
+
stopReason: "auto_nudge",
|
|
12959
|
+
systemMessage:
|
|
12960
|
+
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
12961
|
+
});
|
|
12962
|
+
} finally {
|
|
12963
|
+
await rm(cwd, { recursive: true, force: true });
|
|
12964
|
+
}
|
|
12965
|
+
});
|
|
12966
|
+
|
|
12967
|
+
it("does not auto-continue native Stop while deep-interview is waiting on an intent-first question", async () => {
|
|
12968
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-deep-interview-question-"));
|
|
12969
|
+
try {
|
|
12970
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
12971
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-auto-question"), { recursive: true });
|
|
12972
|
+
process.env.OMX_SESSION_ID = "sess-stop-auto-question";
|
|
12973
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-auto-question" });
|
|
12974
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-auto-question", "skill-active-state.json"), {
|
|
12975
|
+
version: 1,
|
|
12976
|
+
active: true,
|
|
12977
|
+
skill: "deep-interview",
|
|
12978
|
+
phase: "planning",
|
|
12979
|
+
session_id: "sess-stop-auto-question",
|
|
12980
|
+
thread_id: "thread-stop-auto-question",
|
|
12981
|
+
input_lock: {
|
|
12982
|
+
active: true,
|
|
12983
|
+
scope: "deep-interview-auto-approval",
|
|
12984
|
+
blocked_inputs: ["yes", "proceed"],
|
|
12985
|
+
message: "Deep interview is active; auto-approval shortcuts are blocked until the interview finishes.",
|
|
12986
|
+
},
|
|
12987
|
+
});
|
|
12988
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-auto-question", "deep-interview-state.json"), {
|
|
12989
|
+
active: true,
|
|
12990
|
+
mode: "deep-interview",
|
|
12991
|
+
current_phase: "intent-first",
|
|
12992
|
+
});
|
|
12993
|
+
|
|
12994
|
+
const result = await dispatchCodexNativeHook(
|
|
12995
|
+
{
|
|
12996
|
+
hook_event_name: "Stop",
|
|
12997
|
+
cwd,
|
|
12998
|
+
session_id: "sess-stop-auto-question",
|
|
12999
|
+
thread_id: "thread-stop-auto-question",
|
|
13000
|
+
turn_id: "turn-stop-auto-question-1",
|
|
13001
|
+
last_assistant_message: [
|
|
13002
|
+
"Round 2 | Target: Decision boundary | Ambiguity: 24%",
|
|
13003
|
+
"",
|
|
13004
|
+
"If an existing project spider still declares session_mode = \"owned\", should ZenX fail loudly so the stale attribute is removed, or should it ignore the attribute and initialize the session pool anyway?",
|
|
13005
|
+
"Keep going once I have your answer.",
|
|
13006
|
+
].join("\n"),
|
|
13007
|
+
},
|
|
13008
|
+
{ cwd },
|
|
13009
|
+
);
|
|
13010
|
+
|
|
13011
|
+
assert.equal(result.omxEventName, "stop");
|
|
13012
|
+
assert.equal(result.outputJson, null);
|
|
13013
|
+
} finally {
|
|
13014
|
+
await rm(cwd, { recursive: true, force: true });
|
|
13015
|
+
}
|
|
13016
|
+
});
|
|
13017
|
+
|
|
13018
|
+
it("suppresses native auto-nudge re-fire while session-scoped deep-interview state is still active", async () => {
|
|
13019
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-deep-interview-state-"));
|
|
13020
|
+
try {
|
|
13021
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
13022
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-auto-interview"), { recursive: true });
|
|
13023
|
+
process.env.OMX_SESSION_ID = "sess-stop-auto-interview";
|
|
13024
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-auto-interview" });
|
|
13025
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-auto-interview", "deep-interview-state.json"), {
|
|
13026
|
+
active: true,
|
|
13027
|
+
mode: "deep-interview",
|
|
13028
|
+
current_phase: "intent-first",
|
|
13029
|
+
});
|
|
13030
|
+
|
|
13031
|
+
const result = await dispatchCodexNativeHook(
|
|
13032
|
+
{
|
|
13033
|
+
hook_event_name: "Stop",
|
|
13034
|
+
cwd,
|
|
13035
|
+
session_id: "sess-stop-auto-interview",
|
|
13036
|
+
thread_id: "thread-stop-auto-interview",
|
|
13037
|
+
turn_id: "turn-stop-auto-interview-2",
|
|
13038
|
+
stop_hook_active: true,
|
|
13039
|
+
last_assistant_message: "If you want, I can keep going from here.",
|
|
13040
|
+
},
|
|
13041
|
+
{ cwd },
|
|
13042
|
+
);
|
|
13043
|
+
|
|
13044
|
+
assert.equal(result.omxEventName, "stop");
|
|
13045
|
+
assert.equal(result.outputJson, null);
|
|
13046
|
+
} finally {
|
|
13047
|
+
await rm(cwd, { recursive: true, force: true });
|
|
13048
|
+
}
|
|
13049
|
+
});
|
|
13050
|
+
|
|
13051
|
+
it("suppresses native auto-nudge when root deep-interview mode state is active and no session is known", async () => {
|
|
13052
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-deep-interview-mode-"));
|
|
13053
|
+
try {
|
|
13054
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
13055
|
+
await mkdir(stateDir, { recursive: true });
|
|
13056
|
+
await writeJson(join(stateDir, "deep-interview-state.json"), {
|
|
13057
|
+
active: true,
|
|
13058
|
+
mode: "deep-interview",
|
|
13059
|
+
current_phase: "intent-first",
|
|
13060
|
+
});
|
|
13061
|
+
|
|
13062
|
+
const result = await dispatchCodexNativeHook(
|
|
13063
|
+
{
|
|
13064
|
+
hook_event_name: "Stop",
|
|
13065
|
+
cwd,
|
|
13066
|
+
turn_id: "turn-stop-auto-mode-1",
|
|
13067
|
+
last_assistant_message: "Would you like me to continue with the next step?",
|
|
13068
|
+
},
|
|
13069
|
+
{ cwd },
|
|
13070
|
+
);
|
|
13071
|
+
|
|
13072
|
+
assert.equal(result.omxEventName, "stop");
|
|
13073
|
+
assert.equal(result.outputJson, null);
|
|
13074
|
+
} finally {
|
|
13075
|
+
await rm(cwd, { recursive: true, force: true });
|
|
13076
|
+
}
|
|
13077
|
+
});
|
|
13078
|
+
|
|
13079
|
+
it("treats inherited OMX_SESSION_ID as session-aware for native auto-nudge Stop checks", async () => {
|
|
13080
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-env-session-"));
|
|
13081
|
+
try {
|
|
13082
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
13083
|
+
await mkdir(stateDir, { recursive: true });
|
|
13084
|
+
process.env.OMX_SESSION_ID = "sess-stop-auto-mode";
|
|
13085
|
+
|
|
13086
|
+
const result = await dispatchCodexNativeHook(
|
|
13087
|
+
{
|
|
13088
|
+
hook_event_name: "Stop",
|
|
13089
|
+
cwd,
|
|
13090
|
+
thread_id: "thread-stop-auto-env-session",
|
|
13091
|
+
turn_id: "turn-stop-auto-env-session-1",
|
|
13092
|
+
last_assistant_message: "Keep going and finish the cleanup.",
|
|
13093
|
+
},
|
|
13094
|
+
{ cwd },
|
|
13095
|
+
);
|
|
13096
|
+
|
|
13097
|
+
assert.equal(result.omxEventName, "stop");
|
|
13098
|
+
assert.deepEqual(result.outputJson, {
|
|
13099
|
+
decision: "block",
|
|
13100
|
+
reason: DEFAULT_AUTO_NUDGE_RESPONSE,
|
|
13101
|
+
stopReason: "auto_nudge",
|
|
13102
|
+
systemMessage:
|
|
13103
|
+
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
13104
|
+
});
|
|
13105
|
+
const stopState = JSON.parse(await readFile(join(stateDir, "native-stop-state.json"), "utf-8")) as Record<string, unknown>;
|
|
13106
|
+
assert.ok((stopState.sessions as Record<string, unknown>)["sess-stop-auto-mode"]);
|
|
13107
|
+
} finally {
|
|
13108
|
+
await rm(cwd, { recursive: true, force: true });
|
|
13109
|
+
}
|
|
13110
|
+
});
|
|
13111
|
+
|
|
13112
|
+
|
|
13113
|
+
it("ignores generic SESSION_ID for native auto-nudge Stop session scoping", async () => {
|
|
13114
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-generic-session-"));
|
|
13115
|
+
try {
|
|
13116
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
13117
|
+
await mkdir(stateDir, { recursive: true });
|
|
13118
|
+
process.env.SESSION_ID = "generic-shell-session";
|
|
13119
|
+
|
|
13120
|
+
const result = await dispatchCodexNativeHook(
|
|
13121
|
+
{
|
|
13122
|
+
hook_event_name: "Stop",
|
|
13123
|
+
cwd,
|
|
13124
|
+
thread_id: "thread-stop-auto-generic-session",
|
|
13125
|
+
turn_id: "turn-stop-auto-generic-session-1",
|
|
13126
|
+
last_assistant_message: "Keep going and finish the cleanup.",
|
|
13127
|
+
},
|
|
13128
|
+
{ cwd },
|
|
13129
|
+
);
|
|
13130
|
+
|
|
13131
|
+
assert.equal(result.omxEventName, "stop");
|
|
13132
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
13133
|
+
const stopState = JSON.parse(await readFile(join(stateDir, "native-stop-state.json"), "utf-8")) as Record<string, unknown>;
|
|
13134
|
+
const sessions = stopState.sessions as Record<string, unknown>;
|
|
13135
|
+
assert.equal(sessions["generic-shell-session"], undefined);
|
|
13136
|
+
assert.ok(sessions["thread-stop-auto-generic-session"]);
|
|
13137
|
+
} finally {
|
|
13138
|
+
await rm(cwd, { recursive: true, force: true });
|
|
13139
|
+
}
|
|
13140
|
+
});
|
|
13141
|
+
it("does not suppress native auto-nudge from stale root deep-interview mode state when the explicit session-scoped mode state is absent", async () => {
|
|
13142
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-stale-root-mode-"));
|
|
11624
13143
|
try {
|
|
11625
|
-
|
|
11626
|
-
|
|
13144
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
13145
|
+
await mkdir(stateDir, { recursive: true });
|
|
13146
|
+
process.env.OMX_SESSION_ID = "sess-stop-auto-stale-root-mode";
|
|
13147
|
+
await writeJson(join(stateDir, "deep-interview-state.json"), {
|
|
13148
|
+
active: true,
|
|
13149
|
+
mode: "deep-interview",
|
|
13150
|
+
current_phase: "intent-first",
|
|
13151
|
+
});
|
|
11627
13152
|
|
|
11628
13153
|
const result = await dispatchCodexNativeHook(
|
|
11629
13154
|
{
|
|
11630
13155
|
hook_event_name: "Stop",
|
|
11631
13156
|
cwd,
|
|
11632
|
-
session_id: "sess-stop-auto-
|
|
11633
|
-
|
|
13157
|
+
session_id: "sess-stop-auto-stale-root-mode",
|
|
13158
|
+
thread_id: "thread-stop-auto-stale-root-mode",
|
|
13159
|
+
turn_id: "turn-stop-auto-stale-root-mode-1",
|
|
13160
|
+
last_assistant_message: "Keep going and finish the cleanup.",
|
|
11634
13161
|
},
|
|
11635
13162
|
{ cwd },
|
|
11636
13163
|
);
|
|
@@ -11648,18 +13175,26 @@ exit 0
|
|
|
11648
13175
|
}
|
|
11649
13176
|
});
|
|
11650
13177
|
|
|
11651
|
-
it("auto-
|
|
11652
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-
|
|
13178
|
+
it("does not suppress native auto-nudge from stale root deep-interview skill state when the explicit session-scoped canonical skill state is absent", async () => {
|
|
13179
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-stale-root-skill-"));
|
|
11653
13180
|
try {
|
|
11654
|
-
|
|
11655
|
-
|
|
13181
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
13182
|
+
await mkdir(stateDir, { recursive: true });
|
|
13183
|
+
process.env.OMX_SESSION_ID = "sess-stop-auto-stale-root-skill";
|
|
13184
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
13185
|
+
active: true,
|
|
13186
|
+
skill: "deep-interview",
|
|
13187
|
+
phase: "planning",
|
|
13188
|
+
});
|
|
11656
13189
|
|
|
11657
13190
|
const result = await dispatchCodexNativeHook(
|
|
11658
13191
|
{
|
|
11659
13192
|
hook_event_name: "Stop",
|
|
11660
13193
|
cwd,
|
|
11661
|
-
session_id: "sess-stop-auto-
|
|
11662
|
-
|
|
13194
|
+
session_id: "sess-stop-auto-stale-root-skill",
|
|
13195
|
+
thread_id: "thread-stop-auto-stale-root-skill",
|
|
13196
|
+
turn_id: "turn-stop-auto-stale-root-skill-1",
|
|
13197
|
+
last_assistant_message: "Keep going and finish the cleanup.",
|
|
11663
13198
|
},
|
|
11664
13199
|
{ cwd },
|
|
11665
13200
|
);
|
|
@@ -11677,20 +13212,16 @@ exit 0
|
|
|
11677
13212
|
}
|
|
11678
13213
|
});
|
|
11679
13214
|
|
|
11680
|
-
it("does not auto-
|
|
11681
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-
|
|
13215
|
+
it("does not suppress native auto-nudge from stale root deep-interview input lock when the explicit session-scoped canonical skill state is absent", async () => {
|
|
13216
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-stale-root-lock-"));
|
|
11682
13217
|
try {
|
|
11683
13218
|
const stateDir = join(cwd, ".omx", "state");
|
|
11684
|
-
await mkdir(
|
|
11685
|
-
process.env.OMX_SESSION_ID = "sess-stop-auto-
|
|
11686
|
-
await writeJson(join(stateDir, "
|
|
11687
|
-
await writeJson(join(stateDir, "sessions", "sess-stop-auto-question", "skill-active-state.json"), {
|
|
11688
|
-
version: 1,
|
|
13219
|
+
await mkdir(stateDir, { recursive: true });
|
|
13220
|
+
process.env.OMX_SESSION_ID = "sess-stop-auto-stale-root-lock";
|
|
13221
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
11689
13222
|
active: true,
|
|
11690
13223
|
skill: "deep-interview",
|
|
11691
13224
|
phase: "planning",
|
|
11692
|
-
session_id: "sess-stop-auto-question",
|
|
11693
|
-
thread_id: "thread-stop-auto-question",
|
|
11694
13225
|
input_lock: {
|
|
11695
13226
|
active: true,
|
|
11696
13227
|
scope: "deep-interview-auto-approval",
|
|
@@ -11698,44 +13229,45 @@ exit 0
|
|
|
11698
13229
|
message: "Deep interview is active; auto-approval shortcuts are blocked until the interview finishes.",
|
|
11699
13230
|
},
|
|
11700
13231
|
});
|
|
11701
|
-
await writeJson(join(stateDir, "sessions", "sess-stop-auto-question", "deep-interview-state.json"), {
|
|
11702
|
-
active: true,
|
|
11703
|
-
mode: "deep-interview",
|
|
11704
|
-
current_phase: "intent-first",
|
|
11705
|
-
});
|
|
11706
13232
|
|
|
11707
13233
|
const result = await dispatchCodexNativeHook(
|
|
11708
13234
|
{
|
|
11709
13235
|
hook_event_name: "Stop",
|
|
11710
13236
|
cwd,
|
|
11711
|
-
session_id: "sess-stop-auto-
|
|
11712
|
-
thread_id: "thread-stop-auto-
|
|
11713
|
-
turn_id: "turn-stop-auto-
|
|
11714
|
-
last_assistant_message:
|
|
11715
|
-
"Round 2 | Target: Decision boundary | Ambiguity: 24%",
|
|
11716
|
-
"",
|
|
11717
|
-
"If an existing project spider still declares session_mode = \"owned\", should ZenX fail loudly so the stale attribute is removed, or should it ignore the attribute and initialize the session pool anyway?",
|
|
11718
|
-
"Keep going once I have your answer.",
|
|
11719
|
-
].join("\n"),
|
|
13237
|
+
session_id: "sess-stop-auto-stale-root-lock",
|
|
13238
|
+
thread_id: "thread-stop-auto-stale-root-lock",
|
|
13239
|
+
turn_id: "turn-stop-auto-stale-root-lock-1",
|
|
13240
|
+
last_assistant_message: "Keep going and finish the cleanup.",
|
|
11720
13241
|
},
|
|
11721
13242
|
{ cwd },
|
|
11722
13243
|
);
|
|
11723
13244
|
|
|
11724
13245
|
assert.equal(result.omxEventName, "stop");
|
|
11725
|
-
assert.
|
|
13246
|
+
assert.deepEqual(result.outputJson, {
|
|
13247
|
+
decision: "block",
|
|
13248
|
+
reason: DEFAULT_AUTO_NUDGE_RESPONSE,
|
|
13249
|
+
stopReason: "auto_nudge",
|
|
13250
|
+
systemMessage:
|
|
13251
|
+
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
13252
|
+
});
|
|
11726
13253
|
} finally {
|
|
11727
13254
|
await rm(cwd, { recursive: true, force: true });
|
|
11728
13255
|
}
|
|
11729
13256
|
});
|
|
11730
13257
|
|
|
11731
|
-
it("
|
|
11732
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-
|
|
13258
|
+
it("does not suppress native auto-nudge from active root deep-interview state when the current scoped mode state is explicitly inactive", async () => {
|
|
13259
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-inactive-scoped-mode-"));
|
|
11733
13260
|
try {
|
|
11734
13261
|
const stateDir = join(cwd, ".omx", "state");
|
|
11735
|
-
await mkdir(join(stateDir, "sessions", "sess-stop-auto-
|
|
11736
|
-
process.env.OMX_SESSION_ID = "sess-stop-auto-
|
|
11737
|
-
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-auto-
|
|
11738
|
-
await writeJson(join(stateDir, "sessions", "sess-stop-auto-
|
|
13262
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-auto-inactive-mode"), { recursive: true });
|
|
13263
|
+
process.env.OMX_SESSION_ID = "sess-stop-auto-inactive-mode";
|
|
13264
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-auto-inactive-mode" });
|
|
13265
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-auto-inactive-mode", "deep-interview-state.json"), {
|
|
13266
|
+
active: false,
|
|
13267
|
+
mode: "deep-interview",
|
|
13268
|
+
current_phase: "completed",
|
|
13269
|
+
});
|
|
13270
|
+
await writeJson(join(stateDir, "deep-interview-state.json"), {
|
|
11739
13271
|
active: true,
|
|
11740
13272
|
mode: "deep-interview",
|
|
11741
13273
|
current_phase: "intent-first",
|
|
@@ -11745,64 +13277,108 @@ exit 0
|
|
|
11745
13277
|
{
|
|
11746
13278
|
hook_event_name: "Stop",
|
|
11747
13279
|
cwd,
|
|
11748
|
-
session_id: "sess-stop-auto-
|
|
11749
|
-
thread_id: "thread-stop-auto-
|
|
11750
|
-
turn_id: "turn-stop-auto-
|
|
11751
|
-
|
|
11752
|
-
last_assistant_message: "If you want, I can keep going from here.",
|
|
13280
|
+
session_id: "sess-stop-auto-inactive-mode",
|
|
13281
|
+
thread_id: "thread-stop-auto-inactive-mode",
|
|
13282
|
+
turn_id: "turn-stop-auto-inactive-mode-1",
|
|
13283
|
+
last_assistant_message: "Keep going and finish the cleanup.",
|
|
11753
13284
|
},
|
|
11754
13285
|
{ cwd },
|
|
11755
13286
|
);
|
|
11756
13287
|
|
|
11757
13288
|
assert.equal(result.omxEventName, "stop");
|
|
11758
|
-
assert.
|
|
13289
|
+
assert.deepEqual(result.outputJson, {
|
|
13290
|
+
decision: "block",
|
|
13291
|
+
reason: DEFAULT_AUTO_NUDGE_RESPONSE,
|
|
13292
|
+
stopReason: "auto_nudge",
|
|
13293
|
+
systemMessage:
|
|
13294
|
+
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
13295
|
+
});
|
|
11759
13296
|
} finally {
|
|
11760
13297
|
await rm(cwd, { recursive: true, force: true });
|
|
11761
13298
|
}
|
|
11762
13299
|
});
|
|
11763
13300
|
|
|
11764
|
-
it("
|
|
11765
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
13301
|
+
it("clears stale root skill-active state when current session ralplan is terminal", async () => {
|
|
13302
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-root-skill-terminal-"));
|
|
11766
13303
|
try {
|
|
11767
13304
|
const stateDir = join(cwd, ".omx", "state");
|
|
11768
|
-
|
|
11769
|
-
await
|
|
13305
|
+
const sessionId = "sess-stop-terminal-ralplan";
|
|
13306
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
13307
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
13308
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
13309
|
+
active: false,
|
|
13310
|
+
mode: "ralplan",
|
|
13311
|
+
current_phase: "completed",
|
|
13312
|
+
lifecycle_outcome: "finished",
|
|
13313
|
+
run_outcome: "finish",
|
|
13314
|
+
final_artifact: "proposed_plan",
|
|
13315
|
+
});
|
|
13316
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
11770
13317
|
active: true,
|
|
11771
|
-
|
|
11772
|
-
|
|
13318
|
+
skill: "ultrawork",
|
|
13319
|
+
phase: "planning",
|
|
13320
|
+
source: "keyword-detector",
|
|
13321
|
+
active_skills: [
|
|
13322
|
+
{ skill: "ultrawork", phase: "planning", active: true },
|
|
13323
|
+
],
|
|
11773
13324
|
});
|
|
11774
13325
|
|
|
11775
13326
|
const result = await dispatchCodexNativeHook(
|
|
11776
13327
|
{
|
|
11777
13328
|
hook_event_name: "Stop",
|
|
11778
13329
|
cwd,
|
|
11779
|
-
|
|
11780
|
-
|
|
13330
|
+
session_id: sessionId,
|
|
13331
|
+
thread_id: "thread-stop-terminal-ralplan",
|
|
13332
|
+
turn_id: "turn-stop-terminal-ralplan-1",
|
|
13333
|
+
last_assistant_message: "Done.",
|
|
11781
13334
|
},
|
|
11782
13335
|
{ cwd },
|
|
11783
13336
|
);
|
|
11784
13337
|
|
|
11785
13338
|
assert.equal(result.omxEventName, "stop");
|
|
11786
13339
|
assert.equal(result.outputJson, null);
|
|
13340
|
+
|
|
13341
|
+
const rootSkillState = JSON.parse(
|
|
13342
|
+
await readFile(join(stateDir, "skill-active-state.json"), "utf-8"),
|
|
13343
|
+
) as { active?: boolean; active_skills?: unknown[]; reconciliation_reason?: string };
|
|
13344
|
+
assert.equal(rootSkillState.active, false);
|
|
13345
|
+
assert.deepEqual(rootSkillState.active_skills, []);
|
|
13346
|
+
assert.equal(rootSkillState.reconciliation_reason, "stop_hook_session_state_terminal");
|
|
11787
13347
|
} finally {
|
|
11788
13348
|
await rm(cwd, { recursive: true, force: true });
|
|
11789
13349
|
}
|
|
11790
13350
|
});
|
|
11791
13351
|
|
|
11792
|
-
it("
|
|
11793
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
13352
|
+
it("preserves legitimate session-scoped ultrawork blocking while reconciling root skill-active state", async () => {
|
|
13353
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-active-root-skill-session-mode-"));
|
|
11794
13354
|
try {
|
|
11795
13355
|
const stateDir = join(cwd, ".omx", "state");
|
|
11796
|
-
|
|
11797
|
-
|
|
13356
|
+
const sessionId = "sess-stop-active-ultrawork";
|
|
13357
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
13358
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
13359
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ultrawork-state.json"), {
|
|
13360
|
+
active: true,
|
|
13361
|
+
mode: "ultrawork",
|
|
13362
|
+
current_phase: "executing",
|
|
13363
|
+
session_id: sessionId,
|
|
13364
|
+
});
|
|
13365
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
13366
|
+
active: true,
|
|
13367
|
+
skill: "ultrawork",
|
|
13368
|
+
phase: "planning",
|
|
13369
|
+
source: "keyword-detector",
|
|
13370
|
+
active_skills: [
|
|
13371
|
+
{ skill: "ultrawork", phase: "planning", active: true, session_id: sessionId },
|
|
13372
|
+
],
|
|
13373
|
+
});
|
|
11798
13374
|
|
|
11799
13375
|
const result = await dispatchCodexNativeHook(
|
|
11800
13376
|
{
|
|
11801
13377
|
hook_event_name: "Stop",
|
|
11802
13378
|
cwd,
|
|
11803
|
-
|
|
11804
|
-
|
|
11805
|
-
|
|
13379
|
+
session_id: sessionId,
|
|
13380
|
+
thread_id: "thread-stop-active-ultrawork",
|
|
13381
|
+
turn_id: "turn-stop-active-ultrawork-1",
|
|
11806
13382
|
},
|
|
11807
13383
|
{ cwd },
|
|
11808
13384
|
);
|
|
@@ -11810,104 +13386,102 @@ exit 0
|
|
|
11810
13386
|
assert.equal(result.omxEventName, "stop");
|
|
11811
13387
|
assert.deepEqual(result.outputJson, {
|
|
11812
13388
|
decision: "block",
|
|
11813
|
-
reason:
|
|
11814
|
-
stopReason: "
|
|
11815
|
-
systemMessage:
|
|
11816
|
-
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
13389
|
+
reason: "OMX ultrawork is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
13390
|
+
stopReason: "ultrawork_executing",
|
|
13391
|
+
systemMessage: "OMX ultrawork is still active (phase: executing).",
|
|
11817
13392
|
});
|
|
11818
|
-
const stopState = JSON.parse(await readFile(join(stateDir, "native-stop-state.json"), "utf-8")) as Record<string, unknown>;
|
|
11819
|
-
assert.ok((stopState.sessions as Record<string, unknown>)["sess-stop-auto-mode"]);
|
|
11820
|
-
} finally {
|
|
11821
|
-
await rm(cwd, { recursive: true, force: true });
|
|
11822
|
-
}
|
|
11823
|
-
});
|
|
11824
|
-
|
|
11825
|
-
|
|
11826
|
-
it("ignores generic SESSION_ID for native auto-nudge Stop session scoping", async () => {
|
|
11827
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-generic-session-"));
|
|
11828
|
-
try {
|
|
11829
|
-
const stateDir = join(cwd, ".omx", "state");
|
|
11830
|
-
await mkdir(stateDir, { recursive: true });
|
|
11831
|
-
process.env.SESSION_ID = "generic-shell-session";
|
|
11832
|
-
|
|
11833
|
-
const result = await dispatchCodexNativeHook(
|
|
11834
|
-
{
|
|
11835
|
-
hook_event_name: "Stop",
|
|
11836
|
-
cwd,
|
|
11837
|
-
thread_id: "thread-stop-auto-generic-session",
|
|
11838
|
-
turn_id: "turn-stop-auto-generic-session-1",
|
|
11839
|
-
last_assistant_message: "Keep going and finish the cleanup.",
|
|
11840
|
-
},
|
|
11841
|
-
{ cwd },
|
|
11842
|
-
);
|
|
11843
13393
|
|
|
11844
|
-
|
|
11845
|
-
|
|
11846
|
-
|
|
11847
|
-
|
|
11848
|
-
assert.
|
|
11849
|
-
assert.ok(sessions["thread-stop-auto-generic-session"]);
|
|
13394
|
+
const rootSkillState = JSON.parse(
|
|
13395
|
+
await readFile(join(stateDir, "skill-active-state.json"), "utf-8"),
|
|
13396
|
+
) as { active?: boolean; active_skills?: Array<{ skill?: string }> };
|
|
13397
|
+
assert.equal(rootSkillState.active, true);
|
|
13398
|
+
assert.deepEqual(rootSkillState.active_skills?.map((entry) => entry.skill), ["ultrawork"]);
|
|
11850
13399
|
} finally {
|
|
11851
13400
|
await rm(cwd, { recursive: true, force: true });
|
|
11852
13401
|
}
|
|
11853
13402
|
});
|
|
11854
|
-
|
|
11855
|
-
|
|
13403
|
+
|
|
13404
|
+
it("reconciles stale root skill-active state under OMX_ROOT boxed state", async () => {
|
|
13405
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-boxed-source-"));
|
|
13406
|
+
const omxRoot = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-boxed-root-"));
|
|
13407
|
+
const previousOmxRoot = process.env.OMX_ROOT;
|
|
11856
13408
|
try {
|
|
11857
|
-
|
|
11858
|
-
|
|
11859
|
-
|
|
11860
|
-
|
|
13409
|
+
process.env.OMX_ROOT = omxRoot;
|
|
13410
|
+
const stateDir = join(omxRoot, ".omx", "state");
|
|
13411
|
+
const sourceStateDir = join(cwd, ".omx", "state");
|
|
13412
|
+
const sessionId = "sess-stop-boxed-ralplan";
|
|
13413
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
13414
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
13415
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
13416
|
+
active: false,
|
|
13417
|
+
mode: "ralplan",
|
|
13418
|
+
current_phase: "completed",
|
|
13419
|
+
lifecycle_outcome: "finished",
|
|
13420
|
+
run_outcome: "finish",
|
|
13421
|
+
});
|
|
13422
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
11861
13423
|
active: true,
|
|
11862
|
-
|
|
11863
|
-
|
|
13424
|
+
skill: "ultrawork",
|
|
13425
|
+
phase: "planning",
|
|
13426
|
+
source: "keyword-detector",
|
|
13427
|
+
active_skills: [
|
|
13428
|
+
{ skill: "ultrawork", phase: "planning", active: true },
|
|
13429
|
+
],
|
|
11864
13430
|
});
|
|
11865
13431
|
|
|
11866
13432
|
const result = await dispatchCodexNativeHook(
|
|
11867
13433
|
{
|
|
11868
13434
|
hook_event_name: "Stop",
|
|
11869
13435
|
cwd,
|
|
11870
|
-
session_id:
|
|
11871
|
-
thread_id: "thread-stop-
|
|
11872
|
-
turn_id: "turn-stop-
|
|
11873
|
-
last_assistant_message: "
|
|
13436
|
+
session_id: sessionId,
|
|
13437
|
+
thread_id: "thread-stop-boxed-ralplan",
|
|
13438
|
+
turn_id: "turn-stop-boxed-ralplan-1",
|
|
13439
|
+
last_assistant_message: "Done.",
|
|
11874
13440
|
},
|
|
11875
13441
|
{ cwd },
|
|
11876
13442
|
);
|
|
11877
13443
|
|
|
11878
13444
|
assert.equal(result.omxEventName, "stop");
|
|
11879
|
-
assert.
|
|
11880
|
-
|
|
11881
|
-
|
|
11882
|
-
|
|
11883
|
-
|
|
11884
|
-
|
|
11885
|
-
|
|
13445
|
+
assert.equal(result.outputJson, null);
|
|
13446
|
+
|
|
13447
|
+
const boxedRootSkillState = JSON.parse(
|
|
13448
|
+
await readFile(join(stateDir, "skill-active-state.json"), "utf-8"),
|
|
13449
|
+
) as { active?: boolean; active_skills?: unknown[]; reconciliation_reason?: string };
|
|
13450
|
+
assert.equal(boxedRootSkillState.active, false);
|
|
13451
|
+
assert.deepEqual(boxedRootSkillState.active_skills, []);
|
|
13452
|
+
assert.equal(boxedRootSkillState.reconciliation_reason, "stop_hook_session_state_terminal");
|
|
13453
|
+
assert.equal(existsSync(join(sourceStateDir, "skill-active-state.json")), false);
|
|
11886
13454
|
} finally {
|
|
13455
|
+
if (previousOmxRoot === undefined) delete process.env.OMX_ROOT;
|
|
13456
|
+
else process.env.OMX_ROOT = previousOmxRoot;
|
|
11887
13457
|
await rm(cwd, { recursive: true, force: true });
|
|
13458
|
+
await rm(omxRoot, { recursive: true, force: true });
|
|
11888
13459
|
}
|
|
11889
13460
|
});
|
|
11890
13461
|
|
|
11891
|
-
it("
|
|
11892
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-
|
|
13462
|
+
it("auto-continues native Stop for permission-seeking prompts even outside OMX runtime", async () => {
|
|
13463
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-plain-session-"));
|
|
11893
13464
|
try {
|
|
11894
|
-
|
|
11895
|
-
|
|
11896
|
-
|
|
11897
|
-
|
|
11898
|
-
|
|
11899
|
-
|
|
11900
|
-
|
|
11901
|
-
|
|
13465
|
+
await dispatchCodexNativeHook(
|
|
13466
|
+
{
|
|
13467
|
+
hook_event_name: "SessionStart",
|
|
13468
|
+
cwd,
|
|
13469
|
+
session_id: "plain-stop-session",
|
|
13470
|
+
},
|
|
13471
|
+
{
|
|
13472
|
+
cwd,
|
|
13473
|
+
sessionOwnerPid: process.pid,
|
|
13474
|
+
},
|
|
13475
|
+
);
|
|
11902
13476
|
|
|
11903
13477
|
const result = await dispatchCodexNativeHook(
|
|
11904
13478
|
{
|
|
11905
13479
|
hook_event_name: "Stop",
|
|
11906
13480
|
cwd,
|
|
11907
|
-
session_id: "
|
|
11908
|
-
thread_id: "thread
|
|
11909
|
-
turn_id: "turn-
|
|
11910
|
-
last_assistant_message: "
|
|
13481
|
+
session_id: "plain-stop-session",
|
|
13482
|
+
thread_id: "plain-thread",
|
|
13483
|
+
turn_id: "plain-turn-1",
|
|
13484
|
+
last_assistant_message: "If you want, I can continue with the cleanup from here.",
|
|
11911
13485
|
},
|
|
11912
13486
|
{ cwd },
|
|
11913
13487
|
);
|
|
@@ -11925,32 +13499,45 @@ exit 0
|
|
|
11925
13499
|
}
|
|
11926
13500
|
});
|
|
11927
13501
|
|
|
11928
|
-
it("
|
|
11929
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
13502
|
+
it("re-fires team Stop output for a later fresh Stop reply while the team is still active", async () => {
|
|
13503
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-refire-"));
|
|
11930
13504
|
try {
|
|
11931
13505
|
const stateDir = join(cwd, ".omx", "state");
|
|
11932
13506
|
await mkdir(stateDir, { recursive: true });
|
|
11933
|
-
|
|
11934
|
-
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
13507
|
+
await writeJson(join(stateDir, "team-state.json"), {
|
|
11935
13508
|
active: true,
|
|
11936
|
-
|
|
11937
|
-
|
|
11938
|
-
|
|
11939
|
-
|
|
11940
|
-
|
|
11941
|
-
|
|
11942
|
-
|
|
11943
|
-
|
|
13509
|
+
current_phase: "team-exec",
|
|
13510
|
+
team_name: "review-team",
|
|
13511
|
+
session_id: "sess-stop-team-refire",
|
|
13512
|
+
thread_id: "thread-stop-team-refire",
|
|
13513
|
+
});
|
|
13514
|
+
await writeJson(join(stateDir, "team", "review-team", "phase.json"), {
|
|
13515
|
+
current_phase: "team-verify",
|
|
13516
|
+
max_fix_attempts: 3,
|
|
13517
|
+
current_fix_attempt: 0,
|
|
13518
|
+
transitions: [],
|
|
13519
|
+
updated_at: new Date().toISOString(),
|
|
11944
13520
|
});
|
|
11945
13521
|
|
|
13522
|
+
await dispatchCodexNativeHook(
|
|
13523
|
+
{
|
|
13524
|
+
hook_event_name: "Stop",
|
|
13525
|
+
cwd,
|
|
13526
|
+
session_id: "sess-stop-team-refire",
|
|
13527
|
+
thread_id: "thread-stop-team-refire",
|
|
13528
|
+
turn_id: "turn-stop-team-refire-1",
|
|
13529
|
+
},
|
|
13530
|
+
{ cwd },
|
|
13531
|
+
);
|
|
13532
|
+
|
|
11946
13533
|
const result = await dispatchCodexNativeHook(
|
|
11947
13534
|
{
|
|
11948
13535
|
hook_event_name: "Stop",
|
|
11949
13536
|
cwd,
|
|
11950
|
-
session_id: "sess-stop-
|
|
11951
|
-
thread_id: "thread-stop-
|
|
11952
|
-
turn_id: "turn-stop-
|
|
11953
|
-
|
|
13537
|
+
session_id: "sess-stop-team-refire",
|
|
13538
|
+
thread_id: "thread-stop-team-refire",
|
|
13539
|
+
turn_id: "turn-stop-team-refire-2",
|
|
13540
|
+
stop_hook_active: true,
|
|
11954
13541
|
},
|
|
11955
13542
|
{ cwd },
|
|
11956
13543
|
);
|
|
@@ -11958,505 +13545,557 @@ exit 0
|
|
|
11958
13545
|
assert.equal(result.omxEventName, "stop");
|
|
11959
13546
|
assert.deepEqual(result.outputJson, {
|
|
11960
13547
|
decision: "block",
|
|
11961
|
-
reason:
|
|
11962
|
-
|
|
11963
|
-
|
|
11964
|
-
|
|
13548
|
+
reason:
|
|
13549
|
+
`OMX team pipeline is still active (review-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
|
|
13550
|
+
stopReason: "team_team-verify",
|
|
13551
|
+
systemMessage: "OMX team pipeline is still active at phase team-verify.",
|
|
11965
13552
|
});
|
|
11966
13553
|
} finally {
|
|
11967
13554
|
await rm(cwd, { recursive: true, force: true });
|
|
11968
13555
|
}
|
|
11969
13556
|
});
|
|
11970
13557
|
|
|
11971
|
-
it("
|
|
11972
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
13558
|
+
it("suppresses duplicate team Stop replays across native/canonical session-id drift", async () => {
|
|
13559
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-session-drift-"));
|
|
11973
13560
|
try {
|
|
11974
13561
|
const stateDir = join(cwd, ".omx", "state");
|
|
11975
|
-
await mkdir(join(stateDir, "sessions", "
|
|
11976
|
-
process.env.OMX_SESSION_ID = "
|
|
11977
|
-
await writeJson(join(stateDir, "session.json"), {
|
|
11978
|
-
|
|
11979
|
-
|
|
11980
|
-
mode: "deep-interview",
|
|
11981
|
-
current_phase: "completed",
|
|
13562
|
+
await mkdir(join(stateDir, "sessions", "omx-canonical"), { recursive: true });
|
|
13563
|
+
process.env.OMX_SESSION_ID = "omx-canonical";
|
|
13564
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
13565
|
+
session_id: "omx-canonical",
|
|
13566
|
+
native_session_id: "codex-native",
|
|
11982
13567
|
});
|
|
11983
|
-
await writeJson(join(stateDir, "
|
|
13568
|
+
await writeJson(join(stateDir, "sessions", "omx-canonical", "team-state.json"), {
|
|
11984
13569
|
active: true,
|
|
11985
|
-
|
|
11986
|
-
|
|
13570
|
+
current_phase: "starting",
|
|
13571
|
+
team_name: "current-team",
|
|
13572
|
+
session_id: "omx-canonical",
|
|
13573
|
+
});
|
|
13574
|
+
await writeJson(join(stateDir, "team", "current-team", "phase.json"), {
|
|
13575
|
+
current_phase: "team-verify",
|
|
13576
|
+
max_fix_attempts: 3,
|
|
13577
|
+
current_fix_attempt: 1,
|
|
13578
|
+
transitions: [],
|
|
13579
|
+
updated_at: new Date().toISOString(),
|
|
11987
13580
|
});
|
|
11988
13581
|
|
|
11989
|
-
|
|
13582
|
+
await dispatchCodexNativeHook(
|
|
11990
13583
|
{
|
|
11991
13584
|
hook_event_name: "Stop",
|
|
11992
13585
|
cwd,
|
|
11993
|
-
session_id: "
|
|
11994
|
-
thread_id: "thread-stop-
|
|
11995
|
-
turn_id: "turn-stop-
|
|
11996
|
-
last_assistant_message: "Keep going and finish the cleanup.",
|
|
13586
|
+
session_id: "codex-native",
|
|
13587
|
+
thread_id: "thread-stop-team-drift",
|
|
13588
|
+
turn_id: "turn-stop-team-drift-1",
|
|
11997
13589
|
},
|
|
11998
13590
|
{ cwd },
|
|
11999
13591
|
);
|
|
12000
13592
|
|
|
12001
|
-
|
|
12002
|
-
|
|
12003
|
-
|
|
12004
|
-
|
|
12005
|
-
|
|
12006
|
-
|
|
12007
|
-
"
|
|
12008
|
-
|
|
12009
|
-
|
|
12010
|
-
|
|
12011
|
-
|
|
12012
|
-
});
|
|
13593
|
+
const duplicate = await dispatchCodexNativeHook(
|
|
13594
|
+
{
|
|
13595
|
+
hook_event_name: "Stop",
|
|
13596
|
+
cwd,
|
|
13597
|
+
session_id: "omx-canonical",
|
|
13598
|
+
thread_id: "thread-stop-team-drift",
|
|
13599
|
+
turn_id: "turn-stop-team-drift-1",
|
|
13600
|
+
stop_hook_active: true,
|
|
13601
|
+
},
|
|
13602
|
+
{ cwd },
|
|
13603
|
+
);
|
|
12013
13604
|
|
|
12014
|
-
|
|
12015
|
-
|
|
12016
|
-
|
|
12017
|
-
|
|
12018
|
-
|
|
12019
|
-
|
|
12020
|
-
|
|
12021
|
-
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
12022
|
-
active: false,
|
|
12023
|
-
mode: "ralplan",
|
|
12024
|
-
current_phase: "completed",
|
|
12025
|
-
lifecycle_outcome: "finished",
|
|
12026
|
-
run_outcome: "finish",
|
|
12027
|
-
final_artifact: "proposed_plan",
|
|
12028
|
-
});
|
|
12029
|
-
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
12030
|
-
active: true,
|
|
12031
|
-
skill: "ultrawork",
|
|
12032
|
-
phase: "planning",
|
|
12033
|
-
source: "keyword-detector",
|
|
12034
|
-
active_skills: [
|
|
12035
|
-
{ skill: "ultrawork", phase: "planning", active: true },
|
|
12036
|
-
],
|
|
13605
|
+
assert.equal(duplicate.omxEventName, "stop");
|
|
13606
|
+
assert.deepEqual(duplicate.outputJson, {
|
|
13607
|
+
decision: "block",
|
|
13608
|
+
reason:
|
|
13609
|
+
`OMX team pipeline is still active (current-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
|
|
13610
|
+
stopReason: "team_team-verify",
|
|
13611
|
+
systemMessage: "OMX team pipeline is still active at phase team-verify.",
|
|
12037
13612
|
});
|
|
12038
13613
|
|
|
12039
|
-
const
|
|
13614
|
+
const fresh = await dispatchCodexNativeHook(
|
|
12040
13615
|
{
|
|
12041
13616
|
hook_event_name: "Stop",
|
|
12042
13617
|
cwd,
|
|
12043
|
-
session_id:
|
|
12044
|
-
thread_id: "thread-stop-
|
|
12045
|
-
turn_id: "turn-stop-
|
|
12046
|
-
|
|
13618
|
+
session_id: "omx-canonical",
|
|
13619
|
+
thread_id: "thread-stop-team-drift",
|
|
13620
|
+
turn_id: "turn-stop-team-drift-2",
|
|
13621
|
+
stop_hook_active: true,
|
|
12047
13622
|
},
|
|
12048
13623
|
{ cwd },
|
|
12049
13624
|
);
|
|
12050
13625
|
|
|
12051
|
-
assert.equal(
|
|
12052
|
-
assert.
|
|
13626
|
+
assert.equal(fresh.omxEventName, "stop");
|
|
13627
|
+
assert.deepEqual(fresh.outputJson, {
|
|
13628
|
+
decision: "block",
|
|
13629
|
+
reason:
|
|
13630
|
+
`OMX team pipeline is still active (current-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
|
|
13631
|
+
stopReason: "team_team-verify",
|
|
13632
|
+
systemMessage: "OMX team pipeline is still active at phase team-verify.",
|
|
13633
|
+
});
|
|
12053
13634
|
|
|
12054
|
-
const
|
|
12055
|
-
await readFile(join(stateDir, "
|
|
12056
|
-
) as {
|
|
12057
|
-
assert.
|
|
12058
|
-
assert.deepEqual(rootSkillState.active_skills, []);
|
|
12059
|
-
assert.equal(rootSkillState.reconciliation_reason, "stop_hook_session_state_terminal");
|
|
13635
|
+
const persisted = JSON.parse(
|
|
13636
|
+
await readFile(join(stateDir, "native-stop-state.json"), "utf-8"),
|
|
13637
|
+
) as { sessions?: Record<string, unknown> };
|
|
13638
|
+
assert.deepEqual(Object.keys(persisted.sessions ?? {}), ["omx-canonical"]);
|
|
12060
13639
|
} finally {
|
|
12061
13640
|
await rm(cwd, { recursive: true, force: true });
|
|
12062
13641
|
}
|
|
12063
13642
|
});
|
|
12064
13643
|
|
|
12065
|
-
it("
|
|
12066
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
13644
|
+
it("suppresses duplicate ultrawork Stop replays while stop_hook_active stays true", async () => {
|
|
13645
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultrawork-repeat-"));
|
|
12067
13646
|
try {
|
|
12068
13647
|
const stateDir = join(cwd, ".omx", "state");
|
|
12069
|
-
|
|
12070
|
-
await
|
|
12071
|
-
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
12072
|
-
await writeJson(join(stateDir, "sessions", sessionId, "ultrawork-state.json"), {
|
|
12073
|
-
active: true,
|
|
12074
|
-
mode: "ultrawork",
|
|
12075
|
-
current_phase: "executing",
|
|
12076
|
-
session_id: sessionId,
|
|
12077
|
-
});
|
|
12078
|
-
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
13648
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-ultrawork-repeat"), { recursive: true });
|
|
13649
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-ultrawork-repeat", "ultrawork-state.json"), {
|
|
12079
13650
|
active: true,
|
|
12080
|
-
|
|
12081
|
-
phase: "planning",
|
|
12082
|
-
source: "keyword-detector",
|
|
12083
|
-
active_skills: [
|
|
12084
|
-
{ skill: "ultrawork", phase: "planning", active: true, session_id: sessionId },
|
|
12085
|
-
],
|
|
13651
|
+
current_phase: "executing",
|
|
12086
13652
|
});
|
|
12087
13653
|
|
|
12088
|
-
const
|
|
13654
|
+
const first = await dispatchCodexNativeHook(
|
|
12089
13655
|
{
|
|
12090
13656
|
hook_event_name: "Stop",
|
|
12091
13657
|
cwd,
|
|
12092
|
-
session_id:
|
|
12093
|
-
thread_id: "thread-stop-
|
|
12094
|
-
turn_id: "turn-stop-
|
|
13658
|
+
session_id: "sess-stop-ultrawork-repeat",
|
|
13659
|
+
thread_id: "thread-stop-ultrawork-repeat",
|
|
13660
|
+
turn_id: "turn-stop-ultrawork-repeat-1",
|
|
12095
13661
|
},
|
|
12096
13662
|
{ cwd },
|
|
12097
13663
|
);
|
|
12098
13664
|
|
|
12099
|
-
|
|
12100
|
-
|
|
13665
|
+
const repeated = await dispatchCodexNativeHook(
|
|
13666
|
+
{
|
|
13667
|
+
hook_event_name: "Stop",
|
|
13668
|
+
cwd,
|
|
13669
|
+
session_id: "sess-stop-ultrawork-repeat",
|
|
13670
|
+
thread_id: "thread-stop-ultrawork-repeat",
|
|
13671
|
+
turn_id: "turn-stop-ultrawork-repeat-1",
|
|
13672
|
+
stop_hook_active: true,
|
|
13673
|
+
},
|
|
13674
|
+
{ cwd },
|
|
13675
|
+
);
|
|
13676
|
+
|
|
13677
|
+
const fresh = await dispatchCodexNativeHook(
|
|
13678
|
+
{
|
|
13679
|
+
hook_event_name: "Stop",
|
|
13680
|
+
cwd,
|
|
13681
|
+
session_id: "sess-stop-ultrawork-repeat",
|
|
13682
|
+
thread_id: "thread-stop-ultrawork-repeat",
|
|
13683
|
+
turn_id: "turn-stop-ultrawork-repeat-2",
|
|
13684
|
+
stop_hook_active: true,
|
|
13685
|
+
},
|
|
13686
|
+
{ cwd },
|
|
13687
|
+
);
|
|
13688
|
+
|
|
13689
|
+
assert.equal(first.omxEventName, "stop");
|
|
13690
|
+
assert.deepEqual(repeated.outputJson, null);
|
|
13691
|
+
assert.equal(fresh.omxEventName, "stop");
|
|
13692
|
+
assert.deepEqual(fresh.outputJson, {
|
|
12101
13693
|
decision: "block",
|
|
12102
13694
|
reason: "OMX ultrawork is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
12103
13695
|
stopReason: "ultrawork_executing",
|
|
12104
13696
|
systemMessage: "OMX ultrawork is still active (phase: executing).",
|
|
12105
13697
|
});
|
|
12106
|
-
|
|
12107
|
-
const rootSkillState = JSON.parse(
|
|
12108
|
-
await readFile(join(stateDir, "skill-active-state.json"), "utf-8"),
|
|
12109
|
-
) as { active?: boolean; active_skills?: Array<{ skill?: string }> };
|
|
12110
|
-
assert.equal(rootSkillState.active, true);
|
|
12111
|
-
assert.deepEqual(rootSkillState.active_skills?.map((entry) => entry.skill), ["ultrawork"]);
|
|
12112
13698
|
} finally {
|
|
12113
13699
|
await rm(cwd, { recursive: true, force: true });
|
|
12114
13700
|
}
|
|
12115
13701
|
});
|
|
12116
13702
|
|
|
12117
|
-
it("
|
|
12118
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
12119
|
-
const omxRoot = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-boxed-root-"));
|
|
12120
|
-
const previousOmxRoot = process.env.OMX_ROOT;
|
|
13703
|
+
it("re-blocks active ralplan skill state on repeated Stop hooks", async () => {
|
|
13704
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-repeat-"));
|
|
12121
13705
|
try {
|
|
12122
|
-
|
|
12123
|
-
|
|
12124
|
-
|
|
12125
|
-
|
|
12126
|
-
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
12127
|
-
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
12128
|
-
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
12129
|
-
active: false,
|
|
12130
|
-
mode: "ralplan",
|
|
12131
|
-
current_phase: "completed",
|
|
12132
|
-
lifecycle_outcome: "finished",
|
|
12133
|
-
run_outcome: "finish",
|
|
12134
|
-
});
|
|
12135
|
-
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
13706
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
13707
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-skill-repeat"), { recursive: true });
|
|
13708
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-skill-repeat" });
|
|
13709
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-skill-repeat", "skill-active-state.json"), {
|
|
12136
13710
|
active: true,
|
|
12137
|
-
skill: "
|
|
13711
|
+
skill: "ralplan",
|
|
12138
13712
|
phase: "planning",
|
|
12139
|
-
|
|
12140
|
-
|
|
12141
|
-
|
|
12142
|
-
|
|
13713
|
+
});
|
|
13714
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-skill-repeat", "ralplan-state.json"), {
|
|
13715
|
+
active: true,
|
|
13716
|
+
current_phase: "planning",
|
|
12143
13717
|
});
|
|
12144
13718
|
|
|
12145
|
-
|
|
13719
|
+
await dispatchCodexNativeHook(
|
|
12146
13720
|
{
|
|
12147
13721
|
hook_event_name: "Stop",
|
|
12148
13722
|
cwd,
|
|
12149
|
-
session_id:
|
|
12150
|
-
thread_id: "thread-stop-
|
|
12151
|
-
turn_id: "turn-stop-
|
|
12152
|
-
last_assistant_message: "Done.",
|
|
13723
|
+
session_id: "sess-stop-skill-repeat",
|
|
13724
|
+
thread_id: "thread-stop-skill-repeat",
|
|
13725
|
+
turn_id: "turn-stop-skill-repeat-1",
|
|
12153
13726
|
},
|
|
12154
13727
|
{ cwd },
|
|
12155
13728
|
);
|
|
12156
13729
|
|
|
12157
|
-
|
|
12158
|
-
|
|
13730
|
+
const repeated = await dispatchCodexNativeHook(
|
|
13731
|
+
{
|
|
13732
|
+
hook_event_name: "Stop",
|
|
13733
|
+
cwd,
|
|
13734
|
+
session_id: "sess-stop-skill-repeat",
|
|
13735
|
+
thread_id: "thread-stop-skill-repeat",
|
|
13736
|
+
turn_id: "turn-stop-skill-repeat-1",
|
|
13737
|
+
stop_hook_active: true,
|
|
13738
|
+
},
|
|
13739
|
+
{ cwd },
|
|
13740
|
+
);
|
|
12159
13741
|
|
|
12160
|
-
|
|
12161
|
-
|
|
12162
|
-
|
|
12163
|
-
assert.
|
|
12164
|
-
assert.
|
|
12165
|
-
assert.equal(boxedRootSkillState.reconciliation_reason, "stop_hook_session_state_terminal");
|
|
12166
|
-
assert.equal(existsSync(join(sourceStateDir, "skill-active-state.json")), false);
|
|
13742
|
+
assert.equal(repeated.omxEventName, "stop");
|
|
13743
|
+
assert.equal(repeated.outputJson?.decision, "block");
|
|
13744
|
+
assert.match(String(repeated.outputJson?.reason ?? ""), /Status: continue_from_artifact/);
|
|
13745
|
+
assert.match(String(repeated.outputJson?.reason ?? ""), /continue from the current ralplan artifact/i);
|
|
13746
|
+
assert.equal(repeated.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
|
|
12167
13747
|
} finally {
|
|
12168
|
-
if (previousOmxRoot === undefined) delete process.env.OMX_ROOT;
|
|
12169
|
-
else process.env.OMX_ROOT = previousOmxRoot;
|
|
12170
13748
|
await rm(cwd, { recursive: true, force: true });
|
|
12171
|
-
await rm(omxRoot, { recursive: true, force: true });
|
|
12172
13749
|
}
|
|
12173
13750
|
});
|
|
12174
13751
|
|
|
12175
|
-
it("
|
|
12176
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
13752
|
+
it("blocks implementation writes while ralplan is active without execution handoff", async () => {
|
|
13753
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-block-"));
|
|
12177
13754
|
try {
|
|
12178
|
-
|
|
12179
|
-
|
|
12180
|
-
|
|
12181
|
-
|
|
12182
|
-
|
|
12183
|
-
|
|
12184
|
-
|
|
12185
|
-
|
|
12186
|
-
|
|
12187
|
-
},
|
|
12188
|
-
);
|
|
13755
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
13756
|
+
const sessionId = "sess-ralplan-pretool-block";
|
|
13757
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
13758
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
13759
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
13760
|
+
active: true,
|
|
13761
|
+
skill: "ralplan",
|
|
13762
|
+
phase: "planning",
|
|
13763
|
+
session_id: sessionId,
|
|
13764
|
+
active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
|
|
13765
|
+
});
|
|
13766
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
13767
|
+
active: true,
|
|
13768
|
+
mode: "ralplan",
|
|
13769
|
+
current_phase: "critic-review",
|
|
13770
|
+
session_id: sessionId,
|
|
13771
|
+
});
|
|
12189
13772
|
|
|
12190
13773
|
const result = await dispatchCodexNativeHook(
|
|
12191
13774
|
{
|
|
12192
|
-
hook_event_name: "
|
|
13775
|
+
hook_event_name: "PreToolUse",
|
|
12193
13776
|
cwd,
|
|
12194
|
-
session_id:
|
|
12195
|
-
thread_id: "
|
|
12196
|
-
|
|
12197
|
-
|
|
13777
|
+
session_id: sessionId,
|
|
13778
|
+
thread_id: "thread-ralplan-pretool-block",
|
|
13779
|
+
tool_name: "Edit",
|
|
13780
|
+
tool_input: { file_path: "src/runtime.ts" },
|
|
12198
13781
|
},
|
|
12199
13782
|
{ cwd },
|
|
12200
13783
|
);
|
|
12201
13784
|
|
|
12202
|
-
assert.equal(result.omxEventName, "
|
|
12203
|
-
assert.
|
|
12204
|
-
|
|
12205
|
-
|
|
12206
|
-
|
|
12207
|
-
|
|
12208
|
-
|
|
12209
|
-
});
|
|
13785
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
13786
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
13787
|
+
assert.match(String(result.outputJson?.reason ?? ""), /Ralplan is active .*implementation\/write tools are blocked/i);
|
|
13788
|
+
assert.match(
|
|
13789
|
+
String((result.outputJson?.hookSpecificOutput as { additionalContext?: string } | undefined)?.additionalContext ?? ""),
|
|
13790
|
+
/\$ultragoal.*\$team.*\$ralph/i,
|
|
13791
|
+
);
|
|
12210
13792
|
} finally {
|
|
12211
13793
|
await rm(cwd, { recursive: true, force: true });
|
|
12212
13794
|
}
|
|
12213
13795
|
});
|
|
12214
13796
|
|
|
12215
|
-
it("
|
|
12216
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
13797
|
+
it("blocks implementation writes while Autopilot is supervising ralplan without handoff", async () => {
|
|
13798
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-pretool-block-"));
|
|
12217
13799
|
try {
|
|
12218
13800
|
const stateDir = join(cwd, ".omx", "state");
|
|
12219
|
-
|
|
12220
|
-
await
|
|
13801
|
+
const sessionId = "sess-autopilot-ralplan-pretool-block";
|
|
13802
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
13803
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
13804
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
12221
13805
|
active: true,
|
|
12222
|
-
|
|
12223
|
-
|
|
12224
|
-
session_id:
|
|
12225
|
-
|
|
12226
|
-
});
|
|
12227
|
-
await writeJson(join(stateDir, "team", "review-team", "phase.json"), {
|
|
12228
|
-
current_phase: "team-verify",
|
|
12229
|
-
max_fix_attempts: 3,
|
|
12230
|
-
current_fix_attempt: 0,
|
|
12231
|
-
transitions: [],
|
|
12232
|
-
updated_at: new Date().toISOString(),
|
|
13806
|
+
skill: "autopilot",
|
|
13807
|
+
phase: "ralplan",
|
|
13808
|
+
session_id: sessionId,
|
|
13809
|
+
active_skills: [{ skill: "autopilot", phase: "ralplan", active: true, session_id: sessionId }],
|
|
12233
13810
|
});
|
|
12234
|
-
|
|
12235
|
-
|
|
12236
|
-
|
|
12237
|
-
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
|
|
12241
|
-
|
|
13811
|
+
await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
|
|
13812
|
+
active: true,
|
|
13813
|
+
mode: "autopilot",
|
|
13814
|
+
current_phase: "ralplan",
|
|
13815
|
+
session_id: sessionId,
|
|
13816
|
+
state: {
|
|
13817
|
+
handoff_artifacts: {
|
|
13818
|
+
ralplan_consensus_gate: { required: true, complete: false },
|
|
13819
|
+
},
|
|
12242
13820
|
},
|
|
12243
|
-
|
|
12244
|
-
);
|
|
13821
|
+
});
|
|
12245
13822
|
|
|
12246
13823
|
const result = await dispatchCodexNativeHook(
|
|
12247
13824
|
{
|
|
12248
|
-
hook_event_name: "
|
|
13825
|
+
hook_event_name: "PreToolUse",
|
|
12249
13826
|
cwd,
|
|
12250
|
-
session_id:
|
|
12251
|
-
thread_id: "thread-
|
|
12252
|
-
|
|
12253
|
-
|
|
13827
|
+
session_id: sessionId,
|
|
13828
|
+
thread_id: "thread-autopilot-ralplan-pretool-block",
|
|
13829
|
+
tool_name: "Edit",
|
|
13830
|
+
tool_input: { file_path: "src/runtime.ts" },
|
|
12254
13831
|
},
|
|
12255
13832
|
{ cwd },
|
|
12256
13833
|
);
|
|
12257
13834
|
|
|
12258
|
-
assert.equal(result.omxEventName, "
|
|
12259
|
-
assert.
|
|
12260
|
-
|
|
12261
|
-
reason:
|
|
12262
|
-
`OMX team pipeline is still active (review-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
|
|
12263
|
-
stopReason: "team_team-verify",
|
|
12264
|
-
systemMessage: "OMX team pipeline is still active at phase team-verify.",
|
|
12265
|
-
});
|
|
13835
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
13836
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
13837
|
+
assert.match(String(result.outputJson?.reason ?? ""), /Ralplan is active .*implementation\/write tools are blocked/i);
|
|
12266
13838
|
} finally {
|
|
12267
13839
|
await rm(cwd, { recursive: true, force: true });
|
|
12268
13840
|
}
|
|
12269
13841
|
});
|
|
12270
13842
|
|
|
12271
|
-
it("
|
|
12272
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
13843
|
+
it("allows implementation writes when terminal Autopilot run-state shadows stale supervised ralplan state", async () => {
|
|
13844
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-terminal-pretool-"));
|
|
12273
13845
|
try {
|
|
12274
13846
|
const stateDir = join(cwd, ".omx", "state");
|
|
12275
|
-
|
|
12276
|
-
|
|
12277
|
-
await writeJson(join(stateDir, "session.json"), {
|
|
12278
|
-
|
|
12279
|
-
|
|
13847
|
+
const sessionId = "sess-autopilot-ralplan-terminal-pretool";
|
|
13848
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
13849
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
13850
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
13851
|
+
active: true,
|
|
13852
|
+
skill: "autopilot",
|
|
13853
|
+
phase: "ralplan",
|
|
13854
|
+
session_id: sessionId,
|
|
13855
|
+
active_skills: [{ skill: "autopilot", phase: "ralplan", active: true, session_id: sessionId }],
|
|
12280
13856
|
});
|
|
12281
|
-
await writeJson(join(stateDir, "sessions",
|
|
13857
|
+
await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
|
|
12282
13858
|
active: true,
|
|
12283
|
-
|
|
12284
|
-
|
|
12285
|
-
session_id:
|
|
13859
|
+
mode: "autopilot",
|
|
13860
|
+
current_phase: "ralplan",
|
|
13861
|
+
session_id: sessionId,
|
|
12286
13862
|
});
|
|
12287
|
-
await writeJson(join(stateDir, "
|
|
12288
|
-
|
|
12289
|
-
|
|
12290
|
-
|
|
12291
|
-
|
|
12292
|
-
|
|
13863
|
+
await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
|
|
13864
|
+
version: 1,
|
|
13865
|
+
active: false,
|
|
13866
|
+
mode: "autopilot",
|
|
13867
|
+
outcome: "finish",
|
|
13868
|
+
lifecycle_outcome: "finished",
|
|
13869
|
+
current_phase: "complete",
|
|
13870
|
+
completed_at: "2026-05-30T00:00:00.000Z",
|
|
13871
|
+
updated_at: "2026-05-30T00:00:00.000Z",
|
|
12293
13872
|
});
|
|
12294
13873
|
|
|
12295
|
-
await dispatchCodexNativeHook(
|
|
13874
|
+
const result = await dispatchCodexNativeHook(
|
|
12296
13875
|
{
|
|
12297
|
-
hook_event_name: "
|
|
13876
|
+
hook_event_name: "PreToolUse",
|
|
12298
13877
|
cwd,
|
|
12299
|
-
session_id:
|
|
12300
|
-
thread_id: "thread-
|
|
12301
|
-
|
|
13878
|
+
session_id: sessionId,
|
|
13879
|
+
thread_id: "thread-autopilot-ralplan-terminal-pretool",
|
|
13880
|
+
tool_name: "Edit",
|
|
13881
|
+
tool_input: { file_path: "src/runtime.ts" },
|
|
12302
13882
|
},
|
|
12303
13883
|
{ cwd },
|
|
12304
13884
|
);
|
|
12305
13885
|
|
|
12306
|
-
|
|
12307
|
-
|
|
12308
|
-
|
|
12309
|
-
|
|
12310
|
-
|
|
12311
|
-
|
|
12312
|
-
turn_id: "turn-stop-team-drift-1",
|
|
12313
|
-
stop_hook_active: true,
|
|
12314
|
-
},
|
|
12315
|
-
{ cwd },
|
|
12316
|
-
);
|
|
13886
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
13887
|
+
assert.equal(result.outputJson, null);
|
|
13888
|
+
} finally {
|
|
13889
|
+
await rm(cwd, { recursive: true, force: true });
|
|
13890
|
+
}
|
|
13891
|
+
});
|
|
12317
13892
|
|
|
12318
|
-
|
|
12319
|
-
|
|
12320
|
-
|
|
12321
|
-
|
|
12322
|
-
|
|
12323
|
-
|
|
12324
|
-
|
|
13893
|
+
it("blocks bash implementation writes while Autopilot is supervising ralplan without handoff", async () => {
|
|
13894
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-pretool-bash-block-"));
|
|
13895
|
+
try {
|
|
13896
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
13897
|
+
const sessionId = "sess-autopilot-ralplan-pretool-bash-block";
|
|
13898
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
13899
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
13900
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
13901
|
+
active: true,
|
|
13902
|
+
skill: "autopilot",
|
|
13903
|
+
phase: "ralplan",
|
|
13904
|
+
session_id: sessionId,
|
|
13905
|
+
active_skills: [{ skill: "autopilot", phase: "ralplan", active: true, session_id: sessionId }],
|
|
13906
|
+
});
|
|
13907
|
+
await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
|
|
13908
|
+
active: true,
|
|
13909
|
+
mode: "autopilot",
|
|
13910
|
+
current_phase: "ralplan",
|
|
13911
|
+
session_id: sessionId,
|
|
12325
13912
|
});
|
|
12326
13913
|
|
|
12327
|
-
const
|
|
13914
|
+
const result = await dispatchCodexNativeHook(
|
|
12328
13915
|
{
|
|
12329
|
-
hook_event_name: "
|
|
13916
|
+
hook_event_name: "PreToolUse",
|
|
12330
13917
|
cwd,
|
|
12331
|
-
session_id:
|
|
12332
|
-
thread_id: "thread-
|
|
12333
|
-
|
|
12334
|
-
|
|
13918
|
+
session_id: sessionId,
|
|
13919
|
+
thread_id: "thread-autopilot-ralplan-pretool-bash-block",
|
|
13920
|
+
tool_name: "Bash",
|
|
13921
|
+
tool_input: { command: "cat <<'EOF' > src/runtime.ts\nimplementation\nEOF" },
|
|
12335
13922
|
},
|
|
12336
13923
|
{ cwd },
|
|
12337
13924
|
);
|
|
12338
13925
|
|
|
12339
|
-
assert.equal(
|
|
12340
|
-
assert.
|
|
12341
|
-
|
|
12342
|
-
reason:
|
|
12343
|
-
`OMX team pipeline is still active (current-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
|
|
12344
|
-
stopReason: "team_team-verify",
|
|
12345
|
-
systemMessage: "OMX team pipeline is still active at phase team-verify.",
|
|
12346
|
-
});
|
|
12347
|
-
|
|
12348
|
-
const persisted = JSON.parse(
|
|
12349
|
-
await readFile(join(stateDir, "native-stop-state.json"), "utf-8"),
|
|
12350
|
-
) as { sessions?: Record<string, unknown> };
|
|
12351
|
-
assert.deepEqual(Object.keys(persisted.sessions ?? {}), ["omx-canonical"]);
|
|
13926
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
13927
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
13928
|
+
assert.match(String(result.outputJson?.reason ?? ""), /Ralplan is active .*implementation\/write tools are blocked/i);
|
|
12352
13929
|
} finally {
|
|
12353
13930
|
await rm(cwd, { recursive: true, force: true });
|
|
12354
13931
|
}
|
|
12355
13932
|
});
|
|
12356
13933
|
|
|
12357
|
-
it("
|
|
12358
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
13934
|
+
it("allows ralplan planning artifact writes without execution handoff", async () => {
|
|
13935
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-artifact-"));
|
|
12359
13936
|
try {
|
|
12360
13937
|
const stateDir = join(cwd, ".omx", "state");
|
|
12361
|
-
|
|
12362
|
-
await
|
|
13938
|
+
const sessionId = "sess-ralplan-pretool-artifact";
|
|
13939
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
13940
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
13941
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
12363
13942
|
active: true,
|
|
12364
|
-
|
|
13943
|
+
skill: "ralplan",
|
|
13944
|
+
phase: "planning",
|
|
13945
|
+
session_id: sessionId,
|
|
13946
|
+
active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
|
|
13947
|
+
});
|
|
13948
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
13949
|
+
active: true,
|
|
13950
|
+
mode: "ralplan",
|
|
13951
|
+
current_phase: "planning",
|
|
13952
|
+
session_id: sessionId,
|
|
12365
13953
|
});
|
|
12366
13954
|
|
|
12367
|
-
const
|
|
13955
|
+
const result = await dispatchCodexNativeHook(
|
|
12368
13956
|
{
|
|
12369
|
-
hook_event_name: "
|
|
13957
|
+
hook_event_name: "PreToolUse",
|
|
12370
13958
|
cwd,
|
|
12371
|
-
session_id:
|
|
12372
|
-
thread_id: "thread-
|
|
12373
|
-
|
|
13959
|
+
session_id: sessionId,
|
|
13960
|
+
thread_id: "thread-ralplan-pretool-artifact",
|
|
13961
|
+
tool_name: "Write",
|
|
13962
|
+
tool_input: { file_path: ".omx/plans/prd-issue-2603.md" },
|
|
12374
13963
|
},
|
|
12375
13964
|
{ cwd },
|
|
12376
13965
|
);
|
|
12377
13966
|
|
|
12378
|
-
|
|
12379
|
-
|
|
12380
|
-
|
|
12381
|
-
|
|
12382
|
-
|
|
12383
|
-
|
|
12384
|
-
turn_id: "turn-stop-ultrawork-repeat-1",
|
|
12385
|
-
stop_hook_active: true,
|
|
12386
|
-
},
|
|
12387
|
-
{ cwd },
|
|
12388
|
-
);
|
|
13967
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
13968
|
+
assert.equal(result.outputJson, null);
|
|
13969
|
+
} finally {
|
|
13970
|
+
await rm(cwd, { recursive: true, force: true });
|
|
13971
|
+
}
|
|
13972
|
+
});
|
|
12389
13973
|
|
|
12390
|
-
|
|
13974
|
+
it("blocks bash implementation writes while ralplan is active without execution handoff", async () => {
|
|
13975
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-bash-block-"));
|
|
13976
|
+
try {
|
|
13977
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
13978
|
+
const sessionId = "sess-ralplan-pretool-bash-block";
|
|
13979
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
13980
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
13981
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
13982
|
+
active: true,
|
|
13983
|
+
skill: "ralplan",
|
|
13984
|
+
phase: "planning",
|
|
13985
|
+
session_id: sessionId,
|
|
13986
|
+
active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
|
|
13987
|
+
});
|
|
13988
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
13989
|
+
active: true,
|
|
13990
|
+
mode: "ralplan",
|
|
13991
|
+
current_phase: "planning",
|
|
13992
|
+
session_id: sessionId,
|
|
13993
|
+
});
|
|
13994
|
+
|
|
13995
|
+
const result = await dispatchCodexNativeHook(
|
|
12391
13996
|
{
|
|
12392
|
-
hook_event_name: "
|
|
13997
|
+
hook_event_name: "PreToolUse",
|
|
12393
13998
|
cwd,
|
|
12394
|
-
session_id:
|
|
12395
|
-
thread_id: "thread-
|
|
12396
|
-
|
|
12397
|
-
|
|
13999
|
+
session_id: sessionId,
|
|
14000
|
+
thread_id: "thread-ralplan-pretool-bash-block",
|
|
14001
|
+
tool_name: "Bash",
|
|
14002
|
+
tool_input: { command: "cat <<'EOF' > src/runtime.ts\nimplementation\nEOF" },
|
|
12398
14003
|
},
|
|
12399
14004
|
{ cwd },
|
|
12400
14005
|
);
|
|
12401
14006
|
|
|
12402
|
-
assert.equal(
|
|
12403
|
-
assert.
|
|
12404
|
-
assert.
|
|
12405
|
-
assert.deepEqual(fresh.outputJson, {
|
|
12406
|
-
decision: "block",
|
|
12407
|
-
reason: "OMX ultrawork is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
12408
|
-
stopReason: "ultrawork_executing",
|
|
12409
|
-
systemMessage: "OMX ultrawork is still active (phase: executing).",
|
|
12410
|
-
});
|
|
14007
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
14008
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
14009
|
+
assert.match(String(result.outputJson?.reason ?? ""), /Ralplan is active .*implementation\/write tools are blocked/i);
|
|
12411
14010
|
} finally {
|
|
12412
14011
|
await rm(cwd, { recursive: true, force: true });
|
|
12413
14012
|
}
|
|
12414
14013
|
});
|
|
12415
14014
|
|
|
12416
|
-
it("
|
|
12417
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
14015
|
+
it("allows bash planning artifact writes while ralplan is active without execution handoff", async () => {
|
|
14016
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-bash-artifact-"));
|
|
12418
14017
|
try {
|
|
12419
14018
|
const stateDir = join(cwd, ".omx", "state");
|
|
12420
|
-
|
|
12421
|
-
await
|
|
12422
|
-
await writeJson(join(stateDir, "
|
|
14019
|
+
const sessionId = "sess-ralplan-pretool-bash-artifact";
|
|
14020
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
14021
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
14022
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
12423
14023
|
active: true,
|
|
12424
14024
|
skill: "ralplan",
|
|
12425
14025
|
phase: "planning",
|
|
14026
|
+
session_id: sessionId,
|
|
14027
|
+
active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
|
|
12426
14028
|
});
|
|
12427
|
-
await writeJson(join(stateDir, "sessions",
|
|
14029
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
12428
14030
|
active: true,
|
|
14031
|
+
mode: "ralplan",
|
|
12429
14032
|
current_phase: "planning",
|
|
14033
|
+
session_id: sessionId,
|
|
12430
14034
|
});
|
|
12431
14035
|
|
|
12432
|
-
await dispatchCodexNativeHook(
|
|
14036
|
+
const result = await dispatchCodexNativeHook(
|
|
12433
14037
|
{
|
|
12434
|
-
hook_event_name: "
|
|
14038
|
+
hook_event_name: "PreToolUse",
|
|
12435
14039
|
cwd,
|
|
12436
|
-
session_id:
|
|
12437
|
-
thread_id: "thread-
|
|
12438
|
-
|
|
14040
|
+
session_id: sessionId,
|
|
14041
|
+
thread_id: "thread-ralplan-pretool-bash-artifact",
|
|
14042
|
+
tool_name: "Bash",
|
|
14043
|
+
tool_input: { command: "cat <<'EOF' > .omx/plans/prd-issue-2603.md\nplanning\nEOF" },
|
|
12439
14044
|
},
|
|
12440
14045
|
{ cwd },
|
|
12441
14046
|
);
|
|
12442
14047
|
|
|
12443
|
-
|
|
14048
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
14049
|
+
assert.equal(result.outputJson, null);
|
|
14050
|
+
} finally {
|
|
14051
|
+
await rm(cwd, { recursive: true, force: true });
|
|
14052
|
+
}
|
|
14053
|
+
});
|
|
14054
|
+
|
|
14055
|
+
it("allows implementation writes when an explicit execution handoff is active", async () => {
|
|
14056
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-handoff-"));
|
|
14057
|
+
try {
|
|
14058
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
14059
|
+
const sessionId = "sess-ralplan-pretool-handoff";
|
|
14060
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
14061
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
14062
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
14063
|
+
active: true,
|
|
14064
|
+
skill: "ultragoal",
|
|
14065
|
+
phase: "planning",
|
|
14066
|
+
session_id: sessionId,
|
|
14067
|
+
active_skills: [
|
|
14068
|
+
{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId },
|
|
14069
|
+
{ skill: "ultragoal", phase: "planning", active: true, session_id: sessionId },
|
|
14070
|
+
],
|
|
14071
|
+
});
|
|
14072
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
14073
|
+
active: true,
|
|
14074
|
+
mode: "ralplan",
|
|
14075
|
+
current_phase: "complete",
|
|
14076
|
+
session_id: sessionId,
|
|
14077
|
+
});
|
|
14078
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
|
|
14079
|
+
active: true,
|
|
14080
|
+
mode: "ultragoal",
|
|
14081
|
+
current_phase: "planning",
|
|
14082
|
+
session_id: sessionId,
|
|
14083
|
+
});
|
|
14084
|
+
|
|
14085
|
+
const result = await dispatchCodexNativeHook(
|
|
12444
14086
|
{
|
|
12445
|
-
hook_event_name: "
|
|
14087
|
+
hook_event_name: "PreToolUse",
|
|
12446
14088
|
cwd,
|
|
12447
|
-
session_id:
|
|
12448
|
-
thread_id: "thread-
|
|
12449
|
-
|
|
12450
|
-
|
|
14089
|
+
session_id: sessionId,
|
|
14090
|
+
thread_id: "thread-ralplan-pretool-handoff",
|
|
14091
|
+
tool_name: "Edit",
|
|
14092
|
+
tool_input: { file_path: "src/runtime.ts" },
|
|
12451
14093
|
},
|
|
12452
14094
|
{ cwd },
|
|
12453
14095
|
);
|
|
12454
14096
|
|
|
12455
|
-
assert.equal(
|
|
12456
|
-
assert.equal(
|
|
12457
|
-
assert.match(String(repeated.outputJson?.reason ?? ""), /Status: continue_from_artifact/);
|
|
12458
|
-
assert.match(String(repeated.outputJson?.reason ?? ""), /continue from the current ralplan artifact/i);
|
|
12459
|
-
assert.equal(repeated.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
|
|
14097
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
14098
|
+
assert.equal(result.outputJson, null);
|
|
12460
14099
|
} finally {
|
|
12461
14100
|
await rm(cwd, { recursive: true, force: true });
|
|
12462
14101
|
}
|
|
@@ -13091,6 +14730,46 @@ describe("codex native hook triage integration", () => {
|
|
|
13091
14730
|
}
|
|
13092
14731
|
});
|
|
13093
14732
|
|
|
14733
|
+
it("makes bare autopilot command activation observable in state and prompt guidance", async () => {
|
|
14734
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-autopilot-bare-observable-"));
|
|
14735
|
+
try {
|
|
14736
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
14737
|
+
await writeSessionStart(cwd, "sess-autopilot-bare-observable");
|
|
14738
|
+
|
|
14739
|
+
const result = await dispatchCodexNativeHook(
|
|
14740
|
+
{
|
|
14741
|
+
hook_event_name: "UserPromptSubmit",
|
|
14742
|
+
cwd,
|
|
14743
|
+
session_id: "sess-autopilot-bare-observable",
|
|
14744
|
+
thread_id: "thread-autopilot-bare-observable",
|
|
14745
|
+
turn_id: "turn-autopilot-bare-observable",
|
|
14746
|
+
prompt: "run autopilot",
|
|
14747
|
+
},
|
|
14748
|
+
{ cwd },
|
|
14749
|
+
);
|
|
14750
|
+
|
|
14751
|
+
assert.equal(result.skillState?.skill, "autopilot");
|
|
14752
|
+
assert.equal(result.skillState?.phase, "deep-interview");
|
|
14753
|
+
assert.equal(result.skillState?.initialized_state_path, ".omx/state/sessions/sess-autopilot-bare-observable/autopilot-state.json");
|
|
14754
|
+
|
|
14755
|
+
const additionalContext = String(
|
|
14756
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
14757
|
+
);
|
|
14758
|
+
assert.match(additionalContext, /detected workflow keyword "autopilot" -> autopilot/);
|
|
14759
|
+
assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
|
|
14760
|
+
|
|
14761
|
+
const statePath = join(cwd, ".omx", "state", "sessions", "sess-autopilot-bare-observable", "autopilot-state.json");
|
|
14762
|
+
const modeState = JSON.parse(await readFile(statePath, "utf-8")) as {
|
|
14763
|
+
active: boolean;
|
|
14764
|
+
current_phase: string;
|
|
14765
|
+
};
|
|
14766
|
+
assert.equal(modeState.active, true);
|
|
14767
|
+
assert.equal(modeState.current_phase, "deep-interview");
|
|
14768
|
+
} finally {
|
|
14769
|
+
await rm(cwd, { recursive: true, force: true });
|
|
14770
|
+
}
|
|
14771
|
+
});
|
|
14772
|
+
|
|
13094
14773
|
// ── Group 2: HEAVY injection ─────────────────────────────────────────────
|
|
13095
14774
|
|
|
13096
14775
|
it("injects HEAVY advisory and writes prompt-routing-state for a multi-step goal prompt", async () => {
|