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
@@ -782,6 +782,72 @@ describe("codex native hook dispatch", () => {
782
782
  }
783
783
  });
784
784
 
785
+ it("allows Ralplan Markdown draft-only apply_patch on the live CLI path while denying mixed targets", async () => {
786
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-ralplan-draft-boundary-"));
787
+ const sessionId = "sess-cli-ralplan-draft-boundary";
788
+ const stateDir = join(cwd, ".omx", "state");
789
+ try {
790
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, cwd });
791
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
792
+ active: true,
793
+ skill: "ralplan",
794
+ phase: "planning",
795
+ session_id: sessionId,
796
+ active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
797
+ });
798
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
799
+ active: true,
800
+ mode: "ralplan",
801
+ current_phase: "planning",
802
+ session_id: sessionId,
803
+ });
804
+
805
+ for (const [name, target] of [
806
+ ["relative", ".omx/drafts/issue-3105.md"],
807
+ ["repository-absolute", join(cwd, ".omx", "drafts", "issue-3105.md")],
808
+ ] as const) {
809
+ const result = runNativeHookCliResult({
810
+ hook_event_name: "PreToolUse",
811
+ cwd,
812
+ session_id: sessionId,
813
+ thread_id: `thread-cli-ralplan-draft-${name}`,
814
+ tool_name: "apply_patch",
815
+ tool_input: {
816
+ input: `*** Begin Patch\n*** Add File: ${target}\n+# Draft\n*** End Patch\n`,
817
+ },
818
+ }, { cwd });
819
+
820
+ assert.equal(result.status, 0, result.stderr || result.stdout);
821
+ assert.deepEqual(parseSingleJsonStdout(result.stdout), {});
822
+ }
823
+
824
+ const mixedResult = runNativeHookCliResult({
825
+ hook_event_name: "PreToolUse",
826
+ cwd,
827
+ session_id: sessionId,
828
+ thread_id: "thread-cli-ralplan-draft-mixed",
829
+ tool_name: "apply_patch",
830
+ tool_input: {
831
+ 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",
832
+ },
833
+ }, { cwd });
834
+
835
+ assert.equal(mixedResult.status, 0, mixedResult.stderr || mixedResult.stdout);
836
+ const mixedOutput = parseSingleJsonStdout(mixedResult.stdout);
837
+ const hookSpecificOutput = mixedOutput.hookSpecificOutput as Record<string, unknown>;
838
+ assert.deepEqual(Object.keys(mixedOutput).sort(), ["hookSpecificOutput"]);
839
+ assert.equal(hookSpecificOutput.hookEventName, "PreToolUse");
840
+ assert.equal(hookSpecificOutput.permissionDecision, "deny");
841
+ assert.match(String(hookSpecificOutput.permissionDecisionReason ?? ""), /src\/leak\.ts/);
842
+ assert.match(String(hookSpecificOutput.permissionDecisionReason ?? ""), /implementation\/write tools are blocked/);
843
+ assert.equal(mixedOutput.decision, undefined);
844
+ assert.equal(mixedOutput.reason, undefined);
845
+ assert.equal(mixedOutput.systemMessage, undefined);
846
+ } finally {
847
+ await rm(cwd, { recursive: true, force: true });
848
+ }
849
+ });
850
+
785
851
  it("preserves team-worker typed subagent PreToolUse exemption without thread spawn provenance", async () => {
786
852
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-team-worker-typed-pretool-exempt-"));
787
853
  const sessionId = "sess-team-worker-typed-pretool-exempt";
@@ -1518,13 +1584,56 @@ PY`,
1518
1584
  cwd,
1519
1585
  session_id: sessionId,
1520
1586
  thread_id: "thread-autopilot-diagnostic",
1521
- }, { cwd })) as { decision?: string; reason?: string; statePath?: string; canonicalDisagreement?: string };
1587
+ }, { cwd }));
1522
1588
 
1523
1589
  assert.equal(output.decision, "block");
1590
+ assert.equal(output.stopReason, "autopilot_ultragoal");
1524
1591
  assert.match(String(output.reason ?? ""), /state: \.omx\/state\/sessions\/sess-autopilot-diagnostic\/autopilot-state\.json/);
1525
1592
  assert.match(String(output.reason ?? ""), /canonical: canonical_agrees/);
1526
- assert.equal(output.statePath, ".omx/state/sessions/sess-autopilot-diagnostic/autopilot-state.json");
1527
- assert.equal(output.canonicalDisagreement, "canonical_agrees");
1593
+ assert.match(String(output.systemMessage ?? ""), /state: \.omx\/state\/sessions\/sess-autopilot-diagnostic\/autopilot-state\.json/);
1594
+ assert.match(String(output.systemMessage ?? ""), /canonical: canonical_agrees/);
1595
+ assert.deepEqual(Object.keys(output).sort(), ["decision", "reason", "stopReason", "systemMessage"]);
1596
+ assert.equal("statePath" in output, false);
1597
+ assert.equal("canonicalDisagreement" in output, false);
1598
+ } finally {
1599
+ await rm(cwd, { recursive: true, force: true });
1600
+ }
1601
+ });
1602
+
1603
+ it("keeps canonical phase disagreements in active Autopilot Stop diagnostics", async () => {
1604
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-canonical-phase-"));
1605
+ try {
1606
+ const sessionId = "sess-autopilot-canonical-phase";
1607
+ await writeJson(join(cwd, ".omx", "state", "session.json"), { session_id: sessionId, cwd });
1608
+ await writeJson(join(cwd, ".omx", "state", "skill-active-state.json"), {
1609
+ version: 1,
1610
+ active: true,
1611
+ skill: "autopilot",
1612
+ phase: "execution",
1613
+ session_id: sessionId,
1614
+ active_skills: [{ skill: "autopilot", phase: "execution", active: true, session_id: sessionId }],
1615
+ });
1616
+ await writeJson(join(cwd, ".omx", "state", "sessions", sessionId, "autopilot-state.json"), {
1617
+ active: true,
1618
+ mode: "autopilot",
1619
+ current_phase: "ultragoal",
1620
+ session_id: sessionId,
1621
+ });
1622
+
1623
+ const output = parseSingleJsonStdout(runNativeHookCli({
1624
+ hook_event_name: "Stop",
1625
+ cwd,
1626
+ session_id: sessionId,
1627
+ thread_id: "thread-autopilot-canonical-phase",
1628
+ }, { cwd }));
1629
+
1630
+ assert.equal(output.decision, "block");
1631
+ assert.equal(output.stopReason, "autopilot_ultragoal");
1632
+ assert.match(String(output.reason ?? ""), /canonical: canonical_phase:execution/);
1633
+ assert.match(String(output.systemMessage ?? ""), /canonical: canonical_phase:execution/);
1634
+ assert.deepEqual(Object.keys(output).sort(), ["decision", "reason", "stopReason", "systemMessage"]);
1635
+ assert.equal("statePath" in output, false);
1636
+ assert.equal("canonicalDisagreement" in output, false);
1528
1637
  } finally {
1529
1638
  await rm(cwd, { recursive: true, force: true });
1530
1639
  }
@@ -10826,6 +10935,7 @@ exit 0
10826
10935
  for (const path of [
10827
10936
  ".omx/context/findings.md",
10828
10937
  ".omx/plans/issue-2863.md",
10938
+ ".omx/drafts/issue-3105.md",
10829
10939
  ".omx/specs/issue-2863.md",
10830
10940
  ".omx/state/required-planning-state.json",
10831
10941
  ]) {
@@ -10833,6 +10943,24 @@ exit 0
10833
10943
  assert.equal(writeResult.outputJson, null, `Write should be allowed for ${path}`);
10834
10944
  }
10835
10945
 
10946
+ const allowedDraftEdit = await preToolUse("Edit", "tool-ralplan-draft-edit", {
10947
+ file_path: ".omx/drafts/issue-3105.md",
10948
+ old_string: "old",
10949
+ new_string: "new",
10950
+ });
10951
+ assert.equal(allowedDraftEdit.outputJson, null);
10952
+
10953
+ for (const path of [
10954
+ join(cwd, ".omx", "drafts", "issue-3105.md"),
10955
+ ".omx/drafts/subdir/../issue-3105.md",
10956
+ ]) {
10957
+ const normalizedDraftWrite = await preToolUse("Write", `tool-ralplan-normalized-draft-${path}`, {
10958
+ file_path: path,
10959
+ content: "# Draft",
10960
+ });
10961
+ assert.equal(normalizedDraftWrite.outputJson, null, `normalized draft path should be allowed: ${path}`);
10962
+ }
10963
+
10836
10964
  for (const protectedPath of [
10837
10965
  ".omx/state/sessions/sess-ralplan-guard/ralplan-state.json",
10838
10966
  ".omx/state/sessions/sess-ralplan-guard/autopilot-state.json",
@@ -10874,11 +11002,26 @@ exit 0
10874
11002
  });
10875
11003
  assert.equal(allowedPatchUpdate.outputJson, null);
10876
11004
 
11005
+ const allowedDraftPatchAdd = await preToolUse("apply_patch", "tool-ralplan-draft-patch-add", {
11006
+ input: "*** Begin Patch\n*** Add File: .omx/drafts/issue-3105.md\n+# Draft\n*** End Patch\n",
11007
+ });
11008
+ assert.equal(allowedDraftPatchAdd.outputJson, null);
11009
+
11010
+ const allowedDraftPatchUpdate = await preToolUse("ApplyPatch", "tool-ralplan-draft-patch-update", {
11011
+ input: "*** Begin Patch\n*** Update File: .omx/drafts/issue-3105.md\n@@\n-old\n+new\n*** End Patch\n",
11012
+ });
11013
+ assert.equal(allowedDraftPatchUpdate.outputJson, null);
11014
+
10877
11015
  const allowedRedirect = await preToolUse("Bash", "tool-ralplan-redirect-allow", {
10878
11016
  command: "printf '\\nmore\\n' >> .omx/plans/issue-2863.md",
10879
11017
  });
10880
11018
  assert.equal(allowedRedirect.outputJson, null);
10881
11019
 
11020
+ const allowedDraftRedirect = await preToolUse("Bash", "tool-ralplan-draft-redirect-allow", {
11021
+ command: "printf '# Draft\\n' > .omx/drafts/issue-3105.md",
11022
+ });
11023
+ assert.equal(allowedDraftRedirect.outputJson, null);
11024
+
10882
11025
  const allowedTee = await preToolUse("Bash", "tool-ralplan-tee-allow", {
10883
11026
  command: "printf '\\nmore\\n' | tee -a .omx/specs/issue-2863.md",
10884
11027
  });
@@ -10940,6 +11083,41 @@ exit 0
10940
11083
  const editReason = String((blockedEdit.outputJson as { reason?: string } | null)?.reason ?? "");
10941
11084
  assert.match(editReason, /Edit path/);
10942
11085
  assert.match(editReason, /src\/implementation\.ts/);
11086
+ const blockedEditContext = String(
11087
+ (blockedEdit.outputJson as { hookSpecificOutput?: { additionalContext?: string } } | null)
11088
+ ?.hookSpecificOutput?.additionalContext ?? "",
11089
+ );
11090
+ assert.match(blockedEditContext, /Markdown drafts under `\.omx\/drafts\/\*\.md`/);
11091
+ for (const allowedCategory of [
11092
+ ".omx/context/",
11093
+ ".omx/plans/",
11094
+ ".omx/specs/",
11095
+ ".omx/tmp/",
11096
+ ".omx/state/",
11097
+ ".beads/",
11098
+ ]) {
11099
+ assert.match(blockedEditContext, new RegExp(allowedCategory.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
11100
+ }
11101
+
11102
+ for (const blockedDraftPath of [
11103
+ ".omx/drafts/issue-3105.MD",
11104
+ ".omx/Drafts/issue-3105.md",
11105
+ ".omx/drafts/issue-3105",
11106
+ ".omx/drafts/run.sh",
11107
+ ".omx/drafts/subdir/issue-3105.md",
11108
+ ".omx/drafts/../../src/leak.ts",
11109
+ "/tmp/issue-3105.md",
11110
+ ]) {
11111
+ const blockedDraft = await preToolUse("Write", `tool-ralplan-draft-block-${blockedDraftPath}`, {
11112
+ file_path: blockedDraftPath,
11113
+ content: "bad",
11114
+ });
11115
+ assert.equal(
11116
+ (blockedDraft.outputJson as { decision?: string } | null)?.decision,
11117
+ "block",
11118
+ `draft path should be blocked: ${blockedDraftPath}`,
11119
+ );
11120
+ }
10943
11121
 
10944
11122
  const blockedMixedPatch = await preToolUse("apply_patch", "tool-ralplan-patch-mixed", {
10945
11123
  input: "*** Begin Patch\n*** Add File: .omx/plans/ok.md\n+ok\n*** Add File: src/leak.ts\n+leak\n*** End Patch\n",
@@ -10949,6 +11127,18 @@ exit 0
10949
11127
  assert.match(mixedReason, /apply_patch target/);
10950
11128
  assert.match(mixedReason, /src\/leak\.ts/);
10951
11129
 
11130
+ const blockedMixedDraftPatch = await preToolUse("apply_patch", "tool-ralplan-draft-patch-mixed", {
11131
+ 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",
11132
+ });
11133
+ assert.equal((blockedMixedDraftPatch.outputJson as { decision?: string } | null)?.decision, "block");
11134
+ assert.match(String((blockedMixedDraftPatch.outputJson as { reason?: string } | null)?.reason ?? ""), /src\/leak\.ts/);
11135
+
11136
+ const blockedMixedDraftBash = await preToolUse("Bash", "tool-ralplan-draft-bash-mixed", {
11137
+ command: "printf '# Draft\\n' > .omx/drafts/issue-3105.md; printf 'bad\\n' > src/leak.ts",
11138
+ });
11139
+ assert.equal((blockedMixedDraftBash.outputJson as { decision?: string } | null)?.decision, "block");
11140
+ assert.match(String((blockedMixedDraftBash.outputJson as { reason?: string } | null)?.reason ?? ""), /src\/leak\.ts/);
11141
+
10952
11142
  const blockedUnparseablePatch = await preToolUse("apply_patch", "tool-ralplan-patch-unparseable", {
10953
11143
  input: "not a recognizable patch",
10954
11144
  });
@@ -10963,6 +11153,12 @@ exit 0
10963
11153
  assert.match(unresolvedReason, /unresolved Bash write target/);
10964
11154
  assert.match(unresolvedReason, /\$PLAN_PATH/);
10965
11155
 
11156
+ const blockedUnresolvedDraftRedirect = await preToolUse("Bash", "tool-ralplan-draft-redirect-unresolved", {
11157
+ command: "cat > \"$DRAFT_PATH\" <<'EOF'\ncontent\nEOF",
11158
+ });
11159
+ assert.equal((blockedUnresolvedDraftRedirect.outputJson as { decision?: string } | null)?.decision, "block");
11160
+ assert.match(String((blockedUnresolvedDraftRedirect.outputJson as { reason?: string } | null)?.reason ?? ""), /\$DRAFT_PATH/);
11161
+
10966
11162
  const blockedTraversal = await preToolUse("Write", "tool-ralplan-traversal-block", {
10967
11163
  file_path: ".omx/plans/../../src/leak.ts",
10968
11164
  content: "bad",
@@ -11618,6 +11814,175 @@ PY`,
11618
11814
  }
11619
11815
  });
