oh-my-codex 0.20.0 → 0.20.1

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.
Files changed (56) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/dist/cli/__tests__/codex-plugin-layout.test.js +27 -0
  4. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  5. package/dist/cli/__tests__/doctor-context-window-warning.test.js +83 -78
  6. package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +1 -1
  7. package/dist/cli/__tests__/doctor-invalid-config.test.js +9 -0
  8. package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -1
  9. package/dist/cli/__tests__/doctor-warning-copy.test.js +87 -1
  10. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  11. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +54 -2
  12. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
  13. package/dist/cli/__tests__/setup-install-mode.test.js +3 -3
  14. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  15. package/dist/cli/__tests__/setup-refresh.test.js +137 -5
  16. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  17. package/dist/cli/__tests__/setup-scope.test.js +27 -3
  18. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  19. package/dist/cli/__tests__/uninstall.test.js +297 -3
  20. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  21. package/dist/cli/doctor.d.ts +2 -0
  22. package/dist/cli/doctor.d.ts.map +1 -1
  23. package/dist/cli/doctor.js +42 -31
  24. package/dist/cli/doctor.js.map +1 -1
  25. package/dist/cli/setup.js +2 -2
  26. package/dist/cli/setup.js.map +1 -1
  27. package/dist/cli/uninstall.d.ts.map +1 -1
  28. package/dist/cli/uninstall.js +3 -4
  29. package/dist/cli/uninstall.js.map +1 -1
  30. package/dist/config/__tests__/generator-idempotent.test.js +200 -104
  31. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  32. package/dist/config/__tests__/generator-notify.test.js +22 -48
  33. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  34. package/dist/config/generator.d.ts +19 -10
  35. package/dist/config/generator.d.ts.map +1 -1
  36. package/dist/config/generator.js +169 -152
  37. package/dist/config/generator.js.map +1 -1
  38. package/dist/leader/__tests__/contract.test.js +43 -1
  39. package/dist/leader/__tests__/contract.test.js.map +1 -1
  40. package/dist/leader/contract.d.ts.map +1 -1
  41. package/dist/leader/contract.js +23 -5
  42. package/dist/leader/contract.js.map +1 -1
  43. package/dist/scripts/__tests__/codex-native-hook.test.js +1104 -20
  44. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  45. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  46. package/dist/scripts/codex-native-hook.js +139 -19
  47. package/dist/scripts/codex-native-hook.js.map +1 -1
  48. package/dist/utils/__tests__/agents-md.test.js +16 -0
  49. package/dist/utils/__tests__/agents-md.test.js.map +1 -1
  50. package/dist/utils/agents-md.d.ts.map +1 -1
  51. package/dist/utils/agents-md.js +4 -3
  52. package/dist/utils/agents-md.js.map +1 -1
  53. package/package.json +1 -1
  54. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  55. package/src/scripts/__tests__/codex-native-hook.test.ts +1281 -53
  56. package/src/scripts/codex-native-hook.ts +142 -16
@@ -581,6 +581,68 @@ describe("codex native hook dispatch", () => {
581
581
  await rm(cwd, { recursive: true, force: true });
582
582
  }
583
583
  });
