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
|
@@ -160,6 +160,31 @@ if [[ "$cmd" == "capture-pane" ]]; then
|
|
|
160
160
|
printf '%s\\n' ${captureText}
|
|
161
161
|
exit 0
|
|
162
162
|
fi
|
|
163
|
+
if [[ "$cmd" == "set-buffer" ]]; then
|
|
164
|
+
printf '%s' "\${@: -1}" > "${tmuxLogPath}.buffer"
|
|
165
|
+
exit 0
|
|
166
|
+
fi
|
|
167
|
+
if [[ "$cmd" == "show-buffer" ]]; then
|
|
168
|
+
if [[ -f "${tmuxLogPath}.buffer" ]]; then cat "${tmuxLogPath}.buffer"; fi
|
|
169
|
+
exit 0
|
|
170
|
+
fi
|
|
171
|
+
if [[ "$cmd" == "paste-buffer" ]]; then
|
|
172
|
+
target=""
|
|
173
|
+
while [[ "$#" -gt 0 ]]; do
|
|
174
|
+
case "$1" in
|
|
175
|
+
-t) target="$2"; shift 2 ;;
|
|
176
|
+
*) shift ;;
|
|
177
|
+
esac
|
|
178
|
+
done
|
|
179
|
+
if [[ -f "${tmuxLogPath}.buffer" ]]; then
|
|
180
|
+
echo "send-keys -t \${target} -l $(cat "${tmuxLogPath}.buffer")" >> "${tmuxLogPath}"
|
|
181
|
+
fi
|
|
182
|
+
exit 0
|
|
183
|
+
fi
|
|
184
|
+
if [[ "$cmd" == "delete-buffer" ]]; then
|
|
185
|
+
rm -f "${tmuxLogPath}.buffer"
|
|
186
|
+
exit 0
|
|
187
|
+
fi
|
|
163
188
|
if [[ "$cmd" == "send-keys" ]]; then
|
|
164
189
|
${sendDelaySeconds > 0 ? `sleep ${sendDelaySeconds}` : ""}
|
|
165
190
|
${removePathOnSend ? `rm -rf ${removePathOnSend}` : ""}
|
|
@@ -346,9 +371,9 @@ describe("codex native hook dispatch", () => {
|
|
|
346
371
|
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
347
372
|
assert.equal(result.stderr, "");
|
|
348
373
|
const output = parseSingleJsonStdout(result.stdout);
|
|
349
|
-
assert.equal(output.continue,
|
|
374
|
+
assert.equal(output.continue, undefined);
|
|
350
375
|
assert.equal(output.decision, undefined);
|
|
351
|
-
assert.equal(output.stopReason,
|
|
376
|
+
assert.equal(output.stopReason, undefined);
|
|
352
377
|
assert.equal(output.hookSpecificOutput, undefined);
|
|
353
378
|
assert.match(String(output.systemMessage ?? ""), /stdin JSON parsing failed inside codex-native-hook:/);
|
|
354
379
|
}
|
|
@@ -421,6 +446,151 @@ describe("codex native hook dispatch", () => {
|
|
|
421
446
|
assert.equal(output.hookSpecificOutput, undefined);
|
|
422
447
|
assert.match(String(output.systemMessage ?? ""), /stdin JSON parsing failed inside codex-native-hook:/);
|
|
423
448
|
});
|
|
449
|
+
it("emits no-op JSON stdout for PreToolUse non-Bash tools with null output", async () => {
|
|
450
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-pretool-nonbash-noop-"));
|
|
451
|
+
try {
|
|
452
|
+
const result = spawnSync(process.execPath, [nativeHookScriptPath()], {
|
|
453
|
+
cwd,
|
|
454
|
+
input: JSON.stringify({
|
|
455
|
+
hook_event_name: "PreToolUse",
|
|
456
|
+
cwd,
|
|
457
|
+
session_id: "sess-cli-pretool-nonbash-noop",
|
|
458
|
+
thread_id: "thread-cli-pretool-nonbash-noop",
|
|
459
|
+
turn_id: "turn-cli-pretool-nonbash-noop",
|
|
460
|
+
tool_name: "Read",
|
|
461
|
+
tool_input: { file_path: "package.json" },
|
|
462
|
+
}),
|
|
463
|
+
encoding: "utf-8",
|
|
464
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
465
|
+
});
|
|
466
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
467
|
+
assert.equal(result.stderr, "");
|
|
468
|
+
assert.deepEqual(parseSingleJsonStdout(result.stdout), {});
|
|
469
|
+
}
|
|
470
|
+
finally {
|
|
471
|
+
await rm(cwd, { recursive: true, force: true });
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
it("emits PreToolUse CLI advisory JSON with only systemMessage", async () => {
|
|
475
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-pretool-schema-safe-"));
|
|
476
|
+
try {
|
|
477
|
+
const output = parseSingleJsonStdout(runNativeHookCli({
|
|
478
|
+
hook_event_name: "PreToolUse",
|
|
479
|
+
cwd,
|
|
480
|
+
session_id: "sess-cli-pretool-schema-safe",
|
|
481
|
+
thread_id: "thread-cli-pretool-schema-safe",
|
|
482
|
+
turn_id: "turn-cli-pretool-schema-safe",
|
|
483
|
+
tool_name: "Bash",
|
|
484
|
+
tool_input: { command: "rm -rf dist" },
|
|
485
|
+
}, { cwd }));
|
|
486
|
+
assert.deepEqual(output, {
|
|
487
|
+
systemMessage: "Destructive Bash command detected (`rm -rf dist`). Confirm the target and expected side effects before running it.",
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
finally {
|
|
491
|
+
await rm(cwd, { recursive: true, force: true });
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
it("emits PreToolUse CLI block JSON with only systemMessage", async () => {
|
|
495
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-pretool-block-schema-safe-"));
|
|
496
|
+
try {
|
|
497
|
+
const output = parseSingleJsonStdout(runNativeHookCli({
|
|
498
|
+
hook_event_name: "PreToolUse",
|
|
499
|
+
cwd,
|
|
500
|
+
session_id: "sess-cli-pretool-block-schema-safe",
|
|
501
|
+
thread_id: "thread-cli-pretool-block-schema-safe",
|
|
502
|
+
turn_id: "turn-cli-pretool-block-schema-safe",
|
|
503
|
+
tool_name: "Bash",
|
|
504
|
+
tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 git commit -m "fix tests"' },
|
|
505
|
+
}, { cwd }));
|
|
506
|
+
assert.deepEqual(Object.keys(output).sort(), ["systemMessage"]);
|
|
507
|
+
assert.match(String(output.systemMessage ?? ""), /Lore protocol/);
|
|
508
|
+
assert.equal(output.decision, undefined);
|
|
509
|
+
assert.equal(output.stopReason, undefined);
|
|
510
|
+
assert.equal(output.hookSpecificOutput, undefined);
|
|
511
|
+
}
|
|
512
|
+
finally {
|
|
513
|
+
await rm(cwd, { recursive: true, force: true });
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
it("preserves ralplan PreToolUse planning guard as schema-safe CLI systemMessage", async () => {
|
|
517
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-ralplan-pretool-boundary-"));
|
|
518
|
+
const sessionId = "sess-cli-ralplan-pretool-boundary";
|
|
519
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
520
|
+
try {
|
|
521
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
522
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
523
|
+
active: true,
|
|
524
|
+
skill: "ralplan",
|
|
525
|
+
phase: "planning",
|
|
526
|
+
session_id: sessionId,
|
|
527
|
+
active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
|
|
528
|
+
});
|
|
529
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
530
|
+
active: true,
|
|
531
|
+
mode: "ralplan",
|
|
532
|
+
current_phase: "critic-review",
|
|
533
|
+
session_id: sessionId,
|
|
534
|
+
});
|
|
535
|
+
const output = parseSingleJsonStdout(runNativeHookCli({
|
|
536
|
+
hook_event_name: "PreToolUse",
|
|
537
|
+
cwd,
|
|
538
|
+
session_id: sessionId,
|
|
539
|
+
thread_id: "thread-cli-ralplan-pretool-boundary",
|
|
540
|
+
tool_name: "Edit",
|
|
541
|
+
tool_input: { file_path: "src/runtime.ts", old_string: "a", new_string: "b" },
|
|
542
|
+
}, { cwd }));
|
|
543
|
+
assert.deepEqual(Object.keys(output).sort(), ["systemMessage"]);
|
|
544
|
+
assert.match(String(output.systemMessage ?? ""), /Ralplan is active \(phase: critic-review\)/);
|
|
545
|
+
assert.match(String(output.systemMessage ?? ""), /implementation\/write tools are blocked/);
|
|
546
|
+
assert.match(String(output.systemMessage ?? ""), /Write only planning artifacts/);
|
|
547
|
+
assert.equal(output.decision, undefined);
|
|
548
|
+
assert.equal(output.reason, undefined);
|
|
549
|
+
assert.equal(output.hookSpecificOutput, undefined);
|
|
550
|
+
}
|
|
551
|
+
finally {
|
|
552
|
+
await rm(cwd, { recursive: true, force: true });
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
it("preserves deep-interview PreToolUse planning guard as schema-safe CLI systemMessage", async () => {
|
|
556
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-deep-interview-pretool-boundary-"));
|
|
557
|
+
const sessionId = "sess-cli-deep-interview-pretool-boundary";
|
|
558
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
559
|
+
try {
|
|
560
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
561
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
562
|
+
active: true,
|
|
563
|
+
skill: "deep-interview",
|
|
564
|
+
phase: "planning",
|
|
565
|
+
session_id: sessionId,
|
|
566
|
+
active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: sessionId }],
|
|
567
|
+
});
|
|
568
|
+
await writeJson(join(stateDir, "sessions", sessionId, "deep-interview-state.json"), {
|
|
569
|
+
active: true,
|
|
570
|
+
mode: "deep-interview",
|
|
571
|
+
current_phase: "intent-first",
|
|
572
|
+
session_id: sessionId,
|
|
573
|
+
});
|
|
574
|
+
const output = parseSingleJsonStdout(runNativeHookCli({
|
|
575
|
+
hook_event_name: "PreToolUse",
|
|
576
|
+
cwd,
|
|
577
|
+
session_id: sessionId,
|
|
578
|
+
thread_id: "thread-cli-deep-interview-pretool-boundary",
|
|
579
|
+
tool_name: "Write",
|
|
580
|
+
tool_input: { file_path: "src/runtime.ts", content: "export const changed = true;\n" },
|
|
581
|
+
}, { cwd }));
|
|
582
|
+
assert.deepEqual(Object.keys(output).sort(), ["systemMessage"]);
|
|
583
|
+
assert.match(String(output.systemMessage ?? ""), /Deep-interview is active \(phase: intent-first\)/);
|
|
584
|
+
assert.match(String(output.systemMessage ?? ""), /implementation\/write tools are blocked/);
|
|
585
|
+
assert.match(String(output.systemMessage ?? ""), /requirements\/spec mode/);
|
|
586
|
+
assert.equal(output.decision, undefined);
|
|
587
|
+
assert.equal(output.reason, undefined);
|
|
588
|
+
assert.equal(output.hookSpecificOutput, undefined);
|
|
589
|
+
}
|
|
590
|
+
finally {
|
|
591
|
+
await rm(cwd, { recursive: true, force: true });
|
|
592
|
+
}
|
|
593
|
+
});
|
|
424
594
|
it("emits parseable no-op JSON stdout for inactive Stop CLI runs", async () => {
|
|
425
595
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-noop-json-"));
|
|
426
596
|
try {
|
|
@@ -439,6 +609,31 @@ describe("codex native hook dispatch", () => {
|
|
|
439
609
|
await rm(cwd, { recursive: true, force: true });
|
|
440
610
|
}
|
|
441
611
|
});
|
|
612
|
+
it("emits no-op JSON stdout for Stop payloads with no runtime output", async () => {
|
|
613
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-null-output-"));
|
|
614
|
+
try {
|
|
615
|
+
const result = spawnSync(process.execPath, [nativeHookScriptPath()], {
|
|
616
|
+
cwd,
|
|
617
|
+
input: JSON.stringify({
|
|
618
|
+
hook_event_name: "Stop",
|
|
619
|
+
cwd,
|
|
620
|
+
session_id: "sess-cli-stop-null-output",
|
|
621
|
+
thread_id: "thread-cli-stop-null-output",
|
|
622
|
+
turn_id: "turn-cli-stop-null-output",
|
|
623
|
+
stop_hook_active: true,
|
|
624
|
+
}),
|
|
625
|
+
encoding: "utf-8",
|
|
626
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
627
|
+
});
|
|
628
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
629
|
+
assert.equal(result.stderr, "");
|
|
630
|
+
assert.deepEqual(parseSingleJsonStdout(result.stdout), {});
|
|
631
|
+
assert.equal(existsSync(join(cwd, ".omx", "state")), false);
|
|
632
|
+
}
|
|
633
|
+
finally {
|
|
634
|
+
await rm(cwd, { recursive: true, force: true });
|
|
635
|
+
}
|
|
636
|
+
});
|
|
442
637
|
it("returns empty JSON for oversized Stop stdin without parsing or creating inactive state", async () => {
|
|
443
638
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-"));
|
|
444
639
|
try {
|
|
@@ -567,6 +762,79 @@ describe("codex native hook dispatch", () => {
|
|
|
567
762
|
await rm(cwd, { recursive: true, force: true });
|
|
568
763
|
}
|
|
569
764
|
});
|
|
765
|
+
it("does not block Stop on stale session autopilot mirror when canonical skill state is inactive", async () => {
|
|
766
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stale-autopilot-mirror-"));
|
|
767
|
+
try {
|
|
768
|
+
const sessionId = "sess-stale-autopilot-mirror";
|
|
769
|
+
await writeJson(join(cwd, ".omx", "state", "session.json"), { session_id: sessionId, cwd });
|
|
770
|
+
await writeJson(join(cwd, ".omx", "state", "skill-active-state.json"), {
|
|
771
|
+
version: 1,
|
|
772
|
+
active: false,
|
|
773
|
+
skill: "",
|
|
774
|
+
phase: "cancelled",
|
|
775
|
+
active_skills: [],
|
|
776
|
+
});
|
|
777
|
+
await writeJson(join(cwd, ".omx", "state", "sessions", sessionId, "autopilot-state.json"), {
|
|
778
|
+
active: true,
|
|
779
|
+
mode: "autopilot",
|
|
780
|
+
current_phase: "ultragoal",
|
|
781
|
+
session_id: sessionId,
|
|
782
|
+
});
|
|
783
|
+
await writeJson(join(cwd, ".omx", "state", "sessions", sessionId, "skill-active-state.json"), {
|
|
784
|
+
version: 1,
|
|
785
|
+
active: true,
|
|
786
|
+
skill: "autopilot",
|
|
787
|
+
phase: "ultragoal",
|
|
788
|
+
session_id: sessionId,
|
|
789
|
+
active_skills: [{ skill: "autopilot", phase: "ultragoal", active: true, session_id: sessionId }],
|
|
790
|
+
});
|
|
791
|
+
const stdout = runNativeHookCli({
|
|
792
|
+
hook_event_name: "Stop",
|
|
793
|
+
cwd,
|
|
794
|
+
session_id: sessionId,
|
|
795
|
+
thread_id: "thread-stale-autopilot-mirror",
|
|
796
|
+
}, { cwd });
|
|
797
|
+
assert.deepEqual(parseSingleJsonStdout(stdout), {});
|
|
798
|
+
}
|
|
799
|
+
finally {
|
|
800
|
+
await rm(cwd, { recursive: true, force: true });
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
it("includes blocking state source and canonical agreement in Stop diagnostics", async () => {
|
|
804
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-diagnostic-"));
|
|
805
|
+
try {
|
|
806
|
+
const sessionId = "sess-autopilot-diagnostic";
|
|
807
|
+
await writeJson(join(cwd, ".omx", "state", "session.json"), { session_id: sessionId, cwd });
|
|
808
|
+
await writeJson(join(cwd, ".omx", "state", "skill-active-state.json"), {
|
|
809
|
+
version: 1,
|
|
810
|
+
active: true,
|
|
811
|
+
skill: "autopilot",
|
|
812
|
+
phase: "ultragoal",
|
|
813
|
+
session_id: sessionId,
|
|
814
|
+
active_skills: [{ skill: "autopilot", phase: "ultragoal", active: true, session_id: sessionId }],
|
|
815
|
+
});
|
|
816
|
+
await writeJson(join(cwd, ".omx", "state", "sessions", sessionId, "autopilot-state.json"), {
|
|
817
|
+
active: true,
|
|
818
|
+
mode: "autopilot",
|
|
819
|
+
current_phase: "ultragoal",
|
|
820
|
+
session_id: sessionId,
|
|
821
|
+
});
|
|
822
|
+
const output = parseSingleJsonStdout(runNativeHookCli({
|
|
823
|
+
hook_event_name: "Stop",
|
|
824
|
+
cwd,
|
|
825
|
+
session_id: sessionId,
|
|
826
|
+
thread_id: "thread-autopilot-diagnostic",
|
|
827
|
+
}, { cwd }));
|
|
828
|
+
assert.equal(output.decision, "block");
|
|
829
|
+
assert.match(String(output.reason ?? ""), /state: \.omx\/state\/sessions\/sess-autopilot-diagnostic\/autopilot-state\.json/);
|
|
830
|
+
assert.match(String(output.reason ?? ""), /canonical: canonical_agrees/);
|
|
831
|
+
assert.equal(output.statePath, ".omx/state/sessions/sess-autopilot-diagnostic/autopilot-state.json");
|
|
832
|
+
assert.equal(output.canonicalDisagreement, "canonical_agrees");
|
|
833
|
+
}
|
|
834
|
+
finally {
|
|
835
|
+
await rm(cwd, { recursive: true, force: true });
|
|
836
|
+
}
|
|
837
|
+
});
|
|
570
838
|
it("emits exactly one parseable JSON object for active Stop CLI continuation", async () => {
|
|
571
839
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-json-"));
|
|
572
840
|
try {
|
|
@@ -1153,77 +1421,181 @@ describe("codex native hook dispatch", () => {
|
|
|
1153
1421
|
await rm(cwd, { recursive: true, force: true });
|
|
1154
1422
|
}
|
|
1155
1423
|
});
|
|
1156
|
-
it("
|
|
1157
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
1424
|
+
it("suppresses child-agent SessionStart and Stop before the canonical leader session is reconciled (#2831)", async () => {
|
|
1425
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-no-canonical-"));
|
|
1426
|
+
const originalCodexHome = process.env.CODEX_HOME;
|
|
1158
1427
|
try {
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1428
|
+
process.env.CODEX_HOME = join(cwd, "codex-home");
|
|
1429
|
+
await writeJson(join(process.env.CODEX_HOME, ".omx-config.json"), {
|
|
1430
|
+
notifications: {
|
|
1431
|
+
enabled: true,
|
|
1432
|
+
verbosity: "session",
|
|
1433
|
+
telegram: { enabled: true, botToken: "123:abc", chatId: "456" },
|
|
1434
|
+
},
|
|
1165
1435
|
});
|
|
1166
|
-
const
|
|
1436
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
1437
|
+
const leaderNativeSessionId = "codex-leader-thread-no-canonical";
|
|
1438
|
+
const childNativeSessionId = "codex-child-thread-no-canonical";
|
|
1439
|
+
await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
|
|
1440
|
+
await writeFile(join(cwd, ".omx", "hooks", "record-lifecycle.mjs"), [
|
|
1441
|
+
"import { appendFileSync } from 'node:fs';",
|
|
1442
|
+
"export async function onHookEvent(event) {",
|
|
1443
|
+
" appendFileSync('hook-events.jsonl', `${JSON.stringify({ event: event.event })}\\n`);",
|
|
1444
|
+
"}",
|
|
1445
|
+
].join("\n"));
|
|
1446
|
+
const transcriptPath = join(cwd, "no-canonical-subagent-rollout.jsonl");
|
|
1167
1447
|
await writeFile(transcriptPath, `${JSON.stringify({
|
|
1168
1448
|
type: "session_meta",
|
|
1169
1449
|
payload: {
|
|
1170
|
-
id:
|
|
1450
|
+
id: childNativeSessionId,
|
|
1171
1451
|
source: {
|
|
1172
1452
|
subagent: {
|
|
1173
1453
|
thread_spawn: {
|
|
1174
|
-
parent_thread_id:
|
|
1175
|
-
|
|
1176
|
-
agent_nickname: "Architect",
|
|
1177
|
-
agent_role: "architect",
|
|
1454
|
+
parent_thread_id: leaderNativeSessionId,
|
|
1455
|
+
agent_role: "explorer",
|
|
1178
1456
|
},
|
|
1179
1457
|
},
|
|
1180
1458
|
},
|
|
1181
|
-
agent_nickname: "Architect",
|
|
1182
|
-
agent_role: "architect",
|
|
1183
1459
|
},
|
|
1184
1460
|
})}\n`);
|
|
1185
1461
|
await dispatchCodexNativeHook({
|
|
1186
1462
|
hook_event_name: "SessionStart",
|
|
1187
1463
|
cwd,
|
|
1188
|
-
session_id:
|
|
1464
|
+
session_id: childNativeSessionId,
|
|
1189
1465
|
transcript_path: transcriptPath,
|
|
1190
1466
|
}, { cwd, sessionOwnerPid: process.pid });
|
|
1467
|
+
assert.equal(existsSync(join(cwd, "hook-events.jsonl")), false, "child SessionStart must be suppressed even before the canonical leader session is reconciled");
|
|
1468
|
+
assert.equal(existsSync(join(stateDir, "session.json")), false, "child SessionStart must not be promoted into a root/leader session");
|
|
1191
1469
|
const tracking = JSON.parse(await readFile(join(stateDir, "subagent-tracking.json"), "utf-8"));
|
|
1192
|
-
assert.equal(tracking.sessions?.[
|
|
1193
|
-
assert.equal(tracking.sessions?.[
|
|
1194
|
-
|
|
1470
|
+
assert.equal(tracking.sessions?.[leaderNativeSessionId]?.leader_thread_id, leaderNativeSessionId);
|
|
1471
|
+
assert.equal(tracking.sessions?.[leaderNativeSessionId]?.threads?.[childNativeSessionId]?.kind, "subagent");
|
|
1472
|
+
await dispatchCodexNativeHook({
|
|
1473
|
+
hook_event_name: "Stop",
|
|
1474
|
+
cwd,
|
|
1475
|
+
session_id: childNativeSessionId,
|
|
1476
|
+
thread_id: childNativeSessionId,
|
|
1477
|
+
turn_id: "no-canonical-child-stop-turn",
|
|
1478
|
+
}, { cwd });
|
|
1479
|
+
assert.equal(existsSync(join(cwd, "hook-events.jsonl")), false, "child Stop must be suppressed when the start was recognized as subagent-scoped");
|
|
1195
1480
|
}
|
|
1196
1481
|
finally {
|
|
1482
|
+
if (originalCodexHome === undefined) {
|
|
1483
|
+
delete process.env.CODEX_HOME;
|
|
1484
|
+
}
|
|
1485
|
+
else {
|
|
1486
|
+
process.env.CODEX_HOME = originalCodexHome;
|
|
1487
|
+
}
|
|
1197
1488
|
await rm(cwd, { recursive: true, force: true });
|
|
1198
1489
|
}
|
|
1199
1490
|
});
|
|
1200
|
-
it("
|
|
1201
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
1491
|
+
it("preserves root/leader SessionStart dispatch at session verbosity (#2831)", async () => {
|
|
1492
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-root-session-start-preserved-"));
|
|
1493
|
+
const originalCodexHome = process.env.CODEX_HOME;
|
|
1202
1494
|
try {
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
nativeSessionId: leaderNativeSessionId,
|
|
1211
|
-
});
|
|
1212
|
-
await writeJson(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), {
|
|
1213
|
-
active: true,
|
|
1214
|
-
mode: "ralph",
|
|
1215
|
-
current_phase: "executing",
|
|
1216
|
-
iteration: 1,
|
|
1217
|
-
max_iterations: 5,
|
|
1495
|
+
process.env.CODEX_HOME = join(cwd, "codex-home");
|
|
1496
|
+
await writeJson(join(process.env.CODEX_HOME, ".omx-config.json"), {
|
|
1497
|
+
notifications: {
|
|
1498
|
+
enabled: true,
|
|
1499
|
+
verbosity: "session",
|
|
1500
|
+
telegram: { enabled: true, botToken: "123:abc", chatId: "456" },
|
|
1501
|
+
},
|
|
1218
1502
|
});
|
|
1219
|
-
|
|
1220
|
-
await writeFile(
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1503
|
+
await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
|
|
1504
|
+
await writeFile(join(cwd, ".omx", "hooks", "record-lifecycle.mjs"), [
|
|
1505
|
+
"import { appendFileSync } from 'node:fs';",
|
|
1506
|
+
"export async function onHookEvent(event) {",
|
|
1507
|
+
" appendFileSync('hook-events.jsonl', `${JSON.stringify({ event: event.event })}\\n`);",
|
|
1508
|
+
"}",
|
|
1509
|
+
].join("\n"));
|
|
1510
|
+
await dispatchCodexNativeHook({
|
|
1511
|
+
hook_event_name: "SessionStart",
|
|
1512
|
+
cwd,
|
|
1513
|
+
session_id: "codex-root-thread-preserved",
|
|
1514
|
+
}, { cwd, sessionOwnerPid: process.pid });
|
|
1515
|
+
const hookEvents = await readFile(join(cwd, "hook-events.jsonl"), "utf-8");
|
|
1516
|
+
assert.match(hookEvents, /"event":"session-start"/, "root/leader SessionStart must still dispatch at session verbosity");
|
|
1517
|
+
}
|
|
1518
|
+
finally {
|
|
1519
|
+
if (originalCodexHome === undefined) {
|
|
1520
|
+
delete process.env.CODEX_HOME;
|
|
1521
|
+
}
|
|
1522
|
+
else {
|
|
1523
|
+
process.env.CODEX_HOME = originalCodexHome;
|
|
1524
|
+
}
|
|
1525
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1528
|
+
it("keeps a self-parented native role thread as subagent evidence", async () => {
|
|
1529
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-self-parented-subagent-"));
|
|
1530
|
+
try {
|
|
1531
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
1532
|
+
const canonicalSessionId = "omx-autopilot-session";
|
|
1533
|
+
const nativeRoleThreadId = "codex-architect-thread";
|
|
1534
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
1535
|
+
await writeSessionStart(cwd, canonicalSessionId, {
|
|
1536
|
+
nativeSessionId: nativeRoleThreadId,
|
|
1537
|
+
});
|
|
1538
|
+
const transcriptPath = join(cwd, "architect-subagent-rollout.jsonl");
|
|
1539
|
+
await writeFile(transcriptPath, `${JSON.stringify({
|
|
1540
|
+
type: "session_meta",
|
|
1541
|
+
payload: {
|
|
1542
|
+
id: nativeRoleThreadId,
|
|
1543
|
+
source: {
|
|
1544
|
+
subagent: {
|
|
1545
|
+
thread_spawn: {
|
|
1546
|
+
parent_thread_id: nativeRoleThreadId,
|
|
1547
|
+
depth: 1,
|
|
1548
|
+
agent_nickname: "Architect",
|
|
1549
|
+
agent_role: "architect",
|
|
1550
|
+
},
|
|
1551
|
+
},
|
|
1552
|
+
},
|
|
1553
|
+
agent_nickname: "Architect",
|
|
1554
|
+
agent_role: "architect",
|
|
1555
|
+
},
|
|
1556
|
+
})}\n`);
|
|
1557
|
+
await dispatchCodexNativeHook({
|
|
1558
|
+
hook_event_name: "SessionStart",
|
|
1559
|
+
cwd,
|
|
1560
|
+
session_id: nativeRoleThreadId,
|
|
1561
|
+
transcript_path: transcriptPath,
|
|
1562
|
+
}, { cwd, sessionOwnerPid: process.pid });
|
|
1563
|
+
const tracking = JSON.parse(await readFile(join(stateDir, "subagent-tracking.json"), "utf-8"));
|
|
1564
|
+
assert.equal(tracking.sessions?.[canonicalSessionId]?.leader_thread_id, undefined);
|
|
1565
|
+
assert.equal(tracking.sessions?.[canonicalSessionId]?.threads?.[nativeRoleThreadId]?.kind, "subagent");
|
|
1566
|
+
assert.equal(tracking.sessions?.[canonicalSessionId]?.threads?.[nativeRoleThreadId]?.mode, "architect");
|
|
1567
|
+
}
|
|
1568
|
+
finally {
|
|
1569
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1570
|
+
}
|
|
1571
|
+
});
|
|
1572
|
+
it("does not attach a subagent SessionStart to an unrelated canonical leader", async () => {
|
|
1573
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-start-mismatch-"));
|
|
1574
|
+
try {
|
|
1575
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
1576
|
+
const canonicalSessionId = "omx-leader-session-a";
|
|
1577
|
+
const leaderNativeSessionId = "codex-leader-thread-a";
|
|
1578
|
+
const unrelatedParentNativeSessionId = "codex-leader-thread-b";
|
|
1579
|
+
const childNativeSessionId = "codex-child-thread-b";
|
|
1580
|
+
await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
|
|
1581
|
+
await writeSessionStart(cwd, canonicalSessionId, {
|
|
1582
|
+
nativeSessionId: leaderNativeSessionId,
|
|
1583
|
+
});
|
|
1584
|
+
await writeJson(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), {
|
|
1585
|
+
active: true,
|
|
1586
|
+
mode: "ralph",
|
|
1587
|
+
current_phase: "executing",
|
|
1588
|
+
iteration: 1,
|
|
1589
|
+
max_iterations: 5,
|
|
1590
|
+
});
|
|
1591
|
+
const transcriptPath = join(cwd, "unrelated-subagent-rollout.jsonl");
|
|
1592
|
+
await writeFile(transcriptPath, `${JSON.stringify({
|
|
1593
|
+
type: "session_meta",
|
|
1594
|
+
payload: {
|
|
1595
|
+
id: childNativeSessionId,
|
|
1596
|
+
source: {
|
|
1597
|
+
subagent: {
|
|
1598
|
+
thread_spawn: {
|
|
1227
1599
|
parent_thread_id: unrelatedParentNativeSessionId,
|
|
1228
1600
|
depth: 1,
|
|
1229
1601
|
agent_nickname: "Spinoza",
|
|
@@ -2276,6 +2648,60 @@ standardMaxRounds = 15
|
|
|
2276
2648
|
await rm(cwd, { recursive: true, force: true });
|
|
2277
2649
|
}
|
|
2278
2650
|
});
|
|
2651
|
+
it("nudges next-goal prompts when a completed Codex goal cleanup step remains", async () => {
|
|
2652
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-completed-goal-prompt-"));
|
|
2653
|
+
try {
|
|
2654
|
+
await writeJson(join(cwd, ".omx", "goals", "performance", "latency", "state.json"), {
|
|
2655
|
+
version: 1,
|
|
2656
|
+
workflow: "performance-goal",
|
|
2657
|
+
slug: "latency",
|
|
2658
|
+
objective: "Reduce latency",
|
|
2659
|
+
status: "complete",
|
|
2660
|
+
completedAt: "2026-05-20T00:00:00.000Z",
|
|
2661
|
+
});
|
|
2662
|
+
const result = await dispatchCodexNativeHook({
|
|
2663
|
+
hook_event_name: "UserPromptSubmit",
|
|
2664
|
+
cwd,
|
|
2665
|
+
session_id: "sess-completed-goal-prompt",
|
|
2666
|
+
thread_id: "thread-completed-goal-prompt",
|
|
2667
|
+
prompt: "Start the next performance goal now",
|
|
2668
|
+
}, { cwd });
|
|
2669
|
+
const output = JSON.stringify(result.outputJson);
|
|
2670
|
+
assert.match(output, /run \/goal clear/);
|
|
2671
|
+
assert.match(output, /before calling create_goal/);
|
|
2672
|
+
assert.match(output, /hooks only nudge and must not mutate Codex goal state/);
|
|
2673
|
+
assert.doesNotMatch(output, /cleared Codex goal state/i);
|
|
2674
|
+
}
|
|
2675
|
+
finally {
|
|
2676
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2677
|
+
}
|
|
2678
|
+
});
|
|
2679
|
+
it("blocks Stop when a final answer starts another goal over completed state", async () => {
|
|
2680
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-completed-goal-stop-"));
|
|
2681
|
+
try {
|
|
2682
|
+
await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
|
|
2683
|
+
version: 1,
|
|
2684
|
+
aggregateCompletion: { status: "complete", completedAt: "2026-05-20T00:00:00.000Z" },
|
|
2685
|
+
goals: [{ id: "G001-done", status: "complete", objective: "Done" }],
|
|
2686
|
+
});
|
|
2687
|
+
const result = await dispatchCodexNativeHook({
|
|
2688
|
+
hook_event_name: "Stop",
|
|
2689
|
+
cwd,
|
|
2690
|
+
session_id: "sess-completed-goal-stop",
|
|
2691
|
+
thread_id: "thread-completed-goal-stop",
|
|
2692
|
+
last_assistant_message: "Starting another ultragoal now; create_goal payload follows.",
|
|
2693
|
+
}, { cwd });
|
|
2694
|
+
const output = JSON.stringify(result.outputJson);
|
|
2695
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
2696
|
+
assert.equal(result.outputJson?.stopReason, "completed_codex_goal_cleanup_required");
|
|
2697
|
+
assert.match(output, /run \/goal clear/);
|
|
2698
|
+
assert.match(output, /before calling create_goal/);
|
|
2699
|
+
assert.match(output, /hooks only nudge and must not mutate Codex goal state/);
|
|
2700
|
+
}
|
|
2701
|
+
finally {
|
|
2702
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2703
|
+
}
|
|
2704
|
+
});
|
|
2279
2705
|
it("blocks ultragoal Stop for concise generic goal completion claims", async () => {
|
|
2280
2706
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-generic-complete-stop-"));
|
|
2281
2707
|
try {
|
|
@@ -4915,6 +5341,175 @@ exit 0
|
|
|
4915
5341
|
await rm(cwd, { recursive: true, force: true });
|
|
4916
5342
|
}
|
|
4917
5343
|
});
|
|
5344
|
+
it("allows read-only diagnostics mentioning apply_patch while deep-interview blocks real apply_patch invocations", async () => {
|
|
5345
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-grep-apply-patch-"));
|
|
5346
|
+
try {
|
|
5347
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
5348
|
+
const sessionDir = join(stateDir, "sessions", "sess-di-grep");
|
|
5349
|
+
await mkdir(sessionDir, { recursive: true });
|
|
5350
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-di-grep", cwd });
|
|
5351
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
5352
|
+
version: 1,
|
|
5353
|
+
active: true,
|
|
5354
|
+
skill: "deep-interview",
|
|
5355
|
+
phase: "planning",
|
|
5356
|
+
session_id: "sess-di-grep",
|
|
5357
|
+
active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-di-grep" }],
|
|
5358
|
+
});
|
|
5359
|
+
await writeJson(join(sessionDir, "deep-interview-state.json"), {
|
|
5360
|
+
active: true,
|
|
5361
|
+
mode: "deep-interview",
|
|
5362
|
+
current_phase: "intent-first",
|
|
5363
|
+
session_id: "sess-di-grep",
|
|
5364
|
+
});
|
|
5365
|
+
const allowedGrep = await dispatchCodexNativeHook({
|
|
5366
|
+
hook_event_name: "PreToolUse",
|
|
5367
|
+
cwd,
|
|
5368
|
+
session_id: "sess-di-grep",
|
|
5369
|
+
tool_name: "Bash",
|
|
5370
|
+
tool_use_id: "tool-di-grep",
|
|
5371
|
+
tool_input: { command: 'grep -n "apply_patch" dist/scripts/codex-native-hook.js' },
|
|
5372
|
+
}, { cwd });
|
|
5373
|
+
assert.equal(allowedGrep.outputJson, null);
|
|
5374
|
+
const allowedSubshellGrep = await dispatchCodexNativeHook({
|
|
5375
|
+
hook_event_name: "PreToolUse",
|
|
5376
|
+
cwd,
|
|
5377
|
+
session_id: "sess-di-grep",
|
|
5378
|
+
tool_name: "Bash",
|
|
5379
|
+
tool_use_id: "tool-di-grep-subshell",
|
|
5380
|
+
tool_input: { command: '(grep -n "apply_patch" dist/scripts/codex-native-hook.js)' },
|
|
5381
|
+
}, { cwd });
|
|
5382
|
+
assert.equal(allowedSubshellGrep.outputJson, null);
|
|
5383
|
+
// Double-quoted spans that merely mention the literal token, expand a
|
|
5384
|
+
// parameter, or run a substitution that is not `apply_patch` stay allowed
|
|
5385
|
+
// — the quoted-substitution fix must not over-block read-only diagnostics.
|
|
5386
|
+
const allowedQuotedMention = await dispatchCodexNativeHook({
|
|
5387
|
+
hook_event_name: "PreToolUse",
|
|
5388
|
+
cwd,
|
|
5389
|
+
session_id: "sess-di-grep",
|
|
5390
|
+
tool_name: "Bash",
|
|
5391
|
+
tool_use_id: "tool-di-grep-quoted-mention",
|
|
5392
|
+
tool_input: { command: 'echo "${apply_patch} $(echo apply_patch)"' },
|
|
5393
|
+
}, { cwd });
|
|
5394
|
+
assert.equal(allowedQuotedMention.outputJson, null);
|
|
5395
|
+
const blockedApplyPatch = await dispatchCodexNativeHook({
|
|
5396
|
+
hook_event_name: "PreToolUse",
|
|
5397
|
+
cwd,
|
|
5398
|
+
session_id: "sess-di-grep",
|
|
5399
|
+
tool_name: "Bash",
|
|
5400
|
+
tool_use_id: "tool-di-apply-patch-invoke",
|
|
5401
|
+
tool_input: { command: "apply_patch <<'EOF'\n*** Begin Patch\n*** Add File: src/leak.ts\n+export const x = 1;\n*** End Patch\nEOF" },
|
|
5402
|
+
}, { cwd });
|
|
5403
|
+
assert.equal(blockedApplyPatch.outputJson?.decision, "block");
|
|
5404
|
+
const blockedEnvAssignmentApplyPatch = await dispatchCodexNativeHook({
|
|
5405
|
+
hook_event_name: "PreToolUse",
|
|
5406
|
+
cwd,
|
|
5407
|
+
session_id: "sess-di-grep",
|
|
5408
|
+
tool_name: "Bash",
|
|
5409
|
+
tool_use_id: "tool-di-apply-patch-env-assignment",
|
|
5410
|
+
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" },
|
|
5411
|
+
}, { cwd });
|
|
5412
|
+
assert.equal(blockedEnvAssignmentApplyPatch.outputJson?.decision, "block");
|
|
5413
|
+
const blockedEnvWrapperApplyPatch = await dispatchCodexNativeHook({
|
|
5414
|
+
hook_event_name: "PreToolUse",
|
|
5415
|
+
cwd,
|
|
5416
|
+
session_id: "sess-di-grep",
|
|
5417
|
+
tool_name: "Bash",
|
|
5418
|
+
tool_use_id: "tool-di-apply-patch-env-wrapper",
|
|
5419
|
+
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" },
|
|
5420
|
+
}, { cwd });
|
|
5421
|
+
assert.equal(blockedEnvWrapperApplyPatch.outputJson?.decision, "block");
|
|
5422
|
+
const heredocBody = "\n*** Begin Patch\n*** Add File: src/leak.ts\n+export const x = 1;\n*** End Patch\nEOF";
|
|
5423
|
+
const blockedRealApplyPatchForms = [
|
|
5424
|
+
{ id: "tool-di-apply-patch-env-i", command: `env -i apply_patch <<'EOF'${heredocBody}` },
|
|
5425
|
+
{ id: "tool-di-apply-patch-env-unset", command: `env -u FOO apply_patch <<'EOF'${heredocBody}` },
|
|
5426
|
+
{ id: "tool-di-apply-patch-env-i-assignment", command: `env -i FOO=bar apply_patch <<'EOF'${heredocBody}` },
|
|
5427
|
+
{ id: "tool-di-apply-patch-assignment-env", command: `FOO=bar env apply_patch <<'EOF'${heredocBody}` },
|
|
5428
|
+
{ id: "tool-di-apply-patch-exec-env-assignment", command: `exec env FOO=bar apply_patch <<'EOF'${heredocBody}` },
|
|
5429
|
+
{ id: "tool-di-apply-patch-absolute-path", command: `/usr/bin/apply_patch <<'EOF'${heredocBody}` },
|
|
5430
|
+
{ id: "tool-di-apply-patch-relative-path", command: `./apply_patch <<'EOF'${heredocBody}` },
|
|
5431
|
+
{ id: "tool-di-apply-patch-subshell", command: `(apply_patch <<'EOF'${heredocBody}\n)` },
|
|
5432
|
+
{ id: "tool-di-apply-patch-subshell-spaced", command: `( apply_patch <<'EOF'${heredocBody}\n)` },
|
|
5433
|
+
{ id: "tool-di-apply-patch-double-subshell", command: `((apply_patch <<'EOF'${heredocBody}\n))` },
|
|
5434
|
+
{ id: "tool-di-apply-patch-pipe-subshell", command: `true | (apply_patch <<'EOF'${heredocBody}\n)` },
|
|
5435
|
+
{ id: "tool-di-apply-patch-subshell-env", command: `(env apply_patch <<'EOF'${heredocBody}\n)` },
|
|
5436
|
+
{ id: "tool-di-apply-patch-command-substitution", command: `x=$(apply_patch <<'EOF'${heredocBody}\n)` },
|
|
5437
|
+
{ id: "tool-di-apply-patch-brace-group", command: `{ apply_patch <<'EOF'${heredocBody}\n}` },
|
|
5438
|
+
// Command substitution runs even inside double quotes, so quoting the
|
|
5439
|
+
// already-blocked `$(…)` / `` `…` `` form must not bypass the guard.
|
|
5440
|
+
{ id: "tool-di-apply-patch-quoted-command-substitution", command: `echo "$(apply_patch <<'EOF'${heredocBody}\n)"` },
|
|
5441
|
+
{ id: "tool-di-apply-patch-quoted-backtick", command: `echo "\`apply_patch <<'EOF'${heredocBody}\`"` },
|
|
5442
|
+
{ id: "tool-di-apply-patch-quoted-command-substitution-prefixed", command: `echo "patched: $(apply_patch <<'EOF'${heredocBody}\n) done"` },
|
|
5443
|
+
];
|
|
5444
|
+
for (const form of blockedRealApplyPatchForms) {
|
|
5445
|
+
const blockedForm = await dispatchCodexNativeHook({
|
|
5446
|
+
hook_event_name: "PreToolUse",
|
|
5447
|
+
cwd,
|
|
5448
|
+
session_id: "sess-di-grep",
|
|
5449
|
+
tool_name: "Bash",
|
|
5450
|
+
tool_use_id: form.id,
|
|
5451
|
+
tool_input: { command: form.command },
|
|
5452
|
+
}, { cwd });
|
|
5453
|
+
assert.equal(blockedForm.outputJson?.decision, "block", `expected deep-interview to block real apply_patch form: ${form.command}`);
|
|
5454
|
+
}
|
|
5455
|
+
}
|
|
5456
|
+
finally {
|
|
5457
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5458
|
+
}
|
|
5459
|
+
});
|
|
5460
|
+
it("allows deep-interview same-command literal variable redirects to artifacts while blocking variable redirects outside them", async () => {
|
|
5461
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-var-redirect-"));
|
|
5462
|
+
try {
|
|
5463
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
5464
|
+
const sessionDir = join(stateDir, "sessions", "sess-di-var-redirect");
|
|
5465
|
+
await mkdir(sessionDir, { recursive: true });
|
|
5466
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-di-var-redirect", cwd });
|
|
5467
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
5468
|
+
version: 1,
|
|
5469
|
+
active: true,
|
|
5470
|
+
skill: "deep-interview",
|
|
5471
|
+
phase: "planning",
|
|
5472
|
+
session_id: "sess-di-var-redirect",
|
|
5473
|
+
active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-di-var-redirect" }],
|
|
5474
|
+
});
|
|
5475
|
+
await writeJson(join(sessionDir, "deep-interview-state.json"), {
|
|
5476
|
+
active: true,
|
|
5477
|
+
mode: "deep-interview",
|
|
5478
|
+
current_phase: "intent-first",
|
|
5479
|
+
session_id: "sess-di-var-redirect",
|
|
5480
|
+
});
|
|
5481
|
+
const allowedVarRedirect = await dispatchCodexNativeHook({
|
|
5482
|
+
hook_event_name: "PreToolUse",
|
|
5483
|
+
cwd,
|
|
5484
|
+
session_id: "sess-di-var-redirect",
|
|
5485
|
+
tool_name: "Bash",
|
|
5486
|
+
tool_use_id: "tool-di-var-redirect-allow",
|
|
5487
|
+
tool_input: { command: 'SNAP=".omx/context/example.md"\ncat > "$SNAP" <<\'EOF\'\ncontent\nEOF' },
|
|
5488
|
+
}, { cwd });
|
|
5489
|
+
assert.equal(allowedVarRedirect.outputJson, null);
|
|
5490
|
+
const blockedVarRedirect = await dispatchCodexNativeHook({
|
|
5491
|
+
hook_event_name: "PreToolUse",
|
|
5492
|
+
cwd,
|
|
5493
|
+
session_id: "sess-di-var-redirect",
|
|
5494
|
+
tool_name: "Bash",
|
|
5495
|
+
tool_use_id: "tool-di-var-redirect-block",
|
|
5496
|
+
tool_input: { command: 'SNAP="src/leak.ts"\ncat > "$SNAP" <<\'EOF\'\ncontent\nEOF' },
|
|
5497
|
+
}, { cwd });
|
|
5498
|
+
assert.equal(blockedVarRedirect.outputJson?.decision, "block");
|
|
5499
|
+
const blockedUnresolvedVarRedirect = await dispatchCodexNativeHook({
|
|
5500
|
+
hook_event_name: "PreToolUse",
|
|
5501
|
+
cwd,
|
|
5502
|
+
session_id: "sess-di-var-redirect",
|
|
5503
|
+
tool_name: "Bash",
|
|
5504
|
+
tool_use_id: "tool-di-var-redirect-unresolved",
|
|
5505
|
+
tool_input: { command: 'cat > "$SNAP" <<\'EOF\'\ncontent\nEOF' },
|
|
5506
|
+
}, { cwd });
|
|
5507
|
+
assert.equal(blockedUnresolvedVarRedirect.outputJson?.decision, "block");
|
|
5508
|
+
}
|
|
5509
|
+
finally {
|
|
5510
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5511
|
+
}
|
|
5512
|
+
});
|
|
4918
5513
|
it("allows deep-interview apply_patch artifact writes from freeform patch text while blocking outside paths", async () => {
|
|
4919
5514
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-apply-patch-"));
|
|
4920
5515
|
try {
|
|
@@ -5006,6 +5601,127 @@ exit 0
|
|
|
5006
5601
|
await rm(cwd, { recursive: true, force: true });
|
|
5007
5602
|
}
|
|
5008
5603
|
});
|
|
5604
|
+
it("allows Ralplan read-only inspection and planning artifact writes while blocking implementation targets", async () => {
|
|
5605
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-ralplan-guard-"));
|
|
5606
|
+
try {
|
|
5607
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
5608
|
+
const sessionId = "sess-ralplan-guard";
|
|
5609
|
+
const sessionDir = join(stateDir, "sessions", sessionId);
|
|
5610
|
+
await mkdir(sessionDir, { recursive: true });
|
|
5611
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, cwd });
|
|
5612
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
5613
|
+
version: 1,
|
|
5614
|
+
active: true,
|
|
5615
|
+
skill: "ralplan",
|
|
5616
|
+
phase: "planning",
|
|
5617
|
+
session_id: sessionId,
|
|
5618
|
+
active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
|
|
5619
|
+
});
|
|
5620
|
+
await writeJson(join(sessionDir, "ralplan-state.json"), {
|
|
5621
|
+
active: true,
|
|
5622
|
+
mode: "ralplan",
|
|
5623
|
+
current_phase: "planning",
|
|
5624
|
+
session_id: sessionId,
|
|
5625
|
+
});
|
|
5626
|
+
const preToolUse = async (tool_name, tool_use_id, tool_input) => dispatchCodexNativeHook({
|
|
5627
|
+
hook_event_name: "PreToolUse",
|
|
5628
|
+
cwd,
|
|
5629
|
+
session_id: sessionId,
|
|
5630
|
+
tool_name,
|
|
5631
|
+
tool_use_id,
|
|
5632
|
+
tool_input,
|
|
5633
|
+
}, { cwd });
|
|
5634
|
+
const readOnlyCommands = [
|
|
5635
|
+
"git status --short",
|
|
5636
|
+
"ls -1 .omx/plans",
|
|
5637
|
+
"find .omx/plans -type f -name '*.md'",
|
|
5638
|
+
"grep -RIn \"Scholastic\" doc tests .omx/plans .omx/context",
|
|
5639
|
+
"rg \"Scholastic\" doc tests .omx/plans .omx/context",
|
|
5640
|
+
"sed -n '1,80p' .omx/plans/demo.md",
|
|
5641
|
+
"cat .omx/plans/demo.md",
|
|
5642
|
+
"git status --short; ls -1 .omx/plans; grep -RIn \"Scholastic\" doc tests .omx/plans .omx/context",
|
|
5643
|
+
];
|
|
5644
|
+
for (const [index, command] of readOnlyCommands.entries()) {
|
|
5645
|
+
const result = await preToolUse("Bash", `tool-ralplan-read-${index}`, { command });
|
|
5646
|
+
assert.equal(result.outputJson, null, `read-only command should be allowed: ${command}`);
|
|
5647
|
+
}
|
|
5648
|
+
for (const path of [
|
|
5649
|
+
".omx/context/findings.md",
|
|
5650
|
+
".omx/plans/issue-2863.md",
|
|
5651
|
+
".omx/specs/issue-2863.md",
|
|
5652
|
+
".omx/state/required-planning-state.json",
|
|
5653
|
+
]) {
|
|
5654
|
+
const writeResult = await preToolUse("Write", `tool-ralplan-write-${path}`, { file_path: path, content: "ok" });
|
|
5655
|
+
assert.equal(writeResult.outputJson, null, `Write should be allowed for ${path}`);
|
|
5656
|
+
}
|
|
5657
|
+
const allowedPatchAdd = await preToolUse("apply_patch", "tool-ralplan-patch-add", {
|
|
5658
|
+
input: "*** Begin Patch\n*** Add File: .omx/plans/issue-2863.md\n+# Plan\n*** End Patch\n",
|
|
5659
|
+
});
|
|
5660
|
+
assert.equal(allowedPatchAdd.outputJson, null);
|
|
5661
|
+
const allowedPatchUpdate = await preToolUse("ApplyPatch", "tool-ralplan-patch-update", {
|
|
5662
|
+
input: "*** Begin Patch\n*** Update File: .omx/plans/issue-2863.md\n@@\n-old\n+new\n*** End Patch\n",
|
|
5663
|
+
});
|
|
5664
|
+
assert.equal(allowedPatchUpdate.outputJson, null);
|
|
5665
|
+
const allowedRedirect = await preToolUse("Bash", "tool-ralplan-redirect-allow", {
|
|
5666
|
+
command: "printf '\\nmore\\n' >> .omx/plans/issue-2863.md",
|
|
5667
|
+
});
|
|
5668
|
+
assert.equal(allowedRedirect.outputJson, null);
|
|
5669
|
+
const allowedTee = await preToolUse("Bash", "tool-ralplan-tee-allow", {
|
|
5670
|
+
command: "printf '\\nmore\\n' | tee -a .omx/specs/issue-2863.md",
|
|
5671
|
+
});
|
|
5672
|
+
assert.equal(allowedTee.outputJson, null);
|
|
5673
|
+
const blockedRedirect = await preToolUse("Bash", "tool-ralplan-redirect-block", {
|
|
5674
|
+
command: "printf 'bad' > src/implementation.ts",
|
|
5675
|
+
});
|
|
5676
|
+
assert.equal(blockedRedirect.outputJson?.decision, "block");
|
|
5677
|
+
const redirectReason = String(blockedRedirect.outputJson?.reason ?? "");
|
|
5678
|
+
assert.match(redirectReason, /Bash redirect write/);
|
|
5679
|
+
assert.match(redirectReason, /src\/implementation\.ts/);
|
|
5680
|
+
const blockedEdit = await preToolUse("Edit", "tool-ralplan-edit-block", {
|
|
5681
|
+
file_path: "src/implementation.ts",
|
|
5682
|
+
old_string: "a",
|
|
5683
|
+
new_string: "b",
|
|
5684
|
+
});
|
|
5685
|
+
assert.equal(blockedEdit.outputJson?.decision, "block");
|
|
5686
|
+
const editReason = String(blockedEdit.outputJson?.reason ?? "");
|
|
5687
|
+
assert.match(editReason, /Edit path/);
|
|
5688
|
+
assert.match(editReason, /src\/implementation\.ts/);
|
|
5689
|
+
const blockedMixedPatch = await preToolUse("apply_patch", "tool-ralplan-patch-mixed", {
|
|
5690
|
+
input: "*** Begin Patch\n*** Add File: .omx/plans/ok.md\n+ok\n*** Add File: src/leak.ts\n+leak\n*** End Patch\n",
|
|
5691
|
+
});
|
|
5692
|
+
assert.equal(blockedMixedPatch.outputJson?.decision, "block");
|
|
5693
|
+
const mixedReason = String(blockedMixedPatch.outputJson?.reason ?? "");
|
|
5694
|
+
assert.match(mixedReason, /apply_patch target/);
|
|
5695
|
+
assert.match(mixedReason, /src\/leak\.ts/);
|
|
5696
|
+
const blockedUnparseablePatch = await preToolUse("apply_patch", "tool-ralplan-patch-unparseable", {
|
|
5697
|
+
input: "not a recognizable patch",
|
|
5698
|
+
});
|
|
5699
|
+
assert.equal(blockedUnparseablePatch.outputJson?.decision, "block");
|
|
5700
|
+
assert.match(String(blockedUnparseablePatch.outputJson?.reason ?? ""), /apply_patch target extraction failed/);
|
|
5701
|
+
const blockedUnresolvedRedirect = await preToolUse("Bash", "tool-ralplan-redirect-unresolved", {
|
|
5702
|
+
command: "cat > \"$PLAN_PATH\" <<'EOF'\ncontent\nEOF",
|
|
5703
|
+
});
|
|
5704
|
+
assert.equal(blockedUnresolvedRedirect.outputJson?.decision, "block");
|
|
5705
|
+
const unresolvedReason = String(blockedUnresolvedRedirect.outputJson?.reason ?? "");
|
|
5706
|
+
assert.match(unresolvedReason, /unresolved Bash write target/);
|
|
5707
|
+
assert.match(unresolvedReason, /\$PLAN_PATH/);
|
|
5708
|
+
const blockedTraversal = await preToolUse("Write", "tool-ralplan-traversal-block", {
|
|
5709
|
+
file_path: ".omx/plans/../../src/leak.ts",
|
|
5710
|
+
content: "bad",
|
|
5711
|
+
});
|
|
5712
|
+
assert.equal(blockedTraversal.outputJson?.decision, "block");
|
|
5713
|
+
assert.match(String(blockedTraversal.outputJson?.reason ?? ""), /\.omx\/plans\/\.\.\/\.\.\/src\/leak\.ts/);
|
|
5714
|
+
const blockedAbsolute = await preToolUse("Write", "tool-ralplan-absolute-block", {
|
|
5715
|
+
file_path: "/tmp/leak.ts",
|
|
5716
|
+
content: "bad",
|
|
5717
|
+
});
|
|
5718
|
+
assert.equal(blockedAbsolute.outputJson?.decision, "block");
|
|
5719
|
+
assert.match(String(blockedAbsolute.outputJson?.reason ?? ""), /\/tmp\/leak\.ts/);
|
|
5720
|
+
}
|
|
5721
|
+
finally {
|
|
5722
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5723
|
+
}
|
|
5724
|
+
});
|
|
5009
5725
|
it("allows Autopilot ralplan planning artifacts while blocking implementation writes", async () => {
|
|
5010
5726
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-autopilot-ralplan-artifact-"));
|
|
5011
5727
|
try {
|
|
@@ -5074,6 +5790,225 @@ exit 0
|
|
|
5074
5790
|
await rm(cwd, { recursive: true, force: true });
|
|
5075
5791
|
}
|
|
5076
5792
|
});
|
|
5793
|
+
it("allows Ralplan Beads tracker metadata writes during planning", async () => {
|
|
5794
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-ralplan-beads-"));
|
|
5795
|
+
try {
|
|
5796
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
5797
|
+
const sessionDir = join(stateDir, "sessions", "sess-ralplan-beads");
|
|
5798
|
+
await mkdir(sessionDir, { recursive: true });
|
|
5799
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-ralplan-beads", cwd });
|
|
5800
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
5801
|
+
version: 1,
|
|
5802
|
+
active: true,
|
|
5803
|
+
skill: "ralplan",
|
|
5804
|
+
phase: "planning",
|
|
5805
|
+
session_id: "sess-ralplan-beads",
|
|
5806
|
+
thread_id: "thread-ralplan-beads",
|
|
5807
|
+
active_skills: [
|
|
5808
|
+
{
|
|
5809
|
+
skill: "ralplan",
|
|
5810
|
+
phase: "planning",
|
|
5811
|
+
active: true,
|
|
5812
|
+
session_id: "sess-ralplan-beads",
|
|
5813
|
+
thread_id: "thread-ralplan-beads",
|
|
5814
|
+
},
|
|
5815
|
+
],
|
|
5816
|
+
});
|
|
5817
|
+
await writeJson(join(sessionDir, "ralplan-state.json"), {
|
|
5818
|
+
active: true,
|
|
5819
|
+
mode: "ralplan",
|
|
5820
|
+
current_phase: "planning",
|
|
5821
|
+
session_id: "sess-ralplan-beads",
|
|
5822
|
+
thread_id: "thread-ralplan-beads",
|
|
5823
|
+
});
|
|
5824
|
+
const allowedWrite = await dispatchCodexNativeHook({
|
|
5825
|
+
hook_event_name: "PreToolUse",
|
|
5826
|
+
cwd,
|
|
5827
|
+
session_id: "sess-ralplan-beads",
|
|
5828
|
+
thread_id: "thread-ralplan-beads",
|
|
5829
|
+
tool_name: "Write",
|
|
5830
|
+
tool_use_id: "tool-ralplan-beads-write",
|
|
5831
|
+
tool_input: { file_path: ".beads/issues.db", content: "metadata" },
|
|
5832
|
+
}, { cwd });
|
|
5833
|
+
assert.equal(allowedWrite.outputJson, null);
|
|
5834
|
+
const allowedEdit = await dispatchCodexNativeHook({
|
|
5835
|
+
hook_event_name: "PreToolUse",
|
|
5836
|
+
cwd,
|
|
5837
|
+
session_id: "sess-ralplan-beads",
|
|
5838
|
+
thread_id: "thread-ralplan-beads",
|
|
5839
|
+
tool_name: "Edit",
|
|
5840
|
+
tool_use_id: "tool-ralplan-beads-edit",
|
|
5841
|
+
tool_input: { file_path: ".beads/tasks/issue.json", old_string: "old", new_string: "new" },
|
|
5842
|
+
}, { cwd });
|
|
5843
|
+
assert.equal(allowedEdit.outputJson, null);
|
|
5844
|
+
const allowedPatch = await dispatchCodexNativeHook({
|
|
5845
|
+
hook_event_name: "PreToolUse",
|
|
5846
|
+
cwd,
|
|
5847
|
+
session_id: "sess-ralplan-beads",
|
|
5848
|
+
thread_id: "thread-ralplan-beads",
|
|
5849
|
+
tool_name: "apply_patch",
|
|
5850
|
+
tool_use_id: "tool-ralplan-beads-apply-patch",
|
|
5851
|
+
tool_input: {
|
|
5852
|
+
input: "*** Begin Patch\n*** Add File: .beads/tasks/issue.json\n+{}\n*** End Patch\n",
|
|
5853
|
+
},
|
|
5854
|
+
}, { cwd });
|
|
5855
|
+
assert.equal(allowedPatch.outputJson, null);
|
|
5856
|
+
const allowedCommands = [
|
|
5857
|
+
"bd --db .beads/issues.db create 'Issue title'",
|
|
5858
|
+
"bd --db .beads/issues.db update OMX-1 --title 'Issue title'",
|
|
5859
|
+
"bd --db .beads/issues.db edit OMX-1 --title 'Issue title'",
|
|
5860
|
+
"bd --db .beads/issues.db comments add OMX-1 'evidence'",
|
|
5861
|
+
"bd --db .beads/issues.db close OMX-1",
|
|
5862
|
+
"bd --db .beads/issues.db reopen OMX-1",
|
|
5863
|
+
"bd --db .beads/issues.db status OMX-1",
|
|
5864
|
+
"bd --db .beads/issues.db dep add OMX-1 OMX-2",
|
|
5865
|
+
];
|
|
5866
|
+
for (const [index, command] of allowedCommands.entries()) {
|
|
5867
|
+
const result = await dispatchCodexNativeHook({
|
|
5868
|
+
hook_event_name: "PreToolUse",
|
|
5869
|
+
cwd,
|
|
5870
|
+
session_id: "sess-ralplan-beads",
|
|
5871
|
+
thread_id: "thread-ralplan-beads",
|
|
5872
|
+
tool_name: "Bash",
|
|
5873
|
+
tool_use_id: `tool-ralplan-beads-bd-${index}`,
|
|
5874
|
+
tool_input: { command },
|
|
5875
|
+
}, { cwd });
|
|
5876
|
+
assert.equal(result.outputJson, null, `expected Beads metadata command to be allowed: ${command}`);
|
|
5877
|
+
}
|
|
5878
|
+
}
|
|
5879
|
+
finally {
|
|
5880
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5881
|
+
}
|
|
5882
|
+
});
|
|
5883
|
+
it("blocks unsafe Beads tracker targets and mixed implementation writes during Autopilot ralplan planning", async () => {
|
|
5884
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-autopilot-beads-block-"));
|
|
5885
|
+
try {
|
|
5886
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
5887
|
+
const sessionDir = join(stateDir, "sessions", "sess-autopilot-beads-block");
|
|
5888
|
+
await mkdir(sessionDir, { recursive: true });
|
|
5889
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-autopilot-beads-block", cwd });
|
|
5890
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
5891
|
+
version: 1,
|
|
5892
|
+
active: true,
|
|
5893
|
+
skill: "autopilot",
|
|
5894
|
+
phase: "ralplan",
|
|
5895
|
+
session_id: "sess-autopilot-beads-block",
|
|
5896
|
+
thread_id: "thread-autopilot-beads-block",
|
|
5897
|
+
active_skills: [
|
|
5898
|
+
{
|
|
5899
|
+
skill: "autopilot",
|
|
5900
|
+
phase: "ralplan",
|
|
5901
|
+
active: true,
|
|
5902
|
+
session_id: "sess-autopilot-beads-block",
|
|
5903
|
+
thread_id: "thread-autopilot-beads-block",
|
|
5904
|
+
},
|
|
5905
|
+
],
|
|
5906
|
+
});
|
|
5907
|
+
await writeJson(join(sessionDir, "autopilot-state.json"), {
|
|
5908
|
+
active: true,
|
|
5909
|
+
mode: "autopilot",
|
|
5910
|
+
current_phase: "ralplan",
|
|
5911
|
+
session_id: "sess-autopilot-beads-block",
|
|
5912
|
+
thread_id: "thread-autopilot-beads-block",
|
|
5913
|
+
});
|
|
5914
|
+
const blockedCommands = [
|
|
5915
|
+
"bd --db /tmp/outside.db create 'Issue title'",
|
|
5916
|
+
"bd --db ../outside.db create 'Issue title'",
|
|
5917
|
+
"bd --db .beads create 'Issue title'",
|
|
5918
|
+
"bd --db $DB create 'Issue title'",
|
|
5919
|
+
"DB=.beads/issues.db bd --db $DB create 'Issue title'",
|
|
5920
|
+
"env bd --db /tmp/outside.db create 'Issue title'",
|
|
5921
|
+
"command bd --db /tmp/outside.db create 'Issue title'",
|
|
5922
|
+
"bd --db .beads/issues.db export",
|
|
5923
|
+
"bd --db .beads/issues.db create 'Issue title' > src/leak.ts",
|
|
5924
|
+
"bd --db .beads/issues.db create 'Issue title'; cat > src/leak.ts",
|
|
5925
|
+
];
|
|
5926
|
+
for (const [index, command] of blockedCommands.entries()) {
|
|
5927
|
+
const result = await dispatchCodexNativeHook({
|
|
5928
|
+
hook_event_name: "PreToolUse",
|
|
5929
|
+
cwd,
|
|
5930
|
+
session_id: "sess-autopilot-beads-block",
|
|
5931
|
+
thread_id: "thread-autopilot-beads-block",
|
|
5932
|
+
tool_name: "Bash",
|
|
5933
|
+
tool_use_id: `tool-autopilot-beads-block-${index}`,
|
|
5934
|
+
tool_input: { command },
|
|
5935
|
+
}, { cwd });
|
|
5936
|
+
assert.equal(result.outputJson?.decision, "block", `expected unsafe Beads command to be blocked: ${command}`);
|
|
5937
|
+
}
|
|
5938
|
+
const mentionWithImplementationWrite = await dispatchCodexNativeHook({
|
|
5939
|
+
hook_event_name: "PreToolUse",
|
|
5940
|
+
cwd,
|
|
5941
|
+
session_id: "sess-autopilot-beads-block",
|
|
5942
|
+
thread_id: "thread-autopilot-beads-block",
|
|
5943
|
+
tool_name: "Bash",
|
|
5944
|
+
tool_use_id: "tool-autopilot-beads-mention-write",
|
|
5945
|
+
tool_input: { command: "echo bd --db .beads/issues.db create > src/leak.ts" },
|
|
5946
|
+
}, { cwd });
|
|
5947
|
+
assert.equal(mentionWithImplementationWrite.outputJson?.decision, "block");
|
|
5948
|
+
assert.match(String(mentionWithImplementationWrite.outputJson?.reason ?? ""), /src\/leak\.ts/);
|
|
5949
|
+
const blockedImplementationEdit = await dispatchCodexNativeHook({
|
|
5950
|
+
hook_event_name: "PreToolUse",
|
|
5951
|
+
cwd,
|
|
5952
|
+
session_id: "sess-autopilot-beads-block",
|
|
5953
|
+
thread_id: "thread-autopilot-beads-block",
|
|
5954
|
+
tool_name: "Edit",
|
|
5955
|
+
tool_use_id: "tool-autopilot-beads-src-edit",
|
|
5956
|
+
tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
|
|
5957
|
+
}, { cwd });
|
|
5958
|
+
assert.equal(blockedImplementationEdit.outputJson?.decision, "block");
|
|
5959
|
+
}
|
|
5960
|
+
finally {
|
|
5961
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5962
|
+
}
|
|
5963
|
+
});
|
|
5964
|
+
it("allows Autopilot rework implementation writes while ralplan remains guarded", async () => {
|
|
5965
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-autopilot-rework-"));
|
|
5966
|
+
try {
|
|
5967
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
5968
|
+
const sessionDir = join(stateDir, "sessions", "sess-autopilot-rework");
|
|
5969
|
+
await mkdir(sessionDir, { recursive: true });
|
|
5970
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-autopilot-rework", cwd });
|
|
5971
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
5972
|
+
version: 1,
|
|
5973
|
+
active: true,
|
|
5974
|
+
skill: "autopilot",
|
|
5975
|
+
phase: "rework",
|
|
5976
|
+
session_id: "sess-autopilot-rework",
|
|
5977
|
+
thread_id: "thread-autopilot-rework",
|
|
5978
|
+
active_skills: [
|
|
5979
|
+
{
|
|
5980
|
+
skill: "autopilot",
|
|
5981
|
+
phase: "rework",
|
|
5982
|
+
active: true,
|
|
5983
|
+
session_id: "sess-autopilot-rework",
|
|
5984
|
+
thread_id: "thread-autopilot-rework",
|
|
5985
|
+
},
|
|
5986
|
+
],
|
|
5987
|
+
});
|
|
5988
|
+
await writeJson(join(sessionDir, "autopilot-state.json"), {
|
|
5989
|
+
active: true,
|
|
5990
|
+
mode: "autopilot",
|
|
5991
|
+
current_phase: "rework",
|
|
5992
|
+
review_cycle: 2,
|
|
5993
|
+
review_verdict: { clean: false, recommendation: "REQUEST_CHANGES", findings: ["src/implementation.ts"] },
|
|
5994
|
+
session_id: "sess-autopilot-rework",
|
|
5995
|
+
thread_id: "thread-autopilot-rework",
|
|
5996
|
+
});
|
|
5997
|
+
const allowedImplementationEdit = await dispatchCodexNativeHook({
|
|
5998
|
+
hook_event_name: "PreToolUse",
|
|
5999
|
+
cwd,
|
|
6000
|
+
session_id: "sess-autopilot-rework",
|
|
6001
|
+
thread_id: "thread-autopilot-rework",
|
|
6002
|
+
tool_name: "Edit",
|
|
6003
|
+
tool_use_id: "tool-autopilot-rework-src-edit",
|
|
6004
|
+
tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
|
|
6005
|
+
}, { cwd });
|
|
6006
|
+
assert.equal(allowedImplementationEdit.outputJson, null);
|
|
6007
|
+
}
|
|
6008
|
+
finally {
|
|
6009
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6010
|
+
}
|
|
6011
|
+
});
|
|
5077
6012
|
it("allows null-device fd redirects while deep-interview blocks real Bash writes", async () => {
|
|
5078
6013
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-null-redirect-"));
|
|
5079
6014
|
try {
|
|
@@ -5215,6 +6150,173 @@ exit 0
|
|
|
5215
6150
|
await rm(cwd, { recursive: true, force: true });
|
|
5216
6151
|
}
|
|
5217
6152
|
});
|
|
6153
|
+
it("records native subagent capacity exhaustion from spawn_agent PostToolUse output", async () => {
|
|
6154
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-capacity-record-"));
|
|
6155
|
+
try {
|
|
6156
|
+
const result = await dispatchCodexNativeHook({
|
|
6157
|
+
hook_event_name: "PostToolUse",
|
|
6158
|
+
cwd,
|
|
6159
|
+
session_id: "sess-subagent-capacity-record",
|
|
6160
|
+
thread_id: "thread-subagent-capacity-record",
|
|
6161
|
+
turn_id: "turn-subagent-capacity-record",
|
|
6162
|
+
tool_name: "multi_agent_v1.spawn_agent",
|
|
6163
|
+
tool_response: {
|
|
6164
|
+
error: "collab spawn failed: agent thread limit reached",
|
|
6165
|
+
},
|
|
6166
|
+
}, { cwd });
|
|
6167
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
6168
|
+
assert.equal(result.outputJson, null);
|
|
6169
|
+
const blocker = JSON.parse(await readFile(join(cwd, ".omx", "state", "native-subagent-capacity-blocker.json"), "utf-8"));
|
|
6170
|
+
assert.equal(blocker.reason, "agent_thread_limit_reached");
|
|
6171
|
+
assert.equal(blocker.session_id, "sess-subagent-capacity-record");
|
|
6172
|
+
assert.equal(blocker.thread_id, "thread-subagent-capacity-record");
|
|
6173
|
+
assert.equal(blocker.tool_name, "multi_agent_v1.spawn_agent");
|
|
6174
|
+
assert.match(String(blocker.error_summary), /agent thread limit reached/);
|
|
6175
|
+
assert.ok(Date.parse(String(blocker.expires_at)) > Date.parse(String(blocker.observed_at)));
|
|
6176
|
+
}
|
|
6177
|
+
finally {
|
|
6178
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6179
|
+
}
|
|
6180
|
+
});
|
|
6181
|
+
it("blocks close_agent cleanup after recent native subagent capacity exhaustion", async () => {
|
|
6182
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-capacity-close-block-"));
|
|
6183
|
+
try {
|
|
6184
|
+
await dispatchCodexNativeHook({
|
|
6185
|
+
hook_event_name: "PostToolUse",
|
|
6186
|
+
cwd,
|
|
6187
|
+
session_id: "sess-subagent-capacity-close-block",
|
|
6188
|
+
thread_id: "thread-subagent-capacity-close-block",
|
|
6189
|
+
turn_id: "turn-subagent-capacity-close-block",
|
|
6190
|
+
tool_name: "multi_agent_v1.spawn_agent",
|
|
6191
|
+
tool_response: "collab spawn failed: agent thread limit reached",
|
|
6192
|
+
}, { cwd });
|
|
6193
|
+
const result = await dispatchCodexNativeHook({
|
|
6194
|
+
hook_event_name: "PreToolUse",
|
|
6195
|
+
cwd,
|
|
6196
|
+
session_id: "sess-subagent-capacity-close-block",
|
|
6197
|
+
thread_id: "thread-subagent-capacity-close-block",
|
|
6198
|
+
tool_name: "multi_agent_v1.close_agent",
|
|
6199
|
+
tool_input: { target: "019ecc36-stale" },
|
|
6200
|
+
}, { cwd });
|
|
6201
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
6202
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
6203
|
+
assert.match(JSON.stringify(result.outputJson), /agent thread limit reached/);
|
|
6204
|
+
assert.match(JSON.stringify(result.outputJson), /multi_agent_v1\.close_agent/);
|
|
6205
|
+
assert.match(JSON.stringify(result.outputJson), /multi_tool_use\.parallel/);
|
|
6206
|
+
assert.match(JSON.stringify(result.outputJson), /bounded capacity blocker/);
|
|
6207
|
+
}
|
|
6208
|
+
finally {
|
|
6209
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6210
|
+
}
|
|
6211
|
+
});
|
|
6212
|
+
it("emits schema-safe PreToolUse CLI stdout for close_agent capacity blocks", async () => {
|
|
6213
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-subagent-capacity-close-block-"));
|
|
6214
|
+
try {
|
|
6215
|
+
parseSingleJsonStdout(runNativeHookCli({
|
|
6216
|
+
hook_event_name: "PostToolUse",
|
|
6217
|
+
cwd,
|
|
6218
|
+
session_id: "sess-cli-subagent-capacity-close-block",
|
|
6219
|
+
thread_id: "thread-cli-subagent-capacity-close-block",
|
|
6220
|
+
turn_id: "turn-cli-subagent-capacity-close-block",
|
|
6221
|
+
tool_name: "multi_agent_v1.spawn_agent",
|
|
6222
|
+
tool_response: "collab spawn failed: agent thread limit reached",
|
|
6223
|
+
}, { cwd }));
|
|
6224
|
+
const output = parseSingleJsonStdout(runNativeHookCli({
|
|
6225
|
+
hook_event_name: "PreToolUse",
|
|
6226
|
+
cwd,
|
|
6227
|
+
session_id: "sess-cli-subagent-capacity-close-block",
|
|
6228
|
+
thread_id: "thread-cli-subagent-capacity-close-block",
|
|
6229
|
+
tool_name: "multi_agent_v1.close_agent",
|
|
6230
|
+
tool_input: { target: "019ecc36-stale" },
|
|
6231
|
+
}, { cwd }));
|
|
6232
|
+
assert.deepEqual(Object.keys(output).sort(), ["systemMessage"]);
|
|
6233
|
+
assert.match(String(output.systemMessage ?? ""), /agent thread limit reached/);
|
|
6234
|
+
assert.match(String(output.systemMessage ?? ""), /Do not call multi_agent_v1\.close_agent/);
|
|
6235
|
+
assert.equal(output.decision, undefined);
|
|
6236
|
+
assert.equal(output.hookSpecificOutput, undefined);
|
|
6237
|
+
}
|
|
6238
|
+
finally {
|
|
6239
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6240
|
+
}
|
|
6241
|
+
});
|
|
6242
|
+
it("blocks parallel close_agent cleanup after recent native subagent capacity exhaustion", async () => {
|
|
6243
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-capacity-parallel-close-block-"));
|
|
6244
|
+
try {
|
|
6245
|
+
await dispatchCodexNativeHook({
|
|
6246
|
+
hook_event_name: "PostToolUse",
|
|
6247
|
+
cwd,
|
|
6248
|
+
session_id: "sess-subagent-capacity-parallel-close-block",
|
|
6249
|
+
thread_id: "thread-subagent-capacity-parallel-close-block",
|
|
6250
|
+
tool_name: "multi_agent_v1.spawn_agent",
|
|
6251
|
+
tool_response: "agent thread limit reached",
|
|
6252
|
+
}, { cwd });
|
|
6253
|
+
const result = await dispatchCodexNativeHook({
|
|
6254
|
+
hook_event_name: "PreToolUse",
|
|
6255
|
+
cwd,
|
|
6256
|
+
session_id: "sess-subagent-capacity-parallel-close-block",
|
|
6257
|
+
thread_id: "thread-subagent-capacity-parallel-close-block",
|
|
6258
|
+
tool_name: "multi_tool_use.parallel",
|
|
6259
|
+
tool_input: {
|
|
6260
|
+
tool_uses: [
|
|
6261
|
+
{ recipient_name: "multi_agent_v1.close_agent", parameters: { target: "stale-1" } },
|
|
6262
|
+
{ recipient_name: "multi_agent_v1.close_agent", parameters: { target: "stale-2" } },
|
|
6263
|
+
],
|
|
6264
|
+
},
|
|
6265
|
+
}, { cwd });
|
|
6266
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
6267
|
+
assert.match(JSON.stringify(result.outputJson), /Do not call multi_agent_v1\.close_agent/);
|
|
6268
|
+
assert.match(JSON.stringify(result.outputJson), /do not batch close_agent through multi_tool_use\.parallel/);
|
|
6269
|
+
}
|
|
6270
|
+
finally {
|
|
6271
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6272
|
+
}
|
|
6273
|
+
});
|
|
6274
|
+
it("does not block close_agent without a recent native capacity blocker", async () => {
|
|
6275
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-capacity-close-no-blocker-"));
|
|
6276
|
+
try {
|
|
6277
|
+
const result = await dispatchCodexNativeHook({
|
|
6278
|
+
hook_event_name: "PreToolUse",
|
|
6279
|
+
cwd,
|
|
6280
|
+
session_id: "sess-subagent-capacity-close-no-blocker",
|
|
6281
|
+
thread_id: "thread-subagent-capacity-close-no-blocker",
|
|
6282
|
+
tool_name: "multi_agent_v1.close_agent",
|
|
6283
|
+
tool_input: { target: "completed-agent" },
|
|
6284
|
+
}, { cwd });
|
|
6285
|
+
assert.equal(result.omxEventName, "pre-tool-use");
|
|
6286
|
+
assert.equal(result.outputJson, null);
|
|
6287
|
+
}
|
|
6288
|
+
finally {
|
|
6289
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6290
|
+
}
|
|
6291
|
+
});
|
|
6292
|
+
it("does not block close_agent when the native capacity blocker is expired", async () => {
|
|
6293
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-capacity-close-expired-"));
|
|
6294
|
+
try {
|
|
6295
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
6296
|
+
await mkdir(stateDir, { recursive: true });
|
|
6297
|
+
await writeJson(join(stateDir, "native-subagent-capacity-blocker.json"), {
|
|
6298
|
+
schema_version: 1,
|
|
6299
|
+
reason: "agent_thread_limit_reached",
|
|
6300
|
+
session_id: "sess-subagent-capacity-close-expired",
|
|
6301
|
+
error_summary: "agent thread limit reached",
|
|
6302
|
+
observed_at: "2026-06-20T00:00:00.000Z",
|
|
6303
|
+
expires_at: "2026-06-20T00:30:00.000Z",
|
|
6304
|
+
cwd,
|
|
6305
|
+
});
|
|
6306
|
+
const result = await dispatchCodexNativeHook({
|
|
6307
|
+
hook_event_name: "PreToolUse",
|
|
6308
|
+
cwd,
|
|
6309
|
+
session_id: "sess-subagent-capacity-close-expired",
|
|
6310
|
+
thread_id: "thread-subagent-capacity-close-expired",
|
|
6311
|
+
tool_name: "multi_agent_v1.close_agent",
|
|
6312
|
+
tool_input: { target: "completed-agent" },
|
|
6313
|
+
}, { cwd });
|
|
6314
|
+
assert.equal(result.outputJson, null);
|
|
6315
|
+
}
|
|
6316
|
+
finally {
|
|
6317
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6318
|
+
}
|
|
6319
|
+
});
|
|
5218
6320
|
it("warns on PreToolUse for vague sloppy fallback implementation framing", async () => {
|
|
5219
6321
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-slop-warn-"));
|
|
5220
6322
|
try {
|