oh-my-codex 0.18.1 → 0.18.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +6 -6
- package/Cargo.toml +1 -1
- package/README.md +4 -2
- package/dist/agents/__tests__/definitions.test.js +23 -0
- package/dist/agents/__tests__/definitions.test.js.map +1 -1
- package/dist/agents/__tests__/native-config.test.js +20 -0
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/agents/definitions.d.ts.map +1 -1
- package/dist/agents/definitions.js +40 -0
- package/dist/agents/definitions.js.map +1 -1
- package/dist/agents/native-config.d.ts +1 -0
- package/dist/agents/native-config.d.ts.map +1 -1
- package/dist/agents/native-config.js +4 -0
- package/dist/agents/native-config.js.map +1 -1
- package/dist/auth/__tests__/config-sessions.test.d.ts +2 -0
- package/dist/auth/__tests__/config-sessions.test.d.ts.map +1 -0
- package/dist/auth/__tests__/config-sessions.test.js +48 -0
- package/dist/auth/__tests__/config-sessions.test.js.map +1 -0
- package/dist/auth/__tests__/quota-rotation.test.d.ts +2 -0
- package/dist/auth/__tests__/quota-rotation.test.d.ts.map +1 -0
- package/dist/auth/__tests__/quota-rotation.test.js +33 -0
- package/dist/auth/__tests__/quota-rotation.test.js.map +1 -0
- package/dist/auth/__tests__/redact.test.d.ts +2 -0
- package/dist/auth/__tests__/redact.test.d.ts.map +1 -0
- package/dist/auth/__tests__/redact.test.js +20 -0
- package/dist/auth/__tests__/redact.test.js.map +1 -0
- package/dist/auth/__tests__/storage.test.d.ts +2 -0
- package/dist/auth/__tests__/storage.test.d.ts.map +1 -0
- package/dist/auth/__tests__/storage.test.js +108 -0
- package/dist/auth/__tests__/storage.test.js.map +1 -0
- package/dist/auth/config.d.ts +9 -0
- package/dist/auth/config.d.ts.map +1 -0
- package/dist/auth/config.js +77 -0
- package/dist/auth/config.js.map +1 -0
- package/dist/auth/hotswap.d.ts +36 -0
- package/dist/auth/hotswap.d.ts.map +1 -0
- package/dist/auth/hotswap.js +159 -0
- package/dist/auth/hotswap.js.map +1 -0
- package/dist/auth/index.d.ts +8 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +8 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/paths.d.ts +12 -0
- package/dist/auth/paths.d.ts.map +1 -0
- package/dist/auth/paths.js +78 -0
- package/dist/auth/paths.js.map +1 -0
- package/dist/auth/quota-detector.d.ts +10 -0
- package/dist/auth/quota-detector.d.ts.map +1 -0
- package/dist/auth/quota-detector.js +40 -0
- package/dist/auth/quota-detector.js.map +1 -0
- package/dist/auth/redact.d.ts +2 -0
- package/dist/auth/redact.d.ts.map +1 -0
- package/dist/auth/redact.js +26 -0
- package/dist/auth/redact.js.map +1 -0
- package/dist/auth/rotation.d.ts +9 -0
- package/dist/auth/rotation.d.ts.map +1 -0
- package/dist/auth/rotation.js +26 -0
- package/dist/auth/rotation.js.map +1 -0
- package/dist/auth/sessions.d.ts +15 -0
- package/dist/auth/sessions.d.ts.map +1 -0
- package/dist/auth/sessions.js +62 -0
- package/dist/auth/sessions.js.map +1 -0
- package/dist/auth/storage.d.ts +27 -0
- package/dist/auth/storage.d.ts.map +1 -0
- package/dist/auth/storage.js +111 -0
- package/dist/auth/storage.js.map +1 -0
- package/dist/catalog/__tests__/generator.test.js +4 -0
- package/dist/catalog/__tests__/generator.test.js.map +1 -1
- package/dist/cli/__tests__/auth.test.d.ts +2 -0
- package/dist/cli/__tests__/auth.test.d.ts.map +1 -0
- package/dist/cli/__tests__/auth.test.js +168 -0
- package/dist/cli/__tests__/auth.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-warning-copy.test.js +112 -5
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/explore.test.js +20 -0
- package/dist/cli/__tests__/explore.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +171 -21
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +51 -3
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/nested-help-routing.test.js +1 -0
- package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
- package/dist/cli/__tests__/question.test.js +2 -2
- package/dist/cli/__tests__/question.test.js.map +1 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +30 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +47 -0
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/auth.d.ts +4 -0
- package/dist/cli/auth.d.ts.map +1 -0
- package/dist/cli/auth.js +89 -0
- package/dist/cli/auth.js.map +1 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +190 -7
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/explore.d.ts.map +1 -1
- package/dist/cli/explore.js +12 -0
- package/dist/cli/explore.js.map +1 -1
- package/dist/cli/index.d.ts +27 -3
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +245 -47
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +11 -3
- package/dist/cli/setup.js.map +1 -1
- package/dist/config/__tests__/codex-hooks.test.js +3 -3
- package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
- package/dist/config/__tests__/deep-interview.test.d.ts +2 -0
- package/dist/config/__tests__/deep-interview.test.d.ts.map +1 -0
- package/dist/config/__tests__/deep-interview.test.js +239 -0
- package/dist/config/__tests__/deep-interview.test.js.map +1 -0
- package/dist/config/__tests__/generator-idempotent.test.js +123 -0
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/codex-hooks.d.ts +1 -0
- package/dist/config/codex-hooks.d.ts.map +1 -1
- package/dist/config/codex-hooks.js +2 -4
- package/dist/config/codex-hooks.js.map +1 -1
- package/dist/config/deep-interview.d.ts +22 -0
- package/dist/config/deep-interview.d.ts.map +1 -0
- package/dist/config/deep-interview.js +151 -0
- package/dist/config/deep-interview.js.map +1 -0
- package/dist/config/generator.d.ts +19 -2
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +198 -29
- package/dist/config/generator.js.map +1 -1
- package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +21 -0
- package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.d.ts +3 -0
- package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.js +45 -2
- package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
- package/dist/hooks/__tests__/agents-overlay.test.js +2 -0
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +17 -0
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/explore-routing.test.js +1 -0
- package/dist/hooks/__tests__/explore-routing.test.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +471 -15
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/prometheus-strict-contract.test.d.ts +2 -0
- package/dist/hooks/__tests__/prometheus-strict-contract.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/prometheus-strict-contract.test.js +320 -0
- package/dist/hooks/__tests__/prometheus-strict-contract.test.js.map +1 -0
- package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +12 -0
- package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
- package/dist/hooks/__tests__/research-workflow-boundaries.test.d.ts +2 -0
- package/dist/hooks/__tests__/research-workflow-boundaries.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/research-workflow-boundaries.test.js +35 -0
- package/dist/hooks/__tests__/research-workflow-boundaries.test.js.map +1 -0
- package/dist/hooks/deep-interview-config-instruction.d.ts +3 -0
- package/dist/hooks/deep-interview-config-instruction.d.ts.map +1 -0
- package/dist/hooks/deep-interview-config-instruction.js +47 -0
- package/dist/hooks/deep-interview-config-instruction.js.map +1 -0
- package/dist/hooks/explore-routing.d.ts.map +1 -1
- package/dist/hooks/explore-routing.js +1 -0
- package/dist/hooks/explore-routing.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts +6 -1
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +80 -14
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/keyword-registry.d.ts.map +1 -1
- package/dist/hooks/keyword-registry.js +1 -0
- package/dist/hooks/keyword-registry.js.map +1 -1
- package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
- package/dist/hooks/prompt-guidance-contract.js +11 -0
- package/dist/hooks/prompt-guidance-contract.js.map +1 -1
- package/dist/hud/__tests__/hud-tmux-injection.test.js +22 -0
- package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +213 -17
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/__tests__/render.test.js +84 -0
- package/dist/hud/__tests__/render.test.js.map +1 -1
- package/dist/hud/__tests__/state.test.js +51 -1
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/__tests__/tmux.test.js +171 -23
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/index.d.ts +1 -1
- package/dist/hud/index.d.ts.map +1 -1
- package/dist/hud/index.js +8 -3
- package/dist/hud/index.js.map +1 -1
- package/dist/hud/reconcile.d.ts +1 -1
- package/dist/hud/reconcile.d.ts.map +1 -1
- package/dist/hud/reconcile.js +14 -3
- package/dist/hud/reconcile.js.map +1 -1
- package/dist/hud/render.d.ts.map +1 -1
- package/dist/hud/render.js +26 -0
- package/dist/hud/render.js.map +1 -1
- package/dist/hud/state.d.ts +2 -1
- package/dist/hud/state.d.ts.map +1 -1
- package/dist/hud/state.js +62 -1
- package/dist/hud/state.js.map +1 -1
- package/dist/hud/tmux.d.ts +17 -3
- package/dist/hud/tmux.d.ts.map +1 -1
- package/dist/hud/tmux.js +96 -10
- package/dist/hud/tmux.js.map +1 -1
- package/dist/hud/types.d.ts +22 -0
- package/dist/hud/types.d.ts.map +1 -1
- package/dist/hud/types.js.map +1 -1
- package/dist/pipeline/__tests__/orchestrator.test.js +63 -1
- package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +410 -4
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +29 -2
- package/dist/pipeline/orchestrator.js.map +1 -1
- package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
- package/dist/pipeline/stages/ralplan.js +41 -6
- package/dist/pipeline/stages/ralplan.js.map +1 -1
- package/dist/question/__tests__/ui.test.js +43 -10
- package/dist/question/__tests__/ui.test.js.map +1 -1
- package/dist/question/deep-interview.d.ts +2 -0
- package/dist/question/deep-interview.d.ts.map +1 -1
- package/dist/question/deep-interview.js.map +1 -1
- package/dist/question/ui.d.ts +12 -0
- package/dist/question/ui.d.ts.map +1 -1
- package/dist/question/ui.js +83 -46
- package/dist/question/ui.js.map +1 -1
- package/dist/ralplan/__tests__/runtime.test.js +200 -10
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/consensus-gate.d.ts +23 -0
- package/dist/ralplan/consensus-gate.d.ts.map +1 -0
- package/dist/ralplan/consensus-gate.js +212 -0
- package/dist/ralplan/consensus-gate.js.map +1 -0
- package/dist/ralplan/runtime.d.ts +25 -0
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +144 -8
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +1034 -28
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/docs-site-contract.test.d.ts +2 -0
- package/dist/scripts/__tests__/docs-site-contract.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/docs-site-contract.test.js +42 -0
- package/dist/scripts/__tests__/docs-site-contract.test.js.map +1 -0
- package/dist/scripts/__tests__/notify-dispatcher.test.js +115 -2
- package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
- package/dist/scripts/__tests__/run-test-files.test.js +57 -0
- package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
- package/dist/scripts/__tests__/verify-native-agents.test.js +2 -2
- package/dist/scripts/__tests__/verify-native-agents.test.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +238 -36
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/notify-dispatcher.js +188 -4
- package/dist/scripts/notify-dispatcher.js.map +1 -1
- package/dist/scripts/run-test-files.js +13 -0
- package/dist/scripts/run-test-files.js.map +1 -1
- package/dist/state/__tests__/planning-gate.test.d.ts +2 -0
- package/dist/state/__tests__/planning-gate.test.d.ts.map +1 -0
- package/dist/state/__tests__/planning-gate.test.js +219 -0
- package/dist/state/__tests__/planning-gate.test.js.map +1 -0
- package/dist/state/__tests__/workflow-transition.test.js +6 -0
- package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
- package/dist/state/workflow-transition.d.ts +24 -1
- package/dist/state/workflow-transition.d.ts.map +1 -1
- package/dist/state/workflow-transition.js +70 -0
- package/dist/state/workflow-transition.js.map +1 -1
- package/dist/subagents/tracker.d.ts.map +1 -1
- package/dist/subagents/tracker.js +4 -3
- package/dist/subagents/tracker.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +36 -44
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +144 -18
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +10 -20
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +22 -6
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/ultragoal/__tests__/artifacts.test.js +50 -0
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +28 -2
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/package.json +1 -1
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/skills/autopilot/SKILL.md +16 -4
- package/plugins/oh-my-codex/skills/autoresearch/SKILL.md +4 -0
- package/plugins/oh-my-codex/skills/autoresearch-goal/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/best-practice-research/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +10 -0
- package/plugins/oh-my-codex/skills/pipeline/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/plan/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/prometheus-strict/README.md +35 -0
- package/plugins/oh-my-codex/skills/prometheus-strict/SKILL.md +219 -0
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +24 -5
- package/prompts/prometheus-strict-metis.md +274 -0
- package/prompts/prometheus-strict-momus.md +82 -0
- package/prompts/prometheus-strict-oracle.md +107 -0
- package/prompts/researcher.md +22 -3
- package/prompts/scholastic.md +11 -0
- package/skills/autopilot/SKILL.md +16 -4
- package/skills/autoresearch/SKILL.md +4 -0
- package/skills/autoresearch-goal/SKILL.md +1 -1
- package/skills/best-practice-research/SKILL.md +1 -1
- package/skills/deep-interview/SKILL.md +10 -0
- package/skills/pipeline/SKILL.md +1 -1
- package/skills/plan/SKILL.md +1 -1
- package/skills/prometheus-strict/README.md +35 -0
- package/skills/prometheus-strict/SKILL.md +219 -0
- package/skills/ralplan/SKILL.md +24 -5
- package/src/scripts/__tests__/codex-native-hook.test.ts +1307 -61
- package/src/scripts/__tests__/docs-site-contract.test.ts +47 -0
- package/src/scripts/__tests__/notify-dispatcher.test.ts +132 -3
- package/src/scripts/__tests__/run-test-files.test.ts +67 -0
- package/src/scripts/__tests__/verify-native-agents.test.ts +2 -2
- package/src/scripts/codex-native-hook.ts +260 -31
- package/src/scripts/notify-dispatcher.ts +202 -4
- package/src/scripts/run-test-files.ts +13 -0
- package/templates/catalog-manifest.json +27 -0
|
@@ -25,10 +25,13 @@ import { writeSessionStart } from "../../hooks/session.js";
|
|
|
25
25
|
import { resetTriageConfigCache } from "../../hooks/triage-config.js";
|
|
26
26
|
import { executeStateOperation } from "../../state/operations.js";
|
|
27
27
|
import { OMX_TMUX_HUD_OWNER_ENV } from "../../hud/reconcile.js";
|
|
28
|
+
import { OMX_TMUX_HUD_LEADER_PANE_ENV } from "../../hud/tmux.js";
|
|
28
29
|
import { readAllState } from "../../hud/state.js";
|
|
30
|
+
import { renderHud } from "../../hud/render.js";
|
|
29
31
|
import { getLegacyWikiDir, serializePage, writePage } from "../../wiki/storage.js";
|
|
30
32
|
import { WIKI_SCHEMA_VERSION } from "../../wiki/types.js";
|
|
31
33
|
import { createUltragoalPlan, readUltragoalPlan } from "../../ultragoal/artifacts.js";
|
|
34
|
+
import { getBaseStateDir } from "../../state/paths.js";
|
|
32
35
|
|
|
33
36
|
function nativeHookScriptPath(): string {
|
|
34
37
|
return join(process.cwd(), "dist", "scripts", "codex-native-hook.js");
|
|
@@ -63,6 +66,19 @@ async function writeJson(path: string, value: unknown): Promise<void> {
|
|
|
63
66
|
await writeFile(path, JSON.stringify(value, null, 2));
|
|
64
67
|
}
|
|
65
68
|
|
|
69
|
+
async function withIsolatedHome<T>(prefix: string, run: (homeDir: string) => Promise<T>): Promise<T> {
|
|
70
|
+
const homeDir = await mkdtemp(join(tmpdir(), `omx-native-hook-home-${prefix}-`));
|
|
71
|
+
const previousHome = process.env.HOME;
|
|
72
|
+
try {
|
|
73
|
+
process.env.HOME = homeDir;
|
|
74
|
+
return await run(homeDir);
|
|
75
|
+
} finally {
|
|
76
|
+
if (typeof previousHome === "string") process.env.HOME = previousHome;
|
|
77
|
+
else delete process.env.HOME;
|
|
78
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
66
82
|
async function withLoreGuardConfig<T>(
|
|
67
83
|
value: string,
|
|
68
84
|
prefix: string,
|
|
@@ -285,7 +301,7 @@ describe("codex native hook config", () => {
|
|
|
285
301
|
matcher?: string;
|
|
286
302
|
hooks?: Array<Record<string, unknown>>;
|
|
287
303
|
};
|
|
288
|
-
assert.equal(preToolUse.matcher,
|
|
304
|
+
assert.equal(preToolUse.matcher, undefined);
|
|
289
305
|
assert.match(
|
|
290
306
|
String(preToolUse.hooks?.[0]?.command || ""),
|
|
291
307
|
/codex-native-hook\.js"?$/,
|
|
@@ -1618,6 +1634,405 @@ describe("codex native hook dispatch", () => {
|
|
|
1618
1634
|
}
|
|
1619
1635
|
});
|
|
1620
1636
|
|
|
1637
|
+
it("injects deep-interview config overrides into UserPromptSubmit developer context", async () => {
|
|
1638
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-config-"));
|
|
1639
|
+
try {
|
|
1640
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
1641
|
+
await writeFile(
|
|
1642
|
+
join(cwd, ".omx", "config.toml"),
|
|
1643
|
+
`[omx.deepInterview]
|
|
1644
|
+
defaultProfile = "standard"
|
|
1645
|
+
standardThreshold = 0.05
|
|
1646
|
+
standardMaxRounds = 15
|
|
1647
|
+
enableChallengeModes = false
|
|
1648
|
+
`,
|
|
1649
|
+
);
|
|
1650
|
+
|
|
1651
|
+
const result = await dispatchCodexNativeHook(
|
|
1652
|
+
{
|
|
1653
|
+
hook_event_name: "UserPromptSubmit",
|
|
1654
|
+
cwd,
|
|
1655
|
+
session_id: "sess-deep-interview-config",
|
|
1656
|
+
thread_id: "thread-1",
|
|
1657
|
+
turn_id: "turn-1",
|
|
1658
|
+
prompt: "$deep-interview prove config reflection",
|
|
1659
|
+
},
|
|
1660
|
+
{ cwd },
|
|
1661
|
+
);
|
|
1662
|
+
|
|
1663
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
1664
|
+
assert.equal(result.skillState?.skill, "deep-interview");
|
|
1665
|
+
const serializedOutput = JSON.stringify(result.outputJson);
|
|
1666
|
+
assert.match(serializedOutput, /Deep-interview config override active/);
|
|
1667
|
+
assert.match(serializedOutput, /threshold=0\.05/);
|
|
1668
|
+
assert.match(serializedOutput, /max_rounds=15/);
|
|
1669
|
+
assert.match(serializedOutput, /enableChallengeModes=false/);
|
|
1670
|
+
|
|
1671
|
+
const modeState = JSON.parse(
|
|
1672
|
+
await readFile(join(cwd, ".omx", "state", "sessions", "sess-deep-interview-config", "deep-interview-state.json"), "utf-8"),
|
|
1673
|
+
) as { threshold?: number; max_rounds?: number; profile?: string };
|
|
1674
|
+
assert.equal(modeState.profile, "standard");
|
|
1675
|
+
assert.equal(modeState.threshold, 0.05);
|
|
1676
|
+
assert.equal(modeState.max_rounds, 15);
|
|
1677
|
+
} finally {
|
|
1678
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
it("proves UserPromptSubmit context changes before and after adding deep-interview config", async () => {
|
|
1683
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-config-before-after-"));
|
|
1684
|
+
const sessionId = "sess-deep-interview-config-before-after";
|
|
1685
|
+
try {
|
|
1686
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
1687
|
+
|
|
1688
|
+
const before = await withIsolatedHome("deep-interview-config-before-after", async () => (
|
|
1689
|
+
dispatchCodexNativeHook(
|
|
1690
|
+
{
|
|
1691
|
+
hook_event_name: "UserPromptSubmit",
|
|
1692
|
+
cwd,
|
|
1693
|
+
session_id: sessionId,
|
|
1694
|
+
thread_id: "thread-before-after",
|
|
1695
|
+
turn_id: "turn-before",
|
|
1696
|
+
prompt: "$deep-interview prove before config context",
|
|
1697
|
+
},
|
|
1698
|
+
{ cwd },
|
|
1699
|
+
)
|
|
1700
|
+
));
|
|
1701
|
+
const beforeOutput = JSON.stringify(before.outputJson);
|
|
1702
|
+
const beforeState = JSON.parse(
|
|
1703
|
+
await readFile(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json"), "utf-8"),
|
|
1704
|
+
) as {
|
|
1705
|
+
deep_interview_config?: unknown;
|
|
1706
|
+
threshold?: number;
|
|
1707
|
+
max_rounds?: number;
|
|
1708
|
+
};
|
|
1709
|
+
assert.equal(before.skillState?.skill, "deep-interview");
|
|
1710
|
+
assert.doesNotMatch(beforeOutput, /Deep-interview config override active/);
|
|
1711
|
+
assert.equal(before.skillState?.deep_interview_config, undefined);
|
|
1712
|
+
assert.equal(beforeState.deep_interview_config, undefined);
|
|
1713
|
+
assert.equal(beforeState.threshold, undefined);
|
|
1714
|
+
assert.equal(beforeState.max_rounds, undefined);
|
|
1715
|
+
|
|
1716
|
+
await writeFile(
|
|
1717
|
+
join(cwd, ".omx", "config.toml"),
|
|
1718
|
+
`[omx.deepInterview]
|
|
1719
|
+
defaultProfile = "standard"
|
|
1720
|
+
standardThreshold = 0.05
|
|
1721
|
+
standardMaxRounds = 15
|
|
1722
|
+
`,
|
|
1723
|
+
);
|
|
1724
|
+
|
|
1725
|
+
const after = await dispatchCodexNativeHook(
|
|
1726
|
+
{
|
|
1727
|
+
hook_event_name: "UserPromptSubmit",
|
|
1728
|
+
cwd,
|
|
1729
|
+
session_id: sessionId,
|
|
1730
|
+
thread_id: "thread-before-after",
|
|
1731
|
+
turn_id: "turn-after",
|
|
1732
|
+
prompt: "$deep-interview prove after config context",
|
|
1733
|
+
},
|
|
1734
|
+
{ cwd },
|
|
1735
|
+
);
|
|
1736
|
+
const afterOutput = JSON.stringify(after.outputJson);
|
|
1737
|
+
const afterState = JSON.parse(
|
|
1738
|
+
await readFile(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json"), "utf-8"),
|
|
1739
|
+
) as {
|
|
1740
|
+
deep_interview_config?: { profile?: string; threshold?: number; maxRounds?: number };
|
|
1741
|
+
threshold?: number;
|
|
1742
|
+
max_rounds?: number;
|
|
1743
|
+
};
|
|
1744
|
+
assert.equal(after.skillState?.deep_interview_config?.profile, "standard");
|
|
1745
|
+
assert.match(afterOutput, /Deep-interview config override active/);
|
|
1746
|
+
assert.match(afterOutput, /threshold=0\.05/);
|
|
1747
|
+
assert.match(afterOutput, /max_rounds=15/);
|
|
1748
|
+
assert.equal(afterState.deep_interview_config?.profile, "standard");
|
|
1749
|
+
assert.equal(afterState.threshold, 0.05);
|
|
1750
|
+
assert.equal(afterState.max_rounds, 15);
|
|
1751
|
+
} finally {
|
|
1752
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1753
|
+
}
|
|
1754
|
+
});
|
|
1755
|
+
|
|
1756
|
+
it("injects deep-interview config for mixed workflow prompts that defer execution modes", async () => {
|
|
1757
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-config-mixed-"));
|
|
1758
|
+
const sessionId = "sess-deep-interview-config-mixed";
|
|
1759
|
+
try {
|
|
1760
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
1761
|
+
await writeFile(
|
|
1762
|
+
join(cwd, ".omx", "config.toml"),
|
|
1763
|
+
`[omx.deepInterview]
|
|
1764
|
+
defaultProfile = "deep"
|
|
1765
|
+
deepThreshold = 0.13
|
|
1766
|
+
deepMaxRounds = 21
|
|
1767
|
+
enableChallengeModes = false
|
|
1768
|
+
`,
|
|
1769
|
+
);
|
|
1770
|
+
|
|
1771
|
+
const result = await withIsolatedHome("deep-interview-config-mixed", async () => (
|
|
1772
|
+
dispatchCodexNativeHook(
|
|
1773
|
+
{
|
|
1774
|
+
hook_event_name: "UserPromptSubmit",
|
|
1775
|
+
cwd,
|
|
1776
|
+
session_id: sessionId,
|
|
1777
|
+
thread_id: "thread-mixed-config",
|
|
1778
|
+
turn_id: "turn-mixed-config",
|
|
1779
|
+
prompt: "$autopilot $deep-interview prove mixed config context",
|
|
1780
|
+
},
|
|
1781
|
+
{ cwd },
|
|
1782
|
+
)
|
|
1783
|
+
));
|
|
1784
|
+
const serializedOutput = JSON.stringify(result.outputJson);
|
|
1785
|
+
const modeState = JSON.parse(
|
|
1786
|
+
await readFile(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json"), "utf-8"),
|
|
1787
|
+
) as {
|
|
1788
|
+
deep_interview_config?: { profile?: string; threshold?: number; maxRounds?: number; enableChallengeModes?: boolean };
|
|
1789
|
+
profile?: string;
|
|
1790
|
+
threshold?: number;
|
|
1791
|
+
max_rounds?: number;
|
|
1792
|
+
enable_challenge_modes?: boolean;
|
|
1793
|
+
};
|
|
1794
|
+
|
|
1795
|
+
assert.equal(result.skillState?.skill, "deep-interview");
|
|
1796
|
+
assert.deepEqual(result.skillState?.deferred_skills, ["autopilot"]);
|
|
1797
|
+
assert.equal(result.skillState?.deep_interview_config?.profile, "deep");
|
|
1798
|
+
assert.equal(result.skillState?.deep_interview_config?.threshold, 0.13);
|
|
1799
|
+
assert.equal(result.skillState?.deep_interview_config?.maxRounds, 21);
|
|
1800
|
+
assert.equal(result.skillState?.deep_interview_config?.enableChallengeModes, false);
|
|
1801
|
+
assert.match(serializedOutput, /Deep-interview config override active/);
|
|
1802
|
+
assert.match(serializedOutput, /profile=deep/);
|
|
1803
|
+
assert.match(serializedOutput, /threshold=0\.13/);
|
|
1804
|
+
assert.match(serializedOutput, /max_rounds=21/);
|
|
1805
|
+
assert.match(serializedOutput, /enableChallengeModes=false/);
|
|
1806
|
+
assert.equal(modeState.deep_interview_config?.profile, "deep");
|
|
1807
|
+
assert.equal(modeState.profile, "deep");
|
|
1808
|
+
assert.equal(modeState.threshold, 0.13);
|
|
1809
|
+
assert.equal(modeState.max_rounds, 21);
|
|
1810
|
+
assert.equal(modeState.enable_challenge_modes, false);
|
|
1811
|
+
} finally {
|
|
1812
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1813
|
+
}
|
|
1814
|
+
});
|
|
1815
|
+
|
|
1816
|
+
it("keeps deep-interview config override context on continuation prompts", async () => {
|
|
1817
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-config-continuation-"));
|
|
1818
|
+
const sessionId = "sess-deep-interview-config-continuation";
|
|
1819
|
+
try {
|
|
1820
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
1821
|
+
await writeFile(
|
|
1822
|
+
join(cwd, ".omx", "config.toml"),
|
|
1823
|
+
`[omx.deepInterview]
|
|
1824
|
+
defaultProfile = "standard"
|
|
1825
|
+
standardThreshold = 0.05
|
|
1826
|
+
standardMaxRounds = 15
|
|
1827
|
+
`,
|
|
1828
|
+
);
|
|
1829
|
+
|
|
1830
|
+
await dispatchCodexNativeHook(
|
|
1831
|
+
{
|
|
1832
|
+
hook_event_name: "UserPromptSubmit",
|
|
1833
|
+
cwd,
|
|
1834
|
+
session_id: sessionId,
|
|
1835
|
+
thread_id: "thread-continuation",
|
|
1836
|
+
turn_id: "turn-start",
|
|
1837
|
+
prompt: "$deep-interview prove config continuation",
|
|
1838
|
+
},
|
|
1839
|
+
{ cwd },
|
|
1840
|
+
);
|
|
1841
|
+
const continued = await dispatchCodexNativeHook(
|
|
1842
|
+
{
|
|
1843
|
+
hook_event_name: "UserPromptSubmit",
|
|
1844
|
+
cwd,
|
|
1845
|
+
session_id: sessionId,
|
|
1846
|
+
thread_id: "thread-continuation",
|
|
1847
|
+
turn_id: "turn-continue",
|
|
1848
|
+
prompt: "continue",
|
|
1849
|
+
},
|
|
1850
|
+
{ cwd },
|
|
1851
|
+
);
|
|
1852
|
+
const serializedOutput = JSON.stringify(continued.outputJson);
|
|
1853
|
+
const modeState = JSON.parse(
|
|
1854
|
+
await readFile(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json"), "utf-8"),
|
|
1855
|
+
) as { threshold?: number; max_rounds?: number; profile?: string };
|
|
1856
|
+
|
|
1857
|
+
assert.equal(continued.skillState?.skill, "deep-interview");
|
|
1858
|
+
assert.match(serializedOutput, /Deep-interview config override active/);
|
|
1859
|
+
assert.match(serializedOutput, /threshold=0\.05/);
|
|
1860
|
+
assert.match(serializedOutput, /max_rounds=15/);
|
|
1861
|
+
assert.equal(modeState.profile, "standard");
|
|
1862
|
+
assert.equal(modeState.threshold, 0.05);
|
|
1863
|
+
assert.equal(modeState.max_rounds, 15);
|
|
1864
|
+
} finally {
|
|
1865
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1866
|
+
}
|
|
1867
|
+
});
|
|
1868
|
+
|
|
1869
|
+
it("keeps explicit deep-interview profile flags reflected on continuation prompts", async () => {
|
|
1870
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-config-profile-continuation-"));
|
|
1871
|
+
const sessionId = "sess-deep-interview-config-profile-continuation";
|
|
1872
|
+
try {
|
|
1873
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
1874
|
+
await writeFile(
|
|
1875
|
+
join(cwd, ".omx", "config.toml"),
|
|
1876
|
+
`[omx.deepInterview]
|
|
1877
|
+
defaultProfile = "standard"
|
|
1878
|
+
standardThreshold = 0.22
|
|
1879
|
+
standardMaxRounds = 13
|
|
1880
|
+
deepThreshold = 0.13
|
|
1881
|
+
deepMaxRounds = 21
|
|
1882
|
+
`,
|
|
1883
|
+
);
|
|
1884
|
+
|
|
1885
|
+
await dispatchCodexNativeHook(
|
|
1886
|
+
{
|
|
1887
|
+
hook_event_name: "UserPromptSubmit",
|
|
1888
|
+
cwd,
|
|
1889
|
+
session_id: sessionId,
|
|
1890
|
+
thread_id: "thread-profile-continuation",
|
|
1891
|
+
turn_id: "turn-start",
|
|
1892
|
+
prompt: "$deep-interview --deep prove explicit profile continuation",
|
|
1893
|
+
},
|
|
1894
|
+
{ cwd },
|
|
1895
|
+
);
|
|
1896
|
+
const continued = await dispatchCodexNativeHook(
|
|
1897
|
+
{
|
|
1898
|
+
hook_event_name: "UserPromptSubmit",
|
|
1899
|
+
cwd,
|
|
1900
|
+
session_id: sessionId,
|
|
1901
|
+
thread_id: "thread-profile-continuation",
|
|
1902
|
+
turn_id: "turn-continue",
|
|
1903
|
+
prompt: "continue",
|
|
1904
|
+
},
|
|
1905
|
+
{ cwd },
|
|
1906
|
+
);
|
|
1907
|
+
const serializedOutput = JSON.stringify(continued.outputJson);
|
|
1908
|
+
const modeState = JSON.parse(
|
|
1909
|
+
await readFile(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json"), "utf-8"),
|
|
1910
|
+
) as { threshold?: number; max_rounds?: number; profile?: string; deep_interview_config?: { profile?: string } };
|
|
1911
|
+
|
|
1912
|
+
assert.equal(continued.skillState?.skill, "deep-interview");
|
|
1913
|
+
assert.equal(continued.skillState?.deep_interview_config?.profile, "deep");
|
|
1914
|
+
assert.match(serializedOutput, /Deep-interview config override active/);
|
|
1915
|
+
assert.match(serializedOutput, /profile=deep/);
|
|
1916
|
+
assert.match(serializedOutput, /threshold=0\.13/);
|
|
1917
|
+
assert.match(serializedOutput, /max_rounds=21/);
|
|
1918
|
+
assert.equal(modeState.deep_interview_config?.profile, "deep");
|
|
1919
|
+
assert.equal(modeState.profile, "deep");
|
|
1920
|
+
assert.equal(modeState.threshold, 0.13);
|
|
1921
|
+
assert.equal(modeState.max_rounds, 21);
|
|
1922
|
+
} finally {
|
|
1923
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1924
|
+
}
|
|
1925
|
+
});
|
|
1926
|
+
|
|
1927
|
+
it("keeps the documented deep-interview Suggested Config reflected in UserPromptSubmit context", async () => {
|
|
1928
|
+
const skillDoc = await readFile(join(process.cwd(), "skills", "deep-interview", "SKILL.md"), "utf-8");
|
|
1929
|
+
const markerIndex = skillDoc.indexOf("## Suggested Config (optional)");
|
|
1930
|
+
assert.notEqual(markerIndex, -1);
|
|
1931
|
+
const configMatch = skillDoc.slice(markerIndex).match(/```toml\n([\s\S]*?)\n```/);
|
|
1932
|
+
assert.ok(configMatch);
|
|
1933
|
+
const documentedConfig = configMatch[1]?.trimEnd();
|
|
1934
|
+
assert.ok(documentedConfig);
|
|
1935
|
+
assert.match(documentedConfig, /standardThreshold = 0\.20/);
|
|
1936
|
+
assert.match(documentedConfig, /standardMaxRounds = 12/);
|
|
1937
|
+
|
|
1938
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-doc-config-"));
|
|
1939
|
+
const sessionId = "sess-deep-interview-doc-config";
|
|
1940
|
+
try {
|
|
1941
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
1942
|
+
await writeFile(join(cwd, ".omx", "config.toml"), `${documentedConfig}\n`);
|
|
1943
|
+
|
|
1944
|
+
const result = await dispatchCodexNativeHook(
|
|
1945
|
+
{
|
|
1946
|
+
hook_event_name: "UserPromptSubmit",
|
|
1947
|
+
cwd,
|
|
1948
|
+
session_id: sessionId,
|
|
1949
|
+
thread_id: "thread-doc-config",
|
|
1950
|
+
turn_id: "turn-doc-config",
|
|
1951
|
+
prompt: "$deep-interview prove documented config context",
|
|
1952
|
+
},
|
|
1953
|
+
{ cwd },
|
|
1954
|
+
);
|
|
1955
|
+
const serializedOutput = JSON.stringify(result.outputJson);
|
|
1956
|
+
const modeState = JSON.parse(
|
|
1957
|
+
await readFile(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json"), "utf-8"),
|
|
1958
|
+
) as {
|
|
1959
|
+
deep_interview_config?: { profile?: string; threshold?: number; maxRounds?: number };
|
|
1960
|
+
profile?: string;
|
|
1961
|
+
threshold?: number;
|
|
1962
|
+
max_rounds?: number;
|
|
1963
|
+
};
|
|
1964
|
+
|
|
1965
|
+
assert.equal(result.skillState?.deep_interview_config?.profile, "standard");
|
|
1966
|
+
assert.equal(result.skillState?.deep_interview_config?.threshold, 0.2);
|
|
1967
|
+
assert.equal(result.skillState?.deep_interview_config?.maxRounds, 12);
|
|
1968
|
+
assert.match(serializedOutput, /Deep-interview config override active/);
|
|
1969
|
+
assert.match(serializedOutput, /profile=standard/);
|
|
1970
|
+
assert.match(serializedOutput, /threshold=0\.2/);
|
|
1971
|
+
assert.match(serializedOutput, /max_rounds=12/);
|
|
1972
|
+
assert.equal(modeState.deep_interview_config?.profile, "standard");
|
|
1973
|
+
assert.equal(modeState.profile, "standard");
|
|
1974
|
+
assert.equal(modeState.threshold, 0.2);
|
|
1975
|
+
assert.equal(modeState.max_rounds, 12);
|
|
1976
|
+
} finally {
|
|
1977
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1978
|
+
}
|
|
1979
|
+
});
|
|
1980
|
+
|
|
1981
|
+
it("injects deep-interview config overrides when state is boxed under OMX_ROOT", async () => {
|
|
1982
|
+
const root = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-config-boxed-"));
|
|
1983
|
+
const cwd = join(root, "source");
|
|
1984
|
+
const omxRoot = join(root, "box");
|
|
1985
|
+
const sessionId = "sess-boxed-deep-interview-config";
|
|
1986
|
+
const previousOmxRoot = process.env.OMX_ROOT;
|
|
1987
|
+
const previousOmxStateRoot = process.env.OMX_STATE_ROOT;
|
|
1988
|
+
const previousTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
1989
|
+
try {
|
|
1990
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
1991
|
+
await writeFile(
|
|
1992
|
+
join(cwd, ".omx", "config.toml"),
|
|
1993
|
+
`[omx.deepInterview]
|
|
1994
|
+
defaultProfile = "standard"
|
|
1995
|
+
standardThreshold = 0.05
|
|
1996
|
+
standardMaxRounds = 15
|
|
1997
|
+
`,
|
|
1998
|
+
);
|
|
1999
|
+
process.env.OMX_ROOT = omxRoot;
|
|
2000
|
+
delete process.env.OMX_STATE_ROOT;
|
|
2001
|
+
delete process.env.OMX_TEAM_STATE_ROOT;
|
|
2002
|
+
|
|
2003
|
+
const result = await dispatchCodexNativeHook(
|
|
2004
|
+
{
|
|
2005
|
+
hook_event_name: "UserPromptSubmit",
|
|
2006
|
+
cwd,
|
|
2007
|
+
session_id: sessionId,
|
|
2008
|
+
thread_id: "thread-boxed",
|
|
2009
|
+
turn_id: "turn-boxed",
|
|
2010
|
+
prompt: "$deep-interview prove boxed config reflection",
|
|
2011
|
+
},
|
|
2012
|
+
{ cwd },
|
|
2013
|
+
);
|
|
2014
|
+
|
|
2015
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
2016
|
+
assert.equal(result.skillState?.initialized_state_path, `.omx/state/sessions/${sessionId}/deep-interview-state.json`);
|
|
2017
|
+
const boxedStatePath = join(omxRoot, ".omx", "state", "sessions", sessionId, "deep-interview-state.json");
|
|
2018
|
+
assert.equal(existsSync(boxedStatePath), true);
|
|
2019
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", sessionId, "deep-interview-state.json")), false);
|
|
2020
|
+
|
|
2021
|
+
const serializedOutput = JSON.stringify(result.outputJson);
|
|
2022
|
+
assert.match(serializedOutput, /Deep-interview config override active/);
|
|
2023
|
+
assert.match(serializedOutput, /threshold=0\.05/);
|
|
2024
|
+
assert.match(serializedOutput, /max_rounds=15/);
|
|
2025
|
+
} finally {
|
|
2026
|
+
if (typeof previousOmxRoot === "string") process.env.OMX_ROOT = previousOmxRoot;
|
|
2027
|
+
else delete process.env.OMX_ROOT;
|
|
2028
|
+
if (typeof previousOmxStateRoot === "string") process.env.OMX_STATE_ROOT = previousOmxStateRoot;
|
|
2029
|
+
else delete process.env.OMX_STATE_ROOT;
|
|
2030
|
+
if (typeof previousTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = previousTeamStateRoot;
|
|
2031
|
+
else delete process.env.OMX_TEAM_STATE_ROOT;
|
|
2032
|
+
await rm(root, { recursive: true, force: true });
|
|
2033
|
+
}
|
|
2034
|
+
});
|
|
2035
|
+
|
|
1621
2036
|
it("records boxed keyword activation mode detail and skill state under OMX_ROOT", async () => {
|
|
1622
2037
|
const root = await mkdtemp(join(tmpdir(), "omx-native-hook-boxed-"));
|
|
1623
2038
|
const cwd = join(root, "source");
|
|
@@ -1937,6 +2352,9 @@ describe("codex native hook dispatch", () => {
|
|
|
1937
2352
|
assert.match(output, /omx ultragoal checkpoint --goal-id G001-demo --status complete/);
|
|
1938
2353
|
assert.match(output, /--status blocked/);
|
|
1939
2354
|
assert.match(output, /Codex goal context/);
|
|
2355
|
+
assert.match(output, /no such table: thread_goals/);
|
|
2356
|
+
assert.match(output, /unavailable get_goal error JSON or path/);
|
|
2357
|
+
assert.match(output, /safe-recovery blocker/);
|
|
1940
2358
|
assert.doesNotMatch(output, /fresh (?:Codex )?(?:thread|session)s?/i);
|
|
1941
2359
|
assert.match(output, /Hooks must not mutate Codex goal state/);
|
|
1942
2360
|
} finally {
|
|
@@ -2233,6 +2651,36 @@ describe("codex native hook dispatch", () => {
|
|
|
2233
2651
|
}
|
|
2234
2652
|
});
|
|
2235
2653
|
|
|
2654
|
+
it("injects autopilot ralplan consensus gate guidance on prompt activation", async () => {
|
|
2655
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-gate-"));
|
|
2656
|
+
try {
|
|
2657
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
2658
|
+
const result = await dispatchCodexNativeHook(
|
|
2659
|
+
{
|
|
2660
|
+
hook_event_name: "UserPromptSubmit",
|
|
2661
|
+
cwd,
|
|
2662
|
+
session_id: "sess-autopilot-ralplan-gate",
|
|
2663
|
+
thread_id: "thread-autopilot-ralplan-gate",
|
|
2664
|
+
turn_id: "turn-autopilot-ralplan-gate",
|
|
2665
|
+
prompt: "$autopilot implement issue #2430",
|
|
2666
|
+
},
|
|
2667
|
+
{ cwd },
|
|
2668
|
+
);
|
|
2669
|
+
|
|
2670
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
2671
|
+
assert.equal(result.skillState?.skill, "autopilot");
|
|
2672
|
+
const message = String(
|
|
2673
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
2674
|
+
);
|
|
2675
|
+
assert.match(message, /Autopilot protocol:/);
|
|
2676
|
+
assert.match(message, /deep-interview -> ralplan -> ultragoal -> code-review -> ultraqa/);
|
|
2677
|
+
assert.match(message, /Planner output has been reviewed sequentially by Architect and then Critic/);
|
|
2678
|
+
assert.match(message, /do not hand off to Ultragoal or implementation until .*ralplan_architect_review.*ralplan_critic_review/);
|
|
2679
|
+
} finally {
|
|
2680
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2681
|
+
}
|
|
2682
|
+
});
|
|
2683
|
+
|
|
2236
2684
|
it("records ultragoal prompt skill activation with goal-tool handoff guidance", async () => {
|
|
2237
2685
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-"));
|
|
2238
2686
|
try {
|
|
@@ -2251,7 +2699,11 @@ describe("codex native hook dispatch", () => {
|
|
|
2251
2699
|
|
|
2252
2700
|
assert.equal(result.omxEventName, "keyword-detector");
|
|
2253
2701
|
assert.equal(result.skillState?.skill, "ultragoal");
|
|
2254
|
-
assert.equal(result.skillState?.initialized_mode,
|
|
2702
|
+
assert.equal(result.skillState?.initialized_mode, "ultragoal");
|
|
2703
|
+
assert.equal(
|
|
2704
|
+
result.skillState?.initialized_state_path,
|
|
2705
|
+
".omx/state/sessions/sess-ultragoal-1/ultragoal-state.json",
|
|
2706
|
+
);
|
|
2255
2707
|
const message = String(
|
|
2256
2708
|
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
2257
2709
|
);
|
|
@@ -2262,40 +2714,112 @@ describe("codex native hook dispatch", () => {
|
|
|
2262
2714
|
assert.match(message, /update_goal/);
|
|
2263
2715
|
assert.match(message, /does not call `\/goal clear`/);
|
|
2264
2716
|
assert.match(message, /multiple sequential ultragoal runs/);
|
|
2265
|
-
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ultragoal-1", "ultragoal-state.json")),
|
|
2717
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ultragoal-1", "ultragoal-state.json")), true);
|
|
2266
2718
|
} finally {
|
|
2267
2719
|
await rm(cwd, { recursive: true, force: true });
|
|
2268
2720
|
}
|
|
2269
2721
|
});
|
|
2270
2722
|
|
|
2271
|
-
it("
|
|
2272
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-
|
|
2723
|
+
it("deactivates active deep-interview state on explicit ultragoal handoff", async () => {
|
|
2724
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-deep-interview-handoff-"));
|
|
2273
2725
|
try {
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2726
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
2727
|
+
const sessionDir = join(stateDir, "sessions", "sess-ultragoal-handoff");
|
|
2728
|
+
await mkdir(sessionDir, { recursive: true });
|
|
2729
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
2730
|
+
version: 1,
|
|
2731
|
+
active: true,
|
|
2732
|
+
skill: "deep-interview",
|
|
2733
|
+
phase: "planning",
|
|
2734
|
+
session_id: "sess-ultragoal-handoff",
|
|
2735
|
+
active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-ultragoal-handoff" }],
|
|
2736
|
+
});
|
|
2737
|
+
await writeJson(join(sessionDir, "deep-interview-state.json"), {
|
|
2738
|
+
active: true,
|
|
2739
|
+
mode: "deep-interview",
|
|
2740
|
+
current_phase: "intent-first",
|
|
2741
|
+
session_id: "sess-ultragoal-handoff",
|
|
2742
|
+
question_enforcement: {
|
|
2743
|
+
obligation_id: "obligation-ultragoal-handoff",
|
|
2744
|
+
source: "omx-question",
|
|
2745
|
+
status: "pending",
|
|
2746
|
+
requested_at: "2026-05-21T03:00:00.000Z",
|
|
2747
|
+
},
|
|
2277
2748
|
});
|
|
2278
2749
|
|
|
2279
|
-
const
|
|
2750
|
+
const result = await dispatchCodexNativeHook(
|
|
2280
2751
|
{
|
|
2281
2752
|
hook_event_name: "UserPromptSubmit",
|
|
2282
2753
|
cwd,
|
|
2283
|
-
session_id: "sess-ultragoal-
|
|
2284
|
-
|
|
2754
|
+
session_id: "sess-ultragoal-handoff",
|
|
2755
|
+
thread_id: "thread-ultragoal-handoff",
|
|
2756
|
+
turn_id: "turn-ultragoal-handoff",
|
|
2757
|
+
prompt: "$ultragoal turn the clarified spec into goals",
|
|
2285
2758
|
},
|
|
2286
2759
|
{ cwd },
|
|
2287
2760
|
);
|
|
2288
|
-
assert.equal(prose.outputJson, null);
|
|
2289
|
-
assert.equal((await readUltragoalPlan(cwd)).goals.length, 1);
|
|
2290
2761
|
|
|
2291
|
-
|
|
2762
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
2763
|
+
assert.equal(result.skillState?.skill, "ultragoal");
|
|
2764
|
+
assert.match(JSON.stringify(result.outputJson), /mode transiting: deep-interview -> ultragoal/);
|
|
2765
|
+
|
|
2766
|
+
const completed = JSON.parse(await readFile(join(sessionDir, "deep-interview-state.json"), "utf-8")) as {
|
|
2767
|
+
active?: boolean;
|
|
2768
|
+
current_phase?: string;
|
|
2769
|
+
question_enforcement?: { status?: string; clear_reason?: string };
|
|
2770
|
+
};
|
|
2771
|
+
assert.equal(completed.active, false);
|
|
2772
|
+
assert.equal(completed.current_phase, "completed");
|
|
2773
|
+
assert.equal(completed.question_enforcement?.status, "cleared");
|
|
2774
|
+
assert.equal(completed.question_enforcement?.clear_reason, "handoff");
|
|
2775
|
+
assert.equal(existsSync(join(sessionDir, "ultragoal-state.json")), true);
|
|
2776
|
+
|
|
2777
|
+
const edit = await dispatchCodexNativeHook(
|
|
2292
2778
|
{
|
|
2293
|
-
hook_event_name: "
|
|
2779
|
+
hook_event_name: "PreToolUse",
|
|
2294
2780
|
cwd,
|
|
2295
|
-
session_id: "sess-ultragoal-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2781
|
+
session_id: "sess-ultragoal-handoff",
|
|
2782
|
+
thread_id: "thread-ultragoal-handoff",
|
|
2783
|
+
tool_name: "Edit",
|
|
2784
|
+
tool_use_id: "tool-ultragoal-post-handoff-edit",
|
|
2785
|
+
tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
|
|
2786
|
+
},
|
|
2787
|
+
{ cwd },
|
|
2788
|
+
);
|
|
2789
|
+
assert.equal(edit.outputJson, null);
|
|
2790
|
+
} finally {
|
|
2791
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2792
|
+
}
|
|
2793
|
+
});
|
|
2794
|
+
|
|
2795
|
+
it("applies only explicit structured UserPromptSubmit ultragoal steering directives", async () => {
|
|
2796
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-steer-"));
|
|
2797
|
+
try {
|
|
2798
|
+
await createUltragoalPlan(cwd, {
|
|
2799
|
+
brief: "G002-cli-and-prompt-submit-bridge .omx/ultragoal hook steering fixture",
|
|
2800
|
+
goals: [{ title: "First", objective: "Complete first milestone with tests." }],
|
|
2801
|
+
});
|
|
2802
|
+
|
|
2803
|
+
const prose = await dispatchCodexNativeHook(
|
|
2804
|
+
{
|
|
2805
|
+
hook_event_name: "UserPromptSubmit",
|
|
2806
|
+
cwd,
|
|
2807
|
+
session_id: "sess-ultragoal-steer-1",
|
|
2808
|
+
prompt: "Please add a subgoal for docs later; this is normal prose, not a directive.",
|
|
2809
|
+
},
|
|
2810
|
+
{ cwd },
|
|
2811
|
+
);
|
|
2812
|
+
assert.equal(prose.outputJson, null);
|
|
2813
|
+
assert.equal((await readUltragoalPlan(cwd)).goals.length, 1);
|
|
2814
|
+
|
|
2815
|
+
const jsonExample = await dispatchCodexNativeHook(
|
|
2816
|
+
{
|
|
2817
|
+
hook_event_name: "UserPromptSubmit",
|
|
2818
|
+
cwd,
|
|
2819
|
+
session_id: "sess-ultragoal-steer-1",
|
|
2820
|
+
prompt: `Here is an inert example:\n\`\`\`json\n${JSON.stringify({
|
|
2821
|
+
kind: "add_subgoal",
|
|
2822
|
+
source: "user_prompt_submit",
|
|
2299
2823
|
evidence: "Example JSON should not mutate .omx/ultragoal.",
|
|
2300
2824
|
rationale: "Only explicit steering fences or labels are executable.",
|
|
2301
2825
|
title: "Inert JSON example",
|
|
@@ -3410,6 +3934,78 @@ esac
|
|
|
3410
3934
|
}
|
|
3411
3935
|
});
|
|
3412
3936
|
|
|
3937
|
+
it("reuses an existing owner-tagged HUD pane when UserPromptSubmit revives with the canonical session id", async () => {
|
|
3938
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-reuse-"));
|
|
3939
|
+
const originalTmux = process.env.TMUX;
|
|
3940
|
+
const originalTmuxPane = process.env.TMUX_PANE;
|
|
3941
|
+
const originalPath = process.env.PATH;
|
|
3942
|
+
const originalHudOwner = process.env[OMX_TMUX_HUD_OWNER_ENV];
|
|
3943
|
+
try {
|
|
3944
|
+
process.env.TMUX = "1";
|
|
3945
|
+
process.env.TMUX_PANE = "%1";
|
|
3946
|
+
process.env[OMX_TMUX_HUD_OWNER_ENV] = "1";
|
|
3947
|
+
const canonicalSessionId = "omx-canonical-hud-reuse";
|
|
3948
|
+
const nativeSessionId = "codex-native-hud-reuse";
|
|
3949
|
+
await mkdir(join(cwd, ".omx", "state", "sessions", canonicalSessionId), { recursive: true });
|
|
3950
|
+
await writeSessionStart(cwd, canonicalSessionId);
|
|
3951
|
+
|
|
3952
|
+
const binDir = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-reuse-bin-"));
|
|
3953
|
+
const tmuxLog = join(cwd, "tmux.log");
|
|
3954
|
+
await writeFile(
|
|
3955
|
+
join(binDir, "tmux"),
|
|
3956
|
+
`#!/usr/bin/env bash
|
|
3957
|
+
set -euo pipefail
|
|
3958
|
+
printf '%s\n' "$*" >> ${JSON.stringify(tmuxLog)}
|
|
3959
|
+
case "$1" in
|
|
3960
|
+
list-panes)
|
|
3961
|
+
printf '%%1\tcodex\tcodex\n'
|
|
3962
|
+
printf '%%2\tnode\texec env OMX_TMUX_HUD_OWNER='"'"'1'"'"' ${OMX_TMUX_HUD_LEADER_PANE_ENV}='"'"'%%1'"'"' /node /omx.js hud --watch\n'
|
|
3963
|
+
;;
|
|
3964
|
+
display-message)
|
|
3965
|
+
printf '80\t24\n'
|
|
3966
|
+
;;
|
|
3967
|
+
resize-pane)
|
|
3968
|
+
;;
|
|
3969
|
+
split-window)
|
|
3970
|
+
printf '%%9\n'
|
|
3971
|
+
;;
|
|
3972
|
+
esac
|
|
3973
|
+
`,
|
|
3974
|
+
);
|
|
3975
|
+
await chmod(join(binDir, "tmux"), 0o755);
|
|
3976
|
+
process.env.PATH = `${binDir}:${originalPath}`;
|
|
3977
|
+
|
|
3978
|
+
const result = await dispatchCodexNativeHook(
|
|
3979
|
+
{
|
|
3980
|
+
hook_event_name: "UserPromptSubmit",
|
|
3981
|
+
cwd,
|
|
3982
|
+
session_id: nativeSessionId,
|
|
3983
|
+
thread_id: "thread-hud-reuse",
|
|
3984
|
+
turn_id: "turn-hud-reuse",
|
|
3985
|
+
prompt: "$ralplan prepare plan",
|
|
3986
|
+
},
|
|
3987
|
+
{ cwd },
|
|
3988
|
+
);
|
|
3989
|
+
|
|
3990
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
3991
|
+
const tmuxCalls = await readFile(tmuxLog, "utf-8");
|
|
3992
|
+
assert.match(tmuxCalls, /list-panes -t %1 -F/);
|
|
3993
|
+
assert.match(tmuxCalls, /resize-pane -t %2 -y 3/);
|
|
3994
|
+
assert.doesNotMatch(tmuxCalls, /split-window/);
|
|
3995
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", canonicalSessionId, "ralplan-state.json")), true);
|
|
3996
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", nativeSessionId, "ralplan-state.json")), false);
|
|
3997
|
+
} finally {
|
|
3998
|
+
if (originalTmux === undefined) delete process.env.TMUX;
|
|
3999
|
+
else process.env.TMUX = originalTmux;
|
|
4000
|
+
if (originalTmuxPane === undefined) delete process.env.TMUX_PANE;
|
|
4001
|
+
else process.env.TMUX_PANE = originalTmuxPane;
|
|
4002
|
+
if (originalHudOwner === undefined) delete process.env[OMX_TMUX_HUD_OWNER_ENV];
|
|
4003
|
+
else process.env[OMX_TMUX_HUD_OWNER_ENV] = originalHudOwner;
|
|
4004
|
+
process.env.PATH = originalPath;
|
|
4005
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4006
|
+
}
|
|
4007
|
+
});
|
|
4008
|
+
|
|
3413
4009
|
it("skips prompt-submit HUD reconciliation inside unowned tmux panes", async () => {
|
|
3414
4010
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-unowned-"));
|
|
3415
4011
|
const originalTmux = process.env.TMUX;
|
|
@@ -3889,6 +4485,183 @@ exit 0
|
|
|
3889
4485
|
}
|
|
3890
4486
|
});
|
|
3891
4487
|
|
|
4488
|
+
it("blocks implementation file edits while deep-interview remains active after a clarified answer", async () => {
|
|
4489
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-edit-block-"));
|
|
4490
|
+
try {
|
|
4491
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4492
|
+
const sessionDir = join(stateDir, "sessions", "sess-di-edit-block");
|
|
4493
|
+
await mkdir(sessionDir, { recursive: true });
|
|
4494
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-di-edit-block", cwd });
|
|
4495
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
4496
|
+
version: 1,
|
|
4497
|
+
active: true,
|
|
4498
|
+
skill: "deep-interview",
|
|
4499
|
+
phase: "planning",
|
|
4500
|
+
session_id: "sess-di-edit-block",
|
|
4501
|
+
thread_id: "thread-di-edit-block",
|
|
4502
|
+
active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-di-edit-block", thread_id: "thread-di-edit-block" }],
|
|
4503
|
+
});
|
|
4504
|
+
await writeJson(join(sessionDir, "deep-interview-state.json"), {
|
|
4505
|
+
active: true,
|
|
4506
|
+
mode: "deep-interview",
|
|
4507
|
+
current_phase: "intent-first",
|
|
4508
|
+
session_id: "sess-di-edit-block",
|
|
4509
|
+
thread_id: "thread-di-edit-block",
|
|
4510
|
+
rounds: [{ answer: "Implement by editing src/hooks/keyword-detector.ts and add tests." }],
|
|
4511
|
+
});
|
|
4512
|
+
|
|
4513
|
+
const result = await dispatchCodexNativeHook(
|
|
4514
|
+
{
|
|
4515
|
+
hook_event_name: "PreToolUse",
|
|
4516
|
+
cwd,
|
|
4517
|
+
session_id: "sess-di-edit-block",
|
|
4518
|
+
thread_id: "thread-di-edit-block",
|
|
4519
|
+
tool_name: "Edit",
|
|
4520
|
+
tool_use_id: "tool-di-edit-block",
|
|
4521
|
+
tool_input: { file_path: "src/hooks/keyword-detector.ts", old_string: "a", new_string: "b" },
|
|
4522
|
+
},
|
|
4523
|
+
{ cwd },
|
|
4524
|
+
);
|
|
4525
|
+
|
|
4526
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
4527
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
4528
|
+
assert.match(String((result.outputJson as { reason?: string } | null)?.reason ?? ""), /Deep-interview is active/);
|
|
4529
|
+
assert.match(JSON.stringify(result.outputJson), /requirements\/spec mode/);
|
|
4530
|
+
assert.match(JSON.stringify(result.outputJson), /\$ralplan/);
|
|
4531
|
+
} finally {
|
|
4532
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4533
|
+
}
|
|
4534
|
+
});
|
|
4535
|
+
|
|
4536
|
+
it("allows deep-interview artifact and state writes while blocking implementation Bash writes", async () => {
|
|
4537
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-artifact-"));
|
|
4538
|
+
try {
|
|
4539
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4540
|
+
const sessionDir = join(stateDir, "sessions", "sess-di-artifact");
|
|
4541
|
+
await mkdir(sessionDir, { recursive: true });
|
|
4542
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-di-artifact", cwd });
|
|
4543
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
4544
|
+
version: 1,
|
|
4545
|
+
active: true,
|
|
4546
|
+
skill: "deep-interview",
|
|
4547
|
+
phase: "planning",
|
|
4548
|
+
session_id: "sess-di-artifact",
|
|
4549
|
+
active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-di-artifact" }],
|
|
4550
|
+
});
|
|
4551
|
+
await writeJson(join(sessionDir, "deep-interview-state.json"), {
|
|
4552
|
+
active: true,
|
|
4553
|
+
mode: "deep-interview",
|
|
4554
|
+
current_phase: "intent-first",
|
|
4555
|
+
session_id: "sess-di-artifact",
|
|
4556
|
+
});
|
|
4557
|
+
|
|
4558
|
+
const allowedWrite = await dispatchCodexNativeHook(
|
|
4559
|
+
{
|
|
4560
|
+
hook_event_name: "PreToolUse",
|
|
4561
|
+
cwd,
|
|
4562
|
+
session_id: "sess-di-artifact",
|
|
4563
|
+
tool_name: "Write",
|
|
4564
|
+
tool_use_id: "tool-di-spec-write",
|
|
4565
|
+
tool_input: { file_path: ".omx/specs/deep-interview-demo.md", content: "# Spec" },
|
|
4566
|
+
},
|
|
4567
|
+
{ cwd },
|
|
4568
|
+
);
|
|
4569
|
+
assert.equal(allowedWrite.outputJson, null);
|
|
4570
|
+
|
|
4571
|
+
const allowedBash = await dispatchCodexNativeHook(
|
|
4572
|
+
{
|
|
4573
|
+
hook_event_name: "PreToolUse",
|
|
4574
|
+
cwd,
|
|
4575
|
+
session_id: "sess-di-artifact",
|
|
4576
|
+
tool_name: "Bash",
|
|
4577
|
+
tool_use_id: "tool-di-context-bash",
|
|
4578
|
+
tool_input: { command: "cat > .omx/context/demo.md <<'EOF'\n# Context\nEOF" },
|
|
4579
|
+
},
|
|
4580
|
+
{ cwd },
|
|
4581
|
+
);
|
|
4582
|
+
assert.equal(allowedBash.outputJson, null);
|
|
4583
|
+
|
|
4584
|
+
const allowedAppendBash = await dispatchCodexNativeHook(
|
|
4585
|
+
{
|
|
4586
|
+
hook_event_name: "PreToolUse",
|
|
4587
|
+
cwd,
|
|
4588
|
+
session_id: "sess-di-artifact",
|
|
4589
|
+
tool_name: "Bash",
|
|
4590
|
+
tool_use_id: "tool-di-context-append-bash",
|
|
4591
|
+
tool_input: { command: "echo more context >> .omx/context/demo.md" },
|
|
4592
|
+
},
|
|
4593
|
+
{ cwd },
|
|
4594
|
+
);
|
|
4595
|
+
assert.equal(allowedAppendBash.outputJson, null);
|
|
4596
|
+
|
|
4597
|
+
const blockedBash = await dispatchCodexNativeHook(
|
|
4598
|
+
{
|
|
4599
|
+
hook_event_name: "PreToolUse",
|
|
4600
|
+
cwd,
|
|
4601
|
+
session_id: "sess-di-artifact",
|
|
4602
|
+
tool_name: "Bash",
|
|
4603
|
+
tool_use_id: "tool-di-src-bash",
|
|
4604
|
+
tool_input: { command: "cat > src/implementation.ts <<'EOF'\nexport const x = 1;\nEOF" },
|
|
4605
|
+
},
|
|
4606
|
+
{ cwd },
|
|
4607
|
+
);
|
|
4608
|
+
assert.equal((blockedBash.outputJson as { decision?: string } | null)?.decision, "block");
|
|
4609
|
+
} finally {
|
|
4610
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4611
|
+
}
|
|
4612
|
+
});
|
|
4613
|
+
|
|
4614
|
+
it("allows implementation tools after an explicit deep-interview handoff deactivates the mode", async () => {
|
|
4615
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-handoff-"));
|
|
4616
|
+
try {
|
|
4617
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
4618
|
+
const sessionDir = join(stateDir, "sessions", "sess-di-handoff");
|
|
4619
|
+
await mkdir(sessionDir, { recursive: true });
|
|
4620
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
4621
|
+
version: 1,
|
|
4622
|
+
active: true,
|
|
4623
|
+
skill: "deep-interview",
|
|
4624
|
+
phase: "planning",
|
|
4625
|
+
session_id: "sess-di-handoff",
|
|
4626
|
+
active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-di-handoff" }],
|
|
4627
|
+
});
|
|
4628
|
+
await writeJson(join(sessionDir, "deep-interview-state.json"), {
|
|
4629
|
+
active: true,
|
|
4630
|
+
mode: "deep-interview",
|
|
4631
|
+
current_phase: "intent-first",
|
|
4632
|
+
session_id: "sess-di-handoff",
|
|
4633
|
+
});
|
|
4634
|
+
|
|
4635
|
+
await dispatchCodexNativeHook(
|
|
4636
|
+
{
|
|
4637
|
+
hook_event_name: "UserPromptSubmit",
|
|
4638
|
+
cwd,
|
|
4639
|
+
session_id: "sess-di-handoff",
|
|
4640
|
+
prompt: "$ralph implement the clarified spec in src/implementation.ts",
|
|
4641
|
+
},
|
|
4642
|
+
{ cwd },
|
|
4643
|
+
);
|
|
4644
|
+
|
|
4645
|
+
const result = await dispatchCodexNativeHook(
|
|
4646
|
+
{
|
|
4647
|
+
hook_event_name: "PreToolUse",
|
|
4648
|
+
cwd,
|
|
4649
|
+
session_id: "sess-di-handoff",
|
|
4650
|
+
tool_name: "Edit",
|
|
4651
|
+
tool_use_id: "tool-di-post-handoff-edit",
|
|
4652
|
+
tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
|
|
4653
|
+
},
|
|
4654
|
+
{ cwd },
|
|
4655
|
+
);
|
|
4656
|
+
|
|
4657
|
+
assert.equal(result.outputJson, null);
|
|
4658
|
+
const completed = JSON.parse(await readFile(join(sessionDir, "deep-interview-state.json"), "utf-8")) as { active?: boolean };
|
|
4659
|
+
assert.equal(completed.active, false);
|
|
4660
|
+
} finally {
|
|
4661
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4662
|
+
}
|
|
4663
|
+
});
|
|
4664
|
+
|
|
3892
4665
|
it("returns a destructive-command caution on PreToolUse for rm -rf dist", async () => {
|
|
3893
4666
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-danger-"));
|
|
3894
4667
|
try {
|
|
@@ -6581,6 +7354,7 @@ exit 0
|
|
|
6581
7354
|
active: true,
|
|
6582
7355
|
current_phase: "team-exec",
|
|
6583
7356
|
team_name: "review-team",
|
|
7357
|
+
session_id: "sess-stop-team",
|
|
6584
7358
|
});
|
|
6585
7359
|
await writeJson(join(stateDir, "team", "review-team", "phase.json"), {
|
|
6586
7360
|
current_phase: "team-verify",
|
|
@@ -7468,73 +8242,157 @@ exit 0
|
|
|
7468
8242
|
}
|
|
7469
8243
|
});
|
|
7470
8244
|
|
|
7471
|
-
it("
|
|
7472
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
8245
|
+
it("does not block Stop from canonical team state owned by another thread", async () => {
|
|
8246
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-canonical-other-thread-"));
|
|
7473
8247
|
try {
|
|
7474
8248
|
await initTeamState(
|
|
7475
|
-
"
|
|
7476
|
-
"
|
|
8249
|
+
"canonical-other-thread-team",
|
|
8250
|
+
"canonical other-thread stop fallback",
|
|
7477
8251
|
"executor",
|
|
7478
8252
|
1,
|
|
7479
8253
|
cwd,
|
|
7480
8254
|
undefined,
|
|
7481
|
-
{ ...process.env, OMX_SESSION_ID: "sess-stop-
|
|
7482
|
-
);
|
|
7483
|
-
await writeReleaseReadinessLeaderAttention(
|
|
7484
|
-
"release-ready-team",
|
|
7485
|
-
"sess-stop-release-ready",
|
|
7486
|
-
cwd,
|
|
7487
|
-
{ workRemaining: false },
|
|
7488
|
-
);
|
|
7489
|
-
await writeReleaseReadinessStateMarker(
|
|
7490
|
-
"sess-stop-release-ready",
|
|
7491
|
-
"release-ready-team",
|
|
7492
|
-
cwd,
|
|
8255
|
+
{ ...process.env, OMX_SESSION_ID: "sess-stop-team-canonical-thread" },
|
|
7493
8256
|
);
|
|
8257
|
+
const manifestPath = join(cwd, ".omx", "state", "team", "canonical-other-thread-team", "manifest.v2.json");
|
|
8258
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf-8")) as Record<string, unknown>;
|
|
8259
|
+
await writeJson(manifestPath, {
|
|
8260
|
+
...manifest,
|
|
8261
|
+
leader: {
|
|
8262
|
+
...(manifest.leader as Record<string, unknown> | undefined),
|
|
8263
|
+
thread_id: "thread-other",
|
|
8264
|
+
},
|
|
8265
|
+
});
|
|
7494
8266
|
|
|
7495
8267
|
const result = await dispatchCodexNativeHook(
|
|
7496
8268
|
{
|
|
7497
8269
|
hook_event_name: "Stop",
|
|
7498
8270
|
cwd,
|
|
7499
|
-
session_id: "sess-stop-
|
|
7500
|
-
thread_id: "thread-
|
|
7501
|
-
turn_id: "turn-stop-release-ready-1",
|
|
7502
|
-
mode: "release-readiness",
|
|
7503
|
-
last_assistant_message: "Launch-ready: yes",
|
|
8271
|
+
session_id: "sess-stop-team-canonical-thread",
|
|
8272
|
+
thread_id: "thread-current",
|
|
7504
8273
|
},
|
|
7505
8274
|
{ cwd },
|
|
7506
8275
|
);
|
|
7507
8276
|
|
|
7508
8277
|
assert.equal(result.omxEventName, "stop");
|
|
7509
|
-
assert.
|
|
7510
|
-
decision: "block",
|
|
7511
|
-
reason:
|
|
7512
|
-
'Stable final recommendation already reached with no active worker tasks. Emit exactly one concise final decision summary aligned to "Launch-ready: yes." with no filler or residual acknowledgements (for example "yes"), then stop.',
|
|
7513
|
-
stopReason: "release_readiness_auto_finalize",
|
|
7514
|
-
systemMessage:
|
|
7515
|
-
"OMX release-readiness detected a stable final recommendation with no active worker tasks; emit one concise final decision summary and finalize.",
|
|
7516
|
-
});
|
|
8278
|
+
assert.equal(result.outputJson, null);
|
|
7517
8279
|
} finally {
|
|
7518
8280
|
await rm(cwd, { recursive: true, force: true });
|
|
7519
8281
|
}
|
|
7520
8282
|
});
|
|
7521
8283
|
|
|
7522
|
-
it("
|
|
7523
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-
|
|
8284
|
+
it("blocks Stop from canonical team state owned by the current thread", async () => {
|
|
8285
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-canonical-current-thread-"));
|
|
7524
8286
|
try {
|
|
7525
8287
|
await initTeamState(
|
|
7526
|
-
"
|
|
7527
|
-
"
|
|
8288
|
+
"canonical-current-thread-team",
|
|
8289
|
+
"canonical current-thread stop fallback",
|
|
7528
8290
|
"executor",
|
|
7529
8291
|
1,
|
|
7530
8292
|
cwd,
|
|
7531
8293
|
undefined,
|
|
7532
|
-
{ ...process.env, OMX_SESSION_ID: "sess-stop-
|
|
8294
|
+
{ ...process.env, OMX_SESSION_ID: "sess-stop-team-canonical-current-thread" },
|
|
7533
8295
|
);
|
|
7534
|
-
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
|
|
8296
|
+
const manifestPath = join(cwd, ".omx", "state", "team", "canonical-current-thread-team", "manifest.v2.json");
|
|
8297
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf-8")) as Record<string, unknown>;
|
|
8298
|
+
await writeJson(manifestPath, {
|
|
8299
|
+
...manifest,
|
|
8300
|
+
leader: {
|
|
8301
|
+
...(manifest.leader as Record<string, unknown> | undefined),
|
|
8302
|
+
thread_id: "thread-current",
|
|
8303
|
+
},
|
|
8304
|
+
});
|
|
8305
|
+
|
|
8306
|
+
const result = await dispatchCodexNativeHook(
|
|
8307
|
+
{
|
|
8308
|
+
hook_event_name: "Stop",
|
|
8309
|
+
cwd,
|
|
8310
|
+
session_id: "sess-stop-team-canonical-current-thread",
|
|
8311
|
+
thread_id: "thread-current",
|
|
8312
|
+
},
|
|
8313
|
+
{ cwd },
|
|
8314
|
+
);
|
|
8315
|
+
|
|
8316
|
+
assert.equal(result.omxEventName, "stop");
|
|
8317
|
+
assert.deepEqual(result.outputJson, {
|
|
8318
|
+
decision: "block",
|
|
8319
|
+
reason:
|
|
8320
|
+
`OMX team pipeline is still active (canonical-current-thread-team) at phase team-exec; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
|
|
8321
|
+
stopReason: "team_team-exec",
|
|
8322
|
+
systemMessage: "OMX team pipeline is still active at phase team-exec.",
|
|
8323
|
+
});
|
|
8324
|
+
} finally {
|
|
8325
|
+
await rm(cwd, { recursive: true, force: true });
|
|
8326
|
+
}
|
|
8327
|
+
});
|
|
8328
|
+
|
|
8329
|
+
it("emits one concise final decision summary and auto-finalize guidance when release-readiness already has a stable final recommendation and no active worker tasks", async () => {
|
|
8330
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-release-readiness-finalize-"));
|
|
8331
|
+
try {
|
|
8332
|
+
await initTeamState(
|
|
8333
|
+
"release-ready-team",
|
|
8334
|
+
"release readiness finalize",
|
|
8335
|
+
"executor",
|
|
8336
|
+
1,
|
|
8337
|
+
cwd,
|
|
8338
|
+
undefined,
|
|
8339
|
+
{ ...process.env, OMX_SESSION_ID: "sess-stop-release-ready" },
|
|
8340
|
+
);
|
|
8341
|
+
await writeReleaseReadinessLeaderAttention(
|
|
8342
|
+
"release-ready-team",
|
|
8343
|
+
"sess-stop-release-ready",
|
|
8344
|
+
cwd,
|
|
8345
|
+
{ workRemaining: false },
|
|
8346
|
+
);
|
|
8347
|
+
await writeReleaseReadinessStateMarker(
|
|
8348
|
+
"sess-stop-release-ready",
|
|
8349
|
+
"release-ready-team",
|
|
8350
|
+
cwd,
|
|
8351
|
+
);
|
|
8352
|
+
|
|
8353
|
+
const result = await dispatchCodexNativeHook(
|
|
8354
|
+
{
|
|
8355
|
+
hook_event_name: "Stop",
|
|
8356
|
+
cwd,
|
|
8357
|
+
session_id: "sess-stop-release-ready",
|
|
8358
|
+
thread_id: "thread-stop-release-ready",
|
|
8359
|
+
turn_id: "turn-stop-release-ready-1",
|
|
8360
|
+
mode: "release-readiness",
|
|
8361
|
+
last_assistant_message: "Launch-ready: yes",
|
|
8362
|
+
},
|
|
8363
|
+
{ cwd },
|
|
8364
|
+
);
|
|
8365
|
+
|
|
8366
|
+
assert.equal(result.omxEventName, "stop");
|
|
8367
|
+
assert.deepEqual(result.outputJson, {
|
|
8368
|
+
decision: "block",
|
|
8369
|
+
reason:
|
|
8370
|
+
'Stable final recommendation already reached with no active worker tasks. Emit exactly one concise final decision summary aligned to "Launch-ready: yes." with no filler or residual acknowledgements (for example "yes"), then stop.',
|
|
8371
|
+
stopReason: "release_readiness_auto_finalize",
|
|
8372
|
+
systemMessage:
|
|
8373
|
+
"OMX release-readiness detected a stable final recommendation with no active worker tasks; emit one concise final decision summary and finalize.",
|
|
8374
|
+
});
|
|
8375
|
+
} finally {
|
|
8376
|
+
await rm(cwd, { recursive: true, force: true });
|
|
8377
|
+
}
|
|
8378
|
+
});
|
|
8379
|
+
|
|
8380
|
+
it("does not auto-finalize non-release team stops that happen to contain a stable recommendation summary", async () => {
|
|
8381
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-non-release-readiness-control-"));
|
|
8382
|
+
try {
|
|
8383
|
+
await initTeamState(
|
|
8384
|
+
"general-review-team",
|
|
8385
|
+
"general team stop control",
|
|
8386
|
+
"executor",
|
|
8387
|
+
1,
|
|
8388
|
+
cwd,
|
|
8389
|
+
undefined,
|
|
8390
|
+
{ ...process.env, OMX_SESSION_ID: "sess-stop-general-review" },
|
|
8391
|
+
);
|
|
8392
|
+
await writeReleaseReadinessLeaderAttention(
|
|
8393
|
+
"general-review-team",
|
|
8394
|
+
"sess-stop-general-review",
|
|
8395
|
+
cwd,
|
|
7538
8396
|
{ workRemaining: false },
|
|
7539
8397
|
);
|
|
7540
8398
|
|
|
@@ -7742,7 +8600,7 @@ exit 0
|
|
|
7742
8600
|
const sharedRoot = join(cwd, "shared-root");
|
|
7743
8601
|
const priorTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
7744
8602
|
try {
|
|
7745
|
-
process.env.OMX_TEAM_STATE_ROOT =
|
|
8603
|
+
process.env.OMX_TEAM_STATE_ROOT = sharedRoot;
|
|
7746
8604
|
await initTeamState(
|
|
7747
8605
|
"canonical-root-team",
|
|
7748
8606
|
"canonical stop root fallback",
|
|
@@ -7750,7 +8608,7 @@ exit 0
|
|
|
7750
8608
|
1,
|
|
7751
8609
|
cwd,
|
|
7752
8610
|
undefined,
|
|
7753
|
-
{ ...process.env, OMX_SESSION_ID: "sess-stop-team-root", OMX_TEAM_STATE_ROOT:
|
|
8611
|
+
{ ...process.env, OMX_SESSION_ID: "sess-stop-team-root", OMX_TEAM_STATE_ROOT: sharedRoot },
|
|
7754
8612
|
);
|
|
7755
8613
|
|
|
7756
8614
|
const result = await dispatchCodexNativeHook(
|
|
@@ -7815,9 +8673,10 @@ exit 0
|
|
|
7815
8673
|
|
|
7816
8674
|
it("returns Stop continuation output from canonical team state rooted via OMX_TEAM_STATE_ROOT", async () => {
|
|
7817
8675
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-env-root-"));
|
|
8676
|
+
const teamStateRoot = join(cwd, "shared-team-state");
|
|
7818
8677
|
const previousTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
7819
8678
|
try {
|
|
7820
|
-
process.env.OMX_TEAM_STATE_ROOT =
|
|
8679
|
+
process.env.OMX_TEAM_STATE_ROOT = teamStateRoot;
|
|
7821
8680
|
await initTeamState(
|
|
7822
8681
|
"env-root-team",
|
|
7823
8682
|
"env root stop fallback",
|
|
@@ -7828,7 +8687,7 @@ exit 0
|
|
|
7828
8687
|
{
|
|
7829
8688
|
...process.env,
|
|
7830
8689
|
OMX_SESSION_ID: "sess-stop-team-env-root",
|
|
7831
|
-
OMX_TEAM_STATE_ROOT:
|
|
8690
|
+
OMX_TEAM_STATE_ROOT: teamStateRoot,
|
|
7832
8691
|
},
|
|
7833
8692
|
);
|
|
7834
8693
|
|
|
@@ -11052,6 +11911,8 @@ exit 0
|
|
|
11052
11911
|
active: true,
|
|
11053
11912
|
current_phase: "team-exec",
|
|
11054
11913
|
team_name: "review-team",
|
|
11914
|
+
session_id: "sess-stop-team-refire",
|
|
11915
|
+
thread_id: "thread-stop-team-refire",
|
|
11055
11916
|
});
|
|
11056
11917
|
await writeJson(join(stateDir, "team", "review-team", "phase.json"), {
|
|
11057
11918
|
current_phase: "team-verify",
|
|
@@ -11291,6 +12152,250 @@ exit 0
|
|
|
11291
12152
|
}
|
|
11292
12153
|
});
|
|
11293
12154
|
|
|
12155
|
+
it("does not block Stop from root team state without team_name when no session is known", async () => {
|
|
12156
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-root-team-no-session-no-name-"));
|
|
12157
|
+
try {
|
|
12158
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
12159
|
+
await mkdir(stateDir, { recursive: true });
|
|
12160
|
+
await writeJson(join(stateDir, "team-state.json"), {
|
|
12161
|
+
active: true,
|
|
12162
|
+
mode: "team",
|
|
12163
|
+
current_phase: "starting",
|
|
12164
|
+
});
|
|
12165
|
+
|
|
12166
|
+
const result = await dispatchCodexNativeHook(
|
|
12167
|
+
{
|
|
12168
|
+
hook_event_name: "Stop",
|
|
12169
|
+
cwd,
|
|
12170
|
+
},
|
|
12171
|
+
{ cwd },
|
|
12172
|
+
);
|
|
12173
|
+
|
|
12174
|
+
assert.equal(result.omxEventName, "stop");
|
|
12175
|
+
assert.equal(result.outputJson, null);
|
|
12176
|
+
} finally {
|
|
12177
|
+
await rm(cwd, { recursive: true, force: true });
|
|
12178
|
+
}
|
|
12179
|
+
});
|
|
12180
|
+
|
|
12181
|
+
it("does not block Stop from root team state without team_name for a foreign session", async () => {
|
|
12182
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-root-team-foreign-no-name-"));
|
|
12183
|
+
try {
|
|
12184
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
12185
|
+
await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
|
|
12186
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
|
|
12187
|
+
await writeJson(join(stateDir, "team-state.json"), {
|
|
12188
|
+
active: true,
|
|
12189
|
+
mode: "team",
|
|
12190
|
+
current_phase: "starting",
|
|
12191
|
+
});
|
|
12192
|
+
|
|
12193
|
+
const result = await dispatchCodexNativeHook(
|
|
12194
|
+
{
|
|
12195
|
+
hook_event_name: "Stop",
|
|
12196
|
+
cwd,
|
|
12197
|
+
session_id: "sess-current",
|
|
12198
|
+
thread_id: "thread-current",
|
|
12199
|
+
},
|
|
12200
|
+
{ cwd },
|
|
12201
|
+
);
|
|
12202
|
+
|
|
12203
|
+
assert.equal(result.omxEventName, "stop");
|
|
12204
|
+
assert.equal(result.outputJson, null);
|
|
12205
|
+
} finally {
|
|
12206
|
+
await rm(cwd, { recursive: true, force: true });
|
|
12207
|
+
}
|
|
12208
|
+
});
|
|
12209
|
+
|
|
12210
|
+
it("does not block Stop from another thread's stale root team state when no scoped team state exists", async () => {
|
|
12211
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-root-team-thread-"));
|
|
12212
|
+
try {
|
|
12213
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
12214
|
+
await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
|
|
12215
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
|
|
12216
|
+
await writeJson(join(stateDir, "team-state.json"), {
|
|
12217
|
+
active: true,
|
|
12218
|
+
current_phase: "starting",
|
|
12219
|
+
team_name: "stale-root-thread-team",
|
|
12220
|
+
session_id: "sess-current",
|
|
12221
|
+
thread_id: "thread-other",
|
|
12222
|
+
});
|
|
12223
|
+
await writeJson(join(stateDir, "team", "stale-root-thread-team", "phase.json"), {
|
|
12224
|
+
current_phase: "team-exec",
|
|
12225
|
+
max_fix_attempts: 3,
|
|
12226
|
+
current_fix_attempt: 0,
|
|
12227
|
+
transitions: [],
|
|
12228
|
+
updated_at: new Date().toISOString(),
|
|
12229
|
+
});
|
|
12230
|
+
|
|
12231
|
+
const result = await dispatchCodexNativeHook(
|
|
12232
|
+
{
|
|
12233
|
+
hook_event_name: "Stop",
|
|
12234
|
+
cwd,
|
|
12235
|
+
session_id: "sess-current",
|
|
12236
|
+
thread_id: "thread-current",
|
|
12237
|
+
},
|
|
12238
|
+
{ cwd },
|
|
12239
|
+
);
|
|
12240
|
+
|
|
12241
|
+
assert.equal(result.omxEventName, "stop");
|
|
12242
|
+
assert.equal(result.outputJson, null);
|
|
12243
|
+
} finally {
|
|
12244
|
+
await rm(cwd, { recursive: true, force: true });
|
|
12245
|
+
}
|
|
12246
|
+
});
|
|
12247
|
+
|
|
12248
|
+
it("does not block Stop from root team state with matching session but missing thread ownership", async () => {
|
|
12249
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-root-team-missing-thread-"));
|
|
12250
|
+
try {
|
|
12251
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
12252
|
+
await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
|
|
12253
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
|
|
12254
|
+
await writeJson(join(stateDir, "team-state.json"), {
|
|
12255
|
+
active: true,
|
|
12256
|
+
current_phase: "starting",
|
|
12257
|
+
team_name: "root-missing-thread-team",
|
|
12258
|
+
session_id: "sess-current",
|
|
12259
|
+
});
|
|
12260
|
+
await writeJson(join(stateDir, "team", "root-missing-thread-team", "phase.json"), {
|
|
12261
|
+
current_phase: "team-exec",
|
|
12262
|
+
max_fix_attempts: 3,
|
|
12263
|
+
current_fix_attempt: 0,
|
|
12264
|
+
transitions: [],
|
|
12265
|
+
updated_at: new Date().toISOString(),
|
|
12266
|
+
});
|
|
12267
|
+
|
|
12268
|
+
const result = await dispatchCodexNativeHook(
|
|
12269
|
+
{
|
|
12270
|
+
hook_event_name: "Stop",
|
|
12271
|
+
cwd,
|
|
12272
|
+
session_id: "sess-current",
|
|
12273
|
+
thread_id: "thread-current",
|
|
12274
|
+
},
|
|
12275
|
+
{ cwd },
|
|
12276
|
+
);
|
|
12277
|
+
|
|
12278
|
+
assert.equal(result.omxEventName, "stop");
|
|
12279
|
+
assert.equal(result.outputJson, null);
|
|
12280
|
+
} finally {
|
|
12281
|
+
await rm(cwd, { recursive: true, force: true });
|
|
12282
|
+
}
|
|
12283
|
+
});
|
|
12284
|
+
|
|
12285
|
+
it("does not block Stop from root team state when canonical phase is missing", async () => {
|
|
12286
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-root-team-missing-phase-"));
|
|
12287
|
+
try {
|
|
12288
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
12289
|
+
await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
|
|
12290
|
+
await mkdir(join(stateDir, "team", "root-missing-phase-team"), { recursive: true });
|
|
12291
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
|
|
12292
|
+
await writeJson(join(stateDir, "team-state.json"), {
|
|
12293
|
+
active: true,
|
|
12294
|
+
current_phase: "starting",
|
|
12295
|
+
team_name: "root-missing-phase-team",
|
|
12296
|
+
session_id: "sess-current",
|
|
12297
|
+
thread_id: "thread-current",
|
|
12298
|
+
});
|
|
12299
|
+
|
|
12300
|
+
const result = await dispatchCodexNativeHook(
|
|
12301
|
+
{
|
|
12302
|
+
hook_event_name: "Stop",
|
|
12303
|
+
cwd,
|
|
12304
|
+
session_id: "sess-current",
|
|
12305
|
+
thread_id: "thread-current",
|
|
12306
|
+
},
|
|
12307
|
+
{ cwd },
|
|
12308
|
+
);
|
|
12309
|
+
|
|
12310
|
+
assert.equal(result.omxEventName, "stop");
|
|
12311
|
+
assert.equal(result.outputJson, null);
|
|
12312
|
+
} finally {
|
|
12313
|
+
await rm(cwd, { recursive: true, force: true });
|
|
12314
|
+
}
|
|
12315
|
+
});
|
|
12316
|
+
|
|
12317
|
+
it("does not block Stop from session-scoped team state owned by another thread", async () => {
|
|
12318
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-scoped-team-other-thread-"));
|
|
12319
|
+
try {
|
|
12320
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
12321
|
+
await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
|
|
12322
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
|
|
12323
|
+
await writeJson(join(stateDir, "sessions", "sess-current", "team-state.json"), {
|
|
12324
|
+
active: true,
|
|
12325
|
+
current_phase: "starting",
|
|
12326
|
+
team_name: "scoped-other-thread-team",
|
|
12327
|
+
session_id: "sess-current",
|
|
12328
|
+
thread_id: "thread-other",
|
|
12329
|
+
});
|
|
12330
|
+
await writeJson(join(stateDir, "team", "scoped-other-thread-team", "phase.json"), {
|
|
12331
|
+
current_phase: "team-exec",
|
|
12332
|
+
max_fix_attempts: 3,
|
|
12333
|
+
current_fix_attempt: 0,
|
|
12334
|
+
transitions: [],
|
|
12335
|
+
updated_at: new Date().toISOString(),
|
|
12336
|
+
});
|
|
12337
|
+
|
|
12338
|
+
const result = await dispatchCodexNativeHook(
|
|
12339
|
+
{
|
|
12340
|
+
hook_event_name: "Stop",
|
|
12341
|
+
cwd,
|
|
12342
|
+
session_id: "sess-current",
|
|
12343
|
+
thread_id: "thread-current",
|
|
12344
|
+
},
|
|
12345
|
+
{ cwd },
|
|
12346
|
+
);
|
|
12347
|
+
|
|
12348
|
+
assert.equal(result.omxEventName, "stop");
|
|
12349
|
+
assert.equal(result.outputJson, null);
|
|
12350
|
+
} finally {
|
|
12351
|
+
await rm(cwd, { recursive: true, force: true });
|
|
12352
|
+
}
|
|
12353
|
+
});
|
|
12354
|
+
|
|
12355
|
+
it("blocks Stop from session-scoped team state owned by the current session and thread", async () => {
|
|
12356
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-scoped-team-current-thread-"));
|
|
12357
|
+
try {
|
|
12358
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
12359
|
+
await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
|
|
12360
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
|
|
12361
|
+
await writeJson(join(stateDir, "sessions", "sess-current", "team-state.json"), {
|
|
12362
|
+
active: true,
|
|
12363
|
+
current_phase: "starting",
|
|
12364
|
+
team_name: "scoped-current-team",
|
|
12365
|
+
session_id: "sess-current",
|
|
12366
|
+
thread_id: "thread-current",
|
|
12367
|
+
});
|
|
12368
|
+
await writeJson(join(stateDir, "team", "scoped-current-team", "phase.json"), {
|
|
12369
|
+
current_phase: "team-exec",
|
|
12370
|
+
max_fix_attempts: 3,
|
|
12371
|
+
current_fix_attempt: 0,
|
|
12372
|
+
transitions: [],
|
|
12373
|
+
updated_at: new Date().toISOString(),
|
|
12374
|
+
});
|
|
12375
|
+
|
|
12376
|
+
const result = await dispatchCodexNativeHook(
|
|
12377
|
+
{
|
|
12378
|
+
hook_event_name: "Stop",
|
|
12379
|
+
cwd,
|
|
12380
|
+
session_id: "sess-current",
|
|
12381
|
+
thread_id: "thread-current",
|
|
12382
|
+
},
|
|
12383
|
+
{ cwd },
|
|
12384
|
+
);
|
|
12385
|
+
|
|
12386
|
+
assert.equal(result.omxEventName, "stop");
|
|
12387
|
+
assert.deepEqual(result.outputJson, {
|
|
12388
|
+
decision: "block",
|
|
12389
|
+
reason:
|
|
12390
|
+
`OMX team pipeline is still active (scoped-current-team) at phase team-exec; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
|
|
12391
|
+
stopReason: "team_team-exec",
|
|
12392
|
+
systemMessage: "OMX team pipeline is still active at phase team-exec.",
|
|
12393
|
+
});
|
|
12394
|
+
} finally {
|
|
12395
|
+
await rm(cwd, { recursive: true, force: true });
|
|
12396
|
+
}
|
|
12397
|
+
});
|
|
12398
|
+
|
|
11294
12399
|
it("does not block Stop from another session's stale root team state when no scoped team state exists", async () => {
|
|
11295
12400
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-root-team-"));
|
|
11296
12401
|
try {
|
|
@@ -11504,6 +12609,91 @@ describe("codex native hook triage integration", () => {
|
|
|
11504
12609
|
}
|
|
11505
12610
|
});
|
|
11506
12611
|
|
|
12612
|
+
|
|
12613
|
+
it("does not activate workflow state for native subagent prompts even when canonical id is the child session", async () => {
|
|
12614
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-subagent-keyword-"));
|
|
12615
|
+
const boxedRoot = await mkdtemp(join(tmpdir(), "omx-native-subagent-keyword-boxed-"));
|
|
12616
|
+
const originalOmxRoot = process.env.OMX_ROOT;
|
|
12617
|
+
const originalOmxStateRoot = process.env.OMX_STATE_ROOT;
|
|
12618
|
+
const originalTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
12619
|
+
try {
|
|
12620
|
+
process.env.OMX_ROOT = boxedRoot;
|
|
12621
|
+
delete process.env.OMX_STATE_ROOT;
|
|
12622
|
+
delete process.env.OMX_TEAM_STATE_ROOT;
|
|
12623
|
+
const boxedStateDir = getBaseStateDir(cwd);
|
|
12624
|
+
await mkdir(boxedStateDir, { recursive: true });
|
|
12625
|
+
await writeJson(join(boxedStateDir, "subagent-tracking.json"), {
|
|
12626
|
+
schemaVersion: 1,
|
|
12627
|
+
sessions: {
|
|
12628
|
+
"omx-parent-session": {
|
|
12629
|
+
session_id: "omx-parent-session",
|
|
12630
|
+
leader_thread_id: "parent-native-thread",
|
|
12631
|
+
updated_at: "2026-05-21T19:04:40.000Z",
|
|
12632
|
+
threads: {
|
|
12633
|
+
"parent-native-thread": {
|
|
12634
|
+
thread_id: "parent-native-thread",
|
|
12635
|
+
kind: "leader",
|
|
12636
|
+
first_seen_at: "2026-05-21T19:04:40.000Z",
|
|
12637
|
+
last_seen_at: "2026-05-21T19:04:40.000Z",
|
|
12638
|
+
turn_count: 1,
|
|
12639
|
+
},
|
|
12640
|
+
"child-native-session": {
|
|
12641
|
+
thread_id: "child-native-session",
|
|
12642
|
+
kind: "subagent",
|
|
12643
|
+
first_seen_at: "2026-05-21T19:04:41.000Z",
|
|
12644
|
+
last_seen_at: "2026-05-21T19:04:41.000Z",
|
|
12645
|
+
turn_count: 1,
|
|
12646
|
+
mode: "review",
|
|
12647
|
+
},
|
|
12648
|
+
},
|
|
12649
|
+
},
|
|
12650
|
+
},
|
|
12651
|
+
});
|
|
12652
|
+
|
|
12653
|
+
const result = await dispatchCodexNativeHook(
|
|
12654
|
+
{
|
|
12655
|
+
hook_event_name: "UserPromptSubmit",
|
|
12656
|
+
cwd,
|
|
12657
|
+
session_id: "child-native-session",
|
|
12658
|
+
thread_id: "child-native-session",
|
|
12659
|
+
turn_id: "turn-subagent-review",
|
|
12660
|
+
prompt: [
|
|
12661
|
+
"Read-only review only. Do not edit files. Do not inspect/mutate OMX state/hooks.",
|
|
12662
|
+
"Context: The user asked for $autopilot, and this subagent must only review the patch.",
|
|
12663
|
+
].join("\n\n"),
|
|
12664
|
+
},
|
|
12665
|
+
{ cwd },
|
|
12666
|
+
);
|
|
12667
|
+
|
|
12668
|
+
const additionalContext = String(
|
|
12669
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
12670
|
+
);
|
|
12671
|
+
assert.equal(additionalContext, "");
|
|
12672
|
+
assert.equal(
|
|
12673
|
+
existsSync(join(boxedStateDir, "sessions", "child-native-session", "skill-active-state.json")),
|
|
12674
|
+
false,
|
|
12675
|
+
);
|
|
12676
|
+
assert.equal(
|
|
12677
|
+
existsSync(join(boxedStateDir, "sessions", "child-native-session", "autopilot-state.json")),
|
|
12678
|
+
false,
|
|
12679
|
+
);
|
|
12680
|
+
assert.equal(
|
|
12681
|
+
existsSync(join(cwd, ".omx", "state", "subagent-tracking.json")),
|
|
12682
|
+
false,
|
|
12683
|
+
"subagent tracking must not leak into the source worktree when OMX_ROOT is boxed",
|
|
12684
|
+
);
|
|
12685
|
+
} finally {
|
|
12686
|
+
if (originalOmxRoot === undefined) delete process.env.OMX_ROOT;
|
|
12687
|
+
else process.env.OMX_ROOT = originalOmxRoot;
|
|
12688
|
+
if (originalOmxStateRoot === undefined) delete process.env.OMX_STATE_ROOT;
|
|
12689
|
+
else process.env.OMX_STATE_ROOT = originalOmxStateRoot;
|
|
12690
|
+
if (originalTeamStateRoot === undefined) delete process.env.OMX_TEAM_STATE_ROOT;
|
|
12691
|
+
else process.env.OMX_TEAM_STATE_ROOT = originalTeamStateRoot;
|
|
12692
|
+
await rm(cwd, { recursive: true, force: true });
|
|
12693
|
+
await rm(boxedRoot, { recursive: true, force: true });
|
|
12694
|
+
}
|
|
12695
|
+
});
|
|
12696
|
+
|
|
11507
12697
|
it("does not inject triage advisory for autopilot keyword prompts", async () => {
|
|
11508
12698
|
const cwd = await mkdtemp(join(tmpdir(), "omx-triage-keyword-autopilot-"));
|
|
11509
12699
|
try {
|
|
@@ -11535,6 +12725,62 @@ describe("codex native hook triage integration", () => {
|
|
|
11535
12725
|
}
|
|
11536
12726
|
});
|
|
11537
12727
|
|
|
12728
|
+
it("makes autopilot keyword activation observable in state, HUD context, and prompt guidance", async () => {
|
|
12729
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-autopilot-observable-"));
|
|
12730
|
+
try {
|
|
12731
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
12732
|
+
await writeSessionStart(cwd, "sess-autopilot-observable");
|
|
12733
|
+
|
|
12734
|
+
const result = await dispatchCodexNativeHook(
|
|
12735
|
+
{
|
|
12736
|
+
hook_event_name: "UserPromptSubmit",
|
|
12737
|
+
cwd,
|
|
12738
|
+
session_id: "sess-autopilot-observable",
|
|
12739
|
+
thread_id: "thread-autopilot-observable",
|
|
12740
|
+
turn_id: "turn-autopilot-observable",
|
|
12741
|
+
prompt: "$autopilot implement issue #2430",
|
|
12742
|
+
},
|
|
12743
|
+
{ cwd },
|
|
12744
|
+
);
|
|
12745
|
+
|
|
12746
|
+
assert.equal(result.skillState?.skill, "autopilot");
|
|
12747
|
+
assert.equal(result.skillState?.phase, "deep-interview");
|
|
12748
|
+
assert.equal(result.skillState?.initialized_state_path, ".omx/state/sessions/sess-autopilot-observable/autopilot-state.json");
|
|
12749
|
+
|
|
12750
|
+
const additionalContext = String(
|
|
12751
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
12752
|
+
);
|
|
12753
|
+
assert.match(additionalContext, /detected workflow keyword "\$autopilot" -> autopilot/);
|
|
12754
|
+
assert.match(additionalContext, /\$deep-interview -> \$ralplan -> \$ultragoal \(\+ \$team if needed\) -> \$code-review -> \$ultraqa/);
|
|
12755
|
+
assert.match(additionalContext, /deep_interview_gate\.skip_reason/);
|
|
12756
|
+
assert.match(additionalContext, /Do not silently fall back to ordinary \$plan\/ralplan-only handling/);
|
|
12757
|
+
assert.match(additionalContext, /Codex goal-mode handoff guidance/);
|
|
12758
|
+
assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
|
|
12759
|
+
|
|
12760
|
+
const statePath = join(cwd, ".omx", "state", "sessions", "sess-autopilot-observable", "autopilot-state.json");
|
|
12761
|
+
const modeState = JSON.parse(await readFile(statePath, "utf-8")) as {
|
|
12762
|
+
active: boolean;
|
|
12763
|
+
current_phase: string;
|
|
12764
|
+
state?: { phase_cycle?: string[]; deep_interview_gate?: { status?: string; skip_reason?: string | null } };
|
|
12765
|
+
};
|
|
12766
|
+
assert.equal(modeState.active, true);
|
|
12767
|
+
assert.equal(modeState.current_phase, "deep-interview");
|
|
12768
|
+
assert.deepEqual(modeState.state?.phase_cycle, ["deep-interview", "ralplan", "ultragoal", "code-review", "ultraqa"]);
|
|
12769
|
+
assert.deepEqual(modeState.state?.deep_interview_gate, {
|
|
12770
|
+
status: "required",
|
|
12771
|
+
skip_reason: null,
|
|
12772
|
+
rationale: "Autopilot starts at the deep-interview gate by default; clear bounded tasks may skip only with an explicit persisted skip reason.",
|
|
12773
|
+
});
|
|
12774
|
+
|
|
12775
|
+
const hudState = await readAllState(cwd);
|
|
12776
|
+
assert.equal(hudState.autopilot?.active, true);
|
|
12777
|
+
assert.equal(hudState.autopilot?.current_phase, "deep-interview");
|
|
12778
|
+
assert.match(renderHud(hudState, "focused"), /autopilot:deep-interview/);
|
|
12779
|
+
} finally {
|
|
12780
|
+
await rm(cwd, { recursive: true, force: true });
|
|
12781
|
+
}
|
|
12782
|
+
});
|
|
12783
|
+
|
|
11538
12784
|
// ── Group 2: HEAVY injection ─────────────────────────────────────────────
|
|
11539
12785
|
|
|
11540
12786
|
it("injects HEAVY advisory and writes prompt-routing-state for a multi-step goal prompt", async () => {
|