584
+ it("allows Ralplan Markdown draft-only apply_patch on the live CLI path while denying mixed targets", async () => {
585
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-ralplan-draft-boundary-"));
586
+ const sessionId = "sess-cli-ralplan-draft-boundary";
587
+ const stateDir = join(cwd, ".omx", "state");
588
+ try {
589
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, cwd });
590
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
591
+ active: true,
592
+ skill: "ralplan",
593
+ phase: "planning",
594
+ session_id: sessionId,
595
+ active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
596
+ });
597
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
598
+ active: true,
599
+ mode: "ralplan",
600
+ current_phase: "planning",
601
+ session_id: sessionId,
602
+ });
603
+ for (const [name, target] of [
604
+ ["relative", ".omx/drafts/issue-3105.md"],
605
+ ["repository-absolute", join(cwd, ".omx", "drafts", "issue-3105.md")],
606
+ ]) {
607
+ const result = runNativeHookCliResult({
608
+ hook_event_name: "PreToolUse",
609
+ cwd,
610
+ session_id: sessionId,
611
+ thread_id: `thread-cli-ralplan-draft-${name}`,
612
+ tool_name: "apply_patch",
613
+ tool_input: {
614
+ input: `*** Begin Patch\n*** Add File: ${target}\n+# Draft\n*** End Patch\n`,
615
+ },
616
+ }, { cwd });
617
+ assert.equal(result.status, 0, result.stderr || result.stdout);
618
+ assert.deepEqual(parseSingleJsonStdout(result.stdout), {});
619
+ }
620
+ const mixedResult = runNativeHookCliResult({
621
+ hook_event_name: "PreToolUse",
622
+ cwd,
623
+ session_id: sessionId,
624
+ thread_id: "thread-cli-ralplan-draft-mixed",
625
+ tool_name: "apply_patch",
626
+ tool_input: {
627
+ input: "*** Begin Patch\n*** Add File: .omx/drafts/issue-3105.md\n+# Draft\n*** Add File: src/leak.ts\n+leak\n*** End Patch\n",
628
+ },
629
+ }, { cwd });
630
+ assert.equal(mixedResult.status, 0, mixedResult.stderr || mixedResult.stdout);
631
+ const mixedOutput = parseSingleJsonStdout(mixedResult.stdout);
632
+ const hookSpecificOutput = mixedOutput.hookSpecificOutput;
633
+ assert.deepEqual(Object.keys(mixedOutput).sort(), ["hookSpecificOutput"]);
634
+ assert.equal(hookSpecificOutput.hookEventName, "PreToolUse");
635
+ assert.equal(hookSpecificOutput.permissionDecision, "deny");
636
+ assert.match(String(hookSpecificOutput.permissionDecisionReason ?? ""), /src\/leak\.ts/);
637
+ assert.match(String(hookSpecificOutput.permissionDecisionReason ?? ""), /implementation\/write tools are blocked/);
638
+ assert.equal(mixedOutput.decision, undefined);
639
+ assert.equal(mixedOutput.reason, undefined);
640
+ assert.equal(mixedOutput.systemMessage, undefined);
641
+ }
642
+ finally {
643
+ await rm(cwd, { recursive: true, force: true });
644
+ }
645
+ });
584
646
  it("preserves team-worker typed subagent PreToolUse exemption without thread spawn provenance", async () => {
585
647
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-team-worker-typed-pretool-exempt-"));
586
648
  const sessionId = "sess-team-worker-typed-pretool-exempt";
@@ -1211,10 +1273,51 @@ PY`,
1211
1273
  thread_id: "thread-autopilot-diagnostic",
1212
1274
  }, { cwd }));
1213
1275
  assert.equal(output.decision, "block");
1276
+ assert.equal(output.stopReason, "autopilot_ultragoal");
1214
1277
  assert.match(String(output.reason ?? ""), /state: \.omx\/state\/sessions\/sess-autopilot-diagnostic\/autopilot-state\.json/);
1215
1278
  assert.match(String(output.reason ?? ""), /canonical: canonical_agrees/);
1216
- assert.equal(output.statePath, ".omx/state/sessions/sess-autopilot-diagnostic/autopilot-state.json");
1217
- assert.equal(output.canonicalDisagreement, "canonical_agrees");
1279
+ assert.match(String(output.systemMessage ?? ""), /state: \.omx\/state\/sessions\/sess-autopilot-diagnostic\/autopilot-state\.json/);
1280
+ assert.match(String(output.systemMessage ?? ""), /canonical: canonical_agrees/);
1281
+ assert.deepEqual(Object.keys(output).sort(), ["decision", "reason", "stopReason", "systemMessage"]);
1282
+ assert.equal("statePath" in output, false);
1283
+ assert.equal("canonicalDisagreement" in output, false);
1284
+ }
1285
+ finally {
1286
+ await rm(cwd, { recursive: true, force: true });
1287
+ }
1288
+ });
1289
+ it("keeps canonical phase disagreements in active Autopilot Stop diagnostics", async () => {
1290
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-canonical-phase-"));
1291
+ try {
1292
+ const sessionId = "sess-autopilot-canonical-phase";
1293
+ await writeJson(join(cwd, ".omx", "state", "session.json"), { session_id: sessionId, cwd });
1294
+ await writeJson(join(cwd, ".omx", "state", "skill-active-state.json"), {
1295
+ version: 1,
1296
+ active: true,
1297
+ skill: "autopilot",
1298
+ phase: "execution",
1299
+ session_id: sessionId,
1300
+ active_skills: [{ skill: "autopilot", phase: "execution", active: true, session_id: sessionId }],
1301
+ });
1302
+ await writeJson(join(cwd, ".omx", "state", "sessions", sessionId, "autopilot-state.json"), {
1303
+ active: true,
1304
+ mode: "autopilot",
1305
+ current_phase: "ultragoal",
1306
+ session_id: sessionId,
1307
+ });
1308
+ const output = parseSingleJsonStdout(runNativeHookCli({
1309
+ hook_event_name: "Stop",
1310
+ cwd,
1311
+ session_id: sessionId,
1312
+ thread_id: "thread-autopilot-canonical-phase",
1313
+ }, { cwd }));
1314
+ assert.equal(output.decision, "block");
1315
+ assert.equal(output.stopReason, "autopilot_ultragoal");
1316
+ assert.match(String(output.reason ?? ""), /canonical: canonical_phase:execution/);
1317
+ assert.match(String(output.systemMessage ?? ""), /canonical: canonical_phase:execution/);
1318
+ assert.deepEqual(Object.keys(output).sort(), ["decision", "reason", "stopReason", "systemMessage"]);
1319
+ assert.equal("statePath" in output, false);
1320
+ assert.equal("canonicalDisagreement" in output, false);
1218
1321
  }
1219
1322
  finally {
1220
1323
  await rm(cwd, { recursive: true, force: true });
@@ -8784,12 +8887,29 @@ exit 0
8784
8887
  for (const path of [
8785
8888
  ".omx/context/findings.md",
8786
8889
  ".omx/plans/issue-2863.md",
8890
+ ".omx/drafts/issue-3105.md",
8787
8891
  ".omx/specs/issue-2863.md",
8788
8892
  ".omx/state/required-planning-state.json",
8789
8893
  ]) {
8790
8894
  const writeResult = await preToolUse("Write", `tool-ralplan-write-${path}`, { file_path: path, content: "ok" });
8791
8895
  assert.equal(writeResult.outputJson, null, `Write should be allowed for ${path}`);
8792
8896
  }
8897
+ const allowedDraftEdit = await preToolUse("Edit", "tool-ralplan-draft-edit", {
8898
+ file_path: ".omx/drafts/issue-3105.md",
8899
+ old_string: "old",
8900
+ new_string: "new",
8901
+ });
8902
+ assert.equal(allowedDraftEdit.outputJson, null);
8903
+ for (const path of [
8904
+ join(cwd, ".omx", "drafts", "issue-3105.md"),
8905
+ ".omx/drafts/subdir/../issue-3105.md",
8906
+ ]) {
8907
+ const normalizedDraftWrite = await preToolUse("Write", `tool-ralplan-normalized-draft-${path}`, {
8908
+ file_path: path,
8909
+ content: "# Draft",
8910
+ });
8911
+ assert.equal(normalizedDraftWrite.outputJson, null, `normalized draft path should be allowed: ${path}`);
8912
+ }
8793
8913
  for (const protectedPath of [
8794
8914
  ".omx/state/sessions/sess-ralplan-guard/ralplan-state.json",
8795
8915
  ".omx/state/sessions/sess-ralplan-guard/autopilot-state.json",
@@ -8822,10 +8942,22 @@ exit 0
8822
8942
  input: "*** Begin Patch\n*** Update File: .omx/plans/issue-2863.md\n@@\n-old\n+new\n*** End Patch\n",
8823
8943
  });
8824
8944
  assert.equal(allowedPatchUpdate.outputJson, null);
8945
+ const allowedDraftPatchAdd = await preToolUse("apply_patch", "tool-ralplan-draft-patch-add", {
8946
+ input: "*** Begin Patch\n*** Add File: .omx/drafts/issue-3105.md\n+# Draft\n*** End Patch\n",
8947
+ });
8948
+ assert.equal(allowedDraftPatchAdd.outputJson, null);
8949
+ const allowedDraftPatchUpdate = await preToolUse("ApplyPatch", "tool-ralplan-draft-patch-update", {
8950
+ input: "*** Begin Patch\n*** Update File: .omx/drafts/issue-3105.md\n@@\n-old\n+new\n*** End Patch\n",
8951
+ });
8952
+ assert.equal(allowedDraftPatchUpdate.outputJson, null);
8825
8953
  const allowedRedirect = await preToolUse("Bash", "tool-ralplan-redirect-allow", {
8826
8954
  command: "printf '\\nmore\\n' >> .omx/plans/issue-2863.md",
8827
8955
  });
8828
8956
  assert.equal(allowedRedirect.outputJson, null);
8957
+ const allowedDraftRedirect = await preToolUse("Bash", "tool-ralplan-draft-redirect-allow", {
8958
+ command: "printf '# Draft\\n' > .omx/drafts/issue-3105.md",
8959
+ });
8960
+ assert.equal(allowedDraftRedirect.outputJson, null);
8829
8961
  const allowedTee = await preToolUse("Bash", "tool-ralplan-tee-allow", {
8830
8962
  command: "printf '\\nmore\\n' | tee -a .omx/specs/issue-2863.md",
8831
8963
  });
@@ -8881,6 +9013,34 @@ exit 0
8881
9013
  const editReason = String(blockedEdit.outputJson?.reason ?? "");
8882
9014
  assert.match(editReason, /Edit path/);
8883
9015
  assert.match(editReason, /src\/implementation\.ts/);
9016
+ const blockedEditContext = String(blockedEdit.outputJson
9017
+ ?.hookSpecificOutput?.additionalContext ?? "");
9018
+ assert.match(blockedEditContext, /Markdown drafts under `\.omx\/drafts\/\*\.md`/);
9019
+ for (const allowedCategory of [
9020
+ ".omx/context/",
9021
+ ".omx/plans/",
9022
+ ".omx/specs/",
9023
+ ".omx/tmp/",
9024
+ ".omx/state/",
9025
+ ".beads/",
9026
+ ]) {
9027
+ assert.match(blockedEditContext, new RegExp(allowedCategory.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
9028
+ }
9029
+ for (const blockedDraftPath of [
9030
+ ".omx/drafts/issue-3105.MD",
9031
+ ".omx/Drafts/issue-3105.md",
9032
+ ".omx/drafts/issue-3105",
9033
+ ".omx/drafts/run.sh",
9034
+ ".omx/drafts/subdir/issue-3105.md",
9035
+ ".omx/drafts/../../src/leak.ts",
9036
+ "/tmp/issue-3105.md",
9037
+ ]) {
9038
+ const blockedDraft = await preToolUse("Write", `tool-ralplan-draft-block-${blockedDraftPath}`, {
9039
+ file_path: blockedDraftPath,
9040
+ content: "bad",
9041
+ });
9042
+ assert.equal(blockedDraft.outputJson?.decision, "block", `draft path should be blocked: ${blockedDraftPath}`);
9043
+ }
8884
9044
  const blockedMixedPatch = await preToolUse("apply_patch", "tool-ralplan-patch-mixed", {
8885
9045
  input: "*** Begin Patch\n*** Add File: .omx/plans/ok.md\n+ok\n*** Add File: src/leak.ts\n+leak\n*** End Patch\n",
8886
9046
  });
@@ -8888,6 +9048,16 @@ exit 0
8888
9048
  const mixedReason = String(blockedMixedPatch.outputJson?.reason ?? "");
8889
9049
  assert.match(mixedReason, /apply_patch target/);
8890
9050
  assert.match(mixedReason, /src\/leak\.ts/);
9051
+ const blockedMixedDraftPatch = await preToolUse("apply_patch", "tool-ralplan-draft-patch-mixed", {
9052
+ input: "*** Begin Patch\n*** Add File: .omx/drafts/issue-3105.md\n+ok\n*** Add File: src/leak.ts\n+leak\n*** End Patch\n",
9053
+ });
9054
+ assert.equal(blockedMixedDraftPatch.outputJson?.decision, "block");
9055
+ assert.match(String(blockedMixedDraftPatch.outputJson?.reason ?? ""), /src\/leak\.ts/);
9056
+ const blockedMixedDraftBash = await preToolUse("Bash", "tool-ralplan-draft-bash-mixed", {
9057
+ command: "printf '# Draft\\n' > .omx/drafts/issue-3105.md; printf 'bad\\n' > src/leak.ts",
9058
+ });
9059
+ assert.equal(blockedMixedDraftBash.outputJson?.decision, "block");
9060
+ assert.match(String(blockedMixedDraftBash.outputJson?.reason ?? ""), /src\/leak\.ts/);
8891
9061
  const blockedUnparseablePatch = await preToolUse("apply_patch", "tool-ralplan-patch-unparseable", {
8892
9062
  input: "not a recognizable patch",
8893
9063
  });
@@ -8900,6 +9070,11 @@ exit 0
8900
9070
  const unresolvedReason = String(blockedUnresolvedRedirect.outputJson?.reason ?? "");
8901
9071
  assert.match(unresolvedReason, /unresolved Bash write target/);
8902
9072
  assert.match(unresolvedReason, /\$PLAN_PATH/);
9073
+ const blockedUnresolvedDraftRedirect = await preToolUse("Bash", "tool-ralplan-draft-redirect-unresolved", {
9074
+ command: "cat > \"$DRAFT_PATH\" <<'EOF'\ncontent\nEOF",
9075
+ });
9076
+ assert.equal(blockedUnresolvedDraftRedirect.outputJson?.decision, "block");
9077
+ assert.match(String(blockedUnresolvedDraftRedirect.outputJson?.reason ?? ""), /\$DRAFT_PATH/);
8903
9078
  const blockedTraversal = await preToolUse("Write", "tool-ralplan-traversal-block", {
8904
9079
  file_path: ".omx/plans/../../src/leak.ts",
8905
9080
  content: "bad",
@@ -9490,6 +9665,159 @@ PY`,
9490
9665
  await rm(cwd, { recursive: true, force: true });
9491
9666
  }
9492
9667
  });