11620
11816
 
11817
+ it("keeps untyped collaboration child implementation writes blocked under active ralplan planning while typed trusted agents pass (#3116)", async () => {
11818
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-ralplan-untyped-"));
11819
+ process.env.OMX_ROOT = cwd;
11820
+ try {
11821
+ const stateDir = join(cwd, ".omx", "state");
11822
+ const sessionId = "sess-3116-ralplan-untyped";
11823
+ const leaderThreadId = "thread-3116-ralplan-untyped-leader";
11824
+ const childThreadId = "thread-3116-ralplan-untyped-child";
11825
+ const sessionDir = join(stateDir, "sessions", sessionId);
11826
+ const nowIso = new Date().toISOString();
11827
+ await mkdir(sessionDir, { recursive: true });
11828
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId, cwd });
11829
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
11830
+ version: 1,
11831
+ active: true,
11832
+ skill: "autopilot",
11833
+ phase: "ralplan",
11834
+ session_id: sessionId,
11835
+ thread_id: leaderThreadId,
11836
+ active_skills: [
11837
+ { skill: "autopilot", phase: "ralplan", active: true, session_id: sessionId, thread_id: leaderThreadId },
11838
+ ],
11839
+ });
11840
+ await writeJson(join(sessionDir, "autopilot-state.json"), {
11841
+ active: true,
11842
+ mode: "autopilot",
11843
+ current_phase: "ralplan",
11844
+ session_id: sessionId,
11845
+ thread_id: leaderThreadId,
11846
+ });
11847
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
11848
+ schemaVersion: 1,
11849
+ sessions: {
11850
+ [sessionId]: {
11851
+ session_id: sessionId,
11852
+ leader_thread_id: leaderThreadId,
11853
+ updated_at: nowIso,
11854
+ threads: {
11855
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
11856
+ [childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
11857
+ },
11858
+ },
11859
+ },
11860
+ });
11861
+
11862
+ // Untyped tracked/spawn-provenance child must NOT bypass the ralplan planning guard.
11863
+ const untypedChild = await dispatchCodexNativeHook(
11864
+ {
11865
+ hook_event_name: "PreToolUse",
11866
+ cwd,
11867
+ session_id: sessionId,
11868
+ thread_id: childThreadId,
11869
+ agent_role: "collaboration-child",
11870
+ source: {
11871
+ subagent: { thread_spawn: { parent_thread_id: leaderThreadId } },
11872
+ },
11873
+ tool_name: "Edit",
11874
+ tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
11875
+ },
11876
+ { cwd },
11877
+ );
11878
+ assert.equal(untypedChild.outputJson?.decision, "block");
11879
+
11880
+ // A typed trusted agent retains its existing planning exemption.
11881
+ const typedChild = await dispatchCodexNativeHook(
11882
+ {
11883
+ hook_event_name: "PreToolUse",
11884
+ cwd,
11885
+ session_id: sessionId,
11886
+ thread_id: childThreadId,
11887
+ agent_role: "executor",
11888
+ source: {
11889
+ subagent: { thread_spawn: { parent_thread_id: leaderThreadId } },
11890
+ },
11891
+ tool_name: "Edit",
11892
+ tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
11893
+ },
11894
+ { cwd },
11895
+ );
11896
+ assert.equal(typedChild.outputJson, null);
11897
+ } finally {
11898
+ await rm(cwd, { recursive: true, force: true });
11899
+ }
11900
+ });
11901
+
11902
+ it("keeps untyped collaboration child implementation writes blocked under active deep-interview planning while typed trusted agents pass (#3116)", async () => {
11903
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-di-untyped-"));
11904
+ process.env.OMX_ROOT = cwd;
11905
+ try {
11906
+ const stateDir = join(cwd, ".omx", "state");
11907
+ const sessionId = "sess-3116-di-untyped";
11908
+ const leaderThreadId = "thread-3116-di-untyped-leader";
11909
+ const childThreadId = "thread-3116-di-untyped-child";
11910
+ const sessionDir = join(stateDir, "sessions", sessionId);
11911
+ const nowIso = new Date().toISOString();
11912
+ await mkdir(sessionDir, { recursive: true });
11913
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId, cwd });
11914
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
11915
+ version: 1,
11916
+ active: true,
11917
+ skill: "deep-interview",
11918
+ phase: "planning",
11919
+ session_id: sessionId,
11920
+ thread_id: leaderThreadId,
11921
+ active_skills: [
11922
+ { skill: "deep-interview", phase: "planning", active: true, session_id: sessionId, thread_id: leaderThreadId },
11923
+ ],
11924
+ });
11925
+ await writeJson(join(sessionDir, "deep-interview-state.json"), {
11926
+ active: true,
11927
+ mode: "deep-interview",
11928
+ current_phase: "intent-first",
11929
+ session_id: sessionId,
11930
+ });
11931
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
11932
+ schemaVersion: 1,
11933
+ sessions: {
11934
+ [sessionId]: {
11935
+ session_id: sessionId,
11936
+ leader_thread_id: leaderThreadId,
11937
+ updated_at: nowIso,
11938
+ threads: {
11939
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
11940
+ [childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
11941
+ },
11942
+ },
11943
+ },
11944
+ });
11945
+
11946
+ // Untyped tracked/spawn-provenance child must NOT bypass the deep-interview planning guard.
11947
+ const untypedChild = await dispatchCodexNativeHook(
11948
+ {
11949
+ hook_event_name: "PreToolUse",
11950
+ cwd,
11951
+ session_id: sessionId,
11952
+ thread_id: childThreadId,
11953
+ agent_role: "collaboration-child",
11954
+ source: {
11955
+ subagent: { thread_spawn: { parent_thread_id: leaderThreadId } },
11956
+ },
11957
+ tool_name: "Edit",
11958
+ tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
11959
+ },
11960
+ { cwd },
11961
+ );
11962
+ assert.equal(untypedChild.outputJson?.decision, "block");
11963
+
11964
+ // A typed trusted agent retains its existing planning exemption.
11965
+ const typedChild = await dispatchCodexNativeHook(
11966
+ {
11967
+ hook_event_name: "PreToolUse",
11968
+ cwd,
11969
+ session_id: sessionId,
11970
+ thread_id: childThreadId,
11971
+ agent_role: "executor",
11972
+ source: {
11973
+ subagent: { thread_spawn: { parent_thread_id: leaderThreadId } },
11974
+ },
11975
+ tool_name: "Edit",
11976
+ tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
11977
+ },
11978
+ { cwd },
11979
+ );
11980
+ assert.equal(typedChild.outputJson, null);
11981
+ } finally {
11982
+ await rm(cwd, { recursive: true, force: true });
11983
+ }
11984
+ });
11985
+
11621
11986
  it("allows null-device fd redirects while deep-interview blocks real Bash writes", async () => {
11622
11987
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-null-redirect-"));
11623
11988
  try {
@@ -18234,21 +18599,20 @@ PY`,
18234
18599
  execFileSync("git", ["commit", "-m", "init"], { cwd, stdio: "ignore" });
18235
18600
  await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 2;\n", "utf-8");
18236
18601
 
18237
- const result = await dispatchCodexNativeHook(
18238
- {
18239
- hook_event_name: "Stop",
18240
- cwd,
18241
- session_id: "sess-stop-doc-refresh",
18242
- last_assistant_message: "Launch-ready: yes",
18243
- },
18244
- { cwd },
18245
- );
18602
+ const output = parseSingleJsonStdout(runNativeHookCli({
18603
+ hook_event_name: "Stop",
18604
+ cwd,
18605
+ session_id: "sess-stop-doc-refresh",
18606
+ last_assistant_message: "Launch-ready: yes",
18607
+ }, { cwd }));
18246
18608
 
18247
- assert.equal(result.omxEventName, "stop");
18248
- assert.equal((result.outputJson as { decision?: string } | null)?.decision, undefined);
18249
- assert.equal((result.outputJson as { hookSpecificOutput?: { hookEventName?: string } } | null)?.hookSpecificOutput?.hookEventName, "Stop");
18250
- assert.match(JSON.stringify(result.outputJson), /Document-refresh warning/);
18251
- assert.match(JSON.stringify(result.outputJson), /staged \+ unstaged changes/);
18609
+ assert.deepEqual(Object.keys(output).sort(), ["systemMessage"]);
18610
+ assert.equal("hookSpecificOutput" in output, false);
18611
+ assert.equal("decision" in output, false);
18612
+ assert.equal("reason" in output, false);
18613
+ assert.equal("stopReason" in output, false);
18614
+ assert.match(String(output.systemMessage ?? ""), /Document-refresh warning/);
18615
+ assert.match(String(output.systemMessage ?? ""), /staged \+ unstaged changes/);
18252
18616
  } finally {
18253
18617
  await rm(cwd, { recursive: true, force: true });
18254
18618
  }
@@ -21247,9 +21611,24 @@ PY`,
21247
21611
 
21248
21612
  assert.equal(result.omxEventName, "pre-tool-use");
21249
21613
  assert.equal(result.outputJson, null);
21250
- } finally {
21251
- await rm(cwd, { recursive: true, force: true });
21252
- }
21614
+
21615
+ const allowedDraftPatch = await dispatchCodexNativeHook(
21616
+ {
21617
+ hook_event_name: "PreToolUse",
21618
+ cwd,
21619
+ session_id: nativeSessionId,
21620
+ thread_id: "thread-ralplan-native-map-draft-artifact",
21621
+ tool_name: "apply_patch",
21622
+ tool_input: {
21623
+ input: `*** Begin Patch\n*** Add File: ${join(cwd, ".omx", "drafts", "issue-3105.md")}\n+# Draft\n*** End Patch\n`,
21624
+ },
21625
+ },
21626
+ { cwd },
21627
+ );
21628
+ assert.equal(allowedDraftPatch.outputJson, null);
21629
+ } finally {
21630
+ await rm(cwd, { recursive: true, force: true });
21631
+ }
21253
21632
  });
