oh-my-codex 0.18.13 → 0.18.14
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 +31 -11
- package/dist/agents/__tests__/native-config.test.js +25 -0
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/agents/native-config.d.ts.map +1 -1
- package/dist/agents/native-config.js +5 -1
- package/dist/agents/native-config.js.map +1 -1
- package/dist/autopilot/__tests__/fsm.test.js +7 -0
- package/dist/autopilot/__tests__/fsm.test.js.map +1 -1
- package/dist/autopilot/__tests__/planner-routing.test.d.ts +2 -0
- package/dist/autopilot/__tests__/planner-routing.test.d.ts.map +1 -0
- package/dist/autopilot/__tests__/planner-routing.test.js +74 -0
- package/dist/autopilot/__tests__/planner-routing.test.js.map +1 -0
- package/dist/autopilot/__tests__/ralplan-gate.test.js +315 -0
- package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
- package/dist/autopilot/completion-gate.js +1 -1
- package/dist/autopilot/completion-gate.js.map +1 -1
- package/dist/autopilot/fsm.d.ts +2 -2
- package/dist/autopilot/fsm.d.ts.map +1 -1
- package/dist/autopilot/fsm.js +3 -0
- package/dist/autopilot/fsm.js.map +1 -1
- package/dist/autopilot/planner-routing.d.ts +21 -0
- package/dist/autopilot/planner-routing.d.ts.map +1 -0
- package/dist/autopilot/planner-routing.js +59 -0
- package/dist/autopilot/planner-routing.js.map +1 -0
- package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
- package/dist/autopilot/ralplan-gate.js +30 -0
- package/dist/autopilot/ralplan-gate.js.map +1 -1
- package/dist/autoresearch/goal.d.ts.map +1 -1
- package/dist/autoresearch/goal.js +10 -1
- package/dist/autoresearch/goal.js.map +1 -1
- package/dist/cli/__tests__/autoresearch-goal.test.js +33 -0
- package/dist/cli/__tests__/autoresearch-goal.test.js.map +1 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js +30 -0
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-artifact-ownership.test.d.ts +2 -0
- package/dist/cli/__tests__/doctor-artifact-ownership.test.d.ts.map +1 -0
- package/dist/cli/__tests__/doctor-artifact-ownership.test.js +158 -0
- package/dist/cli/__tests__/doctor-artifact-ownership.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-spark-routing.test.js +13 -0
- package/dist/cli/__tests__/doctor-spark-routing.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +1 -0
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +5 -0
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/nested-help-routing.test.js +4 -0
- package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
- package/dist/cli/__tests__/package-bin-contract.test.js +7 -1
- package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
- package/dist/cli/__tests__/performance-goal.test.js +22 -0
- package/dist/cli/__tests__/performance-goal.test.js.map +1 -1
- package/dist/cli/__tests__/resume.test.js +96 -0
- package/dist/cli/__tests__/resume.test.js.map +1 -1
- package/dist/cli/__tests__/session-scoped-runtime.test.js +162 -0
- package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
- package/dist/cli/__tests__/session-search.test.js +36 -0
- package/dist/cli/__tests__/session-search.test.js.map +1 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +35 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +33 -0
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +17 -3
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +37 -0
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +1 -0
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/autoresearch-goal.d.ts.map +1 -1
- package/dist/cli/autoresearch-goal.js +2 -1
- package/dist/cli/autoresearch-goal.js.map +1 -1
- package/dist/cli/doctor.d.ts +22 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +263 -4
- 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 +65 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/performance-goal.d.ts.map +1 -1
- package/dist/cli/performance-goal.js +4 -2
- package/dist/cli/performance-goal.js.map +1 -1
- package/dist/cli/plugin-marketplace.d.ts +1 -0
- package/dist/cli/plugin-marketplace.d.ts.map +1 -1
- package/dist/cli/plugin-marketplace.js +67 -5
- package/dist/cli/plugin-marketplace.js.map +1 -1
- package/dist/cli/project-runtime-codex-homes.d.ts +2 -0
- package/dist/cli/project-runtime-codex-homes.d.ts.map +1 -1
- package/dist/cli/project-runtime-codex-homes.js +112 -5
- package/dist/cli/project-runtime-codex-homes.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +15 -6
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/ultragoal.d.ts.map +1 -1
- package/dist/cli/ultragoal.js +5 -1
- package/dist/cli/ultragoal.js.map +1 -1
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +3 -0
- package/dist/cli/update.js.map +1 -1
- package/dist/config/__tests__/models.test.js +21 -1
- package/dist/config/__tests__/models.test.js.map +1 -1
- package/dist/config/models.d.ts +5 -0
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +23 -0
- package/dist/config/models.js.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.d.ts +2 -0
- package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.js +10 -0
- package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +7 -0
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +46 -0
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +100 -0
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +75 -0
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +225 -0
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +75 -0
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +200 -0
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +187 -12
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +225 -0
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +0 -0
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +75 -0
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +4 -0
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +137 -2
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/__tests__/state.test.js +107 -0
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/__tests__/tmux.test.js +17 -0
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/reconcile.d.ts.map +1 -1
- package/dist/hud/reconcile.js +21 -7
- package/dist/hud/reconcile.js.map +1 -1
- package/dist/hud/render.d.ts.map +1 -1
- package/dist/hud/render.js +9 -0
- package/dist/hud/render.js.map +1 -1
- package/dist/hud/state.d.ts.map +1 -1
- package/dist/hud/state.js +43 -5
- package/dist/hud/state.js.map +1 -1
- package/dist/hud/types.d.ts +6 -0
- package/dist/hud/types.d.ts.map +1 -1
- package/dist/hud/types.js.map +1 -1
- package/dist/modes/__tests__/base-autopilot-gates.test.js +48 -0
- package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -1
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +1 -0
- package/dist/modes/base.js.map +1 -1
- package/dist/performance-goal/artifacts.d.ts.map +1 -1
- package/dist/performance-goal/artifacts.js +10 -1
- package/dist/performance-goal/artifacts.js.map +1 -1
- package/dist/ralplan/__tests__/consensus-gate.test.js +112 -0
- package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
- package/dist/ralplan/consensus-gate.d.ts +23 -0
- package/dist/ralplan/consensus-gate.d.ts.map +1 -1
- package/dist/ralplan/consensus-gate.js +133 -21
- package/dist/ralplan/consensus-gate.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +831 -2
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +486 -21
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.js +87 -8
- package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.js +1 -1
- package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
- package/dist/scripts/run-compiled-ci.d.ts +2 -0
- package/dist/scripts/run-compiled-ci.d.ts.map +1 -0
- package/dist/scripts/run-compiled-ci.js +77 -0
- package/dist/scripts/run-compiled-ci.js.map +1 -0
- package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
- package/dist/scripts/sync-plugin-mirror.js +1 -0
- package/dist/scripts/sync-plugin-mirror.js.map +1 -1
- package/dist/session-history/search.d.ts.map +1 -1
- package/dist/session-history/search.js +24 -9
- package/dist/session-history/search.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +107 -3
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +57 -2
- package/dist/state/operations.js.map +1 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js +22 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
- package/dist/team/__tests__/model-contract.test.js +57 -1
- package/dist/team/__tests__/model-contract.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +1468 -110
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +63 -0
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- package/dist/team/__tests__/state.test.js +24 -1
- package/dist/team/__tests__/state.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +512 -0
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/worker-bootstrap.test.js +42 -9
- package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
- package/dist/team/model-contract.d.ts +16 -0
- package/dist/team/model-contract.d.ts.map +1 -1
- package/dist/team/model-contract.js +41 -1
- package/dist/team/model-contract.js.map +1 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +116 -30
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +9 -1
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/state/types.d.ts +2 -0
- package/dist/team/state/types.d.ts.map +1 -1
- package/dist/team/state/types.js.map +1 -1
- package/dist/team/state.d.ts +3 -0
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +20 -0
- package/dist/team/state.js.map +1 -1
- package/dist/team/tmux-session.d.ts +38 -3
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +275 -36
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worker-bootstrap.d.ts.map +1 -1
- package/dist/team/worker-bootstrap.js +26 -1
- package/dist/team/worker-bootstrap.js.map +1 -1
- package/dist/ultragoal/__tests__/artifacts.test.js +238 -0
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/__tests__/docs-contract.test.js +3 -0
- package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
- package/dist/ultragoal/artifacts.d.ts +15 -0
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +167 -4
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/utils/__tests__/agents-md.test.js +39 -1
- package/dist/utils/__tests__/agents-md.test.js.map +1 -1
- package/dist/utils/__tests__/agents-model-table.test.js +28 -0
- package/dist/utils/__tests__/agents-model-table.test.js.map +1 -1
- package/dist/utils/agents-md.d.ts +4 -0
- package/dist/utils/agents-md.d.ts.map +1 -1
- package/dist/utils/agents-md.js +25 -0
- package/dist/utils/agents-md.js.map +1 -1
- package/dist/utils/agents-model-table.d.ts +7 -3
- package/dist/utils/agents-model-table.d.ts.map +1 -1
- package/dist/utils/agents-model-table.js +16 -8
- package/dist/utils/agents-model-table.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +4 -0
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/package.json +2 -2
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +59 -8
- package/plugins/oh-my-codex/skills/autopilot/SKILL.md +9 -5
- package/plugins/oh-my-codex/skills/autoresearch-goal/SKILL.md +3 -0
- package/plugins/oh-my-codex/skills/performance-goal/SKILL.md +3 -0
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +22 -4
- package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +2 -2
- package/plugins/oh-my-codex/skills/ultrawork/references/agent-tiers.md +61 -0
- package/skills/autopilot/SKILL.md +9 -5
- package/skills/autoresearch-goal/SKILL.md +3 -0
- package/skills/ecomode/SKILL.md +1 -1
- package/skills/ecomode/references/agent-tiers.md +61 -0
- package/skills/performance-goal/SKILL.md +3 -0
- package/skills/ralplan/SKILL.md +1 -1
- package/skills/ultragoal/SKILL.md +22 -4
- package/skills/ultrawork/SKILL.md +2 -2
- package/skills/ultrawork/references/agent-tiers.md +61 -0
- package/src/scripts/__tests__/codex-native-hook.test.ts +975 -40
- package/src/scripts/codex-native-hook.ts +529 -22
- package/src/scripts/notify-hook/team-tmux-guard.ts +92 -8
- package/src/scripts/notify-hook/tmux-injection.ts +1 -1
- package/src/scripts/run-compiled-ci.ts +85 -0
- package/src/scripts/sync-plugin-mirror.ts +1 -0
- package/templates/AGENTS.md +6 -0
|
@@ -214,6 +214,31 @@ if [[ "$cmd" == "capture-pane" ]]; then
|
|
|
214
214
|
printf '%s\\n' ${captureText}
|
|
215
215
|
exit 0
|
|
216
216
|
fi
|
|
217
|
+
if [[ "$cmd" == "set-buffer" ]]; then
|
|
218
|
+
printf '%s' "\${@: -1}" > "${tmuxLogPath}.buffer"
|
|
219
|
+
exit 0
|
|
220
|
+
fi
|
|
221
|
+
if [[ "$cmd" == "show-buffer" ]]; then
|
|
222
|
+
if [[ -f "${tmuxLogPath}.buffer" ]]; then cat "${tmuxLogPath}.buffer"; fi
|
|
223
|
+
exit 0
|
|
224
|
+
fi
|
|
225
|
+
if [[ "$cmd" == "paste-buffer" ]]; then
|
|
226
|
+
target=""
|
|
227
|
+
while [[ "$#" -gt 0 ]]; do
|
|
228
|
+
case "$1" in
|
|
229
|
+
-t) target="$2"; shift 2 ;;
|
|
230
|
+
*) shift ;;
|
|
231
|
+
esac
|
|
232
|
+
done
|
|
233
|
+
if [[ -f "${tmuxLogPath}.buffer" ]]; then
|
|
234
|
+
echo "send-keys -t \${target} -l $(cat "${tmuxLogPath}.buffer")" >> "${tmuxLogPath}"
|
|
235
|
+
fi
|
|
236
|
+
exit 0
|
|
237
|
+
fi
|
|
238
|
+
if [[ "$cmd" == "delete-buffer" ]]; then
|
|
239
|
+
rm -f "${tmuxLogPath}.buffer"
|
|
240
|
+
exit 0
|
|
241
|
+
fi
|
|
217
242
|
if [[ "$cmd" == "send-keys" ]]; then
|
|
218
243
|
${sendDelaySeconds > 0 ? `sleep ${sendDelaySeconds}` : ""}
|
|
219
244
|
${removePathOnSend ? `rm -rf ${removePathOnSend}` : ""}
|
|
@@ -501,9 +526,9 @@ describe("codex native hook dispatch", () => {
|
|
|
501
526
|
hookSpecificOutput?: unknown;
|
|
502
527
|
};
|
|
503
528
|
|
|
504
|
-
assert.equal(output.continue,
|
|
529
|
+
assert.equal(output.continue, undefined);
|
|
505
530
|
assert.equal(output.decision, undefined);
|
|
506
|
-
assert.equal(output.stopReason,
|
|
531
|
+
assert.equal(output.stopReason, undefined);
|
|
507
532
|
assert.equal(output.hookSpecificOutput, undefined);
|
|
508
533
|
assert.match(
|
|
509
534
|
String(output.systemMessage ?? ""),
|
|
@@ -598,6 +623,159 @@ describe("codex native hook dispatch", () => {
|
|
|
598
623
|
);
|
|
599
624
|
});
|
|
600
625
|
|
|
626
|
+
it("emits no-op JSON stdout for PreToolUse non-Bash tools with null output", async () => {
|
|
627
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-pretool-nonbash-noop-"));
|
|
628
|
+
try {
|
|
629
|
+
const result = spawnSync(process.execPath, [nativeHookScriptPath()], {
|
|
630
|
+
cwd,
|
|
631
|
+
input: JSON.stringify({
|
|
632
|
+
hook_event_name: "PreToolUse",
|
|
633
|
+
cwd,
|
|
634
|
+
session_id: "sess-cli-pretool-nonbash-noop",
|
|
635
|
+
thread_id: "thread-cli-pretool-nonbash-noop",
|
|
636
|
+
turn_id: "turn-cli-pretool-nonbash-noop",
|
|
637
|
+
tool_name: "Read",
|
|
638
|
+
tool_input: { file_path: "package.json" },
|
|
639
|
+
}),
|
|
640
|
+
encoding: "utf-8",
|
|
641
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
645
|
+
assert.equal(result.stderr, "");
|
|
646
|
+
assert.deepEqual(parseSingleJsonStdout(result.stdout), {});
|
|
647
|
+
} finally {
|
|
648
|
+
await rm(cwd, { recursive: true, force: true });
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it("emits PreToolUse CLI advisory JSON with only systemMessage", async () => {
|
|
653
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-pretool-schema-safe-"));
|
|
654
|
+
try {
|
|
655
|
+
const output = parseSingleJsonStdout(runNativeHookCli({
|
|
656
|
+
hook_event_name: "PreToolUse",
|
|
657
|
+
cwd,
|
|
658
|
+
session_id: "sess-cli-pretool-schema-safe",
|
|
659
|
+
thread_id: "thread-cli-pretool-schema-safe",
|
|
660
|
+
turn_id: "turn-cli-pretool-schema-safe",
|
|
661
|
+
tool_name: "Bash",
|
|
662
|
+
tool_input: { command: "rm -rf dist" },
|
|
663
|
+
}, { cwd }));
|
|
664
|
+
|
|
665
|
+
assert.deepEqual(output, {
|
|
666
|
+
systemMessage:
|
|
667
|
+
"Destructive Bash command detected (`rm -rf dist`). Confirm the target and expected side effects before running it.",
|
|
668
|
+
});
|
|
669
|
+
} finally {
|
|
670
|
+
await rm(cwd, { recursive: true, force: true });
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it("emits PreToolUse CLI block JSON with only systemMessage", async () => {
|
|
675
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-pretool-block-schema-safe-"));
|
|
676
|
+
try {
|
|
677
|
+
const output = parseSingleJsonStdout(runNativeHookCli({
|
|
678
|
+
hook_event_name: "PreToolUse",
|
|
679
|
+
cwd,
|
|
680
|
+
session_id: "sess-cli-pretool-block-schema-safe",
|
|
681
|
+
thread_id: "thread-cli-pretool-block-schema-safe",
|
|
682
|
+
turn_id: "turn-cli-pretool-block-schema-safe",
|
|
683
|
+
tool_name: "Bash",
|
|
684
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 git commit -m "fix tests"' },
|
|
685
|
+
}, { cwd }));
|
|
686
|
+
|
|
687
|
+
assert.deepEqual(Object.keys(output).sort(), ["systemMessage"]);
|
|
688
|
+
assert.match(String(output.systemMessage ?? ""), /Lore protocol/);
|
|
689
|
+
assert.equal(output.decision, undefined);
|
|
690
|
+
assert.equal(output.stopReason, undefined);
|
|
691
|
+
assert.equal(output.hookSpecificOutput, undefined);
|
|
692
|
+
} finally {
|
|
693
|
+
await rm(cwd, { recursive: true, force: true });
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it("preserves ralplan PreToolUse planning guard as schema-safe CLI systemMessage", async () => {
|
|
698
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-ralplan-pretool-boundary-"));
|
|
699
|
+
const sessionId = "sess-cli-ralplan-pretool-boundary";
|
|
700
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
701
|
+
try {
|
|
702
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
703
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
704
|
+
active: true,
|
|
705
|
+
skill: "ralplan",
|
|
706
|
+
phase: "planning",
|
|
707
|
+
session_id: sessionId,
|
|
708
|
+
active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
|
|
709
|
+
});
|
|
710
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
711
|
+
active: true,
|
|
712
|
+
mode: "ralplan",
|
|
713
|
+
current_phase: "critic-review",
|
|
714
|
+
session_id: sessionId,
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
const output = parseSingleJsonStdout(runNativeHookCli({
|
|
718
|
+
hook_event_name: "PreToolUse",
|
|
719
|
+
cwd,
|
|
720
|
+
session_id: sessionId,
|
|
721
|
+
thread_id: "thread-cli-ralplan-pretool-boundary",
|
|
722
|
+
tool_name: "Edit",
|
|
723
|
+
tool_input: { file_path: "src/runtime.ts", old_string: "a", new_string: "b" },
|
|
724
|
+
}, { cwd }));
|
|
725
|
+
|
|
726
|
+
assert.deepEqual(Object.keys(output).sort(), ["systemMessage"]);
|
|
727
|
+
assert.match(String(output.systemMessage ?? ""), /Ralplan is active \(phase: critic-review\)/);
|
|
728
|
+
assert.match(String(output.systemMessage ?? ""), /implementation\/write tools are blocked/);
|
|
729
|
+
assert.match(String(output.systemMessage ?? ""), /Write only planning artifacts/);
|
|
730
|
+
assert.equal(output.decision, undefined);
|
|
731
|
+
assert.equal(output.reason, undefined);
|
|
732
|
+
assert.equal(output.hookSpecificOutput, undefined);
|
|
733
|
+
} finally {
|
|
734
|
+
await rm(cwd, { recursive: true, force: true });
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it("preserves deep-interview PreToolUse planning guard as schema-safe CLI systemMessage", async () => {
|
|
739
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-deep-interview-pretool-boundary-"));
|
|
740
|
+
const sessionId = "sess-cli-deep-interview-pretool-boundary";
|
|
741
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
742
|
+
try {
|
|
743
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
744
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
745
|
+
active: true,
|
|
746
|
+
skill: "deep-interview",
|
|
747
|
+
phase: "planning",
|
|
748
|
+
session_id: sessionId,
|
|
749
|
+
active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: sessionId }],
|
|
750
|
+
});
|
|
751
|
+
await writeJson(join(stateDir, "sessions", sessionId, "deep-interview-state.json"), {
|
|
752
|
+
active: true,
|
|
753
|
+
mode: "deep-interview",
|
|
754
|
+
current_phase: "intent-first",
|
|
755
|
+
session_id: sessionId,
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
const output = parseSingleJsonStdout(runNativeHookCli({
|
|
759
|
+
hook_event_name: "PreToolUse",
|
|
760
|
+
cwd,
|
|
761
|
+
session_id: sessionId,
|
|
762
|
+
thread_id: "thread-cli-deep-interview-pretool-boundary",
|
|
763
|
+
tool_name: "Write",
|
|
764
|
+
tool_input: { file_path: "src/runtime.ts", content: "export const changed = true;\n" },
|
|
765
|
+
}, { cwd }));
|
|
766
|
+
|
|
767
|
+
assert.deepEqual(Object.keys(output).sort(), ["systemMessage"]);
|
|
768
|
+
assert.match(String(output.systemMessage ?? ""), /Deep-interview is active \(phase: intent-first\)/);
|
|
769
|
+
assert.match(String(output.systemMessage ?? ""), /implementation\/write tools are blocked/);
|
|
770
|
+
assert.match(String(output.systemMessage ?? ""), /requirements\/spec mode/);
|
|
771
|
+
assert.equal(output.decision, undefined);
|
|
772
|
+
assert.equal(output.reason, undefined);
|
|
773
|
+
assert.equal(output.hookSpecificOutput, undefined);
|
|
774
|
+
} finally {
|
|
775
|
+
await rm(cwd, { recursive: true, force: true });
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
|
|
601
779
|
it("emits parseable no-op JSON stdout for inactive Stop CLI runs", async () => {
|
|
602
780
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-noop-json-"));
|
|
603
781
|
try {
|
|
@@ -617,6 +795,32 @@ describe("codex native hook dispatch", () => {
|
|
|
617
795
|
}
|
|
618
796
|
});
|
|
619
797
|
|
|
798
|
+
it("emits no-op JSON stdout for Stop payloads with no runtime output", async () => {
|
|
799
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-null-output-"));
|
|
800
|
+
try {
|
|
801
|
+
const result = spawnSync(process.execPath, [nativeHookScriptPath()], {
|
|
802
|
+
cwd,
|
|
803
|
+
input: JSON.stringify({
|
|
804
|
+
hook_event_name: "Stop",
|
|
805
|
+
cwd,
|
|
806
|
+
session_id: "sess-cli-stop-null-output",
|
|
807
|
+
thread_id: "thread-cli-stop-null-output",
|
|
808
|
+
turn_id: "turn-cli-stop-null-output",
|
|
809
|
+
stop_hook_active: true,
|
|
810
|
+
}),
|
|
811
|
+
encoding: "utf-8",
|
|
812
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
816
|
+
assert.equal(result.stderr, "");
|
|
817
|
+
assert.deepEqual(parseSingleJsonStdout(result.stdout), {});
|
|
818
|
+
assert.equal(existsSync(join(cwd, ".omx", "state")), false);
|
|
819
|
+
} finally {
|
|
820
|
+
await rm(cwd, { recursive: true, force: true });
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
|
|
620
824
|
it("returns empty JSON for oversized Stop stdin without parsing or creating inactive state", async () => {
|
|
621
825
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-"));
|
|
622
826
|
try {
|
|
@@ -760,6 +964,83 @@ describe("codex native hook dispatch", () => {
|
|
|
760
964
|
}
|
|
761
965
|
});
|
|
762
966
|
|
|
967
|
+
it("does not block Stop on stale session autopilot mirror when canonical skill state is inactive", async () => {
|
|
968
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stale-autopilot-mirror-"));
|
|
969
|
+
try {
|
|
970
|
+
const sessionId = "sess-stale-autopilot-mirror";
|
|
971
|
+
await writeJson(join(cwd, ".omx", "state", "session.json"), { session_id: sessionId, cwd });
|
|
972
|
+
await writeJson(join(cwd, ".omx", "state", "skill-active-state.json"), {
|
|
973
|
+
version: 1,
|
|
974
|
+
active: false,
|
|
975
|
+
skill: "",
|
|
976
|
+
phase: "cancelled",
|
|
977
|
+
active_skills: [],
|
|
978
|
+
});
|
|
979
|
+
await writeJson(join(cwd, ".omx", "state", "sessions", sessionId, "autopilot-state.json"), {
|
|
980
|
+
active: true,
|
|
981
|
+
mode: "autopilot",
|
|
982
|
+
current_phase: "ultragoal",
|
|
983
|
+
session_id: sessionId,
|
|
984
|
+
});
|
|
985
|
+
await writeJson(join(cwd, ".omx", "state", "sessions", sessionId, "skill-active-state.json"), {
|
|
986
|
+
version: 1,
|
|
987
|
+
active: true,
|
|
988
|
+
skill: "autopilot",
|
|
989
|
+
phase: "ultragoal",
|
|
990
|
+
session_id: sessionId,
|
|
991
|
+
active_skills: [{ skill: "autopilot", phase: "ultragoal", active: true, session_id: sessionId }],
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
const stdout = runNativeHookCli({
|
|
995
|
+
hook_event_name: "Stop",
|
|
996
|
+
cwd,
|
|
997
|
+
session_id: sessionId,
|
|
998
|
+
thread_id: "thread-stale-autopilot-mirror",
|
|
999
|
+
}, { cwd });
|
|
1000
|
+
|
|
1001
|
+
assert.deepEqual(parseSingleJsonStdout(stdout), {});
|
|
1002
|
+
} finally {
|
|
1003
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
it("includes blocking state source and canonical agreement in Stop diagnostics", async () => {
|
|
1008
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-diagnostic-"));
|
|
1009
|
+
try {
|
|
1010
|
+
const sessionId = "sess-autopilot-diagnostic";
|
|
1011
|
+
await writeJson(join(cwd, ".omx", "state", "session.json"), { session_id: sessionId, cwd });
|
|
1012
|
+
await writeJson(join(cwd, ".omx", "state", "skill-active-state.json"), {
|
|
1013
|
+
version: 1,
|
|
1014
|
+
active: true,
|
|
1015
|
+
skill: "autopilot",
|
|
1016
|
+
phase: "ultragoal",
|
|
1017
|
+
session_id: sessionId,
|
|
1018
|
+
active_skills: [{ skill: "autopilot", phase: "ultragoal", active: true, session_id: sessionId }],
|
|
1019
|
+
});
|
|
1020
|
+
await writeJson(join(cwd, ".omx", "state", "sessions", sessionId, "autopilot-state.json"), {
|
|
1021
|
+
active: true,
|
|
1022
|
+
mode: "autopilot",
|
|
1023
|
+
current_phase: "ultragoal",
|
|
1024
|
+
session_id: sessionId,
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
const output = parseSingleJsonStdout(runNativeHookCli({
|
|
1028
|
+
hook_event_name: "Stop",
|
|
1029
|
+
cwd,
|
|
1030
|
+
session_id: sessionId,
|
|
1031
|
+
thread_id: "thread-autopilot-diagnostic",
|
|
1032
|
+
}, { cwd })) as { decision?: string; reason?: string; statePath?: string; canonicalDisagreement?: string };
|
|
1033
|
+
|
|
1034
|
+
assert.equal(output.decision, "block");
|
|
1035
|
+
assert.match(String(output.reason ?? ""), /state: \.omx\/state\/sessions\/sess-autopilot-diagnostic\/autopilot-state\.json/);
|
|
1036
|
+
assert.match(String(output.reason ?? ""), /canonical: canonical_agrees/);
|
|
1037
|
+
assert.equal(output.statePath, ".omx/state/sessions/sess-autopilot-diagnostic/autopilot-state.json");
|
|
1038
|
+
assert.equal(output.canonicalDisagreement, "canonical_agrees");
|
|
1039
|
+
} finally {
|
|
1040
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
|
|
763
1044
|
it("emits exactly one parseable JSON object for active Stop CLI continuation", async () => {
|
|
764
1045
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-json-"));
|
|
765
1046
|
try {
|
|
@@ -2973,6 +3254,64 @@ standardMaxRounds = 15
|
|
|
2973
3254
|
}
|
|
2974
3255
|
});
|
|
2975
3256
|
|
|
3257
|
+
it("nudges next-goal prompts when a completed Codex goal cleanup step remains", async () => {
|
|
3258
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-completed-goal-prompt-"));
|
|
3259
|
+
try {
|
|
3260
|
+
await writeJson(join(cwd, ".omx", "goals", "performance", "latency", "state.json"), {
|
|
3261
|
+
version: 1,
|
|
3262
|
+
workflow: "performance-goal",
|
|
3263
|
+
slug: "latency",
|
|
3264
|
+
objective: "Reduce latency",
|
|
3265
|
+
status: "complete",
|
|
3266
|
+
completedAt: "2026-05-20T00:00:00.000Z",
|
|
3267
|
+
});
|
|
3268
|
+
|
|
3269
|
+
const result = await dispatchCodexNativeHook({
|
|
3270
|
+
hook_event_name: "UserPromptSubmit",
|
|
3271
|
+
cwd,
|
|
3272
|
+
session_id: "sess-completed-goal-prompt",
|
|
3273
|
+
thread_id: "thread-completed-goal-prompt",
|
|
3274
|
+
prompt: "Start the next performance goal now",
|
|
3275
|
+
}, { cwd });
|
|
3276
|
+
|
|
3277
|
+
const output = JSON.stringify(result.outputJson);
|
|
3278
|
+
assert.match(output, /run \/goal clear/);
|
|
3279
|
+
assert.match(output, /before calling create_goal/);
|
|
3280
|
+
assert.match(output, /hooks only nudge and must not mutate Codex goal state/);
|
|
3281
|
+
assert.doesNotMatch(output, /cleared Codex goal state/i);
|
|
3282
|
+
} finally {
|
|
3283
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3284
|
+
}
|
|
3285
|
+
});
|
|
3286
|
+
|
|
3287
|
+
it("blocks Stop when a final answer starts another goal over completed state", async () => {
|
|
3288
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-completed-goal-stop-"));
|
|
3289
|
+
try {
|
|
3290
|
+
await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
|
|
3291
|
+
version: 1,
|
|
3292
|
+
aggregateCompletion: { status: "complete", completedAt: "2026-05-20T00:00:00.000Z" },
|
|
3293
|
+
goals: [{ id: "G001-done", status: "complete", objective: "Done" }],
|
|
3294
|
+
});
|
|
3295
|
+
|
|
3296
|
+
const result = await dispatchCodexNativeHook({
|
|
3297
|
+
hook_event_name: "Stop",
|
|
3298
|
+
cwd,
|
|
3299
|
+
session_id: "sess-completed-goal-stop",
|
|
3300
|
+
thread_id: "thread-completed-goal-stop",
|
|
3301
|
+
last_assistant_message: "Starting another ultragoal now; create_goal payload follows.",
|
|
3302
|
+
}, { cwd });
|
|
3303
|
+
|
|
3304
|
+
const output = JSON.stringify(result.outputJson);
|
|
3305
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
3306
|
+
assert.equal(result.outputJson?.stopReason, "completed_codex_goal_cleanup_required");
|
|
3307
|
+
assert.match(output, /run \/goal clear/);
|
|
3308
|
+
assert.match(output, /before calling create_goal/);
|
|
3309
|
+
assert.match(output, /hooks only nudge and must not mutate Codex goal state/);
|
|
3310
|
+
} finally {
|
|
3311
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3312
|
+
}
|
|
3313
|
+
});
|
|
3314
|
+
|
|
2976
3315
|
it("blocks ultragoal Stop for concise generic goal completion claims", async () => {
|
|
2977
3316
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-generic-complete-stop-"));
|
|
2978
3317
|
try {
|
|
@@ -6381,61 +6720,199 @@ exit 0
|
|
|
6381
6720
|
}
|
|
6382
6721
|
});
|
|
6383
6722
|
|
|
6384
|
-
it("allows
|
|
6385
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-
|
|
6723
|
+
it("allows Ralplan read-only inspection and planning artifact writes while blocking implementation targets", async () => {
|
|
6724
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-ralplan-guard-"));
|
|
6386
6725
|
try {
|
|
6387
6726
|
const stateDir = join(cwd, ".omx", "state");
|
|
6388
|
-
const
|
|
6727
|
+
const sessionId = "sess-ralplan-guard";
|
|
6728
|
+
const sessionDir = join(stateDir, "sessions", sessionId);
|
|
6389
6729
|
await mkdir(sessionDir, { recursive: true });
|
|
6390
|
-
await writeJson(join(stateDir, "session.json"), { session_id:
|
|
6730
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, cwd });
|
|
6391
6731
|
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
6392
6732
|
version: 1,
|
|
6393
6733
|
active: true,
|
|
6394
|
-
skill: "
|
|
6395
|
-
phase: "
|
|
6396
|
-
session_id:
|
|
6397
|
-
|
|
6398
|
-
active_skills: [
|
|
6399
|
-
{
|
|
6400
|
-
skill: "autopilot",
|
|
6401
|
-
phase: "ralplan",
|
|
6402
|
-
active: true,
|
|
6403
|
-
session_id: "sess-autopilot-ralplan-artifact",
|
|
6404
|
-
thread_id: "thread-autopilot-ralplan-artifact",
|
|
6405
|
-
},
|
|
6406
|
-
],
|
|
6734
|
+
skill: "ralplan",
|
|
6735
|
+
phase: "planning",
|
|
6736
|
+
session_id: sessionId,
|
|
6737
|
+
active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
|
|
6407
6738
|
});
|
|
6408
|
-
await writeJson(join(sessionDir, "
|
|
6739
|
+
await writeJson(join(sessionDir, "ralplan-state.json"), {
|
|
6409
6740
|
active: true,
|
|
6410
|
-
mode: "
|
|
6411
|
-
current_phase: "
|
|
6412
|
-
session_id:
|
|
6413
|
-
thread_id: "thread-autopilot-ralplan-artifact",
|
|
6741
|
+
mode: "ralplan",
|
|
6742
|
+
current_phase: "planning",
|
|
6743
|
+
session_id: sessionId,
|
|
6414
6744
|
});
|
|
6415
6745
|
|
|
6416
|
-
const
|
|
6746
|
+
const preToolUse = async (tool_name: string, tool_use_id: string, tool_input: Record<string, unknown>) => dispatchCodexNativeHook(
|
|
6417
6747
|
{
|
|
6418
6748
|
hook_event_name: "PreToolUse",
|
|
6419
6749
|
cwd,
|
|
6420
|
-
session_id:
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
tool_input: { file_path: ".omx/plans/prd-omx-y7a.md", content: "# Plan" },
|
|
6750
|
+
session_id: sessionId,
|
|
6751
|
+
tool_name,
|
|
6752
|
+
tool_use_id,
|
|
6753
|
+
tool_input,
|
|
6425
6754
|
},
|
|
6426
6755
|
{ cwd },
|
|
6427
6756
|
);
|
|
6428
|
-
assert.equal(allowedPlanWrite.outputJson, null);
|
|
6429
6757
|
|
|
6430
|
-
const
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6758
|
+
const readOnlyCommands = [
|
|
6759
|
+
"git status --short",
|
|
6760
|
+
"ls -1 .omx/plans",
|
|
6761
|
+
"find .omx/plans -type f -name '*.md'",
|
|
6762
|
+
"grep -RIn \"Scholastic\" doc tests .omx/plans .omx/context",
|
|
6763
|
+
"rg \"Scholastic\" doc tests .omx/plans .omx/context",
|
|
6764
|
+
"sed -n '1,80p' .omx/plans/demo.md",
|
|
6765
|
+
"cat .omx/plans/demo.md",
|
|
6766
|
+
"git status --short; ls -1 .omx/plans; grep -RIn \"Scholastic\" doc tests .omx/plans .omx/context",
|
|
6767
|
+
];
|
|
6768
|
+
for (const [index, command] of readOnlyCommands.entries()) {
|
|
6769
|
+
const result = await preToolUse("Bash", `tool-ralplan-read-${index}`, { command });
|
|
6770
|
+
assert.equal(result.outputJson, null, `read-only command should be allowed: ${command}`);
|
|
6771
|
+
}
|
|
6772
|
+
|
|
6773
|
+
for (const path of [
|
|
6774
|
+
".omx/context/findings.md",
|
|
6775
|
+
".omx/plans/issue-2863.md",
|
|
6776
|
+
".omx/specs/issue-2863.md",
|
|
6777
|
+
".omx/state/required-planning-state.json",
|
|
6778
|
+
]) {
|
|
6779
|
+
const writeResult = await preToolUse("Write", `tool-ralplan-write-${path}`, { file_path: path, content: "ok" });
|
|
6780
|
+
assert.equal(writeResult.outputJson, null, `Write should be allowed for ${path}`);
|
|
6781
|
+
}
|
|
6782
|
+
|
|
6783
|
+
const allowedPatchAdd = await preToolUse("apply_patch", "tool-ralplan-patch-add", {
|
|
6784
|
+
input: "*** Begin Patch\n*** Add File: .omx/plans/issue-2863.md\n+# Plan\n*** End Patch\n",
|
|
6785
|
+
});
|
|
6786
|
+
assert.equal(allowedPatchAdd.outputJson, null);
|
|
6787
|
+
|
|
6788
|
+
const allowedPatchUpdate = await preToolUse("ApplyPatch", "tool-ralplan-patch-update", {
|
|
6789
|
+
input: "*** Begin Patch\n*** Update File: .omx/plans/issue-2863.md\n@@\n-old\n+new\n*** End Patch\n",
|
|
6790
|
+
});
|
|
6791
|
+
assert.equal(allowedPatchUpdate.outputJson, null);
|
|
6792
|
+
|
|
6793
|
+
const allowedRedirect = await preToolUse("Bash", "tool-ralplan-redirect-allow", {
|
|
6794
|
+
command: "printf '\\nmore\\n' >> .omx/plans/issue-2863.md",
|
|
6795
|
+
});
|
|
6796
|
+
assert.equal(allowedRedirect.outputJson, null);
|
|
6797
|
+
|
|
6798
|
+
const allowedTee = await preToolUse("Bash", "tool-ralplan-tee-allow", {
|
|
6799
|
+
command: "printf '\\nmore\\n' | tee -a .omx/specs/issue-2863.md",
|
|
6800
|
+
});
|
|
6801
|
+
assert.equal(allowedTee.outputJson, null);
|
|
6802
|
+
|
|
6803
|
+
const blockedRedirect = await preToolUse("Bash", "tool-ralplan-redirect-block", {
|
|
6804
|
+
command: "printf 'bad' > src/implementation.ts",
|
|
6805
|
+
});
|
|
6806
|
+
assert.equal((blockedRedirect.outputJson as { decision?: string } | null)?.decision, "block");
|
|
6807
|
+
const redirectReason = String((blockedRedirect.outputJson as { reason?: string } | null)?.reason ?? "");
|
|
6808
|
+
assert.match(redirectReason, /Bash redirect write/);
|
|
6809
|
+
assert.match(redirectReason, /src\/implementation\.ts/);
|
|
6810
|
+
|
|
6811
|
+
const blockedEdit = await preToolUse("Edit", "tool-ralplan-edit-block", {
|
|
6812
|
+
file_path: "src/implementation.ts",
|
|
6813
|
+
old_string: "a",
|
|
6814
|
+
new_string: "b",
|
|
6815
|
+
});
|
|
6816
|
+
assert.equal((blockedEdit.outputJson as { decision?: string } | null)?.decision, "block");
|
|
6817
|
+
const editReason = String((blockedEdit.outputJson as { reason?: string } | null)?.reason ?? "");
|
|
6818
|
+
assert.match(editReason, /Edit path/);
|
|
6819
|
+
assert.match(editReason, /src\/implementation\.ts/);
|
|
6820
|
+
|
|
6821
|
+
const blockedMixedPatch = await preToolUse("apply_patch", "tool-ralplan-patch-mixed", {
|
|
6822
|
+
input: "*** Begin Patch\n*** Add File: .omx/plans/ok.md\n+ok\n*** Add File: src/leak.ts\n+leak\n*** End Patch\n",
|
|
6823
|
+
});
|
|
6824
|
+
assert.equal((blockedMixedPatch.outputJson as { decision?: string } | null)?.decision, "block");
|
|
6825
|
+
const mixedReason = String((blockedMixedPatch.outputJson as { reason?: string } | null)?.reason ?? "");
|
|
6826
|
+
assert.match(mixedReason, /apply_patch target/);
|
|
6827
|
+
assert.match(mixedReason, /src\/leak\.ts/);
|
|
6828
|
+
|
|
6829
|
+
const blockedUnparseablePatch = await preToolUse("apply_patch", "tool-ralplan-patch-unparseable", {
|
|
6830
|
+
input: "not a recognizable patch",
|
|
6831
|
+
});
|
|
6832
|
+
assert.equal((blockedUnparseablePatch.outputJson as { decision?: string } | null)?.decision, "block");
|
|
6833
|
+
assert.match(String((blockedUnparseablePatch.outputJson as { reason?: string } | null)?.reason ?? ""), /apply_patch target extraction failed/);
|
|
6834
|
+
|
|
6835
|
+
const blockedUnresolvedRedirect = await preToolUse("Bash", "tool-ralplan-redirect-unresolved", {
|
|
6836
|
+
command: "cat > \"$PLAN_PATH\" <<'EOF'\ncontent\nEOF",
|
|
6837
|
+
});
|
|
6838
|
+
assert.equal((blockedUnresolvedRedirect.outputJson as { decision?: string } | null)?.decision, "block");
|
|
6839
|
+
const unresolvedReason = String((blockedUnresolvedRedirect.outputJson as { reason?: string } | null)?.reason ?? "");
|
|
6840
|
+
assert.match(unresolvedReason, /unresolved Bash write target/);
|
|
6841
|
+
assert.match(unresolvedReason, /\$PLAN_PATH/);
|
|
6842
|
+
|
|
6843
|
+
const blockedTraversal = await preToolUse("Write", "tool-ralplan-traversal-block", {
|
|
6844
|
+
file_path: ".omx/plans/../../src/leak.ts",
|
|
6845
|
+
content: "bad",
|
|
6846
|
+
});
|
|
6847
|
+
assert.equal((blockedTraversal.outputJson as { decision?: string } | null)?.decision, "block");
|
|
6848
|
+
assert.match(String((blockedTraversal.outputJson as { reason?: string } | null)?.reason ?? ""), /\.omx\/plans\/\.\.\/\.\.\/src\/leak\.ts/);
|
|
6849
|
+
|
|
6850
|
+
const blockedAbsolute = await preToolUse("Write", "tool-ralplan-absolute-block", {
|
|
6851
|
+
file_path: "/tmp/leak.ts",
|
|
6852
|
+
content: "bad",
|
|
6853
|
+
});
|
|
6854
|
+
assert.equal((blockedAbsolute.outputJson as { decision?: string } | null)?.decision, "block");
|
|
6855
|
+
assert.match(String((blockedAbsolute.outputJson as { reason?: string } | null)?.reason ?? ""), /\/tmp\/leak\.ts/);
|
|
6856
|
+
} finally {
|
|
6857
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6858
|
+
}
|
|
6859
|
+
});
|
|
6860
|
+
|
|
6861
|
+
it("allows Autopilot ralplan planning artifacts while blocking implementation writes", async () => {
|
|
6862
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-autopilot-ralplan-artifact-"));
|
|
6863
|
+
try {
|
|
6864
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
6865
|
+
const sessionDir = join(stateDir, "sessions", "sess-autopilot-ralplan-artifact");
|
|
6866
|
+
await mkdir(sessionDir, { recursive: true });
|
|
6867
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-autopilot-ralplan-artifact", cwd });
|
|
6868
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
6869
|
+
version: 1,
|
|
6870
|
+
active: true,
|
|
6871
|
+
skill: "autopilot",
|
|
6872
|
+
phase: "ralplan",
|
|
6873
|
+
session_id: "sess-autopilot-ralplan-artifact",
|
|
6874
|
+
thread_id: "thread-autopilot-ralplan-artifact",
|
|
6875
|
+
active_skills: [
|
|
6876
|
+
{
|
|
6877
|
+
skill: "autopilot",
|
|
6878
|
+
phase: "ralplan",
|
|
6879
|
+
active: true,
|
|
6880
|
+
session_id: "sess-autopilot-ralplan-artifact",
|
|
6881
|
+
thread_id: "thread-autopilot-ralplan-artifact",
|
|
6882
|
+
},
|
|
6883
|
+
],
|
|
6884
|
+
});
|
|
6885
|
+
await writeJson(join(sessionDir, "autopilot-state.json"), {
|
|
6886
|
+
active: true,
|
|
6887
|
+
mode: "autopilot",
|
|
6888
|
+
current_phase: "ralplan",
|
|
6889
|
+
session_id: "sess-autopilot-ralplan-artifact",
|
|
6890
|
+
thread_id: "thread-autopilot-ralplan-artifact",
|
|
6891
|
+
});
|
|
6892
|
+
|
|
6893
|
+
const allowedPlanWrite = await dispatchCodexNativeHook(
|
|
6894
|
+
{
|
|
6895
|
+
hook_event_name: "PreToolUse",
|
|
6896
|
+
cwd,
|
|
6897
|
+
session_id: "sess-autopilot-ralplan-artifact",
|
|
6898
|
+
thread_id: "thread-autopilot-ralplan-artifact",
|
|
6899
|
+
tool_name: "Write",
|
|
6900
|
+
tool_use_id: "tool-autopilot-ralplan-plan-write",
|
|
6901
|
+
tool_input: { file_path: ".omx/plans/prd-omx-y7a.md", content: "# Plan" },
|
|
6902
|
+
},
|
|
6903
|
+
{ cwd },
|
|
6904
|
+
);
|
|
6905
|
+
assert.equal(allowedPlanWrite.outputJson, null);
|
|
6906
|
+
|
|
6907
|
+
const allowedSpecEdit = await dispatchCodexNativeHook(
|
|
6908
|
+
{
|
|
6909
|
+
hook_event_name: "PreToolUse",
|
|
6910
|
+
cwd,
|
|
6911
|
+
session_id: "sess-autopilot-ralplan-artifact",
|
|
6912
|
+
thread_id: "thread-autopilot-ralplan-artifact",
|
|
6913
|
+
tool_name: "Edit",
|
|
6914
|
+
tool_use_id: "tool-autopilot-ralplan-spec-edit",
|
|
6915
|
+
tool_input: { file_path: ".omx/specs/omx-y7a.md", old_string: "old", new_string: "new" },
|
|
6439
6916
|
},
|
|
6440
6917
|
{ cwd },
|
|
6441
6918
|
);
|
|
@@ -6461,6 +6938,263 @@ exit 0
|
|
|
6461
6938
|
}
|
|
6462
6939
|
});
|
|
6463
6940
|
|
|
6941
|
+
it("allows Ralplan Beads tracker metadata writes during planning", async () => {
|
|
6942
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-ralplan-beads-"));
|
|
6943
|
+
try {
|
|
6944
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
6945
|
+
const sessionDir = join(stateDir, "sessions", "sess-ralplan-beads");
|
|
6946
|
+
await mkdir(sessionDir, { recursive: true });
|
|
6947
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-ralplan-beads", cwd });
|
|
6948
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
6949
|
+
version: 1,
|
|
6950
|
+
active: true,
|
|
6951
|
+
skill: "ralplan",
|
|
6952
|
+
phase: "planning",
|
|
6953
|
+
session_id: "sess-ralplan-beads",
|
|
6954
|
+
thread_id: "thread-ralplan-beads",
|
|
6955
|
+
active_skills: [
|
|
6956
|
+
{
|
|
6957
|
+
skill: "ralplan",
|
|
6958
|
+
phase: "planning",
|
|
6959
|
+
active: true,
|
|
6960
|
+
session_id: "sess-ralplan-beads",
|
|
6961
|
+
thread_id: "thread-ralplan-beads",
|
|
6962
|
+
},
|
|
6963
|
+
],
|
|
6964
|
+
});
|
|
6965
|
+
await writeJson(join(sessionDir, "ralplan-state.json"), {
|
|
6966
|
+
active: true,
|
|
6967
|
+
mode: "ralplan",
|
|
6968
|
+
current_phase: "planning",
|
|
6969
|
+
session_id: "sess-ralplan-beads",
|
|
6970
|
+
thread_id: "thread-ralplan-beads",
|
|
6971
|
+
});
|
|
6972
|
+
|
|
6973
|
+
const allowedWrite = await dispatchCodexNativeHook(
|
|
6974
|
+
{
|
|
6975
|
+
hook_event_name: "PreToolUse",
|
|
6976
|
+
cwd,
|
|
6977
|
+
session_id: "sess-ralplan-beads",
|
|
6978
|
+
thread_id: "thread-ralplan-beads",
|
|
6979
|
+
tool_name: "Write",
|
|
6980
|
+
tool_use_id: "tool-ralplan-beads-write",
|
|
6981
|
+
tool_input: { file_path: ".beads/issues.db", content: "metadata" },
|
|
6982
|
+
},
|
|
6983
|
+
{ cwd },
|
|
6984
|
+
);
|
|
6985
|
+
assert.equal(allowedWrite.outputJson, null);
|
|
6986
|
+
|
|
6987
|
+
const allowedEdit = await dispatchCodexNativeHook(
|
|
6988
|
+
{
|
|
6989
|
+
hook_event_name: "PreToolUse",
|
|
6990
|
+
cwd,
|
|
6991
|
+
session_id: "sess-ralplan-beads",
|
|
6992
|
+
thread_id: "thread-ralplan-beads",
|
|
6993
|
+
tool_name: "Edit",
|
|
6994
|
+
tool_use_id: "tool-ralplan-beads-edit",
|
|
6995
|
+
tool_input: { file_path: ".beads/tasks/issue.json", old_string: "old", new_string: "new" },
|
|
6996
|
+
},
|
|
6997
|
+
{ cwd },
|
|
6998
|
+
);
|
|
6999
|
+
assert.equal(allowedEdit.outputJson, null);
|
|
7000
|
+
|
|
7001
|
+
const allowedPatch = await dispatchCodexNativeHook(
|
|
7002
|
+
{
|
|
7003
|
+
hook_event_name: "PreToolUse",
|
|
7004
|
+
cwd,
|
|
7005
|
+
session_id: "sess-ralplan-beads",
|
|
7006
|
+
thread_id: "thread-ralplan-beads",
|
|
7007
|
+
tool_name: "apply_patch",
|
|
7008
|
+
tool_use_id: "tool-ralplan-beads-apply-patch",
|
|
7009
|
+
tool_input: {
|
|
7010
|
+
input: "*** Begin Patch\n*** Add File: .beads/tasks/issue.json\n+{}\n*** End Patch\n",
|
|
7011
|
+
},
|
|
7012
|
+
},
|
|
7013
|
+
{ cwd },
|
|
7014
|
+
);
|
|
7015
|
+
assert.equal(allowedPatch.outputJson, null);
|
|
7016
|
+
|
|
7017
|
+
const allowedCommands = [
|
|
7018
|
+
"bd --db .beads/issues.db create 'Issue title'",
|
|
7019
|
+
"bd --db .beads/issues.db update OMX-1 --title 'Issue title'",
|
|
7020
|
+
"bd --db .beads/issues.db edit OMX-1 --title 'Issue title'",
|
|
7021
|
+
"bd --db .beads/issues.db comments add OMX-1 'evidence'",
|
|
7022
|
+
"bd --db .beads/issues.db close OMX-1",
|
|
7023
|
+
"bd --db .beads/issues.db reopen OMX-1",
|
|
7024
|
+
"bd --db .beads/issues.db status OMX-1",
|
|
7025
|
+
"bd --db .beads/issues.db dep add OMX-1 OMX-2",
|
|
7026
|
+
];
|
|
7027
|
+
|
|
7028
|
+
for (const [index, command] of allowedCommands.entries()) {
|
|
7029
|
+
const result = await dispatchCodexNativeHook(
|
|
7030
|
+
{
|
|
7031
|
+
hook_event_name: "PreToolUse",
|
|
7032
|
+
cwd,
|
|
7033
|
+
session_id: "sess-ralplan-beads",
|
|
7034
|
+
thread_id: "thread-ralplan-beads",
|
|
7035
|
+
tool_name: "Bash",
|
|
7036
|
+
tool_use_id: `tool-ralplan-beads-bd-${index}`,
|
|
7037
|
+
tool_input: { command },
|
|
7038
|
+
},
|
|
7039
|
+
{ cwd },
|
|
7040
|
+
);
|
|
7041
|
+
assert.equal(result.outputJson, null, `expected Beads metadata command to be allowed: ${command}`);
|
|
7042
|
+
}
|
|
7043
|
+
} finally {
|
|
7044
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7045
|
+
}
|
|
7046
|
+
});
|
|
7047
|
+
|
|
7048
|
+
it("blocks unsafe Beads tracker targets and mixed implementation writes during Autopilot ralplan planning", async () => {
|
|
7049
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-autopilot-beads-block-"));
|
|
7050
|
+
try {
|
|
7051
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7052
|
+
const sessionDir = join(stateDir, "sessions", "sess-autopilot-beads-block");
|
|
7053
|
+
await mkdir(sessionDir, { recursive: true });
|
|
7054
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-autopilot-beads-block", cwd });
|
|
7055
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
7056
|
+
version: 1,
|
|
7057
|
+
active: true,
|
|
7058
|
+
skill: "autopilot",
|
|
7059
|
+
phase: "ralplan",
|
|
7060
|
+
session_id: "sess-autopilot-beads-block",
|
|
7061
|
+
thread_id: "thread-autopilot-beads-block",
|
|
7062
|
+
active_skills: [
|
|
7063
|
+
{
|
|
7064
|
+
skill: "autopilot",
|
|
7065
|
+
phase: "ralplan",
|
|
7066
|
+
active: true,
|
|
7067
|
+
session_id: "sess-autopilot-beads-block",
|
|
7068
|
+
thread_id: "thread-autopilot-beads-block",
|
|
7069
|
+
},
|
|
7070
|
+
],
|
|
7071
|
+
});
|
|
7072
|
+
await writeJson(join(sessionDir, "autopilot-state.json"), {
|
|
7073
|
+
active: true,
|
|
7074
|
+
mode: "autopilot",
|
|
7075
|
+
current_phase: "ralplan",
|
|
7076
|
+
session_id: "sess-autopilot-beads-block",
|
|
7077
|
+
thread_id: "thread-autopilot-beads-block",
|
|
7078
|
+
});
|
|
7079
|
+
|
|
7080
|
+
const blockedCommands = [
|
|
7081
|
+
"bd --db /tmp/outside.db create 'Issue title'",
|
|
7082
|
+
"bd --db ../outside.db create 'Issue title'",
|
|
7083
|
+
"bd --db .beads create 'Issue title'",
|
|
7084
|
+
"bd --db $DB create 'Issue title'",
|
|
7085
|
+
"DB=.beads/issues.db bd --db $DB create 'Issue title'",
|
|
7086
|
+
"env bd --db /tmp/outside.db create 'Issue title'",
|
|
7087
|
+
"command bd --db /tmp/outside.db create 'Issue title'",
|
|
7088
|
+
"bd --db .beads/issues.db export",
|
|
7089
|
+
"bd --db .beads/issues.db create 'Issue title' > src/leak.ts",
|
|
7090
|
+
"bd --db .beads/issues.db create 'Issue title'; cat > src/leak.ts",
|
|
7091
|
+
];
|
|
7092
|
+
|
|
7093
|
+
for (const [index, command] of blockedCommands.entries()) {
|
|
7094
|
+
const result = await dispatchCodexNativeHook(
|
|
7095
|
+
{
|
|
7096
|
+
hook_event_name: "PreToolUse",
|
|
7097
|
+
cwd,
|
|
7098
|
+
session_id: "sess-autopilot-beads-block",
|
|
7099
|
+
thread_id: "thread-autopilot-beads-block",
|
|
7100
|
+
tool_name: "Bash",
|
|
7101
|
+
tool_use_id: `tool-autopilot-beads-block-${index}`,
|
|
7102
|
+
tool_input: { command },
|
|
7103
|
+
},
|
|
7104
|
+
{ cwd },
|
|
7105
|
+
);
|
|
7106
|
+
assert.equal(
|
|
7107
|
+
(result.outputJson as { decision?: string } | null)?.decision,
|
|
7108
|
+
"block",
|
|
7109
|
+
`expected unsafe Beads command to be blocked: ${command}`,
|
|
7110
|
+
);
|
|
7111
|
+
}
|
|
7112
|
+
|
|
7113
|
+
const mentionWithImplementationWrite = await dispatchCodexNativeHook(
|
|
7114
|
+
{
|
|
7115
|
+
hook_event_name: "PreToolUse",
|
|
7116
|
+
cwd,
|
|
7117
|
+
session_id: "sess-autopilot-beads-block",
|
|
7118
|
+
thread_id: "thread-autopilot-beads-block",
|
|
7119
|
+
tool_name: "Bash",
|
|
7120
|
+
tool_use_id: "tool-autopilot-beads-mention-write",
|
|
7121
|
+
tool_input: { command: "echo bd --db .beads/issues.db create > src/leak.ts" },
|
|
7122
|
+
},
|
|
7123
|
+
{ cwd },
|
|
7124
|
+
);
|
|
7125
|
+
assert.equal((mentionWithImplementationWrite.outputJson as { decision?: string } | null)?.decision, "block");
|
|
7126
|
+
assert.match(String((mentionWithImplementationWrite.outputJson as { reason?: string } | null)?.reason ?? ""), /src\/leak\.ts/);
|
|
7127
|
+
|
|
7128
|
+
const blockedImplementationEdit = await dispatchCodexNativeHook(
|
|
7129
|
+
{
|
|
7130
|
+
hook_event_name: "PreToolUse",
|
|
7131
|
+
cwd,
|
|
7132
|
+
session_id: "sess-autopilot-beads-block",
|
|
7133
|
+
thread_id: "thread-autopilot-beads-block",
|
|
7134
|
+
tool_name: "Edit",
|
|
7135
|
+
tool_use_id: "tool-autopilot-beads-src-edit",
|
|
7136
|
+
tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
|
|
7137
|
+
},
|
|
7138
|
+
{ cwd },
|
|
7139
|
+
);
|
|
7140
|
+
assert.equal((blockedImplementationEdit.outputJson as { decision?: string } | null)?.decision, "block");
|
|
7141
|
+
} finally {
|
|
7142
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7143
|
+
}
|
|
7144
|
+
});
|
|
7145
|
+
|
|
7146
|
+
it("allows Autopilot rework implementation writes while ralplan remains guarded", async () => {
|
|
7147
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-autopilot-rework-"));
|
|
7148
|
+
try {
|
|
7149
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7150
|
+
const sessionDir = join(stateDir, "sessions", "sess-autopilot-rework");
|
|
7151
|
+
await mkdir(sessionDir, { recursive: true });
|
|
7152
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-autopilot-rework", cwd });
|
|
7153
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
7154
|
+
version: 1,
|
|
7155
|
+
active: true,
|
|
7156
|
+
skill: "autopilot",
|
|
7157
|
+
phase: "rework",
|
|
7158
|
+
session_id: "sess-autopilot-rework",
|
|
7159
|
+
thread_id: "thread-autopilot-rework",
|
|
7160
|
+
active_skills: [
|
|
7161
|
+
{
|
|
7162
|
+
skill: "autopilot",
|
|
7163
|
+
phase: "rework",
|
|
7164
|
+
active: true,
|
|
7165
|
+
session_id: "sess-autopilot-rework",
|
|
7166
|
+
thread_id: "thread-autopilot-rework",
|
|
7167
|
+
},
|
|
7168
|
+
],
|
|
7169
|
+
});
|
|
7170
|
+
await writeJson(join(sessionDir, "autopilot-state.json"), {
|
|
7171
|
+
active: true,
|
|
7172
|
+
mode: "autopilot",
|
|
7173
|
+
current_phase: "rework",
|
|
7174
|
+
review_cycle: 2,
|
|
7175
|
+
review_verdict: { clean: false, recommendation: "REQUEST_CHANGES", findings: ["src/implementation.ts"] },
|
|
7176
|
+
session_id: "sess-autopilot-rework",
|
|
7177
|
+
thread_id: "thread-autopilot-rework",
|
|
7178
|
+
});
|
|
7179
|
+
|
|
7180
|
+
const allowedImplementationEdit = await dispatchCodexNativeHook(
|
|
7181
|
+
{
|
|
7182
|
+
hook_event_name: "PreToolUse",
|
|
7183
|
+
cwd,
|
|
7184
|
+
session_id: "sess-autopilot-rework",
|
|
7185
|
+
thread_id: "thread-autopilot-rework",
|
|
7186
|
+
tool_name: "Edit",
|
|
7187
|
+
tool_use_id: "tool-autopilot-rework-src-edit",
|
|
7188
|
+
tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
|
|
7189
|
+
},
|
|
7190
|
+
{ cwd },
|
|
7191
|
+
);
|
|
7192
|
+
assert.equal(allowedImplementationEdit.outputJson, null);
|
|
7193
|
+
} finally {
|
|
7194
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7195
|
+
}
|
|
7196
|
+
});
|
|
7197
|
+
|
|
6464
7198
|
it("allows null-device fd redirects while deep-interview blocks real Bash writes", async () => {
|
|
6465
7199
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-null-redirect-"));
|
|
6466
7200
|
try {
|
|
@@ -6630,6 +7364,207 @@ exit 0
|
|
|
6630
7364
|
}
|
|
6631
7365
|
});
|
|
6632
7366
|
|
|
7367
|
+
it("records native subagent capacity exhaustion from spawn_agent PostToolUse output", async () => {
|
|
7368
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-capacity-record-"));
|
|
7369
|
+
try {
|
|
7370
|
+
const result = await dispatchCodexNativeHook(
|
|
7371
|
+
{
|
|
7372
|
+
hook_event_name: "PostToolUse",
|
|
7373
|
+
cwd,
|
|
7374
|
+
session_id: "sess-subagent-capacity-record",
|
|
7375
|
+
thread_id: "thread-subagent-capacity-record",
|
|
7376
|
+
turn_id: "turn-subagent-capacity-record",
|
|
7377
|
+
tool_name: "multi_agent_v1.spawn_agent",
|
|
7378
|
+
tool_response: {
|
|
7379
|
+
error: "collab spawn failed: agent thread limit reached",
|
|
7380
|
+
},
|
|
7381
|
+
},
|
|
7382
|
+
{ cwd },
|
|
7383
|
+
);
|
|
7384
|
+
|
|
7385
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
7386
|
+
assert.equal(result.outputJson, null);
|
|
7387
|
+
|
|
7388
|
+
const blocker = JSON.parse(
|
|
7389
|
+
await readFile(join(cwd, ".omx", "state", "native-subagent-capacity-blocker.json"), "utf-8"),
|
|
7390
|
+
) as Record<string, unknown>;
|
|
7391
|
+
assert.equal(blocker.reason, "agent_thread_limit_reached");
|
|
7392
|
+
assert.equal(blocker.session_id, "sess-subagent-capacity-record");
|
|
7393
|
+
assert.equal(blocker.thread_id, "thread-subagent-capacity-record");
|
|
7394
|
+
assert.equal(blocker.tool_name, "multi_agent_v1.spawn_agent");
|
|
7395
|
+
assert.match(String(blocker.error_summary), /agent thread limit reached/);
|
|
7396
|
+
assert.ok(Date.parse(String(blocker.expires_at)) > Date.parse(String(blocker.observed_at)));
|
|
7397
|
+
} finally {
|
|
7398
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7399
|
+
}
|
|
7400
|
+
});
|
|
7401
|
+
|
|
7402
|
+
it("blocks close_agent cleanup after recent native subagent capacity exhaustion", async () => {
|
|
7403
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-capacity-close-block-"));
|
|
7404
|
+
try {
|
|
7405
|
+
await dispatchCodexNativeHook(
|
|
7406
|
+
{
|
|
7407
|
+
hook_event_name: "PostToolUse",
|
|
7408
|
+
cwd,
|
|
7409
|
+
session_id: "sess-subagent-capacity-close-block",
|
|
7410
|
+
thread_id: "thread-subagent-capacity-close-block",
|
|
7411
|
+
turn_id: "turn-subagent-capacity-close-block",
|
|
7412
|
+
tool_name: "multi_agent_v1.spawn_agent",
|
|
7413
|
+
tool_response: "collab spawn failed: agent thread limit reached",
|
|
7414
|
+
},
|
|
7415
|
+
{ cwd },
|
|
7416
|
+
);
|
|
7417
|
+
|
|
7418
|
+
const result = await dispatchCodexNativeHook(
|
|
7419
|
+
{
|
|
7420
|
+
hook_event_name: "PreToolUse",
|
|
7421
|
+
cwd,
|
|
7422
|
+
session_id: "sess-subagent-capacity-close-block",
|
|
7423
|
+
thread_id: "thread-subagent-capacity-close-block",
|
|
7424
|
+
tool_name: "multi_agent_v1.close_agent",
|
|
7425
|
+
tool_input: { target: "019ecc36-stale" },
|
|
7426
|
+
},
|
|
7427
|
+
{ cwd },
|
|
7428
|
+
);
|
|
7429
|
+
|
|
7430
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
7431
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
7432
|
+
assert.match(JSON.stringify(result.outputJson), /agent thread limit reached/);
|
|
7433
|
+
assert.match(JSON.stringify(result.outputJson), /multi_agent_v1\.close_agent/);
|
|
7434
|
+
assert.match(JSON.stringify(result.outputJson), /multi_tool_use\.parallel/);
|
|
7435
|
+
assert.match(JSON.stringify(result.outputJson), /bounded capacity blocker/);
|
|
7436
|
+
} finally {
|
|
7437
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7438
|
+
}
|
|
7439
|
+
});
|
|
7440
|
+
|
|
7441
|
+
it("emits schema-safe PreToolUse CLI stdout for close_agent capacity blocks", async () => {
|
|
7442
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-subagent-capacity-close-block-"));
|
|
7443
|
+
try {
|
|
7444
|
+
parseSingleJsonStdout(runNativeHookCli({
|
|
7445
|
+
hook_event_name: "PostToolUse",
|
|
7446
|
+
cwd,
|
|
7447
|
+
session_id: "sess-cli-subagent-capacity-close-block",
|
|
7448
|
+
thread_id: "thread-cli-subagent-capacity-close-block",
|
|
7449
|
+
turn_id: "turn-cli-subagent-capacity-close-block",
|
|
7450
|
+
tool_name: "multi_agent_v1.spawn_agent",
|
|
7451
|
+
tool_response: "collab spawn failed: agent thread limit reached",
|
|
7452
|
+
}, { cwd }));
|
|
7453
|
+
|
|
7454
|
+
const output = parseSingleJsonStdout(runNativeHookCli({
|
|
7455
|
+
hook_event_name: "PreToolUse",
|
|
7456
|
+
cwd,
|
|
7457
|
+
session_id: "sess-cli-subagent-capacity-close-block",
|
|
7458
|
+
thread_id: "thread-cli-subagent-capacity-close-block",
|
|
7459
|
+
tool_name: "multi_agent_v1.close_agent",
|
|
7460
|
+
tool_input: { target: "019ecc36-stale" },
|
|
7461
|
+
}, { cwd }));
|
|
7462
|
+
|
|
7463
|
+
assert.deepEqual(Object.keys(output).sort(), ["systemMessage"]);
|
|
7464
|
+
assert.match(String(output.systemMessage ?? ""), /agent thread limit reached/);
|
|
7465
|
+
assert.match(String(output.systemMessage ?? ""), /Do not call multi_agent_v1\.close_agent/);
|
|
7466
|
+
assert.equal(output.decision, undefined);
|
|
7467
|
+
assert.equal(output.hookSpecificOutput, undefined);
|
|
7468
|
+
} finally {
|
|
7469
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7470
|
+
}
|
|
7471
|
+
});
|
|
7472
|
+
|
|
7473
|
+
it("blocks parallel close_agent cleanup after recent native subagent capacity exhaustion", async () => {
|
|
7474
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-capacity-parallel-close-block-"));
|
|
7475
|
+
try {
|
|
7476
|
+
await dispatchCodexNativeHook(
|
|
7477
|
+
{
|
|
7478
|
+
hook_event_name: "PostToolUse",
|
|
7479
|
+
cwd,
|
|
7480
|
+
session_id: "sess-subagent-capacity-parallel-close-block",
|
|
7481
|
+
thread_id: "thread-subagent-capacity-parallel-close-block",
|
|
7482
|
+
tool_name: "multi_agent_v1.spawn_agent",
|
|
7483
|
+
tool_response: "agent thread limit reached",
|
|
7484
|
+
},
|
|
7485
|
+
{ cwd },
|
|
7486
|
+
);
|
|
7487
|
+
|
|
7488
|
+
const result = await dispatchCodexNativeHook(
|
|
7489
|
+
{
|
|
7490
|
+
hook_event_name: "PreToolUse",
|
|
7491
|
+
cwd,
|
|
7492
|
+
session_id: "sess-subagent-capacity-parallel-close-block",
|
|
7493
|
+
thread_id: "thread-subagent-capacity-parallel-close-block",
|
|
7494
|
+
tool_name: "multi_tool_use.parallel",
|
|
7495
|
+
tool_input: {
|
|
7496
|
+
tool_uses: [
|
|
7497
|
+
{ recipient_name: "multi_agent_v1.close_agent", parameters: { target: "stale-1" } },
|
|
7498
|
+
{ recipient_name: "multi_agent_v1.close_agent", parameters: { target: "stale-2" } },
|
|
7499
|
+
],
|
|
7500
|
+
},
|
|
7501
|
+
},
|
|
7502
|
+
{ cwd },
|
|
7503
|
+
);
|
|
7504
|
+
|
|
7505
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
|
|
7506
|
+
assert.match(JSON.stringify(result.outputJson), /Do not call multi_agent_v1\.close_agent/);
|
|
7507
|
+
assert.match(JSON.stringify(result.outputJson), /do not batch close_agent through multi_tool_use\.parallel/);
|
|
7508
|
+
} finally {
|
|
7509
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7510
|
+
}
|
|
7511
|
+
});
|
|
7512
|
+
|
|
7513
|
+
it("does not block close_agent without a recent native capacity blocker", async () => {
|
|
7514
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-capacity-close-no-blocker-"));
|
|
7515
|
+
try {
|
|
7516
|
+
const result = await dispatchCodexNativeHook(
|
|
7517
|
+
{
|
|
7518
|
+
hook_event_name: "PreToolUse",
|
|
7519
|
+
cwd,
|
|
7520
|
+
session_id: "sess-subagent-capacity-close-no-blocker",
|
|
7521
|
+
thread_id: "thread-subagent-capacity-close-no-blocker",
|
|
7522
|
+
tool_name: "multi_agent_v1.close_agent",
|
|
7523
|
+
tool_input: { target: "completed-agent" },
|
|
7524
|
+
},
|
|
7525
|
+
{ cwd },
|
|
7526
|
+
);
|
|
7527
|
+
|
|
7528
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
7529
|
+
assert.equal(result.outputJson, null);
|
|
7530
|
+
} finally {
|
|
7531
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7532
|
+
}
|
|
7533
|
+
});
|
|
7534
|
+
|
|
7535
|
+
it("does not block close_agent when the native capacity blocker is expired", async () => {
|
|
7536
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-capacity-close-expired-"));
|
|
7537
|
+
try {
|
|
7538
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
7539
|
+
await mkdir(stateDir, { recursive: true });
|
|
7540
|
+
await writeJson(join(stateDir, "native-subagent-capacity-blocker.json"), {
|
|
7541
|
+
schema_version: 1,
|
|
7542
|
+
reason: "agent_thread_limit_reached",
|
|
7543
|
+
session_id: "sess-subagent-capacity-close-expired",
|
|
7544
|
+
error_summary: "agent thread limit reached",
|
|
7545
|
+
observed_at: "2026-06-20T00:00:00.000Z",
|
|
7546
|
+
expires_at: "2026-06-20T00:30:00.000Z",
|
|
7547
|
+
cwd,
|
|
7548
|
+
});
|
|
7549
|
+
|
|
7550
|
+
const result = await dispatchCodexNativeHook(
|
|
7551
|
+
{
|
|
7552
|
+
hook_event_name: "PreToolUse",
|
|
7553
|
+
cwd,
|
|
7554
|
+
session_id: "sess-subagent-capacity-close-expired",
|
|
7555
|
+
thread_id: "thread-subagent-capacity-close-expired",
|
|
7556
|
+
tool_name: "multi_agent_v1.close_agent",
|
|
7557
|
+
tool_input: { target: "completed-agent" },
|
|
7558
|
+
},
|
|
7559
|
+
{ cwd },
|
|
7560
|
+
);
|
|
7561
|
+
|
|
7562
|
+
assert.equal(result.outputJson, null);
|
|
7563
|
+
} finally {
|
|
7564
|
+
await rm(cwd, { recursive: true, force: true });
|
|
7565
|
+
}
|
|
7566
|
+
});
|
|
7567
|
+
|
|
6633
7568
|
it("warns on PreToolUse for vague sloppy fallback implementation framing", async () => {
|
|
6634
7569
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-slop-warn-"));
|
|
6635
7570
|
try {
|