9668
+ it("keeps untyped collaboration child implementation writes blocked under active ralplan planning while typed trusted agents pass (#3116)", async () => {
9669
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-ralplan-untyped-"));
9670
+ process.env.OMX_ROOT = cwd;
9671
+ try {
9672
+ const stateDir = join(cwd, ".omx", "state");
9673
+ const sessionId = "sess-3116-ralplan-untyped";
9674
+ const leaderThreadId = "thread-3116-ralplan-untyped-leader";
9675
+ const childThreadId = "thread-3116-ralplan-untyped-child";
9676
+ const sessionDir = join(stateDir, "sessions", sessionId);
9677
+ const nowIso = new Date().toISOString();
9678
+ await mkdir(sessionDir, { recursive: true });
9679
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId, cwd });
9680
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
9681
+ version: 1,
9682
+ active: true,
9683
+ skill: "autopilot",
9684
+ phase: "ralplan",
9685
+ session_id: sessionId,
9686
+ thread_id: leaderThreadId,
9687
+ active_skills: [
9688
+ { skill: "autopilot", phase: "ralplan", active: true, session_id: sessionId, thread_id: leaderThreadId },
9689
+ ],
9690
+ });
9691
+ await writeJson(join(sessionDir, "autopilot-state.json"), {
9692
+ active: true,
9693
+ mode: "autopilot",
9694
+ current_phase: "ralplan",
9695
+ session_id: sessionId,
9696
+ thread_id: leaderThreadId,
9697
+ });
9698
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
9699
+ schemaVersion: 1,
9700
+ sessions: {
9701
+ [sessionId]: {
9702
+ session_id: sessionId,
9703
+ leader_thread_id: leaderThreadId,
9704
+ updated_at: nowIso,
9705
+ threads: {
9706
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
9707
+ [childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
9708
+ },
9709
+ },
9710
+ },
9711
+ });
9712
+ // Untyped tracked/spawn-provenance child must NOT bypass the ralplan planning guard.
9713
+ const untypedChild = await dispatchCodexNativeHook({
9714
+ hook_event_name: "PreToolUse",
9715
+ cwd,
9716
+ session_id: sessionId,
9717
+ thread_id: childThreadId,
9718
+ agent_role: "collaboration-child",
9719
+ source: {
9720
+ subagent: { thread_spawn: { parent_thread_id: leaderThreadId } },
9721
+ },
9722
+ tool_name: "Edit",
9723
+ tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
9724
+ }, { cwd });
9725
+ assert.equal(untypedChild.outputJson?.decision, "block");
9726
+ // A typed trusted agent retains its existing planning exemption.
9727
+ const typedChild = await dispatchCodexNativeHook({
9728
+ hook_event_name: "PreToolUse",
9729
+ cwd,
9730
+ session_id: sessionId,
9731
+ thread_id: childThreadId,
9732
+ agent_role: "executor",
9733
+ source: {
9734
+ subagent: { thread_spawn: { parent_thread_id: leaderThreadId } },
9735
+ },
9736
+ tool_name: "Edit",
9737
+ tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
9738
+ }, { cwd });
9739
+ assert.equal(typedChild.outputJson, null);
9740
+ }
9741
+ finally {
9742
+ await rm(cwd, { recursive: true, force: true });
9743
+ }
9744
+ });
9745
+ it("keeps untyped collaboration child implementation writes blocked under active deep-interview planning while typed trusted agents pass (#3116)", async () => {
9746
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-di-untyped-"));
9747
+ process.env.OMX_ROOT = cwd;
9748
+ try {
9749
+ const stateDir = join(cwd, ".omx", "state");
9750
+ const sessionId = "sess-3116-di-untyped";
9751
+ const leaderThreadId = "thread-3116-di-untyped-leader";
9752
+ const childThreadId = "thread-3116-di-untyped-child";
9753
+ const sessionDir = join(stateDir, "sessions", sessionId);
9754
+ const nowIso = new Date().toISOString();
9755
+ await mkdir(sessionDir, { recursive: true });
9756
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId, cwd });
9757
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
9758
+ version: 1,
9759
+ active: true,
9760
+ skill: "deep-interview",
9761
+ phase: "planning",
9762
+ session_id: sessionId,
9763
+ thread_id: leaderThreadId,
9764
+ active_skills: [
9765
+ { skill: "deep-interview", phase: "planning", active: true, session_id: sessionId, thread_id: leaderThreadId },
9766
+ ],
9767
+ });
9768
+ await writeJson(join(sessionDir, "deep-interview-state.json"), {
9769
+ active: true,
9770
+ mode: "deep-interview",
9771
+ current_phase: "intent-first",
9772
+ session_id: sessionId,
9773
+ });
9774
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
9775
+ schemaVersion: 1,
9776
+ sessions: {
9777
+ [sessionId]: {
9778
+ session_id: sessionId,
9779
+ leader_thread_id: leaderThreadId,
9780
+ updated_at: nowIso,
9781
+ threads: {
9782
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
9783
+ [childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
9784
+ },
9785
+ },
9786
+ },
9787
+ });
9788
+ // Untyped tracked/spawn-provenance child must NOT bypass the deep-interview planning guard.
9789
+ const untypedChild = await dispatchCodexNativeHook({
9790
+ hook_event_name: "PreToolUse",
9791
+ cwd,
9792
+ session_id: sessionId,
9793
+ thread_id: childThreadId,
9794
+ agent_role: "collaboration-child",
9795
+ source: {
9796
+ subagent: { thread_spawn: { parent_thread_id: leaderThreadId } },
9797
+ },
9798
+ tool_name: "Edit",
9799
+ tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
9800
+ }, { cwd });
9801
+ assert.equal(untypedChild.outputJson?.decision, "block");
9802
+ // A typed trusted agent retains its existing planning exemption.
9803
+ const typedChild = await dispatchCodexNativeHook({
9804
+ hook_event_name: "PreToolUse",
9805
+ cwd,
9806
+ session_id: sessionId,
9807
+ thread_id: childThreadId,
9808
+ agent_role: "executor",
9809
+ source: {
9810
+ subagent: { thread_spawn: { parent_thread_id: leaderThreadId } },
9811
+ },
9812
+ tool_name: "Edit",
9813
+ tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
9814
+ }, { cwd });
9815
+ assert.equal(typedChild.outputJson, null);
9816
+ }
9817
+ finally {
9818
+ await rm(cwd, { recursive: true, force: true });
9819
+ }
9820
+ });
9493
9821
  it("allows null-device fd redirects while deep-interview blocks real Bash writes", async () => {
9494
9822
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-null-redirect-"));
9495
9823
  try {
@@ -15060,17 +15388,19 @@ PY`,
15060
15388
  execFileSync("git", ["add", "src/scripts/codex-native-hook.ts"], { cwd, stdio: "ignore" });
15061
15389
  execFileSync("git", ["commit", "-m", "init"], { cwd, stdio: "ignore" });
15062
15390
  await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 2;\n", "utf-8");
15063
- const result = await dispatchCodexNativeHook({
15391
+ const output = parseSingleJsonStdout(runNativeHookCli({
15064
15392
  hook_event_name: "Stop",
15065
15393
  cwd,
15066
15394
  session_id: "sess-stop-doc-refresh",
15067
15395
  last_assistant_message: "Launch-ready: yes",
15068
- }, { cwd });
15069
- assert.equal(result.omxEventName, "stop");
15070
- assert.equal(result.outputJson?.decision, undefined);
15071
- assert.equal(result.outputJson?.hookSpecificOutput?.hookEventName, "Stop");
15072
- assert.match(JSON.stringify(result.outputJson), /Document-refresh warning/);
15073
- assert.match(JSON.stringify(result.outputJson), /staged \+ unstaged changes/);
15396
+ }, { cwd }));
15397
+ assert.deepEqual(Object.keys(output).sort(), ["systemMessage"]);
15398
+ assert.equal("hookSpecificOutput" in output, false);
15399
+ assert.equal("decision" in output, false);
15400
+ assert.equal("reason" in output, false);
15401
+ assert.equal("stopReason" in output, false);
15402
+ assert.match(String(output.systemMessage ?? ""), /Document-refresh warning/);
15403
+ assert.match(String(output.systemMessage ?? ""), /staged \+ unstaged changes/);
15074
15404
  }
15075
15405
  finally {
15076
15406
  await rm(cwd, { recursive: true, force: true });
@@ -17579,6 +17909,17 @@ PY`,
17579
17909
  }, { cwd });
17580
17910
  assert.equal(result.omxEventName, "pre-tool-use");
17581
17911
  assert.equal(result.outputJson, null);
17912
+ const allowedDraftPatch = await dispatchCodexNativeHook({
17913
+ hook_event_name: "PreToolUse",
17914
+ cwd,
17915
+ session_id: nativeSessionId,
17916
+ thread_id: "thread-ralplan-native-map-draft-artifact",
17917
+ tool_name: "apply_patch",
17918
+ tool_input: {
17919
+ input: `*** Begin Patch\n*** Add File: ${join(cwd, ".omx", "drafts", "issue-3105.md")}\n+# Draft\n*** End Patch\n`,
17920
+ },
17921
+ }, { cwd });
17922
+ assert.equal(allowedDraftPatch.outputJson, null);
17582
17923
  }