21254
21633
 
21255
21634
  it("blocks mapped implementation writes when explicit ultragoal conductor handoff is active", async () => {
@@ -21365,6 +21744,21 @@ PY`,
21365
21744
  cwd: ownerCwd,
21366
21745
  });
21367
21746
 
21747
+ const allowedForeignDraft = await dispatchCodexNativeHook(
21748
+ {
21749
+ hook_event_name: "PreToolUse",
21750
+ cwd,
21751
+ session_id: "019e-ralplan-live-root-unresolved-current",
21752
+ thread_id: "thread-ralplan-live-root-conflict",
21753
+ tool_name: "apply_patch",
21754
+ tool_input: {
21755
+ input: `*** Begin Patch\n*** Add File: ${join(cwd, ".omx", "drafts", "issue-3105.md")}\n+# Draft\n*** End Patch\n`,
21756
+ },
21757
+ },
21758
+ { cwd },
21759
+ );
21760
+ assert.equal(allowedForeignDraft.outputJson, null);
21761
+
21368
21762
  const result = await dispatchCodexNativeHook(
21369
21763
  {
21370
21764
  hook_event_name: "PreToolUse",
@@ -21458,6 +21852,21 @@ PY`,
21458
21852
 
21459
21853
  assert.equal(result.omxEventName, "pre-tool-use");
21460
21854
  assert.equal(result.outputJson, null);
21855
+
21856
+ const allowedDraftPatch = await dispatchCodexNativeHook(
21857
+ {
21858
+ hook_event_name: "PreToolUse",
21859
+ cwd,
21860
+ session_id: nativeSessionId,
21861
+ thread_id: "thread-ralplan-live-root-owner-draft-pass",
21862
+ tool_name: "apply_patch",
21863
+ tool_input: {
21864
+ input: `*** Begin Patch\n*** Add File: ${join(cwd, ".omx", "drafts", "live-root-owner.md")}\n+# Draft\n*** End Patch\n`,
21865
+ },
21866
+ },
21867
+ { cwd },
21868
+ );
21869
+ assert.equal(allowedDraftPatch.outputJson, null);
21461
21870
  } finally {
21462
21871
  await rm(cwd, { recursive: true, force: true });
21463
21872
  }
