oh-my-codex 0.18.12 → 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 +38 -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 +936 -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 +62 -18
- 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-invalid-config.test.js +35 -0
- package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -1
- 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 +59 -1
- 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 +313 -1
- 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-help.test.js +3 -2
- package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
- package/dist/cli/__tests__/session-search.test.js +100 -2
- 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 +39 -5
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/setup-prompts-overwrite.test.js +74 -0
- package/dist/cli/__tests__/setup-prompts-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-scope.test.js +45 -0
- package/dist/cli/__tests__/setup-scope.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 +6 -2
- 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 +272 -5
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +10 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +274 -11
- 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 +8 -0
- package/dist/cli/project-runtime-codex-homes.d.ts.map +1 -0
- package/dist/cli/project-runtime-codex-homes.js +134 -0
- package/dist/cli/project-runtime-codex-homes.js.map +1 -0
- package/dist/cli/session-search.d.ts.map +1 -1
- package/dist/cli/session-search.js +8 -1
- package/dist/cli/session-search.js.map +1 -1
- package/dist/cli/setup.d.ts +1 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +183 -10
- 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 +5 -0
- package/dist/cli/update.js.map +1 -1
- package/dist/config/__tests__/codex-hooks.test.js +38 -24
- package/dist/config/__tests__/codex-hooks.test.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/codex-hooks.d.ts +6 -0
- package/dist/config/codex-hooks.d.ts.map +1 -1
- package/dist/config/codex-hooks.js +34 -49
- package/dist/config/codex-hooks.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/pipeline/__tests__/orchestrator.test.js +125 -0
- package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +109 -0
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +7 -0
- package/dist/pipeline/orchestrator.js.map +1 -1
- package/dist/ralplan/__tests__/consensus-gate.test.js +552 -1
- package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
- package/dist/ralplan/consensus-gate.d.ts +25 -0
- package/dist/ralplan/consensus-gate.d.ts.map +1 -1
- package/dist/ralplan/consensus-gate.js +289 -75
- package/dist/ralplan/consensus-gate.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +1149 -47
- 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 +542 -33
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/codex-native-pre-post.d.ts +1 -0
- package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
- package/dist/scripts/codex-native-pre-post.js +130 -0
- package/dist/scripts/codex-native-pre-post.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/__tests__/search.test.js +166 -0
- package/dist/session-history/__tests__/search.test.js.map +1 -1
- package/dist/session-history/search.d.ts +7 -0
- package/dist/session-history/search.d.ts.map +1 -1
- package/dist/session-history/search.js +99 -25
- package/dist/session-history/search.js.map +1 -1
- package/dist/sidecar/__tests__/collector.test.js +60 -0
- package/dist/sidecar/__tests__/collector.test.js.map +1 -1
- package/dist/sidecar/collector.d.ts.map +1 -1
- package/dist/sidecar/collector.js +3 -6
- package/dist/sidecar/collector.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 +8 -2
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +71 -3
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.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 +1417 -119
- package/src/scripts/codex-native-hook.ts +597 -40
- package/src/scripts/codex-native-pre-post.ts +137 -0
- 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 {
|
|
@@ -1464,36 +1745,46 @@ describe("codex native hook dispatch", () => {
|
|
|
1464
1745
|
}
|
|
1465
1746
|
});
|
|
1466
1747
|
|
|
1467
|
-
it("
|
|
1468
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
1748
|
+
it("suppresses child-agent SessionStart and Stop before the canonical leader session is reconciled (#2831)", async () => {
|
|
1749
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-no-canonical-"));
|
|
1750
|
+
const originalCodexHome = process.env.CODEX_HOME;
|
|
1469
1751
|
try {
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1752
|
+
process.env.CODEX_HOME = join(cwd, "codex-home");
|
|
1753
|
+
await writeJson(join(process.env.CODEX_HOME, ".omx-config.json"), {
|
|
1754
|
+
notifications: {
|
|
1755
|
+
enabled: true,
|
|
1756
|
+
verbosity: "session",
|
|
1757
|
+
telegram: { enabled: true, botToken: "123:abc", chatId: "456" },
|
|
1758
|
+
},
|
|
1476
1759
|
});
|
|
1477
|
-
|
|
1478
|
-
const
|
|
1760
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
1761
|
+
const leaderNativeSessionId = "codex-leader-thread-no-canonical";
|
|
1762
|
+
const childNativeSessionId = "codex-child-thread-no-canonical";
|
|
1763
|
+
await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
|
|
1764
|
+
await writeFile(
|
|
1765
|
+
join(cwd, ".omx", "hooks", "record-lifecycle.mjs"),
|
|
1766
|
+
[
|
|
1767
|
+
"import { appendFileSync } from 'node:fs';",
|
|
1768
|
+
"export async function onHookEvent(event) {",
|
|
1769
|
+
" appendFileSync('hook-events.jsonl', `${JSON.stringify({ event: event.event })}\\n`);",
|
|
1770
|
+
"}",
|
|
1771
|
+
].join("\n"),
|
|
1772
|
+
);
|
|
1773
|
+
const transcriptPath = join(cwd, "no-canonical-subagent-rollout.jsonl");
|
|
1479
1774
|
await writeFile(
|
|
1480
1775
|
transcriptPath,
|
|
1481
1776
|
`${JSON.stringify({
|
|
1482
1777
|
type: "session_meta",
|
|
1483
1778
|
payload: {
|
|
1484
|
-
id:
|
|
1779
|
+
id: childNativeSessionId,
|
|
1485
1780
|
source: {
|
|
1486
1781
|
subagent: {
|
|
1487
1782
|
thread_spawn: {
|
|
1488
|
-
parent_thread_id:
|
|
1489
|
-
|
|
1490
|
-
agent_nickname: "Architect",
|
|
1491
|
-
agent_role: "architect",
|
|
1783
|
+
parent_thread_id: leaderNativeSessionId,
|
|
1784
|
+
agent_role: "explorer",
|
|
1492
1785
|
},
|
|
1493
1786
|
},
|
|
1494
1787
|
},
|
|
1495
|
-
agent_nickname: "Architect",
|
|
1496
|
-
agent_role: "architect",
|
|
1497
1788
|
},
|
|
1498
1789
|
})}\n`,
|
|
1499
1790
|
);
|
|
@@ -1502,12 +1793,23 @@ describe("codex native hook dispatch", () => {
|
|
|
1502
1793
|
{
|
|
1503
1794
|
hook_event_name: "SessionStart",
|
|
1504
1795
|
cwd,
|
|
1505
|
-
session_id:
|
|
1796
|
+
session_id: childNativeSessionId,
|
|
1506
1797
|
transcript_path: transcriptPath,
|
|
1507
1798
|
},
|
|
1508
1799
|
{ cwd, sessionOwnerPid: process.pid },
|
|
1509
1800
|
);
|
|
1510
1801
|
|
|
1802
|
+
assert.equal(
|
|
1803
|
+
existsSync(join(cwd, "hook-events.jsonl")),
|
|
1804
|
+
false,
|
|
1805
|
+
"child SessionStart must be suppressed even before the canonical leader session is reconciled",
|
|
1806
|
+
);
|
|
1807
|
+
assert.equal(
|
|
1808
|
+
existsSync(join(stateDir, "session.json")),
|
|
1809
|
+
false,
|
|
1810
|
+
"child SessionStart must not be promoted into a root/leader session",
|
|
1811
|
+
);
|
|
1812
|
+
|
|
1511
1813
|
const tracking = JSON.parse(
|
|
1512
1814
|
await readFile(join(stateDir, "subagent-tracking.json"), "utf-8"),
|
|
1513
1815
|
) as {
|
|
@@ -1516,27 +1818,156 @@ describe("codex native hook dispatch", () => {
|
|
|
1516
1818
|
threads?: Record<string, { kind?: string; mode?: string }>;
|
|
1517
1819
|
}>;
|
|
1518
1820
|
};
|
|
1519
|
-
assert.equal(tracking.sessions?.[
|
|
1520
|
-
assert.equal(tracking.sessions?.[
|
|
1521
|
-
|
|
1821
|
+
assert.equal(tracking.sessions?.[leaderNativeSessionId]?.leader_thread_id, leaderNativeSessionId);
|
|
1822
|
+
assert.equal(tracking.sessions?.[leaderNativeSessionId]?.threads?.[childNativeSessionId]?.kind, "subagent");
|
|
1823
|
+
|
|
1824
|
+
await dispatchCodexNativeHook(
|
|
1825
|
+
{
|
|
1826
|
+
hook_event_name: "Stop",
|
|
1827
|
+
cwd,
|
|
1828
|
+
session_id: childNativeSessionId,
|
|
1829
|
+
thread_id: childNativeSessionId,
|
|
1830
|
+
turn_id: "no-canonical-child-stop-turn",
|
|
1831
|
+
},
|
|
1832
|
+
{ cwd },
|
|
1833
|
+
);
|
|
1834
|
+
|
|
1835
|
+
assert.equal(
|
|
1836
|
+
existsSync(join(cwd, "hook-events.jsonl")),
|
|
1837
|
+
false,
|
|
1838
|
+
"child Stop must be suppressed when the start was recognized as subagent-scoped",
|
|
1839
|
+
);
|
|
1522
1840
|
} finally {
|
|
1841
|
+
if (originalCodexHome === undefined) {
|
|
1842
|
+
delete process.env.CODEX_HOME;
|
|
1843
|
+
} else {
|
|
1844
|
+
process.env.CODEX_HOME = originalCodexHome;
|
|
1845
|
+
}
|
|
1523
1846
|
await rm(cwd, { recursive: true, force: true });
|
|
1524
1847
|
}
|
|
1525
1848
|
});
|
|
1526
1849
|
|
|
1527
|
-
it("
|
|
1528
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
1850
|
+
it("preserves root/leader SessionStart dispatch at session verbosity (#2831)", async () => {
|
|
1851
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-root-session-start-preserved-"));
|
|
1852
|
+
const originalCodexHome = process.env.CODEX_HOME;
|
|
1529
1853
|
try {
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
});
|
|
1539
|
-
await
|
|
1854
|
+
process.env.CODEX_HOME = join(cwd, "codex-home");
|
|
1855
|
+
await writeJson(join(process.env.CODEX_HOME, ".omx-config.json"), {
|
|
1856
|
+
notifications: {
|
|
1857
|
+
enabled: true,
|
|
1858
|
+
verbosity: "session",
|
|
1859
|
+
telegram: { enabled: true, botToken: "123:abc", chatId: "456" },
|
|
1860
|
+
},
|
|
1861
|
+
});
|
|
1862
|
+
await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
|
|
1863
|
+
await writeFile(
|
|
1864
|
+
join(cwd, ".omx", "hooks", "record-lifecycle.mjs"),
|
|
1865
|
+
[
|
|
1866
|
+
"import { appendFileSync } from 'node:fs';",
|
|
1867
|
+
"export async function onHookEvent(event) {",
|
|
1868
|
+
" appendFileSync('hook-events.jsonl', `${JSON.stringify({ event: event.event })}\\n`);",
|
|
1869
|
+
"}",
|
|
1870
|
+
].join("\n"),
|
|
1871
|
+
);
|
|
1872
|
+
|
|
1873
|
+
await dispatchCodexNativeHook(
|
|
1874
|
+
{
|
|
1875
|
+
hook_event_name: "SessionStart",
|
|
1876
|
+
cwd,
|
|
1877
|
+
session_id: "codex-root-thread-preserved",
|
|
1878
|
+
},
|
|
1879
|
+
{ cwd, sessionOwnerPid: process.pid },
|
|
1880
|
+
);
|
|
1881
|
+
|
|
1882
|
+
const hookEvents = await readFile(join(cwd, "hook-events.jsonl"), "utf-8");
|
|
1883
|
+
assert.match(
|
|
1884
|
+
hookEvents,
|
|
1885
|
+
/"event":"session-start"/,
|
|
1886
|
+
"root/leader SessionStart must still dispatch at session verbosity",
|
|
1887
|
+
);
|
|
1888
|
+
} finally {
|
|
1889
|
+
if (originalCodexHome === undefined) {
|
|
1890
|
+
delete process.env.CODEX_HOME;
|
|
1891
|
+
} else {
|
|
1892
|
+
process.env.CODEX_HOME = originalCodexHome;
|
|
1893
|
+
}
|
|
1894
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1895
|
+
}
|
|
1896
|
+
});
|
|
1897
|
+
|
|
1898
|
+
it("keeps a self-parented native role thread as subagent evidence", async () => {
|
|
1899
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-self-parented-subagent-"));
|
|
1900
|
+
try {
|
|
1901
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
1902
|
+
const canonicalSessionId = "omx-autopilot-session";
|
|
1903
|
+
const nativeRoleThreadId = "codex-architect-thread";
|
|
1904
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
1905
|
+
await writeSessionStart(cwd, canonicalSessionId, {
|
|
1906
|
+
nativeSessionId: nativeRoleThreadId,
|
|
1907
|
+
});
|
|
1908
|
+
|
|
1909
|
+
const transcriptPath = join(cwd, "architect-subagent-rollout.jsonl");
|
|
1910
|
+
await writeFile(
|
|
1911
|
+
transcriptPath,
|
|
1912
|
+
`${JSON.stringify({
|
|
1913
|
+
type: "session_meta",
|
|
1914
|
+
payload: {
|
|
1915
|
+
id: nativeRoleThreadId,
|
|
1916
|
+
source: {
|
|
1917
|
+
subagent: {
|
|
1918
|
+
thread_spawn: {
|
|
1919
|
+
parent_thread_id: nativeRoleThreadId,
|
|
1920
|
+
depth: 1,
|
|
1921
|
+
agent_nickname: "Architect",
|
|
1922
|
+
agent_role: "architect",
|
|
1923
|
+
},
|
|
1924
|
+
},
|
|
1925
|
+
},
|
|
1926
|
+
agent_nickname: "Architect",
|
|
1927
|
+
agent_role: "architect",
|
|
1928
|
+
},
|
|
1929
|
+
})}\n`,
|
|
1930
|
+
);
|
|
1931
|
+
|
|
1932
|
+
await dispatchCodexNativeHook(
|
|
1933
|
+
{
|
|
1934
|
+
hook_event_name: "SessionStart",
|
|
1935
|
+
cwd,
|
|
1936
|
+
session_id: nativeRoleThreadId,
|
|
1937
|
+
transcript_path: transcriptPath,
|
|
1938
|
+
},
|
|
1939
|
+
{ cwd, sessionOwnerPid: process.pid },
|
|
1940
|
+
);
|
|
1941
|
+
|
|
1942
|
+
const tracking = JSON.parse(
|
|
1943
|
+
await readFile(join(stateDir, "subagent-tracking.json"), "utf-8"),
|
|
1944
|
+
) as {
|
|
1945
|
+
sessions?: Record<string, {
|
|
1946
|
+
leader_thread_id?: string;
|
|
1947
|
+
threads?: Record<string, { kind?: string; mode?: string }>;
|
|
1948
|
+
}>;
|
|
1949
|
+
};
|
|
1950
|
+
assert.equal(tracking.sessions?.[canonicalSessionId]?.leader_thread_id, undefined);
|
|
1951
|
+
assert.equal(tracking.sessions?.[canonicalSessionId]?.threads?.[nativeRoleThreadId]?.kind, "subagent");
|
|
1952
|
+
assert.equal(tracking.sessions?.[canonicalSessionId]?.threads?.[nativeRoleThreadId]?.mode, "architect");
|
|
1953
|
+
} finally {
|
|
1954
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1955
|
+
}
|
|
1956
|
+
});
|
|
1957
|
+
|
|
1958
|
+
it("does not attach a subagent SessionStart to an unrelated canonical leader", async () => {
|
|
1959
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-start-mismatch-"));
|
|
1960
|
+
try {
|
|
1961
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
1962
|
+
const canonicalSessionId = "omx-leader-session-a";
|
|
1963
|
+
const leaderNativeSessionId = "codex-leader-thread-a";
|
|
1964
|
+
const unrelatedParentNativeSessionId = "codex-leader-thread-b";
|
|
1965
|
+
const childNativeSessionId = "codex-child-thread-b";
|
|
1966
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
1967
|
+
await writeSessionStart(cwd, canonicalSessionId, {
|
|
1968
|
+
nativeSessionId: leaderNativeSessionId,
|
|
1969
|
+
});
|
|
1970
|
+
await writeJson(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), {
|
|
1540
1971
|
active: true,
|
|
1541
1972
|
mode: "ralph",
|
|
1542
1973
|
current_phase: "executing",
|
|
@@ -2823,6 +3254,64 @@ standardMaxRounds = 15
|
|
|
2823
3254
|
}
|
|
2824
3255
|
});
|
|
2825
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
|
+
|
|
2826
3315
|
it("blocks ultragoal Stop for concise generic goal completion claims", async () => {
|
|
2827
3316
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-generic-complete-stop-"));
|
|
2828
3317
|
try {
|
|
@@ -5903,161 +6392,512 @@ exit 0
|
|
|
5903
6392
|
}
|
|
5904
6393
|
});
|
|
5905
6394
|
|
|
5906
|
-
it("allows
|
|
5907
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-apply-patch-"));
|
|
6395
|
+
it("allows read-only diagnostics mentioning apply_patch while deep-interview blocks real apply_patch invocations", async () => {
|
|
6396
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-grep-apply-patch-"));
|
|
5908
6397
|
try {
|
|
5909
6398
|
const stateDir = join(cwd, ".omx", "state");
|
|
5910
|
-
const sessionDir = join(stateDir, "sessions", "sess-di-
|
|
6399
|
+
const sessionDir = join(stateDir, "sessions", "sess-di-grep");
|
|
5911
6400
|
await mkdir(sessionDir, { recursive: true });
|
|
5912
|
-
await writeJson(join(stateDir, "session.json"), { session_id: "sess-di-
|
|
6401
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-di-grep", cwd });
|
|
5913
6402
|
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
5914
6403
|
version: 1,
|
|
5915
6404
|
active: true,
|
|
5916
6405
|
skill: "deep-interview",
|
|
5917
6406
|
phase: "planning",
|
|
5918
|
-
session_id: "sess-di-
|
|
5919
|
-
active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-di-
|
|
6407
|
+
session_id: "sess-di-grep",
|
|
6408
|
+
active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-di-grep" }],
|
|
5920
6409
|
});
|
|
5921
6410
|
await writeJson(join(sessionDir, "deep-interview-state.json"), {
|
|
5922
6411
|
active: true,
|
|
5923
6412
|
mode: "deep-interview",
|
|
5924
6413
|
current_phase: "intent-first",
|
|
5925
|
-
session_id: "sess-di-
|
|
6414
|
+
session_id: "sess-di-grep",
|
|
5926
6415
|
});
|
|
5927
6416
|
|
|
5928
|
-
const
|
|
6417
|
+
const allowedGrep = await dispatchCodexNativeHook(
|
|
5929
6418
|
{
|
|
5930
6419
|
hook_event_name: "PreToolUse",
|
|
5931
6420
|
cwd,
|
|
5932
|
-
session_id: "sess-di-
|
|
5933
|
-
tool_name: "
|
|
5934
|
-
tool_use_id: "tool-di-
|
|
5935
|
-
tool_input: {
|
|
5936
|
-
input: "*** Begin Patch\n*** Add File: .omx/context/findings.md\n+# Findings\n*** End Patch\n",
|
|
5937
|
-
},
|
|
6421
|
+
session_id: "sess-di-grep",
|
|
6422
|
+
tool_name: "Bash",
|
|
6423
|
+
tool_use_id: "tool-di-grep",
|
|
6424
|
+
tool_input: { command: 'grep -n "apply_patch" dist/scripts/codex-native-hook.js' },
|
|
5938
6425
|
},
|
|
5939
6426
|
{ cwd },
|
|
5940
6427
|
);
|
|
5941
|
-
assert.equal(
|
|
6428
|
+
assert.equal(allowedGrep.outputJson, null);
|
|
5942
6429
|
|
|
5943
|
-
const
|
|
6430
|
+
const allowedSubshellGrep = await dispatchCodexNativeHook(
|
|
5944
6431
|
{
|
|
5945
6432
|
hook_event_name: "PreToolUse",
|
|
5946
6433
|
cwd,
|
|
5947
|
-
session_id: "sess-di-
|
|
5948
|
-
tool_name: "
|
|
5949
|
-
tool_use_id: "tool-di-
|
|
5950
|
-
tool_input: {
|
|
5951
|
-
input: "*** Begin Patch\n*** Update File: .omx/specs/deep-interview-demo.md\n@@\n-old\n+new\n*** End Patch\n",
|
|
5952
|
-
},
|
|
6434
|
+
session_id: "sess-di-grep",
|
|
6435
|
+
tool_name: "Bash",
|
|
6436
|
+
tool_use_id: "tool-di-grep-subshell",
|
|
6437
|
+
tool_input: { command: '(grep -n "apply_patch" dist/scripts/codex-native-hook.js)' },
|
|
5953
6438
|
},
|
|
5954
6439
|
{ cwd },
|
|
5955
6440
|
);
|
|
5956
|
-
assert.equal(
|
|
6441
|
+
assert.equal(allowedSubshellGrep.outputJson, null);
|
|
5957
6442
|
|
|
5958
|
-
|
|
6443
|
+
// Double-quoted spans that merely mention the literal token, expand a
|
|
6444
|
+
// parameter, or run a substitution that is not `apply_patch` stay allowed
|
|
6445
|
+
// — the quoted-substitution fix must not over-block read-only diagnostics.
|
|
6446
|
+
const allowedQuotedMention = await dispatchCodexNativeHook(
|
|
5959
6447
|
{
|
|
5960
6448
|
hook_event_name: "PreToolUse",
|
|
5961
6449
|
cwd,
|
|
5962
|
-
session_id: "sess-di-
|
|
5963
|
-
tool_name: "
|
|
5964
|
-
tool_use_id: "tool-di-
|
|
5965
|
-
tool_input: {
|
|
5966
|
-
input: "*** Begin Patch\n*** Add File: .omx/state/deep-interview-notes.json\n+{}\n*** End Patch\n",
|
|
5967
|
-
},
|
|
6450
|
+
session_id: "sess-di-grep",
|
|
6451
|
+
tool_name: "Bash",
|
|
6452
|
+
tool_use_id: "tool-di-grep-quoted-mention",
|
|
6453
|
+
tool_input: { command: 'echo "${apply_patch} $(echo apply_patch)"' },
|
|
5968
6454
|
},
|
|
5969
6455
|
{ cwd },
|
|
5970
6456
|
);
|
|
5971
|
-
assert.equal(
|
|
6457
|
+
assert.equal(allowedQuotedMention.outputJson, null);
|
|
5972
6458
|
|
|
5973
|
-
const
|
|
6459
|
+
const blockedApplyPatch = await dispatchCodexNativeHook(
|
|
5974
6460
|
{
|
|
5975
6461
|
hook_event_name: "PreToolUse",
|
|
5976
6462
|
cwd,
|
|
5977
|
-
session_id: "sess-di-
|
|
5978
|
-
tool_name: "
|
|
5979
|
-
tool_use_id: "tool-di-apply-patch-
|
|
5980
|
-
tool_input: {
|
|
5981
|
-
input: "*** Begin Patch\n*** Add File: src/implementation.ts\n+export const x = 1;\n*** End Patch\n",
|
|
5982
|
-
},
|
|
6463
|
+
session_id: "sess-di-grep",
|
|
6464
|
+
tool_name: "Bash",
|
|
6465
|
+
tool_use_id: "tool-di-apply-patch-invoke",
|
|
6466
|
+
tool_input: { command: "apply_patch <<'EOF'\n*** Begin Patch\n*** Add File: src/leak.ts\n+export const x = 1;\n*** End Patch\nEOF" },
|
|
5983
6467
|
},
|
|
5984
6468
|
{ cwd },
|
|
5985
6469
|
);
|
|
5986
|
-
assert.equal((
|
|
5987
|
-
assert.match(String((blockedOutsidePath.outputJson as { reason?: string } | null)?.reason ?? ""), /Deep-interview is active/);
|
|
6470
|
+
assert.equal((blockedApplyPatch.outputJson as { decision?: string } | null)?.decision, "block");
|
|
5988
6471
|
|
|
5989
|
-
const
|
|
6472
|
+
const blockedEnvAssignmentApplyPatch = await dispatchCodexNativeHook(
|
|
5990
6473
|
{
|
|
5991
6474
|
hook_event_name: "PreToolUse",
|
|
5992
6475
|
cwd,
|
|
5993
|
-
session_id: "sess-di-
|
|
5994
|
-
tool_name: "
|
|
5995
|
-
tool_use_id: "tool-di-apply-patch-
|
|
5996
|
-
tool_input: {
|
|
5997
|
-
input: "*** Begin Patch\n*** Add File: .omx/context/ok.md\n+ok\n*** Add File: src/leak.ts\n+leak\n*** End Patch\n",
|
|
5998
|
-
},
|
|
6476
|
+
session_id: "sess-di-grep",
|
|
6477
|
+
tool_name: "Bash",
|
|
6478
|
+
tool_use_id: "tool-di-apply-patch-env-assignment",
|
|
6479
|
+
tool_input: { command: "FOO=bar apply_patch <<'EOF'\n*** Begin Patch\n*** Add File: src/leak.ts\n+export const x = 1;\n*** End Patch\nEOF" },
|
|
5999
6480
|
},
|
|
6000
6481
|
{ cwd },
|
|
6001
6482
|
);
|
|
6002
|
-
assert.equal((
|
|
6483
|
+
assert.equal((blockedEnvAssignmentApplyPatch.outputJson as { decision?: string } | null)?.decision, "block");
|
|
6003
6484
|
|
|
6004
|
-
const
|
|
6485
|
+
const blockedEnvWrapperApplyPatch = await dispatchCodexNativeHook(
|
|
6005
6486
|
{
|
|
6006
6487
|
hook_event_name: "PreToolUse",
|
|
6007
6488
|
cwd,
|
|
6008
|
-
session_id: "sess-di-
|
|
6009
|
-
tool_name: "
|
|
6010
|
-
tool_use_id: "tool-di-apply-patch-
|
|
6011
|
-
tool_input: {
|
|
6012
|
-
},
|
|
6013
|
-
{ cwd },
|
|
6014
|
-
);
|
|
6015
|
-
assert.equal((
|
|
6489
|
+
session_id: "sess-di-grep",
|
|
6490
|
+
tool_name: "Bash",
|
|
6491
|
+
tool_use_id: "tool-di-apply-patch-env-wrapper",
|
|
6492
|
+
tool_input: { command: "env FOO=bar apply_patch <<'EOF'\n*** Begin Patch\n*** Add File: src/leak.ts\n+export const x = 1;\n*** End Patch\nEOF" },
|
|
6493
|
+
},
|
|
6494
|
+
{ cwd },
|
|
6495
|
+
);
|
|
6496
|
+
assert.equal((blockedEnvWrapperApplyPatch.outputJson as { decision?: string } | null)?.decision, "block");
|
|
6497
|
+
|
|
6498
|
+
const heredocBody = "\n*** Begin Patch\n*** Add File: src/leak.ts\n+export const x = 1;\n*** End Patch\nEOF";
|
|
6499
|
+
const blockedRealApplyPatchForms: Array<{ id: string; command: string }> = [
|
|
6500
|
+
{ id: "tool-di-apply-patch-env-i", command: `env -i apply_patch <<'EOF'${heredocBody}` },
|
|
6501
|
+
{ id: "tool-di-apply-patch-env-unset", command: `env -u FOO apply_patch <<'EOF'${heredocBody}` },
|
|
6502
|
+
{ id: "tool-di-apply-patch-env-i-assignment", command: `env -i FOO=bar apply_patch <<'EOF'${heredocBody}` },
|
|
6503
|
+
{ id: "tool-di-apply-patch-assignment-env", command: `FOO=bar env apply_patch <<'EOF'${heredocBody}` },
|
|
6504
|
+
{ id: "tool-di-apply-patch-exec-env-assignment", command: `exec env FOO=bar apply_patch <<'EOF'${heredocBody}` },
|
|
6505
|
+
{ id: "tool-di-apply-patch-absolute-path", command: `/usr/bin/apply_patch <<'EOF'${heredocBody}` },
|
|
6506
|
+
{ id: "tool-di-apply-patch-relative-path", command: `./apply_patch <<'EOF'${heredocBody}` },
|
|
6507
|
+
{ id: "tool-di-apply-patch-subshell", command: `(apply_patch <<'EOF'${heredocBody}\n)` },
|
|
6508
|
+
{ id: "tool-di-apply-patch-subshell-spaced", command: `( apply_patch <<'EOF'${heredocBody}\n)` },
|
|
6509
|
+
{ id: "tool-di-apply-patch-double-subshell", command: `((apply_patch <<'EOF'${heredocBody}\n))` },
|
|
6510
|
+
{ id: "tool-di-apply-patch-pipe-subshell", command: `true | (apply_patch <<'EOF'${heredocBody}\n)` },
|
|
6511
|
+
{ id: "tool-di-apply-patch-subshell-env", command: `(env apply_patch <<'EOF'${heredocBody}\n)` },
|
|
6512
|
+
{ id: "tool-di-apply-patch-command-substitution", command: `x=$(apply_patch <<'EOF'${heredocBody}\n)` },
|
|
6513
|
+
{ id: "tool-di-apply-patch-brace-group", command: `{ apply_patch <<'EOF'${heredocBody}\n}` },
|
|
6514
|
+
// Command substitution runs even inside double quotes, so quoting the
|
|
6515
|
+
// already-blocked `$(…)` / `` `…` `` form must not bypass the guard.
|
|
6516
|
+
{ id: "tool-di-apply-patch-quoted-command-substitution", command: `echo "$(apply_patch <<'EOF'${heredocBody}\n)"` },
|
|
6517
|
+
{ id: "tool-di-apply-patch-quoted-backtick", command: `echo "\`apply_patch <<'EOF'${heredocBody}\`"` },
|
|
6518
|
+
{ id: "tool-di-apply-patch-quoted-command-substitution-prefixed", command: `echo "patched: $(apply_patch <<'EOF'${heredocBody}\n) done"` },
|
|
6519
|
+
];
|
|
6520
|
+
for (const form of blockedRealApplyPatchForms) {
|
|
6521
|
+
const blockedForm = await dispatchCodexNativeHook(
|
|
6522
|
+
{
|
|
6523
|
+
hook_event_name: "PreToolUse",
|
|
6524
|
+
cwd,
|
|
6525
|
+
session_id: "sess-di-grep",
|
|
6526
|
+
tool_name: "Bash",
|
|
6527
|
+
tool_use_id: form.id,
|
|
6528
|
+
tool_input: { command: form.command },
|
|
6529
|
+
},
|
|
6530
|
+
{ cwd },
|
|
6531
|
+
);
|
|
6532
|
+
assert.equal(
|
|
6533
|
+
(blockedForm.outputJson as { decision?: string } | null)?.decision,
|
|
6534
|
+
"block",
|
|
6535
|
+
`expected deep-interview to block real apply_patch form: ${form.command}`,
|
|
6536
|
+
);
|
|
6537
|
+
}
|
|
6016
6538
|
} finally {
|
|
6017
6539
|
await rm(cwd, { recursive: true, force: true });
|
|
6018
6540
|
}
|
|
6019
6541
|
});
|
|
6020
6542
|
|
|
6021
|
-
it("allows
|
|
6022
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-
|
|
6543
|
+
it("allows deep-interview same-command literal variable redirects to artifacts while blocking variable redirects outside them", async () => {
|
|
6544
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-var-redirect-"));
|
|
6023
6545
|
try {
|
|
6024
6546
|
const stateDir = join(cwd, ".omx", "state");
|
|
6025
|
-
const sessionDir = join(stateDir, "sessions", "sess-
|
|
6547
|
+
const sessionDir = join(stateDir, "sessions", "sess-di-var-redirect");
|
|
6026
6548
|
await mkdir(sessionDir, { recursive: true });
|
|
6027
|
-
await writeJson(join(stateDir, "session.json"), { session_id: "sess-
|
|
6549
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-di-var-redirect", cwd });
|
|
6028
6550
|
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
6029
6551
|
version: 1,
|
|
6030
6552
|
active: true,
|
|
6031
|
-
skill: "
|
|
6032
|
-
phase: "
|
|
6033
|
-
session_id: "sess-
|
|
6034
|
-
|
|
6035
|
-
active_skills: [
|
|
6036
|
-
{
|
|
6037
|
-
skill: "autopilot",
|
|
6038
|
-
phase: "ralplan",
|
|
6039
|
-
active: true,
|
|
6040
|
-
session_id: "sess-autopilot-ralplan-artifact",
|
|
6041
|
-
thread_id: "thread-autopilot-ralplan-artifact",
|
|
6042
|
-
},
|
|
6043
|
-
],
|
|
6553
|
+
skill: "deep-interview",
|
|
6554
|
+
phase: "planning",
|
|
6555
|
+
session_id: "sess-di-var-redirect",
|
|
6556
|
+
active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-di-var-redirect" }],
|
|
6044
6557
|
});
|
|
6045
|
-
await writeJson(join(sessionDir, "
|
|
6558
|
+
await writeJson(join(sessionDir, "deep-interview-state.json"), {
|
|
6046
6559
|
active: true,
|
|
6047
|
-
mode: "
|
|
6048
|
-
current_phase: "
|
|
6049
|
-
session_id: "sess-
|
|
6050
|
-
thread_id: "thread-autopilot-ralplan-artifact",
|
|
6560
|
+
mode: "deep-interview",
|
|
6561
|
+
current_phase: "intent-first",
|
|
6562
|
+
session_id: "sess-di-var-redirect",
|
|
6051
6563
|
});
|
|
6052
6564
|
|
|
6053
|
-
const
|
|
6565
|
+
const allowedVarRedirect = await dispatchCodexNativeHook(
|
|
6054
6566
|
{
|
|
6055
6567
|
hook_event_name: "PreToolUse",
|
|
6056
6568
|
cwd,
|
|
6057
|
-
session_id: "sess-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6569
|
+
session_id: "sess-di-var-redirect",
|
|
6570
|
+
tool_name: "Bash",
|
|
6571
|
+
tool_use_id: "tool-di-var-redirect-allow",
|
|
6572
|
+
tool_input: { command: 'SNAP=".omx/context/example.md"\ncat > "$SNAP" <<\'EOF\'\ncontent\nEOF' },
|
|
6573
|
+
},
|
|
6574
|
+
{ cwd },
|
|
6575
|
+
);
|
|
6576
|
+
assert.equal(allowedVarRedirect.outputJson, null);
|
|
6577
|
+
|
|
6578
|
+
const blockedVarRedirect = await dispatchCodexNativeHook(
|
|
6579
|
+
{
|
|
6580
|
+
hook_event_name: "PreToolUse",
|
|
6581
|
+
cwd,
|
|
6582
|
+
session_id: "sess-di-var-redirect",
|
|
6583
|
+
tool_name: "Bash",
|
|
6584
|
+
tool_use_id: "tool-di-var-redirect-block",
|
|
6585
|
+
tool_input: { command: 'SNAP="src/leak.ts"\ncat > "$SNAP" <<\'EOF\'\ncontent\nEOF' },
|
|
6586
|
+
},
|
|
6587
|
+
{ cwd },
|
|
6588
|
+
);
|
|
6589
|
+
assert.equal((blockedVarRedirect.outputJson as { decision?: string } | null)?.decision, "block");
|
|
6590
|
+
|
|
6591
|
+
const blockedUnresolvedVarRedirect = await dispatchCodexNativeHook(
|
|
6592
|
+
{
|
|
6593
|
+
hook_event_name: "PreToolUse",
|
|
6594
|
+
cwd,
|
|
6595
|
+
session_id: "sess-di-var-redirect",
|
|
6596
|
+
tool_name: "Bash",
|
|
6597
|
+
tool_use_id: "tool-di-var-redirect-unresolved",
|
|
6598
|
+
tool_input: { command: 'cat > "$SNAP" <<\'EOF\'\ncontent\nEOF' },
|
|
6599
|
+
},
|
|
6600
|
+
{ cwd },
|
|
6601
|
+
);
|
|
6602
|
+
assert.equal((blockedUnresolvedVarRedirect.outputJson as { decision?: string } | null)?.decision, "block");
|
|
6603
|
+
} finally {
|
|
6604
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6605
|
+
}
|
|
6606
|
+
});
|
|
6607
|
+
|
|
6608
|
+
it("allows deep-interview apply_patch artifact writes from freeform patch text while blocking outside paths", async () => {
|
|
6609
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-apply-patch-"));
|
|
6610
|
+
try {
|
|
6611
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
6612
|
+
const sessionDir = join(stateDir, "sessions", "sess-di-apply-patch");
|
|
6613
|
+
await mkdir(sessionDir, { recursive: true });
|
|
6614
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-di-apply-patch", cwd });
|
|
6615
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
6616
|
+
version: 1,
|
|
6617
|
+
active: true,
|
|
6618
|
+
skill: "deep-interview",
|
|
6619
|
+
phase: "planning",
|
|
6620
|
+
session_id: "sess-di-apply-patch",
|
|
6621
|
+
active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-di-apply-patch" }],
|
|
6622
|
+
});
|
|
6623
|
+
await writeJson(join(sessionDir, "deep-interview-state.json"), {
|
|
6624
|
+
active: true,
|
|
6625
|
+
mode: "deep-interview",
|
|
6626
|
+
current_phase: "intent-first",
|
|
6627
|
+
session_id: "sess-di-apply-patch",
|
|
6628
|
+
});
|
|
6629
|
+
|
|
6630
|
+
const allowedAddFile = await dispatchCodexNativeHook(
|
|
6631
|
+
{
|
|
6632
|
+
hook_event_name: "PreToolUse",
|
|
6633
|
+
cwd,
|
|
6634
|
+
session_id: "sess-di-apply-patch",
|
|
6635
|
+
tool_name: "apply_patch",
|
|
6636
|
+
tool_use_id: "tool-di-apply-patch-add",
|
|
6637
|
+
tool_input: {
|
|
6638
|
+
input: "*** Begin Patch\n*** Add File: .omx/context/findings.md\n+# Findings\n*** End Patch\n",
|
|
6639
|
+
},
|
|
6640
|
+
},
|
|
6641
|
+
{ cwd },
|
|
6642
|
+
);
|
|
6643
|
+
assert.equal(allowedAddFile.outputJson, null);
|
|
6644
|
+
|
|
6645
|
+
const allowedUpdateFile = await dispatchCodexNativeHook(
|
|
6646
|
+
{
|
|
6647
|
+
hook_event_name: "PreToolUse",
|
|
6648
|
+
cwd,
|
|
6649
|
+
session_id: "sess-di-apply-patch",
|
|
6650
|
+
tool_name: "ApplyPatch",
|
|
6651
|
+
tool_use_id: "tool-di-apply-patch-update",
|
|
6652
|
+
tool_input: {
|
|
6653
|
+
input: "*** Begin Patch\n*** Update File: .omx/specs/deep-interview-demo.md\n@@\n-old\n+new\n*** End Patch\n",
|
|
6654
|
+
},
|
|
6655
|
+
},
|
|
6656
|
+
{ cwd },
|
|
6657
|
+
);
|
|
6658
|
+
assert.equal(allowedUpdateFile.outputJson, null);
|
|
6659
|
+
|
|
6660
|
+
const allowedStateWrite = await dispatchCodexNativeHook(
|
|
6661
|
+
{
|
|
6662
|
+
hook_event_name: "PreToolUse",
|
|
6663
|
+
cwd,
|
|
6664
|
+
session_id: "sess-di-apply-patch",
|
|
6665
|
+
tool_name: "apply_patch",
|
|
6666
|
+
tool_use_id: "tool-di-apply-patch-state",
|
|
6667
|
+
tool_input: {
|
|
6668
|
+
input: "*** Begin Patch\n*** Add File: .omx/state/deep-interview-notes.json\n+{}\n*** End Patch\n",
|
|
6669
|
+
},
|
|
6670
|
+
},
|
|
6671
|
+
{ cwd },
|
|
6672
|
+
);
|
|
6673
|
+
assert.equal(allowedStateWrite.outputJson, null);
|
|
6674
|
+
|
|
6675
|
+
const blockedOutsidePath = await dispatchCodexNativeHook(
|
|
6676
|
+
{
|
|
6677
|
+
hook_event_name: "PreToolUse",
|
|
6678
|
+
cwd,
|
|
6679
|
+
session_id: "sess-di-apply-patch",
|
|
6680
|
+
tool_name: "apply_patch",
|
|
6681
|
+
tool_use_id: "tool-di-apply-patch-outside",
|
|
6682
|
+
tool_input: {
|
|
6683
|
+
input: "*** Begin Patch\n*** Add File: src/implementation.ts\n+export const x = 1;\n*** End Patch\n",
|
|
6684
|
+
},
|
|
6685
|
+
},
|
|
6686
|
+
{ cwd },
|
|
6687
|
+
);
|
|
6688
|
+
assert.equal((blockedOutsidePath.outputJson as { decision?: string } | null)?.decision, "block");
|
|
6689
|
+
assert.match(String((blockedOutsidePath.outputJson as { reason?: string } | null)?.reason ?? ""), /Deep-interview is active/);
|
|
6690
|
+
|
|
6691
|
+
const blockedMixedPaths = await dispatchCodexNativeHook(
|
|
6692
|
+
{
|
|
6693
|
+
hook_event_name: "PreToolUse",
|
|
6694
|
+
cwd,
|
|
6695
|
+
session_id: "sess-di-apply-patch",
|
|
6696
|
+
tool_name: "apply_patch",
|
|
6697
|
+
tool_use_id: "tool-di-apply-patch-mixed",
|
|
6698
|
+
tool_input: {
|
|
6699
|
+
input: "*** Begin Patch\n*** Add File: .omx/context/ok.md\n+ok\n*** Add File: src/leak.ts\n+leak\n*** End Patch\n",
|
|
6700
|
+
},
|
|
6701
|
+
},
|
|
6702
|
+
{ cwd },
|
|
6703
|
+
);
|
|
6704
|
+
assert.equal((blockedMixedPaths.outputJson as { decision?: string } | null)?.decision, "block");
|
|
6705
|
+
|
|
6706
|
+
const blockedUnparseablePatch = await dispatchCodexNativeHook(
|
|
6707
|
+
{
|
|
6708
|
+
hook_event_name: "PreToolUse",
|
|
6709
|
+
cwd,
|
|
6710
|
+
session_id: "sess-di-apply-patch",
|
|
6711
|
+
tool_name: "apply_patch",
|
|
6712
|
+
tool_use_id: "tool-di-apply-patch-empty",
|
|
6713
|
+
tool_input: { input: "not a recognizable patch" },
|
|
6714
|
+
},
|
|
6715
|
+
{ cwd },
|
|
6716
|
+
);
|
|
6717
|
+
assert.equal((blockedUnparseablePatch.outputJson as { decision?: string } | null)?.decision, "block");
|
|
6718
|
+
} finally {
|
|
6719
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6720
|
+
}
|
|
6721
|
+
});
|
|
6722
|
+
|
|
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-"));
|
|
6725
|
+
try {
|
|
6726
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
6727
|
+
const sessionId = "sess-ralplan-guard";
|
|
6728
|
+
const sessionDir = join(stateDir, "sessions", sessionId);
|
|
6729
|
+
await mkdir(sessionDir, { recursive: true });
|
|
6730
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, cwd });
|
|
6731
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
6732
|
+
version: 1,
|
|
6733
|
+
active: true,
|
|
6734
|
+
skill: "ralplan",
|
|
6735
|
+
phase: "planning",
|
|
6736
|
+
session_id: sessionId,
|
|
6737
|
+
active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
|
|
6738
|
+
});
|
|
6739
|
+
await writeJson(join(sessionDir, "ralplan-state.json"), {
|
|
6740
|
+
active: true,
|
|
6741
|
+
mode: "ralplan",
|
|
6742
|
+
current_phase: "planning",
|
|
6743
|
+
session_id: sessionId,
|
|
6744
|
+
});
|
|
6745
|
+
|
|
6746
|
+
const preToolUse = async (tool_name: string, tool_use_id: string, tool_input: Record<string, unknown>) => dispatchCodexNativeHook(
|
|
6747
|
+
{
|
|
6748
|
+
hook_event_name: "PreToolUse",
|
|
6749
|
+
cwd,
|
|
6750
|
+
session_id: sessionId,
|
|
6751
|
+
tool_name,
|
|
6752
|
+
tool_use_id,
|
|
6753
|
+
tool_input,
|
|
6754
|
+
},
|
|
6755
|
+
{ cwd },
|
|
6756
|
+
);
|
|
6757
|
+
|
|
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",
|
|
6061
6901
|
tool_input: { file_path: ".omx/plans/prd-omx-y7a.md", content: "# Plan" },
|
|
6062
6902
|
},
|
|
6063
6903
|
{ cwd },
|
|
@@ -6098,6 +6938,263 @@ exit 0
|
|
|
6098
6938
|
}
|
|
6099
6939
|
});
|
|
6100
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
|
+
|
|
6101
7198
|
it("allows null-device fd redirects while deep-interview blocks real Bash writes", async () => {
|
|
6102
7199
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-null-redirect-"));
|
|
6103
7200
|
try {
|
|
@@ -6267,6 +7364,207 @@ exit 0
|
|
|
6267
7364
|
}
|
|
6268
7365
|
});
|
|
6269
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
|
+
|
|
6270
7568
|
it("warns on PreToolUse for vague sloppy fallback implementation framing", async () => {
|
|
6271
7569
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-slop-warn-"));
|
|
6272
7570
|
try {
|