17583
17924
  finally {
17584
17925
  await rm(cwd, { recursive: true, force: true });
@@ -17686,6 +18027,17 @@ PY`,
17686
18027
  session_id: ownerSessionId,
17687
18028
  cwd: ownerCwd,
17688
18029
  });
18030
+ const allowedForeignDraft = await dispatchCodexNativeHook({
18031
+ hook_event_name: "PreToolUse",
18032
+ cwd,
18033
+ session_id: "019e-ralplan-live-root-unresolved-current",
18034
+ thread_id: "thread-ralplan-live-root-conflict",
18035
+ tool_name: "apply_patch",
18036
+ tool_input: {
18037
+ input: `*** Begin Patch\n*** Add File: ${join(cwd, ".omx", "drafts", "issue-3105.md")}\n+# Draft\n*** End Patch\n`,
18038
+ },
18039
+ }, { cwd });
18040
+ assert.equal(allowedForeignDraft.outputJson, null);
17689
18041
  const result = await dispatchCodexNativeHook({
17690
18042
  hook_event_name: "PreToolUse",
17691
18043
  cwd,
@@ -17758,6 +18110,17 @@ PY`,
17758
18110
  }, { cwd });
17759
18111
  assert.equal(result.omxEventName, "pre-tool-use");
17760
18112
  assert.equal(result.outputJson, null);
18113
+ const allowedDraftPatch = await dispatchCodexNativeHook({
18114
+ hook_event_name: "PreToolUse",
18115
+ cwd,
18116
+ session_id: nativeSessionId,
18117
+ thread_id: "thread-ralplan-live-root-owner-draft-pass",
18118
+ tool_name: "apply_patch",
18119
+ tool_input: {
18120
+ input: `*** Begin Patch\n*** Add File: ${join(cwd, ".omx", "drafts", "live-root-owner.md")}\n+# Draft\n*** End Patch\n`,
18121
+ },
18122
+ }, { cwd });
18123
+ assert.equal(allowedDraftPatch.outputJson, null);
17761
18124
  }
17762
18125
  finally {
17763
18126
  await rm(cwd, { recursive: true, force: true });
@@ -18121,28 +18484,135 @@ PY`,
18121
18484
  await rm(cwd, { recursive: true, force: true });
18122
18485
  }
18123
18486
  });
18124
- it("blocks Main-root ralplan writes even when payload has only a typed agent_role", async () => {
18125
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-agent-role-main-"));
18487
+ it("keeps the Conductor unblocked: quoted redirect/source text is not a write target and terminal blocked-state writes survive genuinely unsupported native delegation (#3119)", async () => {
18488
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-conductor-3119-quote-deadlock-"));
18126
18489
  try {
18127
18490
  const stateDir = join(cwd, ".omx", "state");
18128
- const sessionId = "sess-ralplan-agent-role-main";
18491
+ const sessionId = "sess-conductor-3119-quote-deadlock";
18129
18492
  await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
18130
18493
  await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
18131
- await writeSessionSkillActiveState(stateDir, sessionId, "ralplan", "planning");
18132
- await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
18494
+ await writeSessionSkillActiveState(stateDir, sessionId, "ultragoal", "planning");
18495
+ await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
18133
18496
  active: true,
18134
- mode: "ralplan",
18497
+ mode: "ultragoal",
18135
18498
  current_phase: "planning",
18136
18499
  session_id: sessionId,
18137
18500
  });
18138
- const result = await dispatchCodexNativeHook({
18501
+ // Native delegation is genuinely unsupported (explicit negative evidence).
18502
+ await writeJson(join(stateDir, "native-subagent-support.json"), {
18503
+ schema_version: 1,
18504
+ status: "unsupported",
18505
+ reason: "multi_agent_v1_unavailable",
18506
+ session_id: sessionId,
18507
+ evidence: "unknown tool: multi_agent_v1.spawn_agent",
18508
+ observed_at: new Date().toISOString(),
18509
+ cwd,
18510
+ });
18511
+ const dispatch = (command) => dispatchCodexNativeHook({
18139
18512
  hook_event_name: "PreToolUse",
18140
18513
  cwd,
18141
18514
  session_id: sessionId,
18142
- thread_id: "thread-ralplan-agent-role-main",
18143
- agent_role: "executor",
18144
- tool_name: "Edit",
18145
- tool_input: { file_path: "src/runtime.ts" },
18515
+ thread_id: "thread-conductor-3119-quote-deadlock",
18516
+ tool_name: "Bash",
18517
+ tool_input: { command },
18518
+ }, { cwd });
18519
+ // Deadlock prevention (defect B): the Conductor must still terminalize its
18520
+ // own workflow state even when delegation is genuinely unsupported, even
18521
+ // when the JSON payload contains a `>` character.
18522
+ const terminalBlockedWrite = await dispatch("omx state write --mode ultragoal --input '{\"active\":true,\"current_phase\":\"blocked\",\"reason\":\"native delegation unavailable -> terminalized\"}' --json");
18523
+ assert.notEqual(terminalBlockedWrite.outputJson?.decision, "block");
18524
+ // Defect C: quoted regex/source text with redirect metacharacters is not a
18525
+ // write target, so issue creation is not falsely blocked.
18526
+ const issueCreate = await dispatch("gh issue create --title x --body 'Guard misparsed regex /[^>]+>{1,2}/ as a redirect target'");
18527
+ assert.notEqual(issueCreate.outputJson?.decision, "block");
18528
+ // Fail-closed preserved: a REAL unquoted redirect to a non-metadata path is
18529
+ // still blocked.
18530
+ const realRedirect = await dispatch("printf pwn > src/runtime.ts");
18531
+ assert.equal(realRedirect.outputJson?.decision, "block");
18532
+ assert.match(String(realRedirect.outputJson?.reason ?? ""), /not workflow state\/ledger\/mailbox\/handoff metadata/);
18533
+ // A real unquoted redirect to allowed workflow metadata still passes.
18534
+ const metadataRedirect = await dispatch("printf blocked > .omx/state/conductor.log");
18535
+ assert.notEqual(metadataRedirect.outputJson?.decision, "block");
18536
+ }
18537
+ finally {
18538
+ await rm(cwd, { recursive: true, force: true });
18539
+ }
18540
+ });
18541
+ it("fail-closed: escaped-quote and ANSI-C quoted redirects to source stay blocked while legit quoted data and nested shells behave (#3119)", async () => {
18542
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-conductor-3119-escaped-quote-"));
18543
+ try {
18544
+ const stateDir = join(cwd, ".omx", "state");
18545
+ const sessionId = "sess-conductor-3119-escaped-quote";
18546
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
18547
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
18548
+ await writeSessionSkillActiveState(stateDir, sessionId, "ultragoal", "planning");
18549
+ await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
18550
+ active: true,
18551
+ mode: "ultragoal",
18552
+ current_phase: "planning",
18553
+ session_id: sessionId,
18554
+ });
18555
+ const dispatch = (command) => dispatchCodexNativeHook({
18556
+ hook_event_name: "PreToolUse",
18557
+ cwd,
18558
+ session_id: sessionId,
18559
+ thread_id: "thread-conductor-3119-escaped-quote",
18560
+ tool_name: "Bash",
18561
+ tool_input: { command },
18562
+ }, { cwd });
18563
+ // A top-level escaped quote (\' \") is a LITERAL char in bash, not a quote
18564
+ // opener, and $'...' is ANSI-C quoting. None of these may hide the real
18565
+ // `>` redirect \u2014 all of these genuinely write src/runtime.ts and MUST block.
18566
+ const mustBlock = [
18567
+ "printf pwn > src/runtime.ts", // plain control
18568
+ "printf pwn \\' > src/runtime.ts \\'", // escaped single quote
18569
+ "printf pwn \\\" > src/runtime.ts \\\"", // escaped double quote
18570
+ "printf pwn $'\\'' > src/runtime.ts $'\\''", // ANSI-C escaped quote
18571
+ "bash -c 'printf pwn > src/runtime.ts'", // nested shell redirect
18572
+ ];
18573
+ for (const command of mustBlock) {
18574
+ const result = await dispatch(command);
18575
+ assert.equal(result.outputJson?.decision, "block", command);
18576
+ assert.match(String(result.outputJson?.reason ?? ""), /not workflow state\/ledger\/mailbox\/handoff metadata/, command);
18577
+ }
18578
+ // Legitimate quoted DATA metacharacters (single quotes, double quotes,
18579
+ // ANSI-C) are not redirects and must not be falsely blocked.
18580
+ const mustNotBlock = [
18581
+ "gh issue create --title x --body 'a > b regex /[^>]+>{1,2}/'",
18582
+ "echo \"value > threshold\"",
18583
+ "printf $'a>b'",
18584
+ ];
18585
+ for (const command of mustNotBlock) {
18586
+ const result = await dispatch(command);
18587
+ assert.notEqual(result.outputJson?.decision, "block", command);
18588
+ }
18589
+ }
18590
+ finally {
18591
+ await rm(cwd, { recursive: true, force: true });
18592
+ }
18593
+ });
18594
+ it("blocks Main-root ralplan writes even when payload has only a typed agent_role", async () => {
18595
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-agent-role-main-"));
18596
+ try {
18597
+ const stateDir = join(cwd, ".omx", "state");
18598
+ const sessionId = "sess-ralplan-agent-role-main";
18599
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
18600
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
18601
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralplan", "planning");
18602
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
18603
+ active: true,
18604
+ mode: "ralplan",
18605
+ current_phase: "planning",
18606
+ session_id: sessionId,
18607
+ });
18608
+ const result = await dispatchCodexNativeHook({
18609
+ hook_event_name: "PreToolUse",
18610
+ cwd,
18611
+ session_id: sessionId,
18612
+ thread_id: "thread-ralplan-agent-role-main",
18613
+ agent_role: "executor",
18614
+ tool_name: "Edit",
18615
+ tool_input: { file_path: "src/runtime.ts" },
18146
18616
  }, { cwd });
18147
18617
  assert.equal(result.outputJson?.decision, "block");
18148
18618
  assert.match(String(result.outputJson?.reason ?? ""), /Ralplan is active \(phase: planning\)/);
@@ -18379,6 +18849,239 @@ PY`,
18379
18849
  await rm(cwd, { recursive: true, force: true });
18380
18850
  }
18381
18851
  });