@@ -21867,6 +22276,140 @@ PY`,
21867
22276
  }
21868
22277
  });
21869
22278
 
22279
+ 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 () => {
22280
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-conductor-3119-quote-deadlock-"));
22281
+ try {
22282
+ const stateDir = join(cwd, ".omx", "state");
22283
+ const sessionId = "sess-conductor-3119-quote-deadlock";
22284
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
22285
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
22286
+ await writeSessionSkillActiveState(stateDir, sessionId, "ultragoal", "planning");
22287
+ await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
22288
+ active: true,
22289
+ mode: "ultragoal",
22290
+ current_phase: "planning",
22291
+ session_id: sessionId,
22292
+ });
22293
+ // Native delegation is genuinely unsupported (explicit negative evidence).
22294
+ await writeJson(join(stateDir, "native-subagent-support.json"), {
22295
+ schema_version: 1,
22296
+ status: "unsupported",
22297
+ reason: "multi_agent_v1_unavailable",
22298
+ session_id: sessionId,
22299
+ evidence: "unknown tool: multi_agent_v1.spawn_agent",
22300
+ observed_at: new Date().toISOString(),
22301
+ cwd,
22302
+ });
22303
+
22304
+ const dispatch = (command: string) =>
22305
+ dispatchCodexNativeHook(
22306
+ {
22307
+ hook_event_name: "PreToolUse",
22308
+ cwd,
22309
+ session_id: sessionId,
22310
+ thread_id: "thread-conductor-3119-quote-deadlock",
22311
+ tool_name: "Bash",
22312
+ tool_input: { command },
22313
+ },
22314
+ { cwd },
22315
+ );
22316
+
22317
+ // Deadlock prevention (defect B): the Conductor must still terminalize its
22318
+ // own workflow state even when delegation is genuinely unsupported, even
22319
+ // when the JSON payload contains a `>` character.
22320
+ const terminalBlockedWrite = await dispatch(
22321
+ "omx state write --mode ultragoal --input '{\"active\":true,\"current_phase\":\"blocked\",\"reason\":\"native delegation unavailable -> terminalized\"}' --json",
22322
+ );
22323
+ assert.notEqual((terminalBlockedWrite.outputJson as { decision?: string } | null)?.decision, "block");
22324
+
22325
+ // Defect C: quoted regex/source text with redirect metacharacters is not a
22326
+ // write target, so issue creation is not falsely blocked.
22327
+ const issueCreate = await dispatch(
22328
+ "gh issue create --title x --body 'Guard misparsed regex /[^>]+>{1,2}/ as a redirect target'",
22329
+ );
22330
+ assert.notEqual((issueCreate.outputJson as { decision?: string } | null)?.decision, "block");
22331
+
22332
+ // Fail-closed preserved: a REAL unquoted redirect to a non-metadata path is
22333
+ // still blocked.
22334
+ const realRedirect = await dispatch("printf pwn > src/runtime.ts");
22335
+ assert.equal((realRedirect.outputJson as { decision?: string } | null)?.decision, "block");
22336
+ assert.match(
22337
+ String((realRedirect.outputJson as { reason?: string } | null)?.reason ?? ""),
22338
+ /not workflow state\/ledger\/mailbox\/handoff metadata/,
22339
+ );
22340
+
22341
+ // A real unquoted redirect to allowed workflow metadata still passes.
22342
+ const metadataRedirect = await dispatch("printf blocked > .omx/state/conductor.log");
22343
+ assert.notEqual((metadataRedirect.outputJson as { decision?: string } | null)?.decision, "block");
22344
+ } finally {
22345
+ await rm(cwd, { recursive: true, force: true });
22346
+ }
22347
+ });
22348
+
22349
+ it("fail-closed: escaped-quote and ANSI-C quoted redirects to source stay blocked while legit quoted data and nested shells behave (#3119)", async () => {
22350
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-conductor-3119-escaped-quote-"));
22351
+ try {
22352
+ const stateDir = join(cwd, ".omx", "state");
22353
+ const sessionId = "sess-conductor-3119-escaped-quote";
22354
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
22355
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
22356
+ await writeSessionSkillActiveState(stateDir, sessionId, "ultragoal", "planning");
22357
+ await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
22358
+ active: true,
22359
+ mode: "ultragoal",
22360
+ current_phase: "planning",
22361
+ session_id: sessionId,
22362
+ });
22363
+
22364
+ const dispatch = (command: string) =>
22365
+ dispatchCodexNativeHook(
22366
+ {
22367
+ hook_event_name: "PreToolUse",
22368
+ cwd,
22369
+ session_id: sessionId,
22370
+ thread_id: "thread-conductor-3119-escaped-quote",
22371
+ tool_name: "Bash",
22372
+ tool_input: { command },
22373
+ },
22374
+ { cwd },
22375
+ );
22376
+
22377
+ // A top-level escaped quote (\' \") is a LITERAL char in bash, not a quote
22378
+ // opener, and $'...' is ANSI-C quoting. None of these may hide the real
22379
+ // `>` redirect \u2014 all of these genuinely write src/runtime.ts and MUST block.
22380
+ const mustBlock = [
22381
+ "printf pwn > src/runtime.ts", // plain control
22382
+ "printf pwn \\' > src/runtime.ts \\'", // escaped single quote
22383
+ "printf pwn \\\" > src/runtime.ts \\\"", // escaped double quote
22384
+ "printf pwn $'\\'' > src/runtime.ts $'\\''", // ANSI-C escaped quote
22385
+ "bash -c 'printf pwn > src/runtime.ts'", // nested shell redirect
22386
+ ];
22387
+ for (const command of mustBlock) {
22388
+ const result = await dispatch(command);
22389
+ assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block", command);
22390
+ assert.match(
22391
+ String((result.outputJson as { reason?: string } | null)?.reason ?? ""),
22392
+ /not workflow state\/ledger\/mailbox\/handoff metadata/,
22393
+ command,
22394
+ );
22395
+ }
22396
+
22397
+ // Legitimate quoted DATA metacharacters (single quotes, double quotes,
22398
+ // ANSI-C) are not redirects and must not be falsely blocked.
22399
+ const mustNotBlock = [
22400
+ "gh issue create --title x --body 'a > b regex /[^>]+>{1,2}/'",
22401
+ "echo \"value > threshold\"",
22402
+ "printf $'a>b'",
22403
+ ];
22404
+ for (const command of mustNotBlock) {
22405
+ const result = await dispatch(command);
22406
+ assert.notEqual((result.outputJson as { decision?: string } | null)?.decision, "block", command);
22407
+ }
22408
+ } finally {
22409
+ await rm(cwd, { recursive: true, force: true });
22410
+ }
22411
+ });
22412
+
21870
22413
  it("blocks Main-root ralplan writes even when payload has only a typed agent_role", async () => {
21871
22414
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-agent-role-main-"));
21872
22415
  try {
@@ -22132,25 +22675,22 @@ PY`,
22132
22675
  }
22133
22676
  });
22134
22677
 
22135
- it("blocks conductor writes when thread_spawn provenance is attached to the leader thread", async () => {
22136
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-conductor-thread-spawn-leader-"));
22137
- const originalOmxRoot = process.env.OMX_ROOT;
22138
- const originalOmxStateRoot = process.env.OMX_STATE_ROOT;
22139
- const originalOmxTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
22678
+ 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 () => {
22679
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3117-corrupt-leader-no-lead-"));
22680
+ process.env.OMX_ROOT = cwd;
22140
22681
  try {
22141
- process.env.OMX_ROOT = cwd;
22142
- delete process.env.OMX_STATE_ROOT;
22143
- delete process.env.OMX_TEAM_STATE_ROOT;
22144
22682
  const stateDir = join(cwd, ".omx", "state");
22145
- const sessionId = "sess-conductor-thread-spawn-leader";
22146
- const leaderThreadId = "thread-conductor-thread-spawn-leader";
22147
- const nowIso = new Date().toISOString();
22683
+ const sessionId = "sess-3117-corrupt-leader-no-lead";
22684
+ const leaderThreadId = "thread-3117-corrupt-leader-no-lead-leader";
22685
+ const childThreadId = "thread-3117-corrupt-leader-no-lead-child";
22148
22686
  await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
22687
+ // native_session_id anchors the leader identity even though the tracker omits
22688
+ // leader_thread_id and corruptly labels the leader kind:"subagent".
22149
22689
  await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
22150
- await writeSessionSkillActiveState(stateDir, sessionId, "ultragoal", "executing");
22151
- await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
22690
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
22691
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
22152
22692
  active: true,
22153
- mode: "ultragoal",
22693
+ mode: "ralph",
22154
22694
  current_phase: "executing",
22155
22695
  session_id: sessionId,
22156
22696
  });
@@ -22159,44 +22699,732 @@ PY`,
22159
22699
  sessions: {
22160
22700
  [sessionId]: {
22161
22701
  session_id: sessionId,
22162
- leader_thread_id: leaderThreadId,
22163
- updated_at: nowIso,
22164
22702
  threads: {
22165
- [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
22703
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "subagent", mode: "collaboration-child" },
22704
+ [childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child" },
22166
22705
  },
22167
22706
  },
22168
22707
  },
22169
22708
  });
22170
22709
 
22171
- const result = await dispatchCodexNativeHook(
22710
+ // Untyped leader payload must stay blocked: leader identity is anchored to
22711
+ // native_session_id, not the corrupt tracker kind.
22712
+ const untypedLeader = await dispatchCodexNativeHook(
22172
22713
  {
22173
22714
  hook_event_name: "PreToolUse",
22174
22715
  cwd,
22175
22716
  session_id: sessionId,
22176
22717
  thread_id: leaderThreadId,
22177
- agent_role: "executor",
22178
- source: {
22179
- subagent: {
22180
- thread_spawn: {
22181
- parent_thread_id: leaderThreadId,
22182
- },
22718
+ tool_name: "apply_patch",
22719
+ tool_input: { file_path: "src/feature.ts" },
22720
+ },
22721
+ { cwd },
22722
+ );
22723
+ assert.equal(untypedLeader.outputJson?.decision, "block");
22724
+ assert.match(String(untypedLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
22725
+
22726
+ // Control: a genuine non-leader child stays trusted (P1 preserved).
22727
+ const untypedChild = await dispatchCodexNativeHook(
22728
+ {
22729
+ hook_event_name: "PreToolUse",
22730
+ cwd,
22731
+ session_id: sessionId,
22732
+ thread_id: childThreadId,
22733
+ tool_name: "apply_patch",
22734
+ tool_input: { file_path: "src/feature.ts" },
22735
+ },
22736
+ { cwd },
22737
+ );
22738
+ assert.equal(untypedChild.outputJson, null);
22739
+ } finally {
22740
+ await rm(cwd, { recursive: true, force: true });
22741
+ }
22742
+ });
22743
+
22744
+ it("fails closed for untyped provenance when no authoritative leader anchor exists (#3117 P2)", async () => {
22745
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3117-no-leader-anchor-"));
22746
+ process.env.OMX_ROOT = cwd;
22747
+ try {
22748
+ const stateDir = join(cwd, ".omx", "state");
22749
+ const sessionId = "sess-3117-no-leader-anchor";
22750
+ const leaderThreadId = "thread-3117-no-leader-anchor-leader";
22751
+ const childThreadId = "thread-3117-no-leader-anchor-child";
22752
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
22753
+ // No native_session_id / owner ids and no tracker leader_thread_id: the leader is
22754
+ // unidentifiable, so untyped provenance must not be inferred from kind:"subagent".
22755
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
22756
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
22757
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
22758
+ active: true,
22759
+ mode: "ralph",
22760
+ current_phase: "executing",
22761
+ session_id: sessionId,
22762
+ });
22763
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
22764
+ schemaVersion: 1,
22765
+ sessions: {
22766
+ [sessionId]: {
22767
+ session_id: sessionId,
22768
+ threads: {
22769
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "subagent", mode: "collaboration-child" },
22770
+ [childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child" },
22771
+ },
22772
+ },
22773
+ },
22774
+ });
22775
+
22776
+ for (const threadId of [leaderThreadId, childThreadId]) {
22777
+ const result = await dispatchCodexNativeHook(
22778
+ {
22779
+ hook_event_name: "PreToolUse",
22780
+ cwd,
22781
+ session_id: sessionId,
22782
+ thread_id: threadId,
22783
+ tool_name: "apply_patch",
22784
+ tool_input: { file_path: "src/feature.ts" },
22785
+ },
22786
+ { cwd },
22787
+ );
22788
+ assert.equal(result.outputJson?.decision, "block", threadId);
22789
+ assert.match(String(result.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
22790
+ }
22791
+ } finally {
22792
+ await rm(cwd, { recursive: true, force: true });
22793
+ }
22794
+ });
22795
+
22796
+ it("does not borrow leader anchors from a foreign root session.json when evaluating another session (#3117 P3)", async () => {
22797
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3117-cross-session-anchor-"));
22798
+ process.env.OMX_ROOT = cwd;
22799
+ try {
22800
+ const stateDir = join(cwd, ".omx", "state");
22801
+ const foreignSessionId = "sess-3117-p3-foreign-A";
22802
+ const foreignLeaderThreadId = "thread-3117-p3-A-leader";
22803
+ const sessionId = "sess-3117-p3-checked-B";
22804
+ const leaderThreadId = "thread-3117-p3-B-leader";
22805
+ const childThreadId = "thread-3117-p3-B-child";
22806
+ const sessionPath = join(stateDir, "session.json");
22807
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
22808
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
22809
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
22810
+ active: true,
22811
+ mode: "ralph",
22812
+ current_phase: "executing",
22813
+ session_id: sessionId,
22814
+ });
22815
+ // Evaluated session B has a corrupt tracker: no leader_thread_id, leader mislabeled kind:"subagent".
22816
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
22817
+ schemaVersion: 1,
22818
+ sessions: {
22819
+ [sessionId]: {
22820
+ session_id: sessionId,
22821
+ threads: {
22822
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "subagent", mode: "collaboration-child" },
22823
+ [childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child" },
22183
22824
  },
22184
22825
  },
22826
+ },
22827
+ });
22828
+
22829
+ const dispatchThread = async (threadId: string) => dispatchCodexNativeHook(
22830
+ {
22831
+ hook_event_name: "PreToolUse",
22832
+ cwd,
22833
+ session_id: sessionId,
22834
+ thread_id: threadId,
22185
22835
  tool_name: "apply_patch",
22186
- tool_input: { file_path: "configs/ws/kalshi_ws_auth_evidence.json" },
22836
+ tool_input: { file_path: "src/feature.ts" },
22187
22837
  },
22188
22838
  { cwd },
22189
22839
  );
22190
22840
 
22191
- assert.equal(result.outputJson?.decision, "block");
22192
- assert.match(String(result.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ultragoal phase: executing\)/);
22841
+ // Root session.json owns a DIFFERENT session A: its native/owner ids must not
22842
+ // anchor session B, so B's mislabeled leader stays blocked (fail closed).
22843
+ await writeJson(sessionPath, { session_id: foreignSessionId, native_session_id: foreignLeaderThreadId });
22844
+ const foreignLeader = await dispatchThread(leaderThreadId);
22845
+ assert.equal(foreignLeader.outputJson?.decision, "block");
22846
+ assert.match(String(foreignLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
22847
+ const foreignChild = await dispatchThread(childThreadId);
22848
+ assert.equal(foreignChild.outputJson?.decision, "block");
22849
+
22850
+ // Control: when the root session.json owns the evaluated session B, its
22851
+ // native_session_id anchors the leader (blocked) while a genuine child is trusted.
22852
+ await writeJson(sessionPath, { session_id: sessionId, native_session_id: leaderThreadId });
22853
+ const ownedLeader = await dispatchThread(leaderThreadId);
22854
+ assert.equal(ownedLeader.outputJson?.decision, "block");
22855
+ assert.match(String(ownedLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
22856
+ const ownedChild = await dispatchThread(childThreadId);
22857
+ assert.equal(ownedChild.outputJson, null);
22858
+ } finally {
22859
+ await rm(cwd, { recursive: true, force: true });
22860
+ }
22861
+ });
22862
+
22863
+ it("fails closed when session.json carries only owner session ids without a leader thread anchor (#3117 P4)", async () => {
22864
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3117-owner-only-anchor-"));
22865
+ process.env.OMX_ROOT = cwd;
22866
+ try {
22867
+ const stateDir = join(cwd, ".omx", "state");
22868
+ const sessionId = "sess-3117-p4-owner-only";
22869
+ const leaderThreadId = "thread-3117-p4-leader";
22870
+ const childThreadId = "thread-3117-p4-child";
22871
+ const untrackedThreadId = "thread-3117-p4-untracked";
22872
+ const sessionPath = join(stateDir, "session.json");
22873
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
22874
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
22875
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
22876
+ active: true,
22877
+ mode: "ralph",
22878
+ current_phase: "executing",
22879
+ session_id: sessionId,
22880
+ });
22881
+ // Corrupt tracker for the evaluated session: no leader_thread_id, leader mislabeled kind:"subagent".
22882
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
22883
+ schemaVersion: 1,
22884
+ sessions: {
22885
+ [sessionId]: {
22886
+ session_id: sessionId,
22887
+ threads: {
22888
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "subagent", mode: "collaboration-child" },
22889
+ [childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child" },
22890
+ },
22891
+ },
22892
+ },
22893
+ });
22894
+
22895
+ const dispatchThread = async (threadId: string) => dispatchCodexNativeHook(
22896
+ {
22897
+ hook_event_name: "PreToolUse",
22898
+ cwd,
22899
+ session_id: sessionId,
22900
+ thread_id: threadId,
22901
+ tool_name: "apply_patch",
22902
+ tool_input: { file_path: "src/feature.ts" },
22903
+ },
22904
+ { cwd },
22905
+ );
22906
+
22907
+ // session.json maps to the evaluated session via owner ids but has NO
22908
+ // native_session_id: owner session ids are not leader thread anchors, so trust
22909
+ // must fail closed rather than treat their presence as an anchor (#3117 P4).
22910
+ await writeJson(sessionPath, {
22911
+ session_id: sessionId,
22912
+ owner_omx_session_id: "owner-omx-3117-p4",
22913
+ owner_codex_session_id: "owner-codex-3117-p4",
22914
+ });
22915
+ const ownerOnlyLeader = await dispatchThread(leaderThreadId);
22916
+ assert.equal(ownerOnlyLeader.outputJson?.decision, "block");
22917
+ assert.match(String(ownerOnlyLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
22918
+ // Control: an untracked thread also stays blocked under the same state.
22919
+ const ownerOnlyUntracked = await dispatchThread(untrackedThreadId);
22920
+ assert.equal(ownerOnlyUntracked.outputJson?.decision, "block");
22921
+
22922
+ // Control: once a genuine native_session_id leader thread anchor exists for this
22923
+ // session, the leader is blocked via that anchor while a real child is trusted.
22924
+ await writeJson(sessionPath, { session_id: sessionId, native_session_id: leaderThreadId });
22925
+ const anchoredLeader = await dispatchThread(leaderThreadId);
22926
+ assert.equal(anchoredLeader.outputJson?.decision, "block");
22927
+ assert.match(String(anchoredLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
22928
+ const anchoredChild = await dispatchThread(childThreadId);
22929
+ assert.equal(anchoredChild.outputJson, null);
22930
+ } finally {
22931
+ await rm(cwd, { recursive: true, force: true });
22932
+ }
22933
+ });
22934
+
22935
+ it("blocks conductor writes when thread_spawn provenance is attached to the leader thread", async () => {
22936
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-conductor-thread-spawn-leader-"));
22937
+ const originalOmxRoot = process.env.OMX_ROOT;
22938
+ const originalOmxStateRoot = process.env.OMX_STATE_ROOT;
22939
+ const originalOmxTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
22940
+ try {
22941
+ process.env.OMX_ROOT = cwd;
22942
+ delete process.env.OMX_STATE_ROOT;
22943
+ delete process.env.OMX_TEAM_STATE_ROOT;
22944
+ const stateDir = join(cwd, ".omx", "state");
22945
+ const sessionId = "sess-conductor-thread-spawn-leader";
22946
+ const leaderThreadId = "thread-conductor-thread-spawn-leader";
22947
+ const nowIso = new Date().toISOString();
22948
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
22949
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
22950
+ await writeSessionSkillActiveState(stateDir, sessionId, "ultragoal", "executing");
22951
+ await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
22952
+ active: true,
22953
+ mode: "ultragoal",
22954
+ current_phase: "executing",
22955
+ session_id: sessionId,
22956
+ });
22957
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
22958
+ schemaVersion: 1,
22959
+ sessions: {
22960
+ [sessionId]: {
22961
+ session_id: sessionId,
22962
+ leader_thread_id: leaderThreadId,
22963
+ updated_at: nowIso,
22964
+ threads: {
22965
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
22966
+ },
22967
+ },
22968
+ },
22969
+ });
22970
+
22971
+ const result = await dispatchCodexNativeHook(
22972
+ {
22973
+ hook_event_name: "PreToolUse",
22974
+ cwd,
22975
+ session_id: sessionId,
22976
+ thread_id: leaderThreadId,
22977
+ agent_role: "executor",
22978
+ source: {
22979
+ subagent: {
22980
+ thread_spawn: {
22981
+ parent_thread_id: leaderThreadId,
22982
+ },
22983
+ },
22984
+ },
22985
+ tool_name: "apply_patch",
22986
+ tool_input: { file_path: "configs/ws/kalshi_ws_auth_evidence.json" },
22987
+ },
22988
+ { cwd },
22989
+ );
22990
+
22991
+ assert.equal(result.outputJson?.decision, "block");
22992
+ assert.match(String(result.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ultragoal phase: executing\)/);
22993
+ } finally {
22994
+ if (originalOmxRoot === undefined) delete process.env.OMX_ROOT;
22995
+ else process.env.OMX_ROOT = originalOmxRoot;
22996
+ if (originalOmxStateRoot === undefined) delete process.env.OMX_STATE_ROOT;
22997
+ else process.env.OMX_STATE_ROOT = originalOmxStateRoot;
22998
+ if (originalOmxTeamStateRoot === undefined) delete process.env.OMX_TEAM_STATE_ROOT;
22999
+ else process.env.OMX_TEAM_STATE_ROOT = originalOmxTeamStateRoot;
23000
+ await rm(cwd, { recursive: true, force: true });
23001
+ }
23002
+ });
23003
+
23004
+ it("denies Main-root conductor apply_patch from the session leader thread during active Ralph (#3116)", async () => {
23005
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-root-denial-"));
23006
+ process.env.OMX_ROOT = cwd;
23007
+ try {
23008
+ const stateDir = join(cwd, ".omx", "state");
23009
+ const sessionId = "sess-3116-root-denial";
23010
+ const leaderThreadId = "thread-3116-root-denial-leader";
23011
+ const nowIso = new Date().toISOString();
23012
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
23013
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
23014
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
23015
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
23016
+ active: true,
23017
+ mode: "ralph",
23018
+ current_phase: "executing",
23019
+ session_id: sessionId,
23020
+ });
23021
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
23022
+ schemaVersion: 1,
23023
+ sessions: {
23024
+ [sessionId]: {
23025
+ session_id: sessionId,
23026
+ leader_thread_id: leaderThreadId,
23027
+ updated_at: nowIso,
23028
+ threads: {
23029
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
23030
+ },
23031
+ },
23032
+ },
23033
+ });
23034
+
23035
+ const result = await dispatchCodexNativeHook(
23036
+ {
23037
+ hook_event_name: "PreToolUse",
23038
+ cwd,
23039
+ session_id: sessionId,
23040
+ thread_id: leaderThreadId,
23041
+ tool_name: "apply_patch",
23042
+ tool_input: { file_path: "src/feature.ts" },
23043
+ },
23044
+ { cwd },
23045
+ );
23046
+
23047
+ assert.equal(result.outputJson?.decision, "block");
23048
+ assert.match(String(result.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
23049
+ } finally {
23050
+ await rm(cwd, { recursive: true, force: true });
23051
+ }
23052
+ });
23053
+
23054
+ it("allows apply_patch from an untyped collaboration.spawn_agent child tracked as a subagent during active Ralph (#3116)", async () => {
23055
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-trusted-child-"));
23056
+ process.env.OMX_ROOT = cwd;
23057
+ try {
23058
+ const stateDir = join(cwd, ".omx", "state");
23059
+ const sessionId = "sess-3116-trusted-child";
23060
+ const leaderThreadId = "thread-3116-trusted-child-leader";
23061
+ const childThreadId = "thread-3116-trusted-child-worker";
23062
+ const nowIso = new Date().toISOString();
23063
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
23064
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
23065
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
23066
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
23067
+ active: true,
23068
+ mode: "ralph",
23069
+ current_phase: "executing",
23070
+ session_id: sessionId,
23071
+ });
23072
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
23073
+ schemaVersion: 1,
23074
+ sessions: {
23075
+ [sessionId]: {
23076
+ session_id: sessionId,
23077
+ leader_thread_id: leaderThreadId,
23078
+ updated_at: nowIso,
23079
+ threads: {
23080
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
23081
+ [childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
23082
+ },
23083
+ },
23084
+ },
23085
+ });
23086
+
23087
+ // Untyped role (not in the typed-agent catalog) must not defeat tracker-backed trust.
23088
+ const result = await dispatchCodexNativeHook(
23089
+ {
23090
+ hook_event_name: "PreToolUse",
23091
+ cwd,
23092
+ session_id: sessionId,
23093
+ thread_id: childThreadId,
23094
+ agent_role: "collaboration-child",
23095
+ source: {
23096
+ subagent: {
23097
+ thread_spawn: {
23098
+ parent_thread_id: leaderThreadId,
23099
+ depth: 1,
23100
+ agent_nickname: "Helper",
23101
+ },
23102
+ },
23103
+ },
23104
+ tool_name: "apply_patch",
23105
+ tool_input: { file_path: "src/feature.ts" },
23106
+ },
23107
+ { cwd },
23108
+ );
23109
+
23110
+ assert.equal(result.outputJson, null);
23111
+ } finally {
23112
+ await rm(cwd, { recursive: true, force: true });
23113
+ }
23114
+ });
23115
+
23116
+ it("allows apply_patch from untyped collaboration descendants via tracked thread and tracked parent chain (#3116)", async () => {
23117
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-descendant-"));
23118
+ process.env.OMX_ROOT = cwd;
23119
+ try {
23120
+ const stateDir = join(cwd, ".omx", "state");
23121
+ const sessionId = "sess-3116-descendant";
23122
+ const leaderThreadId = "thread-3116-descendant-leader";
23123
+ const childThreadId = "thread-3116-descendant-child";
23124
+ const grandchildThreadId = "thread-3116-descendant-grandchild";
23125
+ const greatGrandchildThreadId = "thread-3116-descendant-great-grandchild";
23126
+ const nowIso = new Date().toISOString();
23127
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
23128
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
23129
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
23130
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
23131
+ active: true,
23132
+ mode: "ralph",
23133
+ current_phase: "executing",
23134
+ session_id: sessionId,
23135
+ });
23136
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
23137
+ schemaVersion: 1,
23138
+ sessions: {
23139
+ [sessionId]: {
23140
+ session_id: sessionId,
23141
+ leader_thread_id: leaderThreadId,
23142
+ updated_at: nowIso,
23143
+ threads: {
23144
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
23145
+ [childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
23146
+ [grandchildThreadId]: { thread_id: grandchildThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: childThreadId },
23147
+ },
23148
+ },
23149
+ },
23150
+ });
23151
+
23152
+ // (a) A descendant whose own thread is tracked as a subagent is trusted directly.
23153
+ const trackedDescendant = await dispatchCodexNativeHook(
23154
+ {
23155
+ hook_event_name: "PreToolUse",
23156
+ cwd,
23157
+ session_id: sessionId,
23158
+ thread_id: grandchildThreadId,
23159
+ tool_name: "apply_patch",
23160
+ tool_input: { file_path: "src/feature.ts" },
23161
+ },
23162
+ { cwd },
23163
+ );
23164
+ assert.equal(trackedDescendant.outputJson, null);
23165
+
23166
+ // (b) A not-yet-tracked descendant is trusted when its runtime spawn parent is a tracked thread.
23167
+ const chainedDescendant = await dispatchCodexNativeHook(
23168
+ {
23169
+ hook_event_name: "PreToolUse",
23170
+ cwd,
23171
+ session_id: sessionId,
23172
+ thread_id: greatGrandchildThreadId,
23173
+ agent_role: "collaboration-child",
23174
+ source: {
23175
+ subagent: {
23176
+ thread_spawn: {
23177
+ parent_thread_id: grandchildThreadId,
23178
+ depth: 3,
23179
+ },
23180
+ },
23181
+ },
23182
+ tool_name: "apply_patch",
23183
+ tool_input: { file_path: "src/feature.ts" },
23184
+ },
23185
+ { cwd },
23186
+ );
23187
+ assert.equal(chainedDescendant.outputJson, null);
23188
+ } finally {
23189
+ await rm(cwd, { recursive: true, force: true });
23190
+ }
23191
+ });
23192
+
23193
+ it("resolves the session-pinned Ralph state so collaboration children edit while the leader stays blocked (#3116)", async () => {
23194
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-pinned-state-"));
23195
+ process.env.OMX_ROOT = cwd;
23196
+ try {
23197
+ const stateDir = join(cwd, ".omx", "state");
23198
+ const sessionId = "sess-3116-pinned-state";
23199
+ const leaderThreadId = "thread-3116-pinned-state-leader";
23200
+ const childThreadId = "thread-3116-pinned-state-child";
23201
+ const nowIso = new Date().toISOString();
23202
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
23203
+ // Native session id differs from the canonical session id: the guard must resolve it.
23204
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
23205
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
23206
+ const ralphStatePath = join(stateDir, "sessions", sessionId, "ralph-state.json");
23207
+ await writeJson(ralphStatePath, {
23208
+ active: true,
23209
+ mode: "ralph",
23210
+ current_phase: "executing",
23211
+ session_id: sessionId,
23212
+ });
23213
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
23214
+ schemaVersion: 1,
23215
+ sessions: {
23216
+ [sessionId]: {
23217
+ session_id: sessionId,
23218
+ leader_thread_id: leaderThreadId,
23219
+ updated_at: nowIso,
23220
+ threads: {
23221
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
23222
+ [childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
23223
+ },
23224
+ },
23225
+ },
23226
+ });
23227
+
23228
+ // Payload carries the native session id, which must map to the canonical session.
23229
+ const childEdit = await dispatchCodexNativeHook(
23230
+ {
23231
+ hook_event_name: "PreToolUse",
23232
+ cwd,
23233
+ session_id: leaderThreadId,
23234
+ thread_id: childThreadId,
23235
+ tool_name: "apply_patch",
23236
+ tool_input: { file_path: "src/feature.ts" },
23237
+ },
23238
+ { cwd },
23239
+ );
23240
+ assert.equal(childEdit.outputJson, null);
23241
+
23242
+ const leaderEdit = await dispatchCodexNativeHook(
23243
+ {
23244
+ hook_event_name: "PreToolUse",
23245
+ cwd,
23246
+ session_id: leaderThreadId,
23247
+ thread_id: leaderThreadId,
23248
+ tool_name: "apply_patch",
23249
+ tool_input: { file_path: "src/feature.ts" },
23250
+ },
23251
+ { cwd },
23252
+ );
23253
+ assert.equal(leaderEdit.outputJson?.decision, "block");
23254
+ assert.match(String(leaderEdit.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
23255
+
23256
+ // The guard reads the session-pinned phase: flipping it to "starting" releases the leader too.
23257
+ await writeJson(ralphStatePath, {
23258
+ active: true,
23259
+ mode: "ralph",
23260
+ current_phase: "starting",
23261
+ session_id: sessionId,
23262
+ });
23263
+ const leaderEditAfterStarting = await dispatchCodexNativeHook(
23264
+ {
23265
+ hook_event_name: "PreToolUse",
23266
+ cwd,
23267
+ session_id: leaderThreadId,
23268
+ thread_id: leaderThreadId,
23269
+ tool_name: "apply_patch",
23270
+ tool_input: { file_path: "src/feature.ts" },
23271
+ },
23272
+ { cwd },
23273
+ );
23274
+ assert.equal(leaderEditAfterStarting.outputJson, null);
23275
+ } finally {
23276
+ await rm(cwd, { recursive: true, force: true });
23277
+ }
23278
+ });
23279
+
23280
+ it("trusts the native collaboration.spawn_agent child surface via runtime spawn provenance during active Ralph (#3116)", async () => {
23281
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-collab-surface-"));
23282
+ process.env.OMX_ROOT = cwd;
23283
+ try {
23284
+ const stateDir = join(cwd, ".omx", "state");
23285
+ const sessionId = "sess-3116-collab-surface";
23286
+ const leaderThreadId = "thread-3116-collab-surface-leader";
23287
+ const childThreadId = "thread-3116-collab-surface-child";
23288
+ const nowIso = new Date().toISOString();
23289
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
23290
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
23291
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
23292
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
23293
+ active: true,
23294
+ mode: "ralph",
23295
+ current_phase: "executing",
23296
+ session_id: sessionId,
23297
+ });
23298
+ // Child SessionStart has not been recorded yet: only the leader is tracked.
23299
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
23300
+ schemaVersion: 1,
23301
+ sessions: {
23302
+ [sessionId]: {
23303
+ session_id: sessionId,
23304
+ leader_thread_id: leaderThreadId,
23305
+ updated_at: nowIso,
23306
+ threads: {
23307
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
23308
+ },
23309
+ },
23310
+ },
23311
+ });
23312
+
23313
+ // No typed agent_role at all: a raw collaboration.spawn_agent child payload shape.
23314
+ const result = await dispatchCodexNativeHook(
23315
+ {
23316
+ hook_event_name: "PreToolUse",
23317
+ cwd,
23318
+ session_id: sessionId,
23319
+ thread_id: childThreadId,
23320
+ source: {
23321
+ subagent: {
23322
+ thread_spawn: {
23323
+ parent_thread_id: leaderThreadId,
23324
+ depth: 1,
23325
+ agent_nickname: "Implementer",
23326
+ },
23327
+ },
23328
+ },
23329
+ tool_name: "apply_patch",
23330
+ tool_input: { file_path: "src/feature.ts" },
23331
+ },
23332
+ { cwd },
23333
+ );
23334
+
23335
+ assert.equal(result.outputJson, null);
23336
+ } finally {
23337
+ await rm(cwd, { recursive: true, force: true });
23338
+ }
23339
+ });
23340
+
23341
+ it("denies spoofed or untrusted collaboration provenance while the main root stays protected during active Ralph (#3116)", async () => {
23342
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-spoof-denial-"));
23343
+ process.env.OMX_ROOT = cwd;
23344
+ try {
23345
+ const stateDir = join(cwd, ".omx", "state");
23346
+ const sessionId = "sess-3116-spoof-denial";
23347
+ const leaderThreadId = "thread-3116-spoof-denial-leader";
23348
+ const nowIso = new Date().toISOString();
23349
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
23350
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
23351
+ await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
23352
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
23353
+ active: true,
23354
+ mode: "ralph",
23355
+ current_phase: "executing",
23356
+ session_id: sessionId,
23357
+ });
23358
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
23359
+ schemaVersion: 1,
23360
+ sessions: {
23361
+ [sessionId]: {
23362
+ session_id: sessionId,
23363
+ leader_thread_id: leaderThreadId,
23364
+ updated_at: nowIso,
23365
+ threads: {
23366
+ [leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
23367
+ },
23368
+ },
23369
+ },
23370
+ });
23371
+
23372
+ // (a) Untracked thread whose declared parent is neither the leader nor a tracked thread.
23373
+ const orphanParent = await dispatchCodexNativeHook(
23374
+ {
23375
+ hook_event_name: "PreToolUse",
23376
+ cwd,
23377
+ session_id: sessionId,
23378
+ thread_id: "thread-3116-spoof-orphan",
23379
+ agent_role: "collaboration-child",
23380
+ source: {
23381
+ subagent: {
23382
+ thread_spawn: { parent_thread_id: "thread-3116-spoof-outsider" },
23383
+ },
23384
+ },
23385
+ tool_name: "apply_patch",
23386
+ tool_input: { file_path: "src/feature.ts" },
23387
+ },
23388
+ { cwd },
23389
+ );
23390
+ assert.equal(orphanParent.outputJson?.decision, "block");
23391
+ assert.match(String(orphanParent.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
23392
+
23393
+ // (b) Untyped child with no provenance at all (no tracker thread, no spawn source).
23394
+ const noProvenance = await dispatchCodexNativeHook(
23395
+ {
23396
+ hook_event_name: "PreToolUse",
23397
+ cwd,
23398
+ session_id: sessionId,
23399
+ thread_id: "thread-3116-spoof-bare",
23400
+ agent_role: "collaboration-child",
23401
+ tool_name: "apply_patch",
23402
+ tool_input: { file_path: "src/feature.ts" },
23403
+ },
23404
+ { cwd },
23405
+ );
23406
+ assert.equal(noProvenance.outputJson?.decision, "block");
23407
+
23408
+ // (c) Spawn provenance attached to the leader thread cannot promote the main root.
23409
+ const leaderSelfSpawn = await dispatchCodexNativeHook(
23410
+ {
23411
+ hook_event_name: "PreToolUse",
23412
+ cwd,
23413
+ session_id: sessionId,
23414
+ thread_id: leaderThreadId,
23415
+ agent_role: "collaboration-child",
23416
+ source: {
23417
+ subagent: {
23418
+ thread_spawn: { parent_thread_id: leaderThreadId },
23419
+ },
23420
+ },
23421
+ tool_name: "apply_patch",
23422
+ tool_input: { file_path: "src/feature.ts" },
23423
+ },
23424
+ { cwd },
23425
+ );
23426
+ assert.equal(leaderSelfSpawn.outputJson?.decision, "block");
22193
23427
  } finally {
22194
- if (originalOmxRoot === undefined) delete process.env.OMX_ROOT;
22195
- else process.env.OMX_ROOT = originalOmxRoot;
22196
- if (originalOmxStateRoot === undefined) delete process.env.OMX_STATE_ROOT;
22197
- else process.env.OMX_STATE_ROOT = originalOmxStateRoot;
22198
- if (originalOmxTeamStateRoot === undefined) delete process.env.OMX_TEAM_STATE_ROOT;
22199
- else process.env.OMX_TEAM_STATE_ROOT = originalOmxTeamStateRoot;
22200
23428
  await rm(cwd, { recursive: true, force: true });
22201
23429
  }
22202
23430
  });