oh-my-codex 0.13.2 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +5 -5
- package/Cargo.toml +1 -1
- package/dist/autoresearch/__tests__/skill-validation.test.d.ts +2 -0
- package/dist/autoresearch/__tests__/skill-validation.test.d.ts.map +1 -0
- package/dist/autoresearch/__tests__/skill-validation.test.js +91 -0
- package/dist/autoresearch/__tests__/skill-validation.test.js.map +1 -0
- package/dist/autoresearch/skill-validation.d.ts +13 -0
- package/dist/autoresearch/skill-validation.d.ts.map +1 -0
- package/dist/autoresearch/skill-validation.js +165 -0
- package/dist/autoresearch/skill-validation.js.map +1 -0
- package/dist/catalog/__tests__/schema.test.js +6 -0
- package/dist/catalog/__tests__/schema.test.js.map +1 -1
- package/dist/cli/__tests__/autoresearch-guided.test.js +236 -273
- package/dist/cli/__tests__/autoresearch-guided.test.js.map +1 -1
- package/dist/cli/__tests__/autoresearch.test.js +64 -653
- package/dist/cli/__tests__/autoresearch.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +7 -0
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/nested-help-routing.test.js +2 -1
- package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
- package/dist/cli/__tests__/question.test.d.ts +2 -0
- package/dist/cli/__tests__/question.test.d.ts.map +1 -0
- package/dist/cli/__tests__/question.test.js +113 -0
- package/dist/cli/__tests__/question.test.js.map +1 -0
- package/dist/cli/__tests__/session-search-help.test.js +1 -1
- package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
- package/dist/cli/__tests__/setup-skills-overwrite.test.js +2 -0
- package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
- package/dist/cli/autoresearch-guided.d.ts +24 -7
- package/dist/cli/autoresearch-guided.d.ts.map +1 -1
- package/dist/cli/autoresearch-guided.js +189 -130
- package/dist/cli/autoresearch-guided.js.map +1 -1
- package/dist/cli/autoresearch.d.ts +3 -2
- package/dist/cli/autoresearch.d.ts.map +1 -1
- package/dist/cli/autoresearch.js +29 -305
- package/dist/cli/autoresearch.js.map +1 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +43 -0
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +8 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/question.d.ts +3 -0
- package/dist/cli/question.d.ts.map +1 -0
- package/dist/cli/question.js +182 -0
- package/dist/cli/question.js.map +1 -0
- package/dist/hooks/__tests__/analyze-routing-contract.test.js +22 -13
- package/dist/hooks/__tests__/analyze-routing-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/anti-slop-workflow.test.js +3 -3
- package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
- package/dist/hooks/__tests__/debugger-log-recency-contract.test.js +2 -2
- package/dist/hooks/__tests__/debugger-log-recency-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/deep-interview-contract.test.js +22 -5
- package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js +2 -2
- package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +308 -17
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +570 -2
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +717 -16
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +25 -0
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +894 -1
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +34 -0
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +132 -0
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js +22 -4
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-fragments.test.js +4 -2
- package/dist/hooks/__tests__/prompt-guidance-fragments.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-test-helpers.d.ts +1 -0
- package/dist/hooks/__tests__/prompt-guidance-test-helpers.d.ts.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-test-helpers.js +4 -1
- package/dist/hooks/__tests__/prompt-guidance-test-helpers.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +28 -0
- package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-orchestration-boundary.test.js +5 -4
- package/dist/hooks/__tests__/prompt-orchestration-boundary.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-team-routing.test.js +2 -2
- package/dist/hooks/__tests__/prompt-team-routing.test.js.map +1 -1
- package/dist/hooks/__tests__/triage-config.test.d.ts +2 -0
- package/dist/hooks/__tests__/triage-config.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/triage-config.test.js +211 -0
- package/dist/hooks/__tests__/triage-config.test.js.map +1 -0
- package/dist/hooks/__tests__/triage-heuristic.test.d.ts +2 -0
- package/dist/hooks/__tests__/triage-heuristic.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/triage-heuristic.test.js +230 -0
- package/dist/hooks/__tests__/triage-heuristic.test.js.map +1 -0
- package/dist/hooks/__tests__/triage-state.test.d.ts +2 -0
- package/dist/hooks/__tests__/triage-state.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/triage-state.test.js +426 -0
- package/dist/hooks/__tests__/triage-state.test.js.map +1 -0
- package/dist/hooks/keyword-detector.d.ts +26 -7
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +97 -26
- 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 +16 -9
- package/dist/hooks/keyword-registry.js.map +1 -1
- package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
- package/dist/hooks/prompt-guidance-contract.js +28 -1
- package/dist/hooks/prompt-guidance-contract.js.map +1 -1
- package/dist/hooks/triage-config.d.ts +33 -0
- package/dist/hooks/triage-config.d.ts.map +1 -0
- package/dist/hooks/triage-config.js +87 -0
- package/dist/hooks/triage-config.js.map +1 -0
- package/dist/hooks/triage-heuristic.d.ts +20 -0
- package/dist/hooks/triage-heuristic.d.ts.map +1 -0
- package/dist/hooks/triage-heuristic.js +210 -0
- package/dist/hooks/triage-heuristic.js.map +1 -0
- package/dist/hooks/triage-state.d.ts +63 -0
- package/dist/hooks/triage-state.d.ts.map +1 -0
- package/dist/hooks/triage-state.js +138 -0
- package/dist/hooks/triage-state.js.map +1 -0
- package/dist/hud/__tests__/reconcile.test.js +20 -0
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/reconcile.d.ts +1 -0
- package/dist/hud/reconcile.d.ts.map +1 -1
- package/dist/hud/reconcile.js +2 -1
- package/dist/hud/reconcile.js.map +1 -1
- package/dist/mcp/__tests__/state-server.test.js +1 -0
- package/dist/mcp/__tests__/state-server.test.js.map +1 -1
- package/dist/mcp/state-server.d.ts +8 -0
- package/dist/mcp/state-server.d.ts.map +1 -1
- package/dist/mcp/state-server.js +4 -0
- package/dist/mcp/state-server.js.map +1 -1
- package/dist/modes/__tests__/base-ralph-contract.test.js +15 -0
- package/dist/modes/__tests__/base-ralph-contract.test.js.map +1 -1
- package/dist/modes/base.d.ts +1 -0
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +22 -6
- package/dist/modes/base.js.map +1 -1
- package/dist/notifications/__tests__/index.test.js +78 -0
- package/dist/notifications/__tests__/index.test.js.map +1 -1
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +39 -22
- package/dist/notifications/index.js.map +1 -1
- package/dist/openclaw/index.d.ts +5 -3
- package/dist/openclaw/index.d.ts.map +1 -1
- package/dist/openclaw/index.js +5 -3
- package/dist/openclaw/index.js.map +1 -1
- package/dist/question/__tests__/client.test.d.ts +2 -0
- package/dist/question/__tests__/client.test.d.ts.map +1 -0
- package/dist/question/__tests__/client.test.js +70 -0
- package/dist/question/__tests__/client.test.js.map +1 -0
- package/dist/question/__tests__/deep-interview.test.d.ts +2 -0
- package/dist/question/__tests__/deep-interview.test.d.ts.map +1 -0
- package/dist/question/__tests__/deep-interview.test.js +108 -0
- package/dist/question/__tests__/deep-interview.test.js.map +1 -0
- package/dist/question/__tests__/policy.test.d.ts +2 -0
- package/dist/question/__tests__/policy.test.d.ts.map +1 -0
- package/dist/question/__tests__/policy.test.js +107 -0
- package/dist/question/__tests__/policy.test.js.map +1 -0
- package/dist/question/__tests__/renderer.test.d.ts +2 -0
- package/dist/question/__tests__/renderer.test.d.ts.map +1 -0
- package/dist/question/__tests__/renderer.test.js +88 -0
- package/dist/question/__tests__/renderer.test.js.map +1 -0
- package/dist/question/__tests__/state.test.d.ts +2 -0
- package/dist/question/__tests__/state.test.d.ts.map +1 -0
- package/dist/question/__tests__/state.test.js +55 -0
- package/dist/question/__tests__/state.test.js.map +1 -0
- package/dist/question/__tests__/types.test.d.ts +2 -0
- package/dist/question/__tests__/types.test.d.ts.map +1 -0
- package/dist/question/__tests__/types.test.js +44 -0
- package/dist/question/__tests__/types.test.js.map +1 -0
- package/dist/question/__tests__/ui.test.d.ts +2 -0
- package/dist/question/__tests__/ui.test.d.ts.map +1 -0
- package/dist/question/__tests__/ui.test.js +169 -0
- package/dist/question/__tests__/ui.test.js.map +1 -0
- package/dist/question/client.d.ts +54 -0
- package/dist/question/client.d.ts.map +1 -0
- package/dist/question/client.js +77 -0
- package/dist/question/client.js.map +1 -0
- package/dist/question/deep-interview.d.ts +27 -0
- package/dist/question/deep-interview.d.ts.map +1 -0
- package/dist/question/deep-interview.js +101 -0
- package/dist/question/deep-interview.js.map +1 -0
- package/dist/question/policy.d.ts +18 -0
- package/dist/question/policy.d.ts.map +1 -0
- package/dist/question/policy.js +77 -0
- package/dist/question/policy.js.map +1 -0
- package/dist/question/renderer.d.ts +18 -0
- package/dist/question/renderer.d.ts.map +1 -0
- package/dist/question/renderer.js +128 -0
- package/dist/question/renderer.js.map +1 -0
- package/dist/question/state.d.ts +19 -0
- package/dist/question/state.d.ts.map +1 -0
- package/dist/question/state.js +108 -0
- package/dist/question/state.js.map +1 -0
- package/dist/question/types.d.ts +66 -0
- package/dist/question/types.d.ts.map +1 -0
- package/dist/question/types.js +82 -0
- package/dist/question/types.js.map +1 -0
- package/dist/question/ui.d.ts +38 -0
- package/dist/question/ui.d.ts.map +1 -0
- package/dist/question/ui.js +321 -0
- package/dist/question/ui.js.map +1 -0
- package/dist/ralph/contract.d.ts +1 -1
- package/dist/ralph/contract.d.ts.map +1 -1
- package/dist/ralph/contract.js +4 -1
- package/dist/ralph/contract.js.map +1 -1
- package/dist/ralplan/runtime.js +1 -1
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/runtime/__tests__/run-loop.test.d.ts +2 -0
- package/dist/runtime/__tests__/run-loop.test.d.ts.map +1 -0
- package/dist/runtime/__tests__/run-loop.test.js +35 -0
- package/dist/runtime/__tests__/run-loop.test.js.map +1 -0
- package/dist/runtime/__tests__/run-outcome.test.d.ts +2 -0
- package/dist/runtime/__tests__/run-outcome.test.d.ts.map +1 -0
- package/dist/runtime/__tests__/run-outcome.test.js +64 -0
- package/dist/runtime/__tests__/run-outcome.test.js.map +1 -0
- package/dist/runtime/run-loop.d.ts +41 -0
- package/dist/runtime/run-loop.d.ts.map +1 -0
- package/dist/runtime/run-loop.js +46 -0
- package/dist/runtime/run-loop.js.map +1 -0
- package/dist/runtime/run-outcome.d.ts +28 -0
- package/dist/runtime/run-outcome.d.ts.map +1 -0
- package/dist/runtime/run-outcome.js +136 -0
- package/dist/runtime/run-outcome.js.map +1 -0
- package/dist/runtime/run-state.d.ts +36 -0
- package/dist/runtime/run-state.d.ts.map +1 -0
- package/dist/runtime/run-state.js +110 -0
- package/dist/runtime/run-state.js.map +1 -0
- package/dist/scripts/__tests__/codex-native-hook.test.js +1128 -85
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts +2 -0
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +199 -11
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/notify-fallback-watcher.js +81 -2
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.d.ts +27 -0
- package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.js +83 -20
- package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -1
- package/dist/scripts/notify-hook/managed-tmux.js +64 -38
- package/dist/scripts/notify-hook/managed-tmux.js.map +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.js +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
- package/dist/scripts/notify-hook.js +15 -5
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/scripts/sync-prompt-guidance-fragments.js +5 -0
- package/dist/scripts/sync-prompt-guidance-fragments.js.map +1 -1
- package/dist/state/__tests__/operations-ralph-phase.test.js +21 -0
- package/dist/state/__tests__/operations-ralph-phase.test.js.map +1 -1
- package/dist/state/__tests__/workflow-transition.test.js +11 -0
- 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 +15 -0
- package/dist/state/operations.js.map +1 -1
- package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
- package/dist/state/workflow-transition-reconcile.js +14 -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 +3 -1
- package/dist/state/workflow-transition.js.map +1 -1
- package/dist/team/__tests__/followup-planner.test.js +15 -0
- package/dist/team/__tests__/followup-planner.test.js.map +1 -1
- package/dist/team/__tests__/role-router.test.js +41 -0
- package/dist/team/__tests__/role-router.test.js.map +1 -1
- package/dist/team/followup-planner.d.ts.map +1 -1
- package/dist/team/followup-planner.js +31 -9
- package/dist/team/followup-planner.js.map +1 -1
- package/dist/team/role-router.d.ts.map +1 -1
- package/dist/team/role-router.js +73 -0
- package/dist/team/role-router.js.map +1 -1
- package/package.json +3 -2
- package/prompts/dependency-expert.md +3 -0
- package/prompts/executor.md +5 -0
- package/prompts/explore.md +2 -0
- package/prompts/planner.md +5 -0
- package/prompts/product-analyst.md +8 -8
- package/prompts/researcher.md +78 -30
- package/prompts/verifier.md +4 -0
- package/skills/autoresearch/SKILL.md +68 -0
- package/skills/deep-interview/SKILL.md +10 -9
- package/skills/help/SKILL.md +3 -1
- package/skills/ralplan/SKILL.md +1 -0
- package/skills/team/SKILL.md +1 -0
- package/skills/ultrawork/SKILL.md +1 -0
- package/src/scripts/__tests__/codex-native-hook.test.ts +1495 -188
- package/src/scripts/codex-native-hook.ts +235 -19
- package/src/scripts/notify-fallback-watcher.ts +92 -2
- package/src/scripts/notify-hook/auto-nudge.ts +89 -20
- package/src/scripts/notify-hook/managed-tmux.ts +70 -31
- package/src/scripts/notify-hook/ralph-session-resume.ts +1 -1
- package/src/scripts/notify-hook.ts +23 -5
- package/src/scripts/sync-prompt-guidance-fragments.ts +4 -0
- package/templates/AGENTS.md +48 -37
- package/templates/catalog-manifest.json +7 -0
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
resolveSessionOwnerPidFromAncestry,
|
|
19
19
|
} from "../codex-native-hook.js";
|
|
20
20
|
import { writeSessionStart } from "../../hooks/session.js";
|
|
21
|
+
import { resetTriageConfigCache } from "../../hooks/triage-config.js";
|
|
21
22
|
|
|
22
23
|
async function writeJson(path: string, value: unknown): Promise<void> {
|
|
23
24
|
await mkdir(dirname(path), { recursive: true }).catch(() => {});
|
|
@@ -262,6 +263,45 @@ describe("codex native hook dispatch", () => {
|
|
|
262
263
|
}
|
|
263
264
|
});
|
|
264
265
|
|
|
266
|
+
it("passes the canonical OMX session id when UserPromptSubmit revives HUD", async () => {
|
|
267
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-session-revive-"));
|
|
268
|
+
try {
|
|
269
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
270
|
+
const canonicalSessionId = "omx-launch-hud";
|
|
271
|
+
const nativeSessionId = "codex-native-hud";
|
|
272
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
273
|
+
await writeSessionStart(cwd, canonicalSessionId);
|
|
274
|
+
|
|
275
|
+
let reconcileCall: { cwd: string; sessionId?: string } | null = null;
|
|
276
|
+
const promptResult = await dispatchCodexNativeHook(
|
|
277
|
+
{
|
|
278
|
+
hook_event_name: "UserPromptSubmit",
|
|
279
|
+
cwd,
|
|
280
|
+
session_id: nativeSessionId,
|
|
281
|
+
thread_id: "thread-hud",
|
|
282
|
+
turn_id: "turn-hud",
|
|
283
|
+
prompt: "$ralplan fix orphaned hud session handoff",
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
cwd,
|
|
287
|
+
reconcileHudForPromptSubmitFn: async (hookCwd, deps = {}) => {
|
|
288
|
+
reconcileCall = { cwd: hookCwd, sessionId: deps.sessionId };
|
|
289
|
+
return { status: 'recreated', paneId: '%9', desiredHeight: 3, duplicateCount: 0 };
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
assert.equal(promptResult.omxEventName, "keyword-detector");
|
|
295
|
+
assert.deepEqual(reconcileCall, { cwd, sessionId: canonicalSessionId });
|
|
296
|
+
assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "skill-active-state.json")), true);
|
|
297
|
+
assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "ralplan-state.json")), true);
|
|
298
|
+
assert.equal(existsSync(join(stateDir, "sessions", nativeSessionId, "skill-active-state.json")), false);
|
|
299
|
+
assert.equal(existsSync(join(stateDir, "sessions", nativeSessionId, "ralplan-state.json")), false);
|
|
300
|
+
} finally {
|
|
301
|
+
await rm(cwd, { recursive: true, force: true });
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
265
305
|
it("appends .omx/ to repo-root .gitignore during SessionStart when missing", async () => {
|
|
266
306
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-session-gitignore-"));
|
|
267
307
|
try {
|
|
@@ -482,7 +522,14 @@ describe("codex native hook dispatch", () => {
|
|
|
482
522
|
|
|
483
523
|
assert.equal(result.omxEventName, "keyword-detector");
|
|
484
524
|
assert.equal(result.skillState, null);
|
|
485
|
-
|
|
525
|
+
// Triage may inject advisory LIGHT/explore context for the question-shaped
|
|
526
|
+
// prompt, but the invariant this test guards is that no Ralph workflow state
|
|
527
|
+
// is seeded and no Ralph-activation message is emitted.
|
|
528
|
+
const advisoryContext = String(
|
|
529
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
530
|
+
);
|
|
531
|
+
assert.doesNotMatch(advisoryContext, /skill:\s*ralph/i);
|
|
532
|
+
assert.doesNotMatch(advisoryContext, /ralph-state\.json/i);
|
|
486
533
|
assert.equal(existsSync(join(cwd, ".omx", "state", "skill-active-state.json")), false);
|
|
487
534
|
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ralph-plain-text", "skill-active-state.json")), false);
|
|
488
535
|
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ralph-plain-text", "ralph-state.json")), false);
|
|
@@ -491,6 +538,67 @@ describe("codex native hook dispatch", () => {
|
|
|
491
538
|
}
|
|
492
539
|
});
|
|
493
540
|
|
|
541
|
+
it("adds execution handoff context for non-keyword prompts that authorize implementation", async () => {
|
|
542
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-execution-handoff-"));
|
|
543
|
+
try {
|
|
544
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
545
|
+
const prompts = [
|
|
546
|
+
"按照这个plan开始执行优化",
|
|
547
|
+
"开始执行",
|
|
548
|
+
"继续优化",
|
|
549
|
+
"直接修复",
|
|
550
|
+
];
|
|
551
|
+
|
|
552
|
+
for (const [index, prompt] of prompts.entries()) {
|
|
553
|
+
const result = await dispatchCodexNativeHook(
|
|
554
|
+
{
|
|
555
|
+
hook_event_name: "UserPromptSubmit",
|
|
556
|
+
cwd,
|
|
557
|
+
session_id: `sess-exec-handoff-${index}`,
|
|
558
|
+
thread_id: `thread-exec-handoff-${index}`,
|
|
559
|
+
turn_id: `turn-exec-handoff-${index}`,
|
|
560
|
+
prompt,
|
|
561
|
+
},
|
|
562
|
+
{ cwd },
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
const message = String(
|
|
566
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
567
|
+
);
|
|
568
|
+
assert.match(message, /execution handoff/i, prompt);
|
|
569
|
+
assert.match(message, /Do not restate the prior plan/i, prompt);
|
|
570
|
+
}
|
|
571
|
+
} finally {
|
|
572
|
+
await rm(cwd, { recursive: true, force: true });
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it("adds latest-followup priority context for short same-thread follow-up prompts", async () => {
|
|
577
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-followup-priority-"));
|
|
578
|
+
try {
|
|
579
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
580
|
+
const result = await dispatchCodexNativeHook(
|
|
581
|
+
{
|
|
582
|
+
hook_event_name: "UserPromptSubmit",
|
|
583
|
+
cwd,
|
|
584
|
+
session_id: "sess-followup-priority",
|
|
585
|
+
thread_id: "thread-followup-priority",
|
|
586
|
+
turn_id: "turn-followup-priority",
|
|
587
|
+
prompt: "这些优化都做了么",
|
|
588
|
+
},
|
|
589
|
+
{ cwd },
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
const message = String(
|
|
593
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
594
|
+
);
|
|
595
|
+
assert.match(message, /same-thread follow-up/i);
|
|
596
|
+
assert.match(message, /prefer it over older unresolved prompts/i);
|
|
597
|
+
} finally {
|
|
598
|
+
await rm(cwd, { recursive: true, force: true });
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
|
|
494
602
|
it("clarifies that prompt-side $ralph activation does not invoke the PRD-gated CLI path", async () => {
|
|
495
603
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralph-routing-"));
|
|
496
604
|
try {
|
|
@@ -521,6 +629,144 @@ describe("codex native hook dispatch", () => {
|
|
|
521
629
|
}
|
|
522
630
|
});
|
|
523
631
|
|
|
632
|
+
it("keeps bare keep-going continuation on the active autopilot skill instead of denying with generic ralph overlap", async () => {
|
|
633
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-bare-continuation-"));
|
|
634
|
+
try {
|
|
635
|
+
const sessionId = "sess-autopilot-cont";
|
|
636
|
+
const sessionDir = join(cwd, ".omx", "state", "sessions", sessionId);
|
|
637
|
+
await mkdir(sessionDir, { recursive: true });
|
|
638
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
639
|
+
version: 1,
|
|
640
|
+
active: true,
|
|
641
|
+
skill: "autopilot",
|
|
642
|
+
keyword: "$autopilot",
|
|
643
|
+
phase: "planning",
|
|
644
|
+
session_id: sessionId,
|
|
645
|
+
active_skills: [
|
|
646
|
+
{ skill: "autopilot", phase: "planning", active: true, session_id: sessionId },
|
|
647
|
+
],
|
|
648
|
+
});
|
|
649
|
+
await writeJson(join(sessionDir, "autopilot-state.json"), {
|
|
650
|
+
active: true,
|
|
651
|
+
mode: "autopilot",
|
|
652
|
+
current_phase: "execution",
|
|
653
|
+
started_at: "2026-04-19T00:00:00.000Z",
|
|
654
|
+
updated_at: "2026-04-19T00:10:00.000Z",
|
|
655
|
+
session_id: sessionId,
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
const result = await dispatchCodexNativeHook(
|
|
659
|
+
{
|
|
660
|
+
hook_event_name: "UserPromptSubmit",
|
|
661
|
+
cwd,
|
|
662
|
+
session_id: sessionId,
|
|
663
|
+
thread_id: "thread-autopilot-cont",
|
|
664
|
+
turn_id: "turn-autopilot-cont",
|
|
665
|
+
prompt: "\ keep going now",
|
|
666
|
+
},
|
|
667
|
+
{ cwd },
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
671
|
+
assert.equal(result.skillState?.skill, "autopilot");
|
|
672
|
+
const message = String(
|
|
673
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
674
|
+
);
|
|
675
|
+
assert.match(message, /"keep going" -> ralph/);
|
|
676
|
+
assert.doesNotMatch(message, /denied workflow keyword/i);
|
|
677
|
+
assert.doesNotMatch(message, /Unsupported workflow overlap: autopilot \+ ralph\./);
|
|
678
|
+
assert.doesNotMatch(message, /Prompt-side `\$ralph` activation/);
|
|
679
|
+
assert.equal(existsSync(join(sessionDir, "ralph-state.json")), false);
|
|
680
|
+
} finally {
|
|
681
|
+
await rm(cwd, { recursive: true, force: true });
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it("clarifies that prompt-side deep-interview activation must use omx question", async () => {
|
|
686
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-routing-"));
|
|
687
|
+
try {
|
|
688
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
689
|
+
const result = await dispatchCodexNativeHook(
|
|
690
|
+
{
|
|
691
|
+
hook_event_name: "UserPromptSubmit",
|
|
692
|
+
cwd,
|
|
693
|
+
session_id: "sess-deep-interview-msg",
|
|
694
|
+
thread_id: "thread-deep-interview-msg",
|
|
695
|
+
turn_id: "turn-deep-interview-msg",
|
|
696
|
+
prompt: "$deep-interview gather requirements",
|
|
697
|
+
},
|
|
698
|
+
{ cwd },
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
702
|
+
assert.equal(result.skillState?.skill, "deep-interview");
|
|
703
|
+
const message = String(
|
|
704
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
705
|
+
);
|
|
706
|
+
assert.match(message, /\$deep-interview" -> deep-interview/);
|
|
707
|
+
assert.match(message, /skill: deep-interview activated and initial state initialized at \.omx\/state\/sessions\/sess-deep-interview-msg\/deep-interview-state\.json; write subsequent updates via omx_state MCP\./);
|
|
708
|
+
assert.match(message, /Deep-interview must ask each interview round via `omx question`/);
|
|
709
|
+
assert.match(message, /do not fall back to `request_user_input` or plain-text questioning/i);
|
|
710
|
+
assert.match(message, /Stop remains blocked while a deep-interview question obligation is pending\./);
|
|
711
|
+
} finally {
|
|
712
|
+
await rm(cwd, { recursive: true, force: true });
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
it("keeps bare keep-going continuation on the active ralph skill without resetting through generic keep-going routing", async () => {
|
|
717
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralph-bare-continuation-"));
|
|
718
|
+
try {
|
|
719
|
+
const sessionId = "sess-ralph-cont";
|
|
720
|
+
const sessionDir = join(cwd, ".omx", "state", "sessions", sessionId);
|
|
721
|
+
await mkdir(sessionDir, { recursive: true });
|
|
722
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
723
|
+
version: 1,
|
|
724
|
+
active: true,
|
|
725
|
+
skill: "ralph",
|
|
726
|
+
keyword: "$ralph",
|
|
727
|
+
phase: "executing",
|
|
728
|
+
session_id: sessionId,
|
|
729
|
+
active_skills: [
|
|
730
|
+
{ skill: "ralph", phase: "executing", active: true, session_id: sessionId },
|
|
731
|
+
],
|
|
732
|
+
});
|
|
733
|
+
await writeJson(join(sessionDir, "ralph-state.json"), {
|
|
734
|
+
active: true,
|
|
735
|
+
mode: "ralph",
|
|
736
|
+
current_phase: "verifying",
|
|
737
|
+
started_at: "2026-04-19T00:00:00.000Z",
|
|
738
|
+
updated_at: "2026-04-19T00:10:00.000Z",
|
|
739
|
+
iteration: 4,
|
|
740
|
+
max_iterations: 50,
|
|
741
|
+
session_id: sessionId,
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
const result = await dispatchCodexNativeHook(
|
|
745
|
+
{
|
|
746
|
+
hook_event_name: "UserPromptSubmit",
|
|
747
|
+
cwd,
|
|
748
|
+
session_id: sessionId,
|
|
749
|
+
thread_id: "thread-ralph-cont",
|
|
750
|
+
turn_id: "turn-ralph-cont",
|
|
751
|
+
prompt: "keep going now",
|
|
752
|
+
},
|
|
753
|
+
{ cwd },
|
|
754
|
+
);
|
|
755
|
+
|
|
756
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
757
|
+
assert.equal(result.skillState?.skill, "ralph");
|
|
758
|
+
const message = String(
|
|
759
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
760
|
+
);
|
|
761
|
+
assert.match(message, /"keep going" -> ralph/);
|
|
762
|
+
assert.doesNotMatch(message, /denied workflow keyword/i);
|
|
763
|
+
assert.doesNotMatch(message, /mode transiting:/);
|
|
764
|
+
} finally {
|
|
765
|
+
await rm(cwd, { recursive: true, force: true });
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
|
|
524
770
|
it("ignores generic wrapper fields so metadata cannot trigger workflow routing or Stop blocking", async () => {
|
|
525
771
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-wrapper-metadata-"));
|
|
526
772
|
try {
|
|
@@ -1942,6 +2188,33 @@ esac
|
|
|
1942
2188
|
}
|
|
1943
2189
|
});
|
|
1944
2190
|
|
|
2191
|
+
it("does not block Stop when an explicit blocked_on_user run_outcome is present on a mode state", async () => {
|
|
2192
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-blocked-outcome-"));
|
|
2193
|
+
try {
|
|
2194
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
2195
|
+
await mkdir(stateDir, { recursive: true });
|
|
2196
|
+
await writeJson(join(stateDir, "autopilot-state.json"), {
|
|
2197
|
+
active: true,
|
|
2198
|
+
current_phase: "execution",
|
|
2199
|
+
run_outcome: "blocked_on_user",
|
|
2200
|
+
});
|
|
2201
|
+
|
|
2202
|
+
const result = await dispatchCodexNativeHook(
|
|
2203
|
+
{
|
|
2204
|
+
hook_event_name: "Stop",
|
|
2205
|
+
cwd,
|
|
2206
|
+
session_id: "sess-stop-autopilot-blocked-outcome",
|
|
2207
|
+
},
|
|
2208
|
+
{ cwd },
|
|
2209
|
+
);
|
|
2210
|
+
|
|
2211
|
+
assert.equal(result.omxEventName, "stop");
|
|
2212
|
+
assert.equal(result.outputJson, null);
|
|
2213
|
+
} finally {
|
|
2214
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2215
|
+
}
|
|
2216
|
+
});
|
|
2217
|
+
|
|
1945
2218
|
it("returns Stop continuation output while Ultrawork is active", async () => {
|
|
1946
2219
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultrawork-"));
|
|
1947
2220
|
try {
|
|
@@ -2699,201 +2972,315 @@ esac
|
|
|
2699
2972
|
}
|
|
2700
2973
|
});
|
|
2701
2974
|
|
|
2702
|
-
it("
|
|
2703
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
2975
|
+
it("blocks Stop while autoresearch is active without validator completion", async () => {
|
|
2976
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autoresearch-"));
|
|
2704
2977
|
try {
|
|
2705
2978
|
const stateDir = join(cwd, ".omx", "state");
|
|
2706
|
-
await mkdir(join(stateDir, "sessions", "sess-stop-
|
|
2707
|
-
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-
|
|
2708
|
-
await writeJson(join(stateDir, "sessions", "sess-stop-
|
|
2709
|
-
active: true,
|
|
2710
|
-
skill: "deep-interview",
|
|
2711
|
-
phase: "planning",
|
|
2712
|
-
});
|
|
2713
|
-
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview", "deep-interview-state.json"), {
|
|
2979
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-autoresearch"), { recursive: true });
|
|
2980
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-autoresearch", cwd });
|
|
2981
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-autoresearch", "autoresearch-state.json"), {
|
|
2714
2982
|
active: true,
|
|
2715
|
-
|
|
2983
|
+
mode: "autoresearch",
|
|
2984
|
+
current_phase: "executing",
|
|
2985
|
+
session_id: "sess-stop-autoresearch",
|
|
2986
|
+
validation_mode: "mission-validator-script",
|
|
2987
|
+
mission_validator_command: "node scripts/validate.js",
|
|
2988
|
+
completion_artifact_path: '.omx/specs/autoresearch-demo/completion.json',
|
|
2716
2989
|
});
|
|
2717
2990
|
|
|
2718
2991
|
const result = await dispatchCodexNativeHook(
|
|
2719
2992
|
{
|
|
2720
2993
|
hook_event_name: "Stop",
|
|
2721
2994
|
cwd,
|
|
2722
|
-
session_id: "sess-stop-
|
|
2995
|
+
session_id: "sess-stop-autoresearch",
|
|
2723
2996
|
},
|
|
2724
2997
|
{ cwd },
|
|
2725
2998
|
);
|
|
2726
2999
|
|
|
2727
|
-
assert.equal(result.
|
|
3000
|
+
assert.equal(result.omxEventName, "stop");
|
|
3001
|
+
assert.deepEqual(result.outputJson, {
|
|
3002
|
+
decision: "block",
|
|
3003
|
+
reason: "OMX autoresearch is still active (phase: executing); continue until validator evidence is complete before stopping.",
|
|
3004
|
+
stopReason: "autoresearch_executing",
|
|
3005
|
+
systemMessage: "OMX autoresearch is still active (phase: executing); continue until validator evidence is complete before stopping.",
|
|
3006
|
+
});
|
|
2728
3007
|
} finally {
|
|
2729
3008
|
await rm(cwd, { recursive: true, force: true });
|
|
2730
3009
|
}
|
|
2731
3010
|
});
|
|
2732
3011
|
|
|
2733
|
-
it("
|
|
2734
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
3012
|
+
it("allows Stop once autoresearch validator evidence is complete", async () => {
|
|
3013
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autoresearch-complete-"));
|
|
2735
3014
|
try {
|
|
2736
3015
|
const stateDir = join(cwd, ".omx", "state");
|
|
2737
|
-
|
|
2738
|
-
await
|
|
3016
|
+
const specDir = join(cwd, '.omx', 'specs', 'autoresearch-demo');
|
|
3017
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-autoresearch-complete"), { recursive: true });
|
|
3018
|
+
await mkdir(specDir, { recursive: true });
|
|
3019
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-autoresearch-complete", cwd });
|
|
3020
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-autoresearch-complete", "autoresearch-state.json"), {
|
|
2739
3021
|
active: true,
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
session_id: "",
|
|
2743
|
-
|
|
3022
|
+
mode: "autoresearch",
|
|
3023
|
+
current_phase: "reviewing",
|
|
3024
|
+
session_id: "sess-stop-autoresearch-complete",
|
|
3025
|
+
validation_mode: "mission-validator-script",
|
|
3026
|
+
mission_validator_command: "node scripts/validate.js",
|
|
3027
|
+
completion_artifact_path: '.omx/specs/autoresearch-demo/completion.json',
|
|
2744
3028
|
});
|
|
3029
|
+
await writeJson(join(specDir, 'completion.json'), { status: 'passed', passed: true });
|
|
2745
3030
|
|
|
2746
3031
|
const result = await dispatchCodexNativeHook(
|
|
2747
3032
|
{
|
|
2748
3033
|
hook_event_name: "Stop",
|
|
2749
3034
|
cwd,
|
|
2750
|
-
session_id: "sess-stop-
|
|
2751
|
-
thread_id: "main-thread",
|
|
3035
|
+
session_id: "sess-stop-autoresearch-complete",
|
|
2752
3036
|
},
|
|
2753
3037
|
{ cwd },
|
|
2754
3038
|
);
|
|
2755
3039
|
|
|
3040
|
+
assert.equal(result.omxEventName, "stop");
|
|
2756
3041
|
assert.equal(result.outputJson, null);
|
|
2757
3042
|
} finally {
|
|
2758
3043
|
await rm(cwd, { recursive: true, force: true });
|
|
2759
3044
|
}
|
|
2760
3045
|
});
|
|
2761
3046
|
|
|
2762
|
-
it("
|
|
2763
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-"));
|
|
3047
|
+
it("does not block Stop from stale root autoresearch state when the explicit session has no scoped autoresearch state", async () => {
|
|
3048
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-root-autoresearch-"));
|
|
2764
3049
|
try {
|
|
2765
3050
|
const stateDir = join(cwd, ".omx", "state");
|
|
2766
|
-
|
|
2767
|
-
await
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
3051
|
+
const specDir = join(cwd, '.omx', 'specs', 'autoresearch-demo');
|
|
3052
|
+
await mkdir(join(stateDir, 'sessions', 'sess-current'), { recursive: true });
|
|
3053
|
+
await mkdir(specDir, { recursive: true });
|
|
3054
|
+
await writeJson(join(stateDir, 'session.json'), { session_id: 'sess-current', cwd });
|
|
3055
|
+
await writeJson(join(stateDir, 'autoresearch-state.json'), {
|
|
3056
|
+
active: true,
|
|
3057
|
+
mode: 'autoresearch',
|
|
3058
|
+
current_phase: 'executing',
|
|
3059
|
+
validation_mode: 'mission-validator-script',
|
|
3060
|
+
mission_validator_command: 'node scripts/validate.js',
|
|
3061
|
+
completion_artifact_path: '.omx/specs/autoresearch-demo/completion.json',
|
|
3062
|
+
});
|
|
2774
3063
|
|
|
2775
3064
|
const result = await dispatchCodexNativeHook(
|
|
2776
3065
|
{
|
|
2777
|
-
hook_event_name:
|
|
3066
|
+
hook_event_name: 'Stop',
|
|
2778
3067
|
cwd,
|
|
3068
|
+
session_id: 'sess-current',
|
|
2779
3069
|
},
|
|
2780
3070
|
{ cwd },
|
|
2781
3071
|
);
|
|
2782
3072
|
|
|
2783
|
-
assert.equal(result.omxEventName,
|
|
2784
|
-
assert.
|
|
2785
|
-
decision: "block",
|
|
2786
|
-
reason:
|
|
2787
|
-
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
2788
|
-
stopReason: "ralph_executing",
|
|
2789
|
-
systemMessage:
|
|
2790
|
-
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
2791
|
-
});
|
|
3073
|
+
assert.equal(result.omxEventName, 'stop');
|
|
3074
|
+
assert.equal(result.outputJson, null);
|
|
2792
3075
|
} finally {
|
|
2793
3076
|
await rm(cwd, { recursive: true, force: true });
|
|
2794
3077
|
}
|
|
2795
3078
|
});
|
|
2796
3079
|
|
|
2797
|
-
it("
|
|
2798
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
3080
|
+
it("does not block Stop solely because deep-interview is active", async () => {
|
|
3081
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-"));
|
|
2799
3082
|
try {
|
|
2800
3083
|
const stateDir = join(cwd, ".omx", "state");
|
|
2801
|
-
await mkdir(join(stateDir, "sessions", "sess-
|
|
2802
|
-
await writeJson(join(stateDir, "session.json"), { session_id: "sess-
|
|
2803
|
-
await writeJson(join(stateDir, "sessions", "sess-
|
|
3084
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-deep-interview"), { recursive: true });
|
|
3085
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-deep-interview" });
|
|
3086
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview", "skill-active-state.json"), {
|
|
2804
3087
|
active: true,
|
|
2805
|
-
|
|
2806
|
-
|
|
3088
|
+
skill: "deep-interview",
|
|
3089
|
+
phase: "planning",
|
|
3090
|
+
});
|
|
3091
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview", "deep-interview-state.json"), {
|
|
3092
|
+
active: true,
|
|
3093
|
+
current_phase: "planning",
|
|
2807
3094
|
});
|
|
2808
3095
|
|
|
2809
3096
|
const result = await dispatchCodexNativeHook(
|
|
2810
3097
|
{
|
|
2811
3098
|
hook_event_name: "Stop",
|
|
2812
3099
|
cwd,
|
|
2813
|
-
session_id: "sess-
|
|
3100
|
+
session_id: "sess-stop-deep-interview",
|
|
2814
3101
|
},
|
|
2815
3102
|
{ cwd },
|
|
2816
3103
|
);
|
|
2817
3104
|
|
|
2818
|
-
assert.equal(result.
|
|
2819
|
-
assert.deepEqual(result.outputJson, {
|
|
2820
|
-
decision: "block",
|
|
2821
|
-
reason:
|
|
2822
|
-
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
2823
|
-
stopReason: "ralph_executing",
|
|
2824
|
-
systemMessage:
|
|
2825
|
-
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
2826
|
-
});
|
|
3105
|
+
assert.equal(result.outputJson, null);
|
|
2827
3106
|
} finally {
|
|
2828
3107
|
await rm(cwd, { recursive: true, force: true });
|
|
2829
3108
|
}
|
|
2830
3109
|
});
|
|
2831
3110
|
|
|
2832
|
-
it("
|
|
2833
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
3111
|
+
it("blocks Stop when deep-interview has a pending omx question obligation", async () => {
|
|
3112
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-question-"));
|
|
2834
3113
|
try {
|
|
2835
3114
|
const stateDir = join(cwd, ".omx", "state");
|
|
2836
|
-
await mkdir(join(stateDir, "sessions", "sess-
|
|
2837
|
-
await
|
|
2838
|
-
await writeJson(join(stateDir, "
|
|
2839
|
-
|
|
3115
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-deep-interview-question"), { recursive: true });
|
|
3116
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-deep-interview-question" });
|
|
3117
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question", "skill-active-state.json"), {
|
|
3118
|
+
version: 1,
|
|
2840
3119
|
active: true,
|
|
2841
|
-
|
|
2842
|
-
|
|
3120
|
+
skill: "deep-interview",
|
|
3121
|
+
phase: "planning",
|
|
3122
|
+
session_id: "sess-stop-deep-interview-question",
|
|
3123
|
+
thread_id: "thread-stop-deep-interview-question",
|
|
3124
|
+
});
|
|
3125
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question", "deep-interview-state.json"), {
|
|
3126
|
+
active: true,
|
|
3127
|
+
mode: "deep-interview",
|
|
3128
|
+
current_phase: "intent-first",
|
|
3129
|
+
session_id: "sess-stop-deep-interview-question",
|
|
3130
|
+
thread_id: "thread-stop-deep-interview-question",
|
|
3131
|
+
question_enforcement: {
|
|
3132
|
+
obligation_id: "obligation-1",
|
|
3133
|
+
source: "omx-question",
|
|
3134
|
+
status: "pending",
|
|
3135
|
+
requested_at: "2026-04-19T03:20:00.000Z",
|
|
3136
|
+
},
|
|
2843
3137
|
});
|
|
2844
3138
|
|
|
2845
3139
|
const result = await dispatchCodexNativeHook(
|
|
2846
3140
|
{
|
|
2847
3141
|
hook_event_name: "Stop",
|
|
2848
3142
|
cwd,
|
|
2849
|
-
session_id: "sess-
|
|
3143
|
+
session_id: "sess-stop-deep-interview-question",
|
|
3144
|
+
thread_id: "thread-stop-deep-interview-question",
|
|
2850
3145
|
},
|
|
2851
3146
|
{ cwd },
|
|
2852
3147
|
);
|
|
2853
3148
|
|
|
2854
3149
|
assert.equal(result.omxEventName, "stop");
|
|
2855
|
-
assert.
|
|
3150
|
+
assert.deepEqual(result.outputJson, {
|
|
3151
|
+
decision: "block",
|
|
3152
|
+
reason:
|
|
3153
|
+
"Deep interview is still active (phase: intent-first) and has a pending structured question obligation; use `omx question` before stopping.",
|
|
3154
|
+
stopReason: "deep_interview_question_required",
|
|
3155
|
+
systemMessage:
|
|
3156
|
+
"OMX deep-interview is still active (phase: intent-first) and requires a structured question via omx question before stopping.",
|
|
3157
|
+
});
|
|
2856
3158
|
} finally {
|
|
2857
3159
|
await rm(cwd, { recursive: true, force: true });
|
|
2858
3160
|
}
|
|
2859
3161
|
});
|
|
2860
3162
|
|
|
2861
|
-
it("
|
|
2862
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
3163
|
+
it("keeps blocking pending deep-interview question Stop replays until the obligation changes", async () => {
|
|
3164
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-question-replay-"));
|
|
2863
3165
|
try {
|
|
2864
3166
|
const stateDir = join(cwd, ".omx", "state");
|
|
2865
|
-
await mkdir(join(stateDir, "sessions", "sess-
|
|
2866
|
-
await writeJson(join(stateDir, "
|
|
3167
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-deep-interview-question-replay"), { recursive: true });
|
|
3168
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-deep-interview-question-replay" });
|
|
3169
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question-replay", "skill-active-state.json"), {
|
|
3170
|
+
version: 1,
|
|
2867
3171
|
active: true,
|
|
2868
|
-
|
|
2869
|
-
|
|
3172
|
+
skill: "deep-interview",
|
|
3173
|
+
phase: "planning",
|
|
3174
|
+
session_id: "sess-stop-deep-interview-question-replay",
|
|
2870
3175
|
});
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
3176
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question-replay", "deep-interview-state.json"), {
|
|
3177
|
+
active: true,
|
|
3178
|
+
mode: "deep-interview",
|
|
3179
|
+
current_phase: "intent-first",
|
|
3180
|
+
question_enforcement: {
|
|
3181
|
+
obligation_id: "obligation-replay",
|
|
3182
|
+
source: "omx-question",
|
|
3183
|
+
status: "pending",
|
|
3184
|
+
requested_at: "2026-04-19T03:20:00.000Z",
|
|
2877
3185
|
},
|
|
2878
|
-
|
|
2879
|
-
);
|
|
3186
|
+
});
|
|
2880
3187
|
|
|
2881
|
-
|
|
2882
|
-
|
|
3188
|
+
const payload = {
|
|
3189
|
+
hook_event_name: "Stop",
|
|
3190
|
+
cwd,
|
|
3191
|
+
session_id: "sess-stop-deep-interview-question-replay",
|
|
3192
|
+
};
|
|
3193
|
+
const expected = {
|
|
3194
|
+
decision: "block",
|
|
3195
|
+
reason:
|
|
3196
|
+
"Deep interview is still active (phase: intent-first) and has a pending structured question obligation; use `omx question` before stopping.",
|
|
3197
|
+
stopReason: "deep_interview_question_required",
|
|
3198
|
+
systemMessage:
|
|
3199
|
+
"OMX deep-interview is still active (phase: intent-first) and requires a structured question via omx question before stopping.",
|
|
3200
|
+
};
|
|
3201
|
+
|
|
3202
|
+
const first = await dispatchCodexNativeHook(payload, { cwd });
|
|
3203
|
+
const replay = await dispatchCodexNativeHook({ ...payload, stop_hook_active: true }, { cwd });
|
|
3204
|
+
|
|
3205
|
+
assert.equal(first.omxEventName, "stop");
|
|
3206
|
+
assert.deepEqual(first.outputJson, expected);
|
|
3207
|
+
assert.equal(replay.omxEventName, "stop");
|
|
3208
|
+
assert.deepEqual(replay.outputJson, expected);
|
|
2883
3209
|
} finally {
|
|
2884
3210
|
await rm(cwd, { recursive: true, force: true });
|
|
2885
3211
|
}
|
|
2886
3212
|
});
|
|
2887
3213
|
|
|
2888
|
-
it("does not block Stop
|
|
2889
|
-
const
|
|
3214
|
+
it("does not block Stop once the deep-interview question obligation is satisfied or cleared", async () => {
|
|
3215
|
+
for (const status of ["satisfied", "cleared"] as const) {
|
|
3216
|
+
const cwd = await mkdtemp(join(tmpdir(), `omx-native-hook-stop-deep-interview-question-${status}-`));
|
|
3217
|
+
try {
|
|
3218
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
3219
|
+
await mkdir(join(stateDir, "sessions", `sess-stop-deep-interview-question-${status}`), { recursive: true });
|
|
3220
|
+
await writeJson(join(stateDir, "session.json"), { session_id: `sess-stop-deep-interview-question-${status}` });
|
|
3221
|
+
await writeJson(join(stateDir, "sessions", `sess-stop-deep-interview-question-${status}`, "skill-active-state.json"), {
|
|
3222
|
+
version: 1,
|
|
3223
|
+
active: true,
|
|
3224
|
+
skill: "deep-interview",
|
|
3225
|
+
phase: "planning",
|
|
3226
|
+
session_id: `sess-stop-deep-interview-question-${status}`,
|
|
3227
|
+
});
|
|
3228
|
+
await writeJson(join(stateDir, "sessions", `sess-stop-deep-interview-question-${status}`, "deep-interview-state.json"), {
|
|
3229
|
+
active: true,
|
|
3230
|
+
mode: "deep-interview",
|
|
3231
|
+
current_phase: "intent-first",
|
|
3232
|
+
question_enforcement: {
|
|
3233
|
+
obligation_id: `obligation-${status}`,
|
|
3234
|
+
source: "omx-question",
|
|
3235
|
+
status,
|
|
3236
|
+
requested_at: "2026-04-19T03:20:00.000Z",
|
|
3237
|
+
...(status === "satisfied"
|
|
3238
|
+
? { question_id: "question-1", satisfied_at: "2026-04-19T03:21:00.000Z" }
|
|
3239
|
+
: { cleared_at: "2026-04-19T03:21:00.000Z", clear_reason: "error" }),
|
|
3240
|
+
},
|
|
3241
|
+
});
|
|
3242
|
+
|
|
3243
|
+
const result = await dispatchCodexNativeHook(
|
|
3244
|
+
{
|
|
3245
|
+
hook_event_name: "Stop",
|
|
3246
|
+
cwd,
|
|
3247
|
+
session_id: `sess-stop-deep-interview-question-${status}`,
|
|
3248
|
+
},
|
|
3249
|
+
{ cwd },
|
|
3250
|
+
);
|
|
3251
|
+
|
|
3252
|
+
assert.equal(result.omxEventName, "stop");
|
|
3253
|
+
assert.equal(result.outputJson, null);
|
|
3254
|
+
} finally {
|
|
3255
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
});
|
|
3259
|
+
|
|
3260
|
+
it("ignores pending deep-interview question obligations from another session", async () => {
|
|
3261
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-question-foreign-session-"));
|
|
2890
3262
|
try {
|
|
2891
3263
|
const stateDir = join(cwd, ".omx", "state");
|
|
3264
|
+
await mkdir(join(stateDir, "sessions", "sess-other"), { recursive: true });
|
|
2892
3265
|
await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
|
|
2893
|
-
await writeJson(join(stateDir, "session.json"), { session_id: "sess-current"
|
|
2894
|
-
await writeJson(join(stateDir, "
|
|
3266
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
|
|
3267
|
+
await writeJson(join(stateDir, "sessions", "sess-other", "skill-active-state.json"), {
|
|
3268
|
+
version: 1,
|
|
2895
3269
|
active: true,
|
|
2896
|
-
|
|
3270
|
+
skill: "deep-interview",
|
|
3271
|
+
phase: "planning",
|
|
3272
|
+
session_id: "sess-other",
|
|
3273
|
+
});
|
|
3274
|
+
await writeJson(join(stateDir, "sessions", "sess-other", "deep-interview-state.json"), {
|
|
3275
|
+
active: true,
|
|
3276
|
+
mode: "deep-interview",
|
|
3277
|
+
current_phase: "intent-first",
|
|
3278
|
+
question_enforcement: {
|
|
3279
|
+
obligation_id: "obligation-foreign",
|
|
3280
|
+
source: "omx-question",
|
|
3281
|
+
status: "pending",
|
|
3282
|
+
requested_at: "2026-04-19T03:20:00.000Z",
|
|
3283
|
+
},
|
|
2897
3284
|
});
|
|
2898
3285
|
|
|
2899
3286
|
const result = await dispatchCodexNativeHook(
|
|
@@ -2912,72 +3299,87 @@ esac
|
|
|
2912
3299
|
}
|
|
2913
3300
|
});
|
|
2914
3301
|
|
|
2915
|
-
it("
|
|
2916
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
3302
|
+
it("blocks a new same-session deep-interview question obligation even after an earlier round was satisfied", async () => {
|
|
3303
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-question-next-round-"));
|
|
2917
3304
|
try {
|
|
2918
3305
|
const stateDir = join(cwd, ".omx", "state");
|
|
2919
|
-
await mkdir(join(stateDir, "sessions", "sess-
|
|
2920
|
-
await writeJson(join(stateDir, "session.json"), { session_id: "sess-
|
|
2921
|
-
await writeJson(join(stateDir, "sessions", "sess-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
3306
|
+
await mkdir(join(stateDir, "sessions", "sess-stop-deep-interview-question-next-round"), { recursive: true });
|
|
3307
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-deep-interview-question-next-round" });
|
|
3308
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question-next-round", "skill-active-state.json"), {
|
|
3309
|
+
version: 1,
|
|
3310
|
+
active: true,
|
|
3311
|
+
skill: "deep-interview",
|
|
3312
|
+
phase: "planning",
|
|
3313
|
+
session_id: "sess-stop-deep-interview-question-next-round",
|
|
2926
3314
|
});
|
|
2927
|
-
await writeJson(join(stateDir, "
|
|
3315
|
+
await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview-question-next-round", "deep-interview-state.json"), {
|
|
2928
3316
|
active: true,
|
|
2929
|
-
|
|
3317
|
+
mode: "deep-interview",
|
|
3318
|
+
current_phase: "intent-first",
|
|
3319
|
+
question_enforcement: {
|
|
3320
|
+
obligation_id: "obligation-next-round",
|
|
3321
|
+
source: "omx-question",
|
|
3322
|
+
status: "pending",
|
|
3323
|
+
requested_at: "2026-04-19T03:22:00.000Z",
|
|
3324
|
+
question_id: "question-old-round",
|
|
3325
|
+
satisfied_at: "2026-04-19T03:21:00.000Z",
|
|
3326
|
+
},
|
|
2930
3327
|
});
|
|
2931
3328
|
|
|
2932
3329
|
const result = await dispatchCodexNativeHook(
|
|
2933
3330
|
{
|
|
2934
3331
|
hook_event_name: "Stop",
|
|
2935
3332
|
cwd,
|
|
2936
|
-
session_id: "sess-
|
|
3333
|
+
session_id: "sess-stop-deep-interview-question-next-round",
|
|
2937
3334
|
},
|
|
2938
3335
|
{ cwd },
|
|
2939
3336
|
);
|
|
2940
3337
|
|
|
2941
3338
|
assert.equal(result.omxEventName, "stop");
|
|
2942
|
-
assert.
|
|
3339
|
+
assert.deepEqual(result.outputJson, {
|
|
3340
|
+
decision: "block",
|
|
3341
|
+
reason:
|
|
3342
|
+
"Deep interview is still active (phase: intent-first) and has a pending structured question obligation; use `omx question` before stopping.",
|
|
3343
|
+
stopReason: "deep_interview_question_required",
|
|
3344
|
+
systemMessage:
|
|
3345
|
+
"OMX deep-interview is still active (phase: intent-first) and requires a structured question via omx question before stopping.",
|
|
3346
|
+
});
|
|
2943
3347
|
} finally {
|
|
2944
3348
|
await rm(cwd, { recursive: true, force: true });
|
|
2945
3349
|
}
|
|
2946
3350
|
});
|
|
2947
3351
|
|
|
2948
|
-
it("
|
|
2949
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
3352
|
+
it("ignores root skill-active fallback from a different thread when evaluating Stop", async () => {
|
|
3353
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-foreign-thread-"));
|
|
2950
3354
|
try {
|
|
2951
3355
|
const stateDir = join(cwd, ".omx", "state");
|
|
2952
3356
|
await mkdir(stateDir, { recursive: true });
|
|
2953
|
-
await writeJson(join(stateDir, "
|
|
2954
|
-
session_id: "sess-elsewhere",
|
|
2955
|
-
cwd: join(cwd, "..", "different-worktree"),
|
|
2956
|
-
});
|
|
2957
|
-
await writeJson(join(stateDir, "ralph-state.json"), {
|
|
3357
|
+
await writeJson(join(stateDir, "skill-active-state.json"), {
|
|
2958
3358
|
active: true,
|
|
2959
|
-
|
|
3359
|
+
skill: "deep-interview",
|
|
3360
|
+
phase: "planning",
|
|
3361
|
+
session_id: "",
|
|
3362
|
+
thread_id: "other-thread",
|
|
2960
3363
|
});
|
|
2961
3364
|
|
|
2962
3365
|
const result = await dispatchCodexNativeHook(
|
|
2963
3366
|
{
|
|
2964
3367
|
hook_event_name: "Stop",
|
|
2965
3368
|
cwd,
|
|
2966
|
-
session_id: "sess-
|
|
3369
|
+
session_id: "sess-stop-main",
|
|
3370
|
+
thread_id: "main-thread",
|
|
2967
3371
|
},
|
|
2968
3372
|
{ cwd },
|
|
2969
3373
|
);
|
|
2970
3374
|
|
|
2971
|
-
assert.equal(result.omxEventName, "stop");
|
|
2972
3375
|
assert.equal(result.outputJson, null);
|
|
2973
3376
|
} finally {
|
|
2974
3377
|
await rm(cwd, { recursive: true, force: true });
|
|
2975
3378
|
}
|
|
2976
3379
|
});
|
|
2977
3380
|
|
|
2978
|
-
it("
|
|
2979
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
2980
|
-
const previousOmxSessionId = process.env.OMX_SESSION_ID;
|
|
3381
|
+
it("returns Stop continuation output while Ralph is active without an explicit session pin", async () => {
|
|
3382
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-"));
|
|
2981
3383
|
try {
|
|
2982
3384
|
const stateDir = join(cwd, ".omx", "state");
|
|
2983
3385
|
await mkdir(stateDir, { recursive: true });
|
|
@@ -2989,55 +3391,45 @@ esac
|
|
|
2989
3391
|
}),
|
|
2990
3392
|
);
|
|
2991
3393
|
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
3394
|
+
const result = await dispatchCodexNativeHook(
|
|
3395
|
+
{
|
|
3396
|
+
hook_event_name: "Stop",
|
|
3397
|
+
cwd,
|
|
3398
|
+
},
|
|
3399
|
+
{ cwd },
|
|
3400
|
+
);
|
|
3401
|
+
|
|
3402
|
+
assert.equal(result.omxEventName, "stop");
|
|
3403
|
+
assert.deepEqual(result.outputJson, {
|
|
2999
3404
|
decision: "block",
|
|
3000
3405
|
reason:
|
|
3001
3406
|
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
3002
3407
|
stopReason: "ralph_executing",
|
|
3003
3408
|
systemMessage:
|
|
3004
3409
|
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
3005
|
-
};
|
|
3006
|
-
|
|
3007
|
-
const first = await dispatchCodexNativeHook(payload, { cwd });
|
|
3008
|
-
const replay = await dispatchCodexNativeHook(
|
|
3009
|
-
{
|
|
3010
|
-
...payload,
|
|
3011
|
-
stop_hook_active: true,
|
|
3012
|
-
},
|
|
3013
|
-
{ cwd },
|
|
3014
|
-
);
|
|
3015
|
-
|
|
3016
|
-
assert.equal(first.omxEventName, "stop");
|
|
3017
|
-
assert.deepEqual(first.outputJson, expected);
|
|
3018
|
-
assert.equal(replay.omxEventName, "stop");
|
|
3019
|
-
assert.deepEqual(replay.outputJson, expected);
|
|
3410
|
+
});
|
|
3020
3411
|
} finally {
|
|
3021
|
-
if (typeof previousOmxSessionId === "string") process.env.OMX_SESSION_ID = previousOmxSessionId;
|
|
3022
|
-
else delete process.env.OMX_SESSION_ID;
|
|
3023
3412
|
await rm(cwd, { recursive: true, force: true });
|
|
3024
3413
|
}
|
|
3025
3414
|
});
|
|
3026
3415
|
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-"));
|
|
3416
|
+
it("blocks Stop from session-scoped Ralph state when session.json points to another session", async () => {
|
|
3417
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-session-mismatch-"));
|
|
3030
3418
|
try {
|
|
3031
3419
|
const stateDir = join(cwd, ".omx", "state");
|
|
3032
|
-
await mkdir(stateDir, { recursive: true });
|
|
3033
|
-
|
|
3420
|
+
await mkdir(join(stateDir, "sessions", "sess-live-ralph"), { recursive: true });
|
|
3421
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-other-ralph" });
|
|
3422
|
+
await writeJson(join(stateDir, "sessions", "sess-live-ralph", "ralph-state.json"), {
|
|
3423
|
+
active: true,
|
|
3424
|
+
current_phase: "executing",
|
|
3425
|
+
session_id: "sess-live-ralph",
|
|
3426
|
+
});
|
|
3034
3427
|
|
|
3035
3428
|
const result = await dispatchCodexNativeHook(
|
|
3036
3429
|
{
|
|
3037
3430
|
hook_event_name: "Stop",
|
|
3038
3431
|
cwd,
|
|
3039
|
-
session_id: "sess-
|
|
3040
|
-
last_assistant_message: "Keep going and finish the cleanup.",
|
|
3432
|
+
session_id: "sess-live-ralph",
|
|
3041
3433
|
},
|
|
3042
3434
|
{ cwd },
|
|
3043
3435
|
);
|
|
@@ -3045,73 +3437,300 @@ esac
|
|
|
3045
3437
|
assert.equal(result.omxEventName, "stop");
|
|
3046
3438
|
assert.deepEqual(result.outputJson, {
|
|
3047
3439
|
decision: "block",
|
|
3048
|
-
reason:
|
|
3049
|
-
|
|
3440
|
+
reason:
|
|
3441
|
+
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
3442
|
+
stopReason: "ralph_executing",
|
|
3050
3443
|
systemMessage:
|
|
3051
|
-
"OMX
|
|
3444
|
+
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
3052
3445
|
});
|
|
3053
3446
|
} finally {
|
|
3054
3447
|
await rm(cwd, { recursive: true, force: true });
|
|
3055
3448
|
}
|
|
3056
3449
|
});
|
|
3057
3450
|
|
|
3058
|
-
it("
|
|
3059
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
3451
|
+
it("does not block Stop from stale session-scoped Ralph state that belongs to another session", async () => {
|
|
3452
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-session-ralph-"));
|
|
3060
3453
|
try {
|
|
3061
3454
|
const stateDir = join(cwd, ".omx", "state");
|
|
3062
|
-
await mkdir(stateDir, { recursive: true });
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
await
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
thread_id: "thread-stop-auto",
|
|
3071
|
-
turn_id: "turn-stop-auto-1",
|
|
3072
|
-
last_assistant_message: "Keep going and finish the cleanup.",
|
|
3073
|
-
},
|
|
3074
|
-
{ cwd },
|
|
3075
|
-
);
|
|
3455
|
+
await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
|
|
3456
|
+
await mkdir(join(stateDir, "sessions", "sess-stale"), { recursive: true });
|
|
3457
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
|
|
3458
|
+
await writeJson(join(stateDir, "sessions", "sess-stale", "ralph-state.json"), {
|
|
3459
|
+
active: true,
|
|
3460
|
+
current_phase: "starting",
|
|
3461
|
+
session_id: "sess-stale",
|
|
3462
|
+
});
|
|
3076
3463
|
|
|
3077
3464
|
const result = await dispatchCodexNativeHook(
|
|
3078
3465
|
{
|
|
3079
3466
|
hook_event_name: "Stop",
|
|
3080
3467
|
cwd,
|
|
3081
|
-
session_id: "sess-
|
|
3082
|
-
thread_id: "thread-stop-auto",
|
|
3083
|
-
turn_id: "turn-stop-auto-1",
|
|
3084
|
-
stop_hook_active: true,
|
|
3085
|
-
last_assistant_message: "Keep going and finish the cleanup.",
|
|
3468
|
+
session_id: "sess-current",
|
|
3086
3469
|
},
|
|
3087
3470
|
{ cwd },
|
|
3088
3471
|
);
|
|
3089
3472
|
|
|
3090
3473
|
assert.equal(result.omxEventName, "stop");
|
|
3091
|
-
assert.
|
|
3092
|
-
decision: "block",
|
|
3093
|
-
reason: DEFAULT_AUTO_NUDGE_RESPONSE,
|
|
3094
|
-
stopReason: "auto_nudge",
|
|
3095
|
-
systemMessage:
|
|
3096
|
-
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
3097
|
-
});
|
|
3474
|
+
assert.equal(result.outputJson, null);
|
|
3098
3475
|
} finally {
|
|
3099
3476
|
await rm(cwd, { recursive: true, force: true });
|
|
3100
3477
|
}
|
|
3101
3478
|
});
|
|
3102
3479
|
|
|
3103
|
-
it("
|
|
3104
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
3480
|
+
it("does not block Stop from another session-scoped Ralph state when an explicit session_id has no active Ralph state", async () => {
|
|
3481
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-explicit-session-ralph-"));
|
|
3105
3482
|
try {
|
|
3106
3483
|
const stateDir = join(cwd, ".omx", "state");
|
|
3107
|
-
await mkdir(stateDir, { recursive: true });
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3484
|
+
await mkdir(join(stateDir, "sessions", "sess-other"), { recursive: true });
|
|
3485
|
+
await writeJson(join(stateDir, "sessions", "sess-other", "ralph-state.json"), {
|
|
3486
|
+
active: true,
|
|
3487
|
+
current_phase: "starting",
|
|
3488
|
+
session_id: "sess-other",
|
|
3112
3489
|
});
|
|
3113
3490
|
|
|
3114
|
-
await dispatchCodexNativeHook(
|
|
3491
|
+
const result = await dispatchCodexNativeHook(
|
|
3492
|
+
{
|
|
3493
|
+
hook_event_name: "Stop",
|
|
3494
|
+
cwd,
|
|
3495
|
+
session_id: "sess-current",
|
|
3496
|
+
},
|
|
3497
|
+
{ cwd },
|
|
3498
|
+
);
|
|
3499
|
+
|
|
3500
|
+
assert.equal(result.omxEventName, "stop");
|
|
3501
|
+
assert.equal(result.outputJson, null);
|
|
3502
|
+
} finally {
|
|
3503
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3504
|
+
}
|
|
3505
|
+
});
|
|
3506
|
+
|
|
3507
|
+
it("does not block Stop from root Ralph fallback when the current session has no scoped Ralph state", async () => {
|
|
3508
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-root-fallback-ralph-"));
|
|
3509
|
+
try {
|
|
3510
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
3511
|
+
await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
|
|
3512
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-current", cwd });
|
|
3513
|
+
await writeJson(join(stateDir, "ralph-state.json"), {
|
|
3514
|
+
active: true,
|
|
3515
|
+
current_phase: "executing",
|
|
3516
|
+
});
|
|
3517
|
+
|
|
3518
|
+
const result = await dispatchCodexNativeHook(
|
|
3519
|
+
{
|
|
3520
|
+
hook_event_name: "Stop",
|
|
3521
|
+
cwd,
|
|
3522
|
+
session_id: "sess-current",
|
|
3523
|
+
},
|
|
3524
|
+
{ cwd },
|
|
3525
|
+
);
|
|
3526
|
+
|
|
3527
|
+
assert.equal(result.omxEventName, "stop");
|
|
3528
|
+
assert.equal(result.outputJson, null);
|
|
3529
|
+
} finally {
|
|
3530
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3531
|
+
}
|
|
3532
|
+
});
|
|
3533
|
+
|
|
3534
|
+
it("does not block Stop when the current session Ralph state is cancelled even if stale root fallback remains", async () => {
|
|
3535
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-cancelled-session-ralph-"));
|
|
3536
|
+
try {
|
|
3537
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
3538
|
+
await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
|
|
3539
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-current", cwd });
|
|
3540
|
+
await writeJson(join(stateDir, "sessions", "sess-current", "ralph-state.json"), {
|
|
3541
|
+
active: false,
|
|
3542
|
+
current_phase: "cancelled",
|
|
3543
|
+
completed_at: "2026-04-10T23:30:38.000Z",
|
|
3544
|
+
session_id: "sess-current",
|
|
3545
|
+
});
|
|
3546
|
+
await writeJson(join(stateDir, "ralph-state.json"), {
|
|
3547
|
+
active: true,
|
|
3548
|
+
current_phase: "starting",
|
|
3549
|
+
});
|
|
3550
|
+
|
|
3551
|
+
const result = await dispatchCodexNativeHook(
|
|
3552
|
+
{
|
|
3553
|
+
hook_event_name: "Stop",
|
|
3554
|
+
cwd,
|
|
3555
|
+
session_id: "sess-current",
|
|
3556
|
+
},
|
|
3557
|
+
{ cwd },
|
|
3558
|
+
);
|
|
3559
|
+
|
|
3560
|
+
assert.equal(result.omxEventName, "stop");
|
|
3561
|
+
assert.equal(result.outputJson, null);
|
|
3562
|
+
} finally {
|
|
3563
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3564
|
+
}
|
|
3565
|
+
});
|
|
3566
|
+
|
|
3567
|
+
it("does not block Stop from root Ralph fallback when an explicit session_id is present and session.json points to another worktree", async () => {
|
|
3568
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-root-fallback-cwd-mismatch-"));
|
|
3569
|
+
try {
|
|
3570
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
3571
|
+
await mkdir(stateDir, { recursive: true });
|
|
3572
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
3573
|
+
session_id: "sess-elsewhere",
|
|
3574
|
+
cwd: join(cwd, "..", "different-worktree"),
|
|
3575
|
+
});
|
|
3576
|
+
await writeJson(join(stateDir, "ralph-state.json"), {
|
|
3577
|
+
active: true,
|
|
3578
|
+
current_phase: "executing",
|
|
3579
|
+
});
|
|
3580
|
+
|
|
3581
|
+
const result = await dispatchCodexNativeHook(
|
|
3582
|
+
{
|
|
3583
|
+
hook_event_name: "Stop",
|
|
3584
|
+
cwd,
|
|
3585
|
+
session_id: "sess-current",
|
|
3586
|
+
},
|
|
3587
|
+
{ cwd },
|
|
3588
|
+
);
|
|
3589
|
+
|
|
3590
|
+
assert.equal(result.omxEventName, "stop");
|
|
3591
|
+
assert.equal(result.outputJson, null);
|
|
3592
|
+
} finally {
|
|
3593
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3594
|
+
}
|
|
3595
|
+
});
|
|
3596
|
+
|
|
3597
|
+
it("keeps blocking Ralph Stop replays until the active task advances", async () => {
|
|
3598
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-replay-"));
|
|
3599
|
+
const previousOmxSessionId = process.env.OMX_SESSION_ID;
|
|
3600
|
+
try {
|
|
3601
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
3602
|
+
await mkdir(stateDir, { recursive: true });
|
|
3603
|
+
await writeFile(
|
|
3604
|
+
join(stateDir, "ralph-state.json"),
|
|
3605
|
+
JSON.stringify({
|
|
3606
|
+
active: true,
|
|
3607
|
+
current_phase: "executing",
|
|
3608
|
+
}),
|
|
3609
|
+
);
|
|
3610
|
+
|
|
3611
|
+
process.env.OMX_SESSION_ID = "sess-stop-ralph-replay";
|
|
3612
|
+
const payload = {
|
|
3613
|
+
hook_event_name: "Stop",
|
|
3614
|
+
cwd,
|
|
3615
|
+
last_assistant_message: "Next active targets:\n\n1. scheduler integration\n\nI am continuing.",
|
|
3616
|
+
};
|
|
3617
|
+
const expected = {
|
|
3618
|
+
decision: "block",
|
|
3619
|
+
reason:
|
|
3620
|
+
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
3621
|
+
stopReason: "ralph_executing",
|
|
3622
|
+
systemMessage:
|
|
3623
|
+
"OMX Ralph is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
|
|
3624
|
+
};
|
|
3625
|
+
|
|
3626
|
+
const first = await dispatchCodexNativeHook(payload, { cwd });
|
|
3627
|
+
const replay = await dispatchCodexNativeHook(
|
|
3628
|
+
{
|
|
3629
|
+
...payload,
|
|
3630
|
+
stop_hook_active: true,
|
|
3631
|
+
},
|
|
3632
|
+
{ cwd },
|
|
3633
|
+
);
|
|
3634
|
+
|
|
3635
|
+
assert.equal(first.omxEventName, "stop");
|
|
3636
|
+
assert.deepEqual(first.outputJson, expected);
|
|
3637
|
+
assert.equal(replay.omxEventName, "stop");
|
|
3638
|
+
assert.deepEqual(replay.outputJson, expected);
|
|
3639
|
+
} finally {
|
|
3640
|
+
if (typeof previousOmxSessionId === "string") process.env.OMX_SESSION_ID = previousOmxSessionId;
|
|
3641
|
+
else delete process.env.OMX_SESSION_ID;
|
|
3642
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3643
|
+
}
|
|
3644
|
+
});
|
|
3645
|
+
|
|
3646
|
+
|
|
3647
|
+
it("returns Stop continuation output for native auto-nudge stall prompts", async () => {
|
|
3648
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-"));
|
|
3649
|
+
try {
|
|
3650
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
3651
|
+
await mkdir(stateDir, { recursive: true });
|
|
3652
|
+
process.env.OMX_SESSION_ID = "sess-stop-auto";
|
|
3653
|
+
|
|
3654
|
+
const result = await dispatchCodexNativeHook(
|
|
3655
|
+
{
|
|
3656
|
+
hook_event_name: "Stop",
|
|
3657
|
+
cwd,
|
|
3658
|
+
session_id: "sess-stop-auto",
|
|
3659
|
+
last_assistant_message: "Keep going and finish the cleanup.",
|
|
3660
|
+
},
|
|
3661
|
+
{ cwd },
|
|
3662
|
+
);
|
|
3663
|
+
|
|
3664
|
+
assert.equal(result.omxEventName, "stop");
|
|
3665
|
+
assert.deepEqual(result.outputJson, {
|
|
3666
|
+
decision: "block",
|
|
3667
|
+
reason: DEFAULT_AUTO_NUDGE_RESPONSE,
|
|
3668
|
+
stopReason: "auto_nudge",
|
|
3669
|
+
systemMessage:
|
|
3670
|
+
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
3671
|
+
});
|
|
3672
|
+
} finally {
|
|
3673
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3674
|
+
}
|
|
3675
|
+
});
|
|
3676
|
+
|
|
3677
|
+
it("re-blocks duplicate native auto-nudge replays for the same Stop reply", async () => {
|
|
3678
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-once-"));
|
|
3679
|
+
try {
|
|
3680
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
3681
|
+
await mkdir(stateDir, { recursive: true });
|
|
3682
|
+
process.env.OMX_SESSION_ID = "sess-stop-auto-once";
|
|
3683
|
+
|
|
3684
|
+
await dispatchCodexNativeHook(
|
|
3685
|
+
{
|
|
3686
|
+
hook_event_name: "Stop",
|
|
3687
|
+
cwd,
|
|
3688
|
+
session_id: "sess-stop-auto-once",
|
|
3689
|
+
thread_id: "thread-stop-auto",
|
|
3690
|
+
turn_id: "turn-stop-auto-1",
|
|
3691
|
+
last_assistant_message: "Keep going and finish the cleanup.",
|
|
3692
|
+
},
|
|
3693
|
+
{ cwd },
|
|
3694
|
+
);
|
|
3695
|
+
|
|
3696
|
+
const result = await dispatchCodexNativeHook(
|
|
3697
|
+
{
|
|
3698
|
+
hook_event_name: "Stop",
|
|
3699
|
+
cwd,
|
|
3700
|
+
session_id: "sess-stop-auto-once",
|
|
3701
|
+
thread_id: "thread-stop-auto",
|
|
3702
|
+
turn_id: "turn-stop-auto-1",
|
|
3703
|
+
stop_hook_active: true,
|
|
3704
|
+
last_assistant_message: "Keep going and finish the cleanup.",
|
|
3705
|
+
},
|
|
3706
|
+
{ cwd },
|
|
3707
|
+
);
|
|
3708
|
+
|
|
3709
|
+
assert.equal(result.omxEventName, "stop");
|
|
3710
|
+
assert.deepEqual(result.outputJson, {
|
|
3711
|
+
decision: "block",
|
|
3712
|
+
reason: DEFAULT_AUTO_NUDGE_RESPONSE,
|
|
3713
|
+
stopReason: "auto_nudge",
|
|
3714
|
+
systemMessage:
|
|
3715
|
+
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
3716
|
+
});
|
|
3717
|
+
} finally {
|
|
3718
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3719
|
+
}
|
|
3720
|
+
});
|
|
3721
|
+
|
|
3722
|
+
it("re-blocks duplicate native auto-nudge replays across native/canonical session-id drift", async () => {
|
|
3723
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-session-drift-"));
|
|
3724
|
+
try {
|
|
3725
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
3726
|
+
await mkdir(stateDir, { recursive: true });
|
|
3727
|
+
process.env.OMX_SESSION_ID = "omx-canonical";
|
|
3728
|
+
await writeJson(join(stateDir, "session.json"), {
|
|
3729
|
+
session_id: "omx-canonical",
|
|
3730
|
+
native_session_id: "codex-native",
|
|
3731
|
+
});
|
|
3732
|
+
|
|
3733
|
+
await dispatchCodexNativeHook(
|
|
3115
3734
|
{
|
|
3116
3735
|
hook_event_name: "Stop",
|
|
3117
3736
|
cwd,
|
|
@@ -3992,3 +4611,691 @@ esac
|
|
|
3992
4611
|
}
|
|
3993
4612
|
});
|
|
3994
4613
|
});
|
|
4614
|
+
|
|
4615
|
+
// ---------------------------------------------------------------------------
|
|
4616
|
+
// Triage layer integration tests
|
|
4617
|
+
// ---------------------------------------------------------------------------
|
|
4618
|
+
|
|
4619
|
+
describe("codex native hook triage integration", () => {
|
|
4620
|
+
const priorCodexHome = process.env.CODEX_HOME;
|
|
4621
|
+
|
|
4622
|
+
beforeEach(() => {
|
|
4623
|
+
resetTriageConfigCache();
|
|
4624
|
+
});
|
|
4625
|
+
|
|
4626
|
+
afterEach(() => {
|
|
4627
|
+
if (typeof priorCodexHome === "string") process.env.CODEX_HOME = priorCodexHome;
|
|
4628
|
+
else delete process.env.CODEX_HOME;
|
|
4629
|
+
resetTriageConfigCache();
|
|
4630
|
+
});
|
|
4631
|
+
|
|
4632
|
+
// ── Group 1: Keyword bypass (triage must NOT run) ────────────────────────
|
|
4633
|
+
|
|
4634
|
+
it("does not inject triage advisory for $ralplan keyword prompts", async () => {
|
|
4635
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-keyword-ralplan-"));
|
|
4636
|
+
try {
|
|
4637
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4638
|
+
const result = await dispatchCodexNativeHook(
|
|
4639
|
+
{
|
|
4640
|
+
hook_event_name: "UserPromptSubmit",
|
|
4641
|
+
cwd,
|
|
4642
|
+
session_id: "triage-kw-ralplan-1",
|
|
4643
|
+
thread_id: "thread-triage-kw-1",
|
|
4644
|
+
turn_id: "turn-triage-kw-1",
|
|
4645
|
+
prompt: "$ralplan implement issue #1307",
|
|
4646
|
+
},
|
|
4647
|
+
{ cwd },
|
|
4648
|
+
);
|
|
4649
|
+
|
|
4650
|
+
const additionalContext = String(
|
|
4651
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
4652
|
+
);
|
|
4653
|
+
assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
|
|
4654
|
+
assert.doesNotMatch(additionalContext, /read-only\/question-shaped/);
|
|
4655
|
+
assert.doesNotMatch(additionalContext, /narrow edit-shaped/);
|
|
4656
|
+
assert.doesNotMatch(additionalContext, /visual\/style request/);
|
|
4657
|
+
|
|
4658
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-kw-ralplan-1", "prompt-routing-state.json");
|
|
4659
|
+
assert.equal(existsSync(stateFile), false);
|
|
4660
|
+
} finally {
|
|
4661
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4662
|
+
}
|
|
4663
|
+
});
|
|
4664
|
+
|
|
4665
|
+
it("does not inject triage advisory for autopilot keyword prompts", async () => {
|
|
4666
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-keyword-autopilot-"));
|
|
4667
|
+
try {
|
|
4668
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4669
|
+
const result = await dispatchCodexNativeHook(
|
|
4670
|
+
{
|
|
4671
|
+
hook_event_name: "UserPromptSubmit",
|
|
4672
|
+
cwd,
|
|
4673
|
+
session_id: "triage-kw-autopilot-1",
|
|
4674
|
+
thread_id: "thread-triage-kw-ap-1",
|
|
4675
|
+
turn_id: "turn-triage-kw-ap-1",
|
|
4676
|
+
prompt: "$autopilot build this",
|
|
4677
|
+
},
|
|
4678
|
+
{ cwd },
|
|
4679
|
+
);
|
|
4680
|
+
|
|
4681
|
+
const additionalContext = String(
|
|
4682
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
4683
|
+
);
|
|
4684
|
+
assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
|
|
4685
|
+
assert.doesNotMatch(additionalContext, /read-only\/question-shaped/);
|
|
4686
|
+
assert.doesNotMatch(additionalContext, /narrow edit-shaped/);
|
|
4687
|
+
assert.doesNotMatch(additionalContext, /visual\/style request/);
|
|
4688
|
+
|
|
4689
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-kw-autopilot-1", "prompt-routing-state.json");
|
|
4690
|
+
assert.equal(existsSync(stateFile), false);
|
|
4691
|
+
} finally {
|
|
4692
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4693
|
+
}
|
|
4694
|
+
});
|
|
4695
|
+
|
|
4696
|
+
// ── Group 2: HEAVY injection ─────────────────────────────────────────────
|
|
4697
|
+
|
|
4698
|
+
it("injects HEAVY advisory and writes prompt-routing-state for a multi-step goal prompt", async () => {
|
|
4699
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-heavy-"));
|
|
4700
|
+
try {
|
|
4701
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4702
|
+
const result = await dispatchCodexNativeHook(
|
|
4703
|
+
{
|
|
4704
|
+
hook_event_name: "UserPromptSubmit",
|
|
4705
|
+
cwd,
|
|
4706
|
+
session_id: "triage-heavy-1",
|
|
4707
|
+
thread_id: "thread-triage-heavy-1",
|
|
4708
|
+
turn_id: "turn-triage-heavy-1",
|
|
4709
|
+
prompt: "add dark mode toggle to the settings page",
|
|
4710
|
+
},
|
|
4711
|
+
{ cwd },
|
|
4712
|
+
);
|
|
4713
|
+
|
|
4714
|
+
const additionalContext = String(
|
|
4715
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
4716
|
+
);
|
|
4717
|
+
assert.match(additionalContext, /multi-step goal with no workflow keyword/);
|
|
4718
|
+
assert.match(additionalContext, /Prefer the existing autopilot-style workflow/);
|
|
4719
|
+
|
|
4720
|
+
// skill-active-state.json must NOT be written (triage is advisory only)
|
|
4721
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "skill-active-state.json")), false);
|
|
4722
|
+
|
|
4723
|
+
// prompt-routing-state.json must be written with lane=HEAVY
|
|
4724
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-heavy-1", "prompt-routing-state.json");
|
|
4725
|
+
assert.equal(existsSync(stateFile), true);
|
|
4726
|
+
const state = JSON.parse(await readFile(stateFile, "utf-8")) as {
|
|
4727
|
+
version?: number;
|
|
4728
|
+
last_triage?: { lane?: string; destination?: string };
|
|
4729
|
+
suppress_followup?: boolean;
|
|
4730
|
+
};
|
|
4731
|
+
assert.equal(state.version, 1);
|
|
4732
|
+
assert.equal(state.last_triage?.lane, "HEAVY");
|
|
4733
|
+
assert.equal(state.last_triage?.destination, "autopilot");
|
|
4734
|
+
assert.equal(state.suppress_followup, true);
|
|
4735
|
+
} finally {
|
|
4736
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4737
|
+
}
|
|
4738
|
+
});
|
|
4739
|
+
|
|
4740
|
+
// ── Group 3: LIGHT/explore ────────────────────────────────────────────────
|
|
4741
|
+
|
|
4742
|
+
it("injects LIGHT/explore advisory and writes state for a question-shaped prompt", async () => {
|
|
4743
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-light-explore-"));
|
|
4744
|
+
try {
|
|
4745
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4746
|
+
const result = await dispatchCodexNativeHook(
|
|
4747
|
+
{
|
|
4748
|
+
hook_event_name: "UserPromptSubmit",
|
|
4749
|
+
cwd,
|
|
4750
|
+
session_id: "triage-explore-1",
|
|
4751
|
+
thread_id: "thread-triage-explore-1",
|
|
4752
|
+
turn_id: "turn-triage-explore-1",
|
|
4753
|
+
prompt: "explain this function",
|
|
4754
|
+
},
|
|
4755
|
+
{ cwd },
|
|
4756
|
+
);
|
|
4757
|
+
|
|
4758
|
+
const additionalContext = String(
|
|
4759
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
4760
|
+
);
|
|
4761
|
+
assert.match(additionalContext, /read-only\/question-shaped/);
|
|
4762
|
+
assert.match(additionalContext, /Prefer the explore role surface/);
|
|
4763
|
+
|
|
4764
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-explore-1", "prompt-routing-state.json");
|
|
4765
|
+
assert.equal(existsSync(stateFile), true);
|
|
4766
|
+
const state = JSON.parse(await readFile(stateFile, "utf-8")) as {
|
|
4767
|
+
last_triage?: { lane?: string; destination?: string };
|
|
4768
|
+
suppress_followup?: boolean;
|
|
4769
|
+
};
|
|
4770
|
+
assert.equal(state.last_triage?.lane, "LIGHT");
|
|
4771
|
+
assert.equal(state.last_triage?.destination, "explore");
|
|
4772
|
+
assert.equal(state.suppress_followup, true);
|
|
4773
|
+
} finally {
|
|
4774
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4775
|
+
}
|
|
4776
|
+
});
|
|
4777
|
+
|
|
4778
|
+
// ── Group 4: LIGHT/executor ───────────────────────────────────────────────
|
|
4779
|
+
|
|
4780
|
+
it("injects LIGHT/executor advisory and writes state for a narrow edit-shaped prompt", async () => {
|
|
4781
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-light-executor-"));
|
|
4782
|
+
try {
|
|
4783
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4784
|
+
const result = await dispatchCodexNativeHook(
|
|
4785
|
+
{
|
|
4786
|
+
hook_event_name: "UserPromptSubmit",
|
|
4787
|
+
cwd,
|
|
4788
|
+
session_id: "triage-executor-1",
|
|
4789
|
+
thread_id: "thread-triage-executor-1",
|
|
4790
|
+
turn_id: "turn-triage-executor-1",
|
|
4791
|
+
prompt: "fix typo in src/foo.ts",
|
|
4792
|
+
},
|
|
4793
|
+
{ cwd },
|
|
4794
|
+
);
|
|
4795
|
+
|
|
4796
|
+
const additionalContext = String(
|
|
4797
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
4798
|
+
);
|
|
4799
|
+
assert.match(additionalContext, /narrow edit-shaped/);
|
|
4800
|
+
assert.match(additionalContext, /Prefer the executor role surface/);
|
|
4801
|
+
|
|
4802
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-executor-1", "prompt-routing-state.json");
|
|
4803
|
+
assert.equal(existsSync(stateFile), true);
|
|
4804
|
+
const state = JSON.parse(await readFile(stateFile, "utf-8")) as {
|
|
4805
|
+
last_triage?: { lane?: string; destination?: string };
|
|
4806
|
+
};
|
|
4807
|
+
assert.equal(state.last_triage?.lane, "LIGHT");
|
|
4808
|
+
assert.equal(state.last_triage?.destination, "executor");
|
|
4809
|
+
} finally {
|
|
4810
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4811
|
+
}
|
|
4812
|
+
});
|
|
4813
|
+
|
|
4814
|
+
// ── Group 5: LIGHT/designer ───────────────────────────────────────────────
|
|
4815
|
+
|
|
4816
|
+
it("injects LIGHT/designer advisory and writes state for a visual/style prompt", async () => {
|
|
4817
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-light-designer-"));
|
|
4818
|
+
try {
|
|
4819
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4820
|
+
const result = await dispatchCodexNativeHook(
|
|
4821
|
+
{
|
|
4822
|
+
hook_event_name: "UserPromptSubmit",
|
|
4823
|
+
cwd,
|
|
4824
|
+
session_id: "triage-designer-1",
|
|
4825
|
+
thread_id: "thread-triage-designer-1",
|
|
4826
|
+
turn_id: "turn-triage-designer-1",
|
|
4827
|
+
prompt: "make the button blue",
|
|
4828
|
+
},
|
|
4829
|
+
{ cwd },
|
|
4830
|
+
);
|
|
4831
|
+
|
|
4832
|
+
const additionalContext = String(
|
|
4833
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
4834
|
+
);
|
|
4835
|
+
assert.match(additionalContext, /visual\/style request/);
|
|
4836
|
+
assert.match(additionalContext, /Prefer the designer role surface/);
|
|
4837
|
+
|
|
4838
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-designer-1", "prompt-routing-state.json");
|
|
4839
|
+
assert.equal(existsSync(stateFile), true);
|
|
4840
|
+
const state = JSON.parse(await readFile(stateFile, "utf-8")) as {
|
|
4841
|
+
last_triage?: { lane?: string; destination?: string };
|
|
4842
|
+
};
|
|
4843
|
+
assert.equal(state.last_triage?.lane, "LIGHT");
|
|
4844
|
+
assert.equal(state.last_triage?.destination, "designer");
|
|
4845
|
+
} finally {
|
|
4846
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4847
|
+
}
|
|
4848
|
+
});
|
|
4849
|
+
|
|
4850
|
+
// ── Group 6: PASS (no triage injection, no state) ────────────────────────
|
|
4851
|
+
|
|
4852
|
+
it("produces no triage advisory and no state for trivial greeting prompts", async () => {
|
|
4853
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-pass-hello-"));
|
|
4854
|
+
try {
|
|
4855
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4856
|
+
const result = await dispatchCodexNativeHook(
|
|
4857
|
+
{
|
|
4858
|
+
hook_event_name: "UserPromptSubmit",
|
|
4859
|
+
cwd,
|
|
4860
|
+
session_id: "triage-pass-hello-1",
|
|
4861
|
+
thread_id: "thread-triage-pass-1",
|
|
4862
|
+
turn_id: "turn-triage-pass-1",
|
|
4863
|
+
prompt: "hello",
|
|
4864
|
+
},
|
|
4865
|
+
{ cwd },
|
|
4866
|
+
);
|
|
4867
|
+
|
|
4868
|
+
const additionalContext = String(
|
|
4869
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
4870
|
+
);
|
|
4871
|
+
assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
|
|
4872
|
+
assert.doesNotMatch(additionalContext, /read-only\/question-shaped/);
|
|
4873
|
+
assert.doesNotMatch(additionalContext, /narrow edit-shaped/);
|
|
4874
|
+
assert.doesNotMatch(additionalContext, /visual\/style request/);
|
|
4875
|
+
|
|
4876
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-pass-hello-1", "prompt-routing-state.json");
|
|
4877
|
+
assert.equal(existsSync(stateFile), false);
|
|
4878
|
+
} finally {
|
|
4879
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4880
|
+
}
|
|
4881
|
+
});
|
|
4882
|
+
|
|
4883
|
+
it("produces no triage advisory and no state for ambiguous short prompts", async () => {
|
|
4884
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-pass-short-"));
|
|
4885
|
+
try {
|
|
4886
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4887
|
+
const result = await dispatchCodexNativeHook(
|
|
4888
|
+
{
|
|
4889
|
+
hook_event_name: "UserPromptSubmit",
|
|
4890
|
+
cwd,
|
|
4891
|
+
session_id: "triage-pass-short-1",
|
|
4892
|
+
thread_id: "thread-triage-pass-short-1",
|
|
4893
|
+
turn_id: "turn-triage-pass-short-1",
|
|
4894
|
+
prompt: "fix the thing",
|
|
4895
|
+
},
|
|
4896
|
+
{ cwd },
|
|
4897
|
+
);
|
|
4898
|
+
|
|
4899
|
+
const additionalContext = String(
|
|
4900
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
4901
|
+
);
|
|
4902
|
+
assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
|
|
4903
|
+
assert.doesNotMatch(additionalContext, /read-only\/question-shaped/);
|
|
4904
|
+
assert.doesNotMatch(additionalContext, /narrow edit-shaped/);
|
|
4905
|
+
assert.doesNotMatch(additionalContext, /visual\/style request/);
|
|
4906
|
+
|
|
4907
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-pass-short-1", "prompt-routing-state.json");
|
|
4908
|
+
assert.equal(existsSync(stateFile), false);
|
|
4909
|
+
} finally {
|
|
4910
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4911
|
+
}
|
|
4912
|
+
});
|
|
4913
|
+
|
|
4914
|
+
// ── Group 7: Turn-2 suppression (same session across two invocations) ────
|
|
4915
|
+
|
|
4916
|
+
it("suppresses HEAVY triage re-injection on a short follow-up in the same session", async () => {
|
|
4917
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-suppress-heavy-"));
|
|
4918
|
+
const sessionId = "triage-suppress-heavy-1";
|
|
4919
|
+
try {
|
|
4920
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4921
|
+
|
|
4922
|
+
// Turn 1: HEAVY fires
|
|
4923
|
+
const turn1 = await dispatchCodexNativeHook(
|
|
4924
|
+
{
|
|
4925
|
+
hook_event_name: "UserPromptSubmit",
|
|
4926
|
+
cwd,
|
|
4927
|
+
session_id: sessionId,
|
|
4928
|
+
thread_id: "thread-suppress-heavy-1",
|
|
4929
|
+
turn_id: "turn-suppress-heavy-1",
|
|
4930
|
+
prompt: "add dark mode toggle to the settings page",
|
|
4931
|
+
},
|
|
4932
|
+
{ cwd },
|
|
4933
|
+
);
|
|
4934
|
+
const ctx1 = String(
|
|
4935
|
+
(turn1.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
4936
|
+
);
|
|
4937
|
+
assert.match(ctx1, /multi-step goal with no workflow keyword/);
|
|
4938
|
+
|
|
4939
|
+
// Turn 2: short follow-up — triage suppressed
|
|
4940
|
+
const turn2 = await dispatchCodexNativeHook(
|
|
4941
|
+
{
|
|
4942
|
+
hook_event_name: "UserPromptSubmit",
|
|
4943
|
+
cwd,
|
|
4944
|
+
session_id: sessionId,
|
|
4945
|
+
thread_id: "thread-suppress-heavy-1",
|
|
4946
|
+
turn_id: "turn-suppress-heavy-2",
|
|
4947
|
+
prompt: "yes, settings page",
|
|
4948
|
+
},
|
|
4949
|
+
{ cwd },
|
|
4950
|
+
);
|
|
4951
|
+
const ctx2 = String(
|
|
4952
|
+
(turn2.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
4953
|
+
);
|
|
4954
|
+
assert.doesNotMatch(ctx2, /multi-step goal/);
|
|
4955
|
+
} finally {
|
|
4956
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4957
|
+
}
|
|
4958
|
+
});
|
|
4959
|
+
|
|
4960
|
+
it("suppresses LIGHT/explore triage re-injection on a short follow-up in the same session", async () => {
|
|
4961
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-suppress-explore-"));
|
|
4962
|
+
const sessionId = "triage-suppress-explore-1";
|
|
4963
|
+
try {
|
|
4964
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4965
|
+
|
|
4966
|
+
// Turn 1: LIGHT/explore fires
|
|
4967
|
+
await dispatchCodexNativeHook(
|
|
4968
|
+
{
|
|
4969
|
+
hook_event_name: "UserPromptSubmit",
|
|
4970
|
+
cwd,
|
|
4971
|
+
session_id: sessionId,
|
|
4972
|
+
thread_id: "thread-suppress-explore-1",
|
|
4973
|
+
turn_id: "turn-suppress-explore-1",
|
|
4974
|
+
prompt: "explain this function",
|
|
4975
|
+
},
|
|
4976
|
+
{ cwd },
|
|
4977
|
+
);
|
|
4978
|
+
|
|
4979
|
+
// Turn 2: short follow-up — no duplicate LIGHT injection
|
|
4980
|
+
const turn2 = await dispatchCodexNativeHook(
|
|
4981
|
+
{
|
|
4982
|
+
hook_event_name: "UserPromptSubmit",
|
|
4983
|
+
cwd,
|
|
4984
|
+
session_id: sessionId,
|
|
4985
|
+
thread_id: "thread-suppress-explore-1",
|
|
4986
|
+
turn_id: "turn-suppress-explore-2",
|
|
4987
|
+
prompt: "the auth helper",
|
|
4988
|
+
},
|
|
4989
|
+
{ cwd },
|
|
4990
|
+
);
|
|
4991
|
+
const ctx2 = String(
|
|
4992
|
+
(turn2.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
4993
|
+
);
|
|
4994
|
+
assert.doesNotMatch(ctx2, /read-only\/question-shaped/);
|
|
4995
|
+
} finally {
|
|
4996
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4997
|
+
}
|
|
4998
|
+
});
|
|
4999
|
+
|
|
5000
|
+
// ── Group 8: First-turn PASS does NOT block later triage ─────────────────
|
|
5001
|
+
|
|
5002
|
+
it("still applies triage on turn 2 when turn 1 was a PASS with no state written", async () => {
|
|
5003
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-pass-then-light-"));
|
|
5004
|
+
const sessionId = "triage-pass-then-light-1";
|
|
5005
|
+
try {
|
|
5006
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
5007
|
+
|
|
5008
|
+
// Turn 1: PASS — no state written
|
|
5009
|
+
await dispatchCodexNativeHook(
|
|
5010
|
+
{
|
|
5011
|
+
hook_event_name: "UserPromptSubmit",
|
|
5012
|
+
cwd,
|
|
5013
|
+
session_id: sessionId,
|
|
5014
|
+
thread_id: "thread-pass-then-light-1",
|
|
5015
|
+
turn_id: "turn-pass-then-light-1",
|
|
5016
|
+
prompt: "hello",
|
|
5017
|
+
},
|
|
5018
|
+
{ cwd },
|
|
5019
|
+
);
|
|
5020
|
+
assert.equal(
|
|
5021
|
+
existsSync(join(cwd, ".omx", "state", "sessions", sessionId, "prompt-routing-state.json")),
|
|
5022
|
+
false,
|
|
5023
|
+
);
|
|
5024
|
+
|
|
5025
|
+
// Turn 2: LIGHT/executor should fire normally
|
|
5026
|
+
const turn2 = await dispatchCodexNativeHook(
|
|
5027
|
+
{
|
|
5028
|
+
hook_event_name: "UserPromptSubmit",
|
|
5029
|
+
cwd,
|
|
5030
|
+
session_id: sessionId,
|
|
5031
|
+
thread_id: "thread-pass-then-light-1",
|
|
5032
|
+
turn_id: "turn-pass-then-light-2",
|
|
5033
|
+
prompt: "fix typo in src/foo.ts",
|
|
5034
|
+
},
|
|
5035
|
+
{ cwd },
|
|
5036
|
+
);
|
|
5037
|
+
const ctx2 = String(
|
|
5038
|
+
(turn2.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
5039
|
+
);
|
|
5040
|
+
assert.match(ctx2, /narrow edit-shaped/);
|
|
5041
|
+
} finally {
|
|
5042
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5043
|
+
}
|
|
5044
|
+
});
|
|
5045
|
+
|
|
5046
|
+
// ── Group 9: Opt-out forces PASS ─────────────────────────────────────────
|
|
5047
|
+
|
|
5048
|
+
it("produces no triage advisory when prompt contains 'just chat' opt-out", async () => {
|
|
5049
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-optout-chat-"));
|
|
5050
|
+
try {
|
|
5051
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
5052
|
+
const result = await dispatchCodexNativeHook(
|
|
5053
|
+
{
|
|
5054
|
+
hook_event_name: "UserPromptSubmit",
|
|
5055
|
+
cwd,
|
|
5056
|
+
session_id: "triage-optout-chat-1",
|
|
5057
|
+
thread_id: "thread-optout-chat-1",
|
|
5058
|
+
turn_id: "turn-optout-chat-1",
|
|
5059
|
+
prompt: "add dark mode toggle to the settings page, but just chat about it",
|
|
5060
|
+
},
|
|
5061
|
+
{ cwd },
|
|
5062
|
+
);
|
|
5063
|
+
|
|
5064
|
+
const additionalContext = String(
|
|
5065
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
5066
|
+
);
|
|
5067
|
+
assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
|
|
5068
|
+
assert.doesNotMatch(additionalContext, /read-only\/question-shaped/);
|
|
5069
|
+
|
|
5070
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-optout-chat-1", "prompt-routing-state.json");
|
|
5071
|
+
assert.equal(existsSync(stateFile), false);
|
|
5072
|
+
} finally {
|
|
5073
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5074
|
+
}
|
|
5075
|
+
});
|
|
5076
|
+
|
|
5077
|
+
it("produces no triage advisory when prompt contains 'no workflow' opt-out", async () => {
|
|
5078
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-optout-noworkflow-"));
|
|
5079
|
+
try {
|
|
5080
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
5081
|
+
const result = await dispatchCodexNativeHook(
|
|
5082
|
+
{
|
|
5083
|
+
hook_event_name: "UserPromptSubmit",
|
|
5084
|
+
cwd,
|
|
5085
|
+
session_id: "triage-optout-noworkflow-1",
|
|
5086
|
+
thread_id: "thread-optout-noworkflow-1",
|
|
5087
|
+
turn_id: "turn-optout-noworkflow-1",
|
|
5088
|
+
prompt: "make the button blue, no workflow",
|
|
5089
|
+
},
|
|
5090
|
+
{ cwd },
|
|
5091
|
+
);
|
|
5092
|
+
|
|
5093
|
+
const additionalContext = String(
|
|
5094
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
5095
|
+
);
|
|
5096
|
+
assert.doesNotMatch(additionalContext, /visual\/style request/);
|
|
5097
|
+
|
|
5098
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-optout-noworkflow-1", "prompt-routing-state.json");
|
|
5099
|
+
assert.equal(existsSync(stateFile), false);
|
|
5100
|
+
} finally {
|
|
5101
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5102
|
+
}
|
|
5103
|
+
});
|
|
5104
|
+
|
|
5105
|
+
// ── Group 10: Keyword on follow-up turn wins cleanly ─────────────────────
|
|
5106
|
+
|
|
5107
|
+
it("keyword on turn 2 suppresses triage and writes no triage state", async () => {
|
|
5108
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-kw-followup-"));
|
|
5109
|
+
const sessionId = "triage-kw-followup-1";
|
|
5110
|
+
try {
|
|
5111
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
5112
|
+
|
|
5113
|
+
// Turn 1: neutral prompt — triage may or may not fire, doesn't matter
|
|
5114
|
+
await dispatchCodexNativeHook(
|
|
5115
|
+
{
|
|
5116
|
+
hook_event_name: "UserPromptSubmit",
|
|
5117
|
+
cwd,
|
|
5118
|
+
session_id: sessionId,
|
|
5119
|
+
thread_id: "thread-kw-followup-1",
|
|
5120
|
+
turn_id: "turn-kw-followup-1",
|
|
5121
|
+
prompt: "hello",
|
|
5122
|
+
},
|
|
5123
|
+
{ cwd },
|
|
5124
|
+
);
|
|
5125
|
+
|
|
5126
|
+
// Turn 2: keyword prompt — keyword fast-path runs, triage does NOT add extra advisory
|
|
5127
|
+
const turn2 = await dispatchCodexNativeHook(
|
|
5128
|
+
{
|
|
5129
|
+
hook_event_name: "UserPromptSubmit",
|
|
5130
|
+
cwd,
|
|
5131
|
+
session_id: sessionId,
|
|
5132
|
+
thread_id: "thread-kw-followup-1",
|
|
5133
|
+
turn_id: "turn-kw-followup-2",
|
|
5134
|
+
prompt: "$ralph continue",
|
|
5135
|
+
},
|
|
5136
|
+
{ cwd },
|
|
5137
|
+
);
|
|
5138
|
+
|
|
5139
|
+
assert.equal(turn2.skillState?.skill, "ralph");
|
|
5140
|
+
|
|
5141
|
+
const ctx2 = String(
|
|
5142
|
+
(turn2.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
5143
|
+
);
|
|
5144
|
+
assert.doesNotMatch(ctx2, /multi-step goal with no workflow keyword/);
|
|
5145
|
+
assert.doesNotMatch(ctx2, /read-only\/question-shaped/);
|
|
5146
|
+
assert.doesNotMatch(ctx2, /narrow edit-shaped/);
|
|
5147
|
+
assert.doesNotMatch(ctx2, /visual\/style request/);
|
|
5148
|
+
|
|
5149
|
+
// No triage state written on the keyword turn
|
|
5150
|
+
const triageState = join(cwd, ".omx", "state", "sessions", sessionId, "prompt-routing-state.json");
|
|
5151
|
+
// The state from turn 1 (if any) must not have been created either (hello = PASS)
|
|
5152
|
+
assert.equal(existsSync(triageState), false);
|
|
5153
|
+
} finally {
|
|
5154
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5155
|
+
}
|
|
5156
|
+
});
|
|
5157
|
+
|
|
5158
|
+
// ── Group 11: Config-disabled path ───────────────────────────────────────
|
|
5159
|
+
|
|
5160
|
+
it("produces no triage advisory and no state when triage is disabled in config", async () => {
|
|
5161
|
+
const tmpHome = await mkdtemp(join(tmpdir(), "omx-triage-config-disabled-home-"));
|
|
5162
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-config-disabled-cwd-"));
|
|
5163
|
+
try {
|
|
5164
|
+
// Write a .omx-config.json in the fake CODEX_HOME that disables triage
|
|
5165
|
+
await writeJson(join(tmpHome, ".omx-config.json"), {
|
|
5166
|
+
promptRouting: { triage: { enabled: false } },
|
|
5167
|
+
});
|
|
5168
|
+
process.env.CODEX_HOME = tmpHome;
|
|
5169
|
+
resetTriageConfigCache();
|
|
5170
|
+
|
|
5171
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
5172
|
+
const result = await dispatchCodexNativeHook(
|
|
5173
|
+
{
|
|
5174
|
+
hook_event_name: "UserPromptSubmit",
|
|
5175
|
+
cwd,
|
|
5176
|
+
session_id: "triage-disabled-1",
|
|
5177
|
+
thread_id: "thread-triage-disabled-1",
|
|
5178
|
+
turn_id: "turn-triage-disabled-1",
|
|
5179
|
+
prompt: "add dark mode toggle to the settings page",
|
|
5180
|
+
},
|
|
5181
|
+
{ cwd },
|
|
5182
|
+
);
|
|
5183
|
+
|
|
5184
|
+
const additionalContext = String(
|
|
5185
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
5186
|
+
);
|
|
5187
|
+
assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
|
|
5188
|
+
|
|
5189
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-disabled-1", "prompt-routing-state.json");
|
|
5190
|
+
assert.equal(existsSync(stateFile), false);
|
|
5191
|
+
} finally {
|
|
5192
|
+
await rm(tmpHome, { recursive: true, force: true });
|
|
5193
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5194
|
+
}
|
|
5195
|
+
});
|
|
5196
|
+
|
|
5197
|
+
it("keeps triage default-enabled when config omits promptRouting.triage.enabled", async () => {
|
|
5198
|
+
const tmpHome = await mkdtemp(join(tmpdir(), "omx-triage-config-omitted-home-"));
|
|
5199
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-config-omitted-cwd-"));
|
|
5200
|
+
const previousCodexHome = process.env.CODEX_HOME;
|
|
5201
|
+
try {
|
|
5202
|
+
await writeJson(join(tmpHome, ".omx-config.json"), {
|
|
5203
|
+
promptRouting: {},
|
|
5204
|
+
});
|
|
5205
|
+
process.env.CODEX_HOME = tmpHome;
|
|
5206
|
+
resetTriageConfigCache();
|
|
5207
|
+
|
|
5208
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
5209
|
+
const result = await dispatchCodexNativeHook(
|
|
5210
|
+
{
|
|
5211
|
+
hook_event_name: "UserPromptSubmit",
|
|
5212
|
+
cwd,
|
|
5213
|
+
session_id: "triage-defaulted-1",
|
|
5214
|
+
thread_id: "thread-triage-defaulted-1",
|
|
5215
|
+
turn_id: "turn-triage-defaulted-1",
|
|
5216
|
+
prompt: "add dark mode toggle to the settings page",
|
|
5217
|
+
},
|
|
5218
|
+
{ cwd },
|
|
5219
|
+
);
|
|
5220
|
+
|
|
5221
|
+
const additionalContext = String(
|
|
5222
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
5223
|
+
);
|
|
5224
|
+
assert.match(additionalContext, /multi-step goal with no workflow keyword/);
|
|
5225
|
+
|
|
5226
|
+
const stateFile = join(cwd, ".omx", "state", "sessions", "triage-defaulted-1", "prompt-routing-state.json");
|
|
5227
|
+
assert.equal(existsSync(stateFile), true);
|
|
5228
|
+
} finally {
|
|
5229
|
+
if (typeof previousCodexHome === "string") process.env.CODEX_HOME = previousCodexHome;
|
|
5230
|
+
else delete process.env.CODEX_HOME;
|
|
5231
|
+
resetTriageConfigCache();
|
|
5232
|
+
await rm(tmpHome, { recursive: true, force: true });
|
|
5233
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5234
|
+
}
|
|
5235
|
+
});
|
|
5236
|
+
|
|
5237
|
+
it("does not suppress a short anchored follow-up that is a new request", async () => {
|
|
5238
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-short-new-request-"));
|
|
5239
|
+
const sessionId = "triage-short-new-request-1";
|
|
5240
|
+
try {
|
|
5241
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
5242
|
+
|
|
5243
|
+
await dispatchCodexNativeHook(
|
|
5244
|
+
{
|
|
5245
|
+
hook_event_name: "UserPromptSubmit",
|
|
5246
|
+
cwd,
|
|
5247
|
+
session_id: sessionId,
|
|
5248
|
+
thread_id: "thread-short-new-request-1",
|
|
5249
|
+
turn_id: "turn-short-new-request-1",
|
|
5250
|
+
prompt: "add dark mode toggle to the settings page",
|
|
5251
|
+
},
|
|
5252
|
+
{ cwd },
|
|
5253
|
+
);
|
|
5254
|
+
|
|
5255
|
+
const turn2 = await dispatchCodexNativeHook(
|
|
5256
|
+
{
|
|
5257
|
+
hook_event_name: "UserPromptSubmit",
|
|
5258
|
+
cwd,
|
|
5259
|
+
session_id: sessionId,
|
|
5260
|
+
thread_id: "thread-short-new-request-1",
|
|
5261
|
+
turn_id: "turn-short-new-request-2",
|
|
5262
|
+
prompt: "fix typo in src/foo.ts",
|
|
5263
|
+
},
|
|
5264
|
+
{ cwd },
|
|
5265
|
+
);
|
|
5266
|
+
|
|
5267
|
+
const ctx2 = String(
|
|
5268
|
+
(turn2.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
5269
|
+
);
|
|
5270
|
+
assert.match(ctx2, /narrow edit-shaped/);
|
|
5271
|
+
} finally {
|
|
5272
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5273
|
+
}
|
|
5274
|
+
});
|
|
5275
|
+
|
|
5276
|
+
it("skips triage state persistence for malformed explicit session ids without writing root state", async () => {
|
|
5277
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-invalid-session-"));
|
|
5278
|
+
try {
|
|
5279
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
5280
|
+
const result = await dispatchCodexNativeHook(
|
|
5281
|
+
{
|
|
5282
|
+
hook_event_name: "UserPromptSubmit",
|
|
5283
|
+
cwd,
|
|
5284
|
+
session_id: "bad/session",
|
|
5285
|
+
thread_id: "thread-triage-invalid-session-1",
|
|
5286
|
+
turn_id: "turn-triage-invalid-session-1",
|
|
5287
|
+
prompt: "add dark mode toggle to the settings page",
|
|
5288
|
+
},
|
|
5289
|
+
{ cwd },
|
|
5290
|
+
);
|
|
5291
|
+
|
|
5292
|
+
const additionalContext = String(
|
|
5293
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
5294
|
+
);
|
|
5295
|
+
assert.match(additionalContext, /multi-step goal with no workflow keyword/);
|
|
5296
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "prompt-routing-state.json")), false);
|
|
5297
|
+
} finally {
|
|
5298
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5299
|
+
}
|
|
5300
|
+
});
|
|
5301
|
+
});
|