18852
+ it("blocks a corrupt kind:subagent leader that omits leader_thread_id via native session identity while still trusting a real non-leader child (#3117 P2)", async () => {
18853
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3117-corrupt-leader-no-lead-"));
18854
+ process.env.OMX_ROOT = cwd;
18855
+ try {
18856
+ const stateDir = join(cwd, ".omx", "state");
18857
+ const sessionId = "sess-3117-corrupt-leader-no-lead";
18858
+ const leaderThreadId = "thread-3117-corrupt-leader-no-lead-leader";
18859
+ const childThreadId = "thread-3117-corrupt-leader-no-lead-child";
18860
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
18861
+ // native_session_id anchors the leader identity even though the tracker omits
18862
+ // leader_thread_id and corruptly labels the leader kind:"subagent".
18863
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
18864
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
18865
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
18866
+ active: true,
18867
+ mode: "ralph",
18868
+ current_phase: "executing",
18869
+ session_id: sessionId,
18870
+ });
18871
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
18872
+ schemaVersion: 1,
18873
+ sessions: {
18874
+ [sessionId]: {
18875
+ session_id: sessionId,
18876
+ threads: {
18877
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "subagent", mode: "collaboration-child" },
18878
+ [childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child" },
18879
+ },
18880
+ },
18881
+ },
18882
+ });
18883
+ // Untyped leader payload must stay blocked: leader identity is anchored to
18884
+ // native_session_id, not the corrupt tracker kind.
18885
+ const untypedLeader = await dispatchCodexNativeHook({
18886
+ hook_event_name: "PreToolUse",
18887
+ cwd,
18888
+ session_id: sessionId,
18889
+ thread_id: leaderThreadId,
18890
+ tool_name: "apply_patch",
18891
+ tool_input: { file_path: "src/feature.ts" },
18892
+ }, { cwd });
18893
+ assert.equal(untypedLeader.outputJson?.decision, "block");
18894
+ assert.match(String(untypedLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
18895
+ // Control: a genuine non-leader child stays trusted (P1 preserved).
18896
+ const untypedChild = await dispatchCodexNativeHook({
18897
+ hook_event_name: "PreToolUse",
18898
+ cwd,
18899
+ session_id: sessionId,
18900
+ thread_id: childThreadId,
18901
+ tool_name: "apply_patch",
18902
+ tool_input: { file_path: "src/feature.ts" },
18903
+ }, { cwd });
18904
+ assert.equal(untypedChild.outputJson, null);
18905
+ }
18906
+ finally {
18907
+ await rm(cwd, { recursive: true, force: true });
18908
+ }
18909
+ });
18910
+ it("fails closed for untyped provenance when no authoritative leader anchor exists (#3117 P2)", async () => {
18911
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3117-no-leader-anchor-"));
18912
+ process.env.OMX_ROOT = cwd;
18913
+ try {
18914
+ const stateDir = join(cwd, ".omx", "state");
18915
+ const sessionId = "sess-3117-no-leader-anchor";
18916
+ const leaderThreadId = "thread-3117-no-leader-anchor-leader";
18917
+ const childThreadId = "thread-3117-no-leader-anchor-child";
18918
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
18919
+ // No native_session_id / owner ids and no tracker leader_thread_id: the leader is
18920
+ // unidentifiable, so untyped provenance must not be inferred from kind:"subagent".
18921
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
18922
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
18923
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
18924
+ active: true,
18925
+ mode: "ralph",
18926
+ current_phase: "executing",
18927
+ session_id: sessionId,
18928
+ });
18929
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
18930
+ schemaVersion: 1,
18931
+ sessions: {
18932
+ [sessionId]: {
18933
+ session_id: sessionId,
18934
+ threads: {
18935
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "subagent", mode: "collaboration-child" },
18936
+ [childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child" },
18937
+ },
18938
+ },
18939
+ },
18940
+ });
18941
+ for (const threadId of [leaderThreadId, childThreadId]) {
18942
+ const result = await dispatchCodexNativeHook({
18943
+ hook_event_name: "PreToolUse",
18944
+ cwd,
18945
+ session_id: sessionId,
18946
+ thread_id: threadId,
18947
+ tool_name: "apply_patch",
18948
+ tool_input: { file_path: "src/feature.ts" },
18949
+ }, { cwd });
18950
+ assert.equal(result.outputJson?.decision, "block", threadId);
18951
+ assert.match(String(result.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
18952
+ }
18953
+ }
18954
+ finally {
18955
+ await rm(cwd, { recursive: true, force: true });
18956
+ }
18957
+ });
18958
+ it("does not borrow leader anchors from a foreign root session.json when evaluating another session (#3117 P3)", async () => {
18959
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3117-cross-session-anchor-"));
18960
+ process.env.OMX_ROOT = cwd;
18961
+ try {
18962
+ const stateDir = join(cwd, ".omx", "state");
18963
+ const foreignSessionId = "sess-3117-p3-foreign-A";
18964
+ const foreignLeaderThreadId = "thread-3117-p3-A-leader";
18965
+ const sessionId = "sess-3117-p3-checked-B";
18966
+ const leaderThreadId = "thread-3117-p3-B-leader";
18967
+ const childThreadId = "thread-3117-p3-B-child";
18968
+ const sessionPath = join(stateDir, "session.json");
18969
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
18970
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
18971
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
18972
+ active: true,
18973
+ mode: "ralph",
18974
+ current_phase: "executing",
18975
+ session_id: sessionId,
18976
+ });
18977
+ // Evaluated session B has a corrupt tracker: no leader_thread_id, leader mislabeled kind:"subagent".
18978
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
18979
+ schemaVersion: 1,
18980
+ sessions: {
18981
+ [sessionId]: {
18982
+ session_id: sessionId,
18983
+ threads: {
18984
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "subagent", mode: "collaboration-child" },
18985
+ [childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child" },
18986
+ },
18987
+ },
18988
+ },
18989
+ });
18990
+ const dispatchThread = async (threadId) => dispatchCodexNativeHook({
18991
+ hook_event_name: "PreToolUse",
18992
+ cwd,
18993
+ session_id: sessionId,
18994
+ thread_id: threadId,
18995
+ tool_name: "apply_patch",
18996
+ tool_input: { file_path: "src/feature.ts" },
18997
+ }, { cwd });
18998
+ // Root session.json owns a DIFFERENT session A: its native/owner ids must not
18999
+ // anchor session B, so B's mislabeled leader stays blocked (fail closed).
19000
+ await writeJson(sessionPath, { session_id: foreignSessionId, native_session_id: foreignLeaderThreadId });
19001
+ const foreignLeader = await dispatchThread(leaderThreadId);
19002
+ assert.equal(foreignLeader.outputJson?.decision, "block");
19003
+ assert.match(String(foreignLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
19004
+ const foreignChild = await dispatchThread(childThreadId);
19005
+ assert.equal(foreignChild.outputJson?.decision, "block");
19006
+ // Control: when the root session.json owns the evaluated session B, its
19007
+ // native_session_id anchors the leader (blocked) while a genuine child is trusted.
19008
+ await writeJson(sessionPath, { session_id: sessionId, native_session_id: leaderThreadId });
19009
+ const ownedLeader = await dispatchThread(leaderThreadId);
19010
+ assert.equal(ownedLeader.outputJson?.decision, "block");
19011
+ assert.match(String(ownedLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
19012
+ const ownedChild = await dispatchThread(childThreadId);
19013
+ assert.equal(ownedChild.outputJson, null);
19014
+ }
19015
+ finally {
19016
+ await rm(cwd, { recursive: true, force: true });
19017
+ }
19018
+ });
19019
+ it("fails closed when session.json carries only owner session ids without a leader thread anchor (#3117 P4)", async () => {
19020
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3117-owner-only-anchor-"));
19021
+ process.env.OMX_ROOT = cwd;
19022
+ try {
19023
+ const stateDir = join(cwd, ".omx", "state");
19024
+ const sessionId = "sess-3117-p4-owner-only";
19025
+ const leaderThreadId = "thread-3117-p4-leader";
19026
+ const childThreadId = "thread-3117-p4-child";
19027
+ const untrackedThreadId = "thread-3117-p4-untracked";
19028
+ const sessionPath = join(stateDir, "session.json");
19029
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
19030
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
19031
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
19032
+ active: true,
19033
+ mode: "ralph",
19034
+ current_phase: "executing",
19035
+ session_id: sessionId,
19036
+ });
19037
+ // Corrupt tracker for the evaluated session: no leader_thread_id, leader mislabeled kind:"subagent".
19038
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
19039
+ schemaVersion: 1,
19040
+ sessions: {
19041
+ [sessionId]: {
19042
+ session_id: sessionId,
19043
+ threads: {
19044
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "subagent", mode: "collaboration-child" },
19045
+ [childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child" },
19046
+ },
19047
+ },
19048
+ },
19049
+ });
19050
+ const dispatchThread = async (threadId) => dispatchCodexNativeHook({
19051
+ hook_event_name: "PreToolUse",
19052
+ cwd,
19053
+ session_id: sessionId,
19054
+ thread_id: threadId,
19055
+ tool_name: "apply_patch",
19056
+ tool_input: { file_path: "src/feature.ts" },
19057
+ }, { cwd });
19058
+ // session.json maps to the evaluated session via owner ids but has NO
19059
+ // native_session_id: owner session ids are not leader thread anchors, so trust
19060
+ // must fail closed rather than treat their presence as an anchor (#3117 P4).
19061
+ await writeJson(sessionPath, {
19062
+ session_id: sessionId,
19063
+ owner_omx_session_id: "owner-omx-3117-p4",
19064
+ owner_codex_session_id: "owner-codex-3117-p4",
19065
+ });
19066
+ const ownerOnlyLeader = await dispatchThread(leaderThreadId);
19067
+ assert.equal(ownerOnlyLeader.outputJson?.decision, "block");
19068
+ assert.match(String(ownerOnlyLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
19069
+ // Control: an untracked thread also stays blocked under the same state.
19070
+ const ownerOnlyUntracked = await dispatchThread(untrackedThreadId);
19071
+ assert.equal(ownerOnlyUntracked.outputJson?.decision, "block");
19072
+ // Control: once a genuine native_session_id leader thread anchor exists for this
19073
+ // session, the leader is blocked via that anchor while a real child is trusted.
19074
+ await writeJson(sessionPath, { session_id: sessionId, native_session_id: leaderThreadId });
19075
+ const anchoredLeader = await dispatchThread(leaderThreadId);
19076
+ assert.equal(anchoredLeader.outputJson?.decision, "block");
19077
+ assert.match(String(anchoredLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
19078
+ const anchoredChild = await dispatchThread(childThreadId);
19079
+ assert.equal(anchoredChild.outputJson, null);
19080
+ }
19081
+ finally {
19082
+ await rm(cwd, { recursive: true, force: true });
19083
+ }
19084
+ });
18382
19085
  it("blocks conductor writes when thread_spawn provenance is attached to the leader thread", async () => {
18383
19086
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-conductor-thread-spawn-leader-"));
18384
19087
  const originalOmxRoot = process.env.OMX_ROOT;
@@ -18449,6 +19152,387 @@ PY`,
18449
19152
  await rm(cwd, { recursive: true, force: true });
18450
19153
  }
18451
19154
  });
19155
+ it("denies Main-root conductor apply_patch from the session leader thread during active Ralph (#3116)", async () => {
19156
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-root-denial-"));
19157
+ process.env.OMX_ROOT = cwd;
19158
+ try {
19159
+ const stateDir = join(cwd, ".omx", "state");
19160
+ const sessionId = "sess-3116-root-denial";
19161
+ const leaderThreadId = "thread-3116-root-denial-leader";
19162
+ const nowIso = new Date().toISOString();
19163
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
19164
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
19165
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
19166
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
19167
+ active: true,
19168
+ mode: "ralph",
19169
+ current_phase: "executing",
19170
+ session_id: sessionId,
19171
+ });
19172
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
19173
+ schemaVersion: 1,
19174
+ sessions: {
19175
+ [sessionId]: {
19176
+ session_id: sessionId,
19177
+ leader_thread_id: leaderThreadId,
19178
+ updated_at: nowIso,
19179
+ threads: {
19180
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
19181
+ },
19182
+ },
19183
+ },
19184
+ });
19185
+ const result = await dispatchCodexNativeHook({
19186
+ hook_event_name: "PreToolUse",
19187
+ cwd,
19188
+ session_id: sessionId,
19189
+ thread_id: leaderThreadId,
19190
+ tool_name: "apply_patch",
19191
+ tool_input: { file_path: "src/feature.ts" },
19192
+ }, { cwd });
19193
+ assert.equal(result.outputJson?.decision, "block");
19194
+ assert.match(String(result.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
19195
+ }
19196
+ finally {
19197
+ await rm(cwd, { recursive: true, force: true });
19198
+ }
19199
+ });
19200
+ it("allows apply_patch from an untyped collaboration.spawn_agent child tracked as a subagent during active Ralph (#3116)", async () => {
19201
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-trusted-child-"));
19202
+ process.env.OMX_ROOT = cwd;
19203
+ try {
19204
+ const stateDir = join(cwd, ".omx", "state");
19205
+ const sessionId = "sess-3116-trusted-child";
19206
+ const leaderThreadId = "thread-3116-trusted-child-leader";
19207
+ const childThreadId = "thread-3116-trusted-child-worker";
19208
+ const nowIso = new Date().toISOString();
19209
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
19210
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
19211
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
19212
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
19213
+ active: true,
19214
+ mode: "ralph",
19215
+ current_phase: "executing",
19216
+ session_id: sessionId,
19217
+ });
19218
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
19219
+ schemaVersion: 1,
19220
+ sessions: {
19221
+ [sessionId]: {
19222
+ session_id: sessionId,
19223
+ leader_thread_id: leaderThreadId,
19224
+ updated_at: nowIso,
19225
+ threads: {
19226
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
19227
+ [childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
19228
+ },
19229
+ },
19230
+ },
19231
+ });
19232
+ // Untyped role (not in the typed-agent catalog) must not defeat tracker-backed trust.
19233
+ const result = await dispatchCodexNativeHook({
19234
+ hook_event_name: "PreToolUse",
19235
+ cwd,
19236
+ session_id: sessionId,
19237
+ thread_id: childThreadId,
19238
+ agent_role: "collaboration-child",
19239
+ source: {
19240
+ subagent: {
19241
+ thread_spawn: {
19242
+ parent_thread_id: leaderThreadId,
19243
+ depth: 1,
19244
+ agent_nickname: "Helper",
19245
+ },
19246
+ },
19247
+ },
19248
+ tool_name: "apply_patch",
19249
+ tool_input: { file_path: "src/feature.ts" },
19250
+ }, { cwd });
19251
+ assert.equal(result.outputJson, null);
19252
+ }
19253
+ finally {
19254
+ await rm(cwd, { recursive: true, force: true });
19255
+ }
19256
+ });
19257
+ it("allows apply_patch from untyped collaboration descendants via tracked thread and tracked parent chain (#3116)", async () => {
19258
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-descendant-"));
19259
+ process.env.OMX_ROOT = cwd;
19260
+ try {
19261
+ const stateDir = join(cwd, ".omx", "state");
19262
+ const sessionId = "sess-3116-descendant";
19263
+ const leaderThreadId = "thread-3116-descendant-leader";
19264
+ const childThreadId = "thread-3116-descendant-child";
19265
+ const grandchildThreadId = "thread-3116-descendant-grandchild";
19266
+ const greatGrandchildThreadId = "thread-3116-descendant-great-grandchild";
19267
+ const nowIso = new Date().toISOString();
19268
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
19269
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
19270
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
19271
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
19272
+ active: true,
19273
+ mode: "ralph",
19274
+ current_phase: "executing",
19275
+ session_id: sessionId,
19276
+ });
19277
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
19278
+ schemaVersion: 1,
19279
+ sessions: {
19280
+ [sessionId]: {
19281
+ session_id: sessionId,
19282
+ leader_thread_id: leaderThreadId,
19283
+ updated_at: nowIso,
19284
+ threads: {
19285
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
19286
+ [childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
19287
+ [grandchildThreadId]: { thread_id: grandchildThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: childThreadId },
19288
+ },
19289
+ },
19290
+ },
19291
+ });
19292
+ // (a) A descendant whose own thread is tracked as a subagent is trusted directly.
19293
+ const trackedDescendant = await dispatchCodexNativeHook({
19294
+ hook_event_name: "PreToolUse",
19295
+ cwd,
19296
+ session_id: sessionId,
19297
+ thread_id: grandchildThreadId,
19298
+ tool_name: "apply_patch",
19299
+ tool_input: { file_path: "src/feature.ts" },
19300
+ }, { cwd });
19301
+ assert.equal(trackedDescendant.outputJson, null);
19302
+ // (b) A not-yet-tracked descendant is trusted when its runtime spawn parent is a tracked thread.
19303
+ const chainedDescendant = await dispatchCodexNativeHook({
19304
+ hook_event_name: "PreToolUse",
19305
+ cwd,
19306
+ session_id: sessionId,
19307
+ thread_id: greatGrandchildThreadId,
19308
+ agent_role: "collaboration-child",
19309
+ source: {
19310
+ subagent: {
19311
+ thread_spawn: {
19312
+ parent_thread_id: grandchildThreadId,
19313
+ depth: 3,
19314
+ },
19315
+ },
19316
+ },
19317
+ tool_name: "apply_patch",
19318
+ tool_input: { file_path: "src/feature.ts" },
19319
+ }, { cwd });
19320
+ assert.equal(chainedDescendant.outputJson, null);
19321
+ }
19322
+ finally {
19323
+ await rm(cwd, { recursive: true, force: true });
19324
+ }
19325
+ });
19326
+ it("resolves the session-pinned Ralph state so collaboration children edit while the leader stays blocked (#3116)", async () => {
19327
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-pinned-state-"));
19328
+ process.env.OMX_ROOT = cwd;
19329
+ try {
19330
+ const stateDir = join(cwd, ".omx", "state");
19331
+ const sessionId = "sess-3116-pinned-state";
19332
+ const leaderThreadId = "thread-3116-pinned-state-leader";
19333
+ const childThreadId = "thread-3116-pinned-state-child";
19334
+ const nowIso = new Date().toISOString();
19335
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
19336
+ // Native session id differs from the canonical session id: the guard must resolve it.
19337
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
19338
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
19339
+ const ralphStatePath = join(stateDir, "sessions", sessionId, "ralph-state.json");
19340
+ await writeJson(ralphStatePath, {
19341
+ active: true,
19342
+ mode: "ralph",
19343
+ current_phase: "executing",
19344
+ session_id: sessionId,
19345
+ });
19346
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
19347
+ schemaVersion: 1,
19348
+ sessions: {
19349
+ [sessionId]: {
19350
+ session_id: sessionId,
19351
+ leader_thread_id: leaderThreadId,
19352
+ updated_at: nowIso,
19353
+ threads: {
19354
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
19355
+ [childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
19356
+ },
19357
+ },
19358
+ },
19359
+ });
19360
+ // Payload carries the native session id, which must map to the canonical session.
19361
+ const childEdit = await dispatchCodexNativeHook({
19362
+ hook_event_name: "PreToolUse",
19363
+ cwd,
19364
+ session_id: leaderThreadId,
19365
+ thread_id: childThreadId,
19366
+ tool_name: "apply_patch",
19367
+ tool_input: { file_path: "src/feature.ts" },
19368
+ }, { cwd });
19369
+ assert.equal(childEdit.outputJson, null);
19370
+ const leaderEdit = await dispatchCodexNativeHook({
19371
+ hook_event_name: "PreToolUse",
19372
+ cwd,
19373
+ session_id: leaderThreadId,
19374
+ thread_id: leaderThreadId,
19375
+ tool_name: "apply_patch",
19376
+ tool_input: { file_path: "src/feature.ts" },
19377
+ }, { cwd });
19378
+ assert.equal(leaderEdit.outputJson?.decision, "block");
19379
+ assert.match(String(leaderEdit.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
19380
+ // The guard reads the session-pinned phase: flipping it to "starting" releases the leader too.
19381
+ await writeJson(ralphStatePath, {
19382
+ active: true,
19383
+ mode: "ralph",
19384
+ current_phase: "starting",
19385
+ session_id: sessionId,
19386
+ });
19387
+ const leaderEditAfterStarting = await dispatchCodexNativeHook({
19388
+ hook_event_name: "PreToolUse",
19389
+ cwd,
19390
+ session_id: leaderThreadId,
19391
+ thread_id: leaderThreadId,
19392
+ tool_name: "apply_patch",
19393
+ tool_input: { file_path: "src/feature.ts" },
19394
+ }, { cwd });
19395
+ assert.equal(leaderEditAfterStarting.outputJson, null);
19396
+ }
19397
+ finally {
19398
+ await rm(cwd, { recursive: true, force: true });
19399
+ }
19400
+ });
19401
+ it("trusts the native collaboration.spawn_agent child surface via runtime spawn provenance during active Ralph (#3116)", async () => {
19402
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-collab-surface-"));
19403
+ process.env.OMX_ROOT = cwd;
19404
+ try {
19405
+ const stateDir = join(cwd, ".omx", "state");
19406
+ const sessionId = "sess-3116-collab-surface";
19407
+ const leaderThreadId = "thread-3116-collab-surface-leader";
19408
+ const childThreadId = "thread-3116-collab-surface-child";
19409
+ const nowIso = new Date().toISOString();
19410
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
19411
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
19412
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
19413
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
19414
+ active: true,
19415
+ mode: "ralph",
19416
+ current_phase: "executing",
19417
+ session_id: sessionId,
19418
+ });
19419
+ // Child SessionStart has not been recorded yet: only the leader is tracked.
19420
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
19421
+ schemaVersion: 1,
19422
+ sessions: {
19423
+ [sessionId]: {
19424
+ session_id: sessionId,
19425
+ leader_thread_id: leaderThreadId,
19426
+ updated_at: nowIso,
19427
+ threads: {
19428
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
19429
+ },
19430
+ },
19431
+ },
19432
+ });
19433
+ // No typed agent_role at all: a raw collaboration.spawn_agent child payload shape.
19434
+ const result = await dispatchCodexNativeHook({
19435
+ hook_event_name: "PreToolUse",
19436
+ cwd,
19437
+ session_id: sessionId,
19438
+ thread_id: childThreadId,
19439
+ source: {
19440
+ subagent: {
19441
+ thread_spawn: {
19442
+ parent_thread_id: leaderThreadId,
19443
+ depth: 1,
19444
+ agent_nickname: "Implementer",
19445
+ },
19446
+ },
19447
+ },
19448
+ tool_name: "apply_patch",
19449
+ tool_input: { file_path: "src/feature.ts" },
19450
+ }, { cwd });
19451
+ assert.equal(result.outputJson, null);
19452
+ }
19453
+ finally {
19454
+ await rm(cwd, { recursive: true, force: true });
19455
+ }
19456
+ });
19457
+ it("denies spoofed or untrusted collaboration provenance while the main root stays protected during active Ralph (#3116)", async () => {
19458
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-spoof-denial-"));
19459
+ process.env.OMX_ROOT = cwd;
19460
+ try {
19461
+ const stateDir = join(cwd, ".omx", "state");
19462
+ const sessionId = "sess-3116-spoof-denial";
19463
+ const leaderThreadId = "thread-3116-spoof-denial-leader";
19464
+ const nowIso = new Date().toISOString();
19465
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
19466
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
19467
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
19468
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
19469
+ active: true,
19470
+ mode: "ralph",
19471
+ current_phase: "executing",
19472
+ session_id: sessionId,
19473
+ });
19474
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
19475
+ schemaVersion: 1,
19476
+ sessions: {
19477
+ [sessionId]: {
19478
+ session_id: sessionId,
19479
+ leader_thread_id: leaderThreadId,
19480
+ updated_at: nowIso,
19481
+ threads: {
19482
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
19483
+ },
19484
+ },
19485
+ },
19486
+ });
19487
+ // (a) Untracked thread whose declared parent is neither the leader nor a tracked thread.
19488
+ const orphanParent = await dispatchCodexNativeHook({
19489
+ hook_event_name: "PreToolUse",
19490
+ cwd,
19491
+ session_id: sessionId,
19492
+ thread_id: "thread-3116-spoof-orphan",
19493
+ agent_role: "collaboration-child",
19494
+ source: {
19495
+ subagent: {
19496
+ thread_spawn: { parent_thread_id: "thread-3116-spoof-outsider" },
19497
+ },
19498
+ },
19499
+ tool_name: "apply_patch",
19500
+ tool_input: { file_path: "src/feature.ts" },
19501
+ }, { cwd });
19502
+ assert.equal(orphanParent.outputJson?.decision, "block");
19503
+ assert.match(String(orphanParent.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
19504
+ // (b) Untyped child with no provenance at all (no tracker thread, no spawn source).
19505
+ const noProvenance = await dispatchCodexNativeHook({
19506
+ hook_event_name: "PreToolUse",
19507
+ cwd,
19508
+ session_id: sessionId,
19509
+ thread_id: "thread-3116-spoof-bare",
19510
+ agent_role: "collaboration-child",
19511
+ tool_name: "apply_patch",
19512
+ tool_input: { file_path: "src/feature.ts" },
19513
+ }, { cwd });
19514
+ assert.equal(noProvenance.outputJson?.decision, "block");
19515
+ // (c) Spawn provenance attached to the leader thread cannot promote the main root.
19516
+ const leaderSelfSpawn = await dispatchCodexNativeHook({
19517
+ hook_event_name: "PreToolUse",
19518
+ cwd,
19519
+ session_id: sessionId,
19520
+ thread_id: leaderThreadId,
19521
+ agent_role: "collaboration-child",
19522
+ source: {
19523
+ subagent: {
19524
+ thread_spawn: { parent_thread_id: leaderThreadId },
19525
+ },
19526
+ },
19527
+ tool_name: "apply_patch",
19528
+ tool_input: { file_path: "src/feature.ts" },
19529
+ }, { cwd });
19530
+ assert.equal(leaderSelfSpawn.outputJson?.decision, "block");
19531
+ }
19532
+ finally {
19533
+ await rm(cwd, { recursive: true, force: true });
19534
+ }
19535
+ });
18452
19536
  it("blocks Main-root ralph conductor source and planning artifact writes while allowing .omx workflow state writes", async () => {
18453
19537
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralph-conductor-write-"));
18454
19538
  try {