oh-my-codex 0.19.1 → 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 (229) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +3 -1
  4. package/crates/omx-api/src/lib.rs +3 -3
  5. package/crates/omx-api/tests/cli.rs +4 -4
  6. package/crates/omx-explore/src/main.rs +4 -4
  7. package/crates/omx-sparkshell/src/codex_bridge.rs +2 -2
  8. package/dist/agents/__tests__/definitions.test.js +3 -3
  9. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  10. package/dist/agents/__tests__/native-config.test.js +71 -42
  11. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  12. package/dist/agents/definitions.d.ts +1 -1
  13. package/dist/agents/definitions.d.ts.map +1 -1
  14. package/dist/agents/definitions.js +3 -3
  15. package/dist/agents/definitions.js.map +1 -1
  16. package/dist/agents/native-config.d.ts +2 -2
  17. package/dist/agents/native-config.d.ts.map +1 -1
  18. package/dist/agents/native-config.js +7 -5
  19. package/dist/agents/native-config.js.map +1 -1
  20. package/dist/autopilot/__tests__/planner-routing.test.js +19 -12
  21. package/dist/autopilot/__tests__/planner-routing.test.js.map +1 -1
  22. package/dist/autopilot/__tests__/ralplan-gate.test.js +151 -0
  23. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
  24. package/dist/autopilot/planner-routing.d.ts.map +1 -1
  25. package/dist/autopilot/planner-routing.js +4 -2
  26. package/dist/autopilot/planner-routing.js.map +1 -1
  27. package/dist/autopilot/ralplan-gate.d.ts +1 -0
  28. package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
  29. package/dist/autopilot/ralplan-gate.js +38 -2
  30. package/dist/autopilot/ralplan-gate.js.map +1 -1
  31. package/dist/autoresearch/runtime.d.ts +2 -0
  32. package/dist/autoresearch/runtime.d.ts.map +1 -1
  33. package/dist/autoresearch/runtime.js +8 -0
  34. package/dist/autoresearch/runtime.js.map +1 -1
  35. package/dist/capabilities/__tests__/lockfile.test.d.ts +2 -0
  36. package/dist/capabilities/__tests__/lockfile.test.d.ts.map +1 -0
  37. package/dist/capabilities/__tests__/lockfile.test.js +115 -0
  38. package/dist/capabilities/__tests__/lockfile.test.js.map +1 -0
  39. package/dist/capabilities/lockfile.d.ts +102 -0
  40. package/dist/capabilities/lockfile.d.ts.map +1 -0
  41. package/dist/capabilities/lockfile.js +334 -0
  42. package/dist/capabilities/lockfile.js.map +1 -0
  43. package/dist/cli/__tests__/agents.test.js +5 -5
  44. package/dist/cli/__tests__/agents.test.js.map +1 -1
  45. package/dist/cli/__tests__/capabilities.test.d.ts +2 -0
  46. package/dist/cli/__tests__/capabilities.test.d.ts.map +1 -0
  47. package/dist/cli/__tests__/capabilities.test.js +135 -0
  48. package/dist/cli/__tests__/capabilities.test.js.map +1 -0
  49. package/dist/cli/__tests__/codex-plugin-layout.test.js +264 -6
  50. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  51. package/dist/cli/__tests__/doctor-context-window-warning.test.js +83 -78
  52. package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +1 -1
  53. package/dist/cli/__tests__/doctor-invalid-config.test.js +10 -1
  54. package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -1
  55. package/dist/cli/__tests__/doctor-spark-routing.test.js +22 -6
  56. package/dist/cli/__tests__/doctor-spark-routing.test.js.map +1 -1
  57. package/dist/cli/__tests__/doctor-warning-copy.test.js +87 -1
  58. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  59. package/dist/cli/__tests__/index.test.js +53 -18
  60. package/dist/cli/__tests__/index.test.js.map +1 -1
  61. package/dist/cli/__tests__/plugin-marketplace-idempotent.test.js +3 -3
  62. package/dist/cli/__tests__/plugin-marketplace-idempotent.test.js.map +1 -1
  63. package/dist/cli/__tests__/resume.test.js +95 -6
  64. package/dist/cli/__tests__/resume.test.js.map +1 -1
  65. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +54 -2
  66. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
  67. package/dist/cli/__tests__/setup-install-mode.test.js +49 -21
  68. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  69. package/dist/cli/__tests__/setup-prompts-overwrite.test.js +3 -3
  70. package/dist/cli/__tests__/setup-refresh.test.js +166 -12
  71. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  72. package/dist/cli/__tests__/setup-scope.test.js +27 -3
  73. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  74. package/dist/cli/__tests__/uninstall.test.js +301 -7
  75. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  76. package/dist/cli/agents.js +1 -1
  77. package/dist/cli/agents.js.map +1 -1
  78. package/dist/cli/capabilities.d.ts +3 -0
  79. package/dist/cli/capabilities.d.ts.map +1 -0
  80. package/dist/cli/capabilities.js +118 -0
  81. package/dist/cli/capabilities.js.map +1 -0
  82. package/dist/cli/doctor.d.ts +3 -1
  83. package/dist/cli/doctor.d.ts.map +1 -1
  84. package/dist/cli/doctor.js +50 -34
  85. package/dist/cli/doctor.js.map +1 -1
  86. package/dist/cli/index.d.ts +7 -4
  87. package/dist/cli/index.d.ts.map +1 -1
  88. package/dist/cli/index.js +77 -19
  89. package/dist/cli/index.js.map +1 -1
  90. package/dist/cli/plugin-marketplace.d.ts +1 -0
  91. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  92. package/dist/cli/plugin-marketplace.js +31 -0
  93. package/dist/cli/plugin-marketplace.js.map +1 -1
  94. package/dist/cli/setup.d.ts.map +1 -1
  95. package/dist/cli/setup.js +9 -6
  96. package/dist/cli/setup.js.map +1 -1
  97. package/dist/cli/uninstall.d.ts.map +1 -1
  98. package/dist/cli/uninstall.js +3 -4
  99. package/dist/cli/uninstall.js.map +1 -1
  100. package/dist/config/__tests__/generator-idempotent.test.js +210 -114
  101. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  102. package/dist/config/__tests__/generator-notify.test.js +26 -52
  103. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  104. package/dist/config/__tests__/models.test.js +51 -26
  105. package/dist/config/__tests__/models.test.js.map +1 -1
  106. package/dist/config/generator.d.ts +20 -11
  107. package/dist/config/generator.d.ts.map +1 -1
  108. package/dist/config/generator.js +169 -152
  109. package/dist/config/generator.js.map +1 -1
  110. package/dist/config/models.d.ts +16 -6
  111. package/dist/config/models.d.ts.map +1 -1
  112. package/dist/config/models.js +19 -8
  113. package/dist/config/models.js.map +1 -1
  114. package/dist/hooks/__tests__/keyword-detector.test.js +12 -4
  115. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  116. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +1 -1
  117. package/dist/hooks/__tests__/prometheus-strict-contract.test.js +2 -2
  118. package/dist/hooks/__tests__/prometheus-strict-contract.test.js.map +1 -1
  119. package/dist/hooks/__tests__/tmux-hook-engine.test.js +2 -2
  120. package/dist/leader/__tests__/contract.test.js +150 -1
  121. package/dist/leader/__tests__/contract.test.js.map +1 -1
  122. package/dist/leader/contract.d.ts +26 -0
  123. package/dist/leader/contract.d.ts.map +1 -1
  124. package/dist/leader/contract.js +188 -0
  125. package/dist/leader/contract.js.map +1 -1
  126. package/dist/modes/base.d.ts +1 -1
  127. package/dist/modes/base.d.ts.map +1 -1
  128. package/dist/pipeline/__tests__/stages.test.js +27 -1
  129. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  130. package/dist/pipeline/stages/ralplan.d.ts +2 -1
  131. package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
  132. package/dist/pipeline/stages/ralplan.js +1 -0
  133. package/dist/pipeline/stages/ralplan.js.map +1 -1
  134. package/dist/pipeline/stages/ultragoal.d.ts +5 -1
  135. package/dist/pipeline/stages/ultragoal.d.ts.map +1 -1
  136. package/dist/pipeline/stages/ultragoal.js +14 -4
  137. package/dist/pipeline/stages/ultragoal.js.map +1 -1
  138. package/dist/ralplan/__tests__/runtime.test.js +132 -0
  139. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  140. package/dist/ralplan/runtime.d.ts +17 -0
  141. package/dist/ralplan/runtime.d.ts.map +1 -1
  142. package/dist/ralplan/runtime.js +97 -1
  143. package/dist/ralplan/runtime.js.map +1 -1
  144. package/dist/scripts/__tests__/codex-native-hook.test.js +1528 -32
  145. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  146. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  147. package/dist/scripts/codex-native-hook.js +377 -31
  148. package/dist/scripts/codex-native-hook.js.map +1 -1
  149. package/dist/state/__tests__/operations.test.js +450 -0
  150. package/dist/state/__tests__/operations.test.js.map +1 -1
  151. package/dist/state/__tests__/workflow-transition.test.js +14 -0
  152. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  153. package/dist/state/operations.d.ts.map +1 -1
  154. package/dist/state/operations.js +72 -11
  155. package/dist/state/operations.js.map +1 -1
  156. package/dist/state/terminal-normalization.d.ts +8 -0
  157. package/dist/state/terminal-normalization.d.ts.map +1 -0
  158. package/dist/state/terminal-normalization.js +127 -0
  159. package/dist/state/terminal-normalization.js.map +1 -0
  160. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  161. package/dist/state/workflow-transition-reconcile.js +3 -1
  162. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  163. package/dist/team/__tests__/delegation-policy.test.js +2 -2
  164. package/dist/team/__tests__/delegation-policy.test.js.map +1 -1
  165. package/dist/team/__tests__/model-contract.test.js +25 -25
  166. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  167. package/dist/team/__tests__/runtime.test.js +28 -28
  168. package/dist/team/__tests__/runtime.test.js.map +1 -1
  169. package/dist/team/__tests__/scaling.test.js +7 -7
  170. package/dist/team/__tests__/scaling.test.js.map +1 -1
  171. package/dist/team/__tests__/tmux-claude-workers-demo.test.js +5 -5
  172. package/dist/team/__tests__/tmux-claude-workers-demo.test.js.map +1 -1
  173. package/dist/team/__tests__/tmux-session.test.js +12 -12
  174. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  175. package/dist/team/__tests__/worker-bootstrap.test.js +57 -9
  176. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  177. package/dist/team/__tests__/worker-runtime-identity.test.js +3 -3
  178. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  179. package/dist/team/model-contract.d.ts +1 -1
  180. package/dist/team/model-contract.d.ts.map +1 -1
  181. package/dist/team/runtime.d.ts.map +1 -1
  182. package/dist/team/runtime.js +11 -0
  183. package/dist/team/runtime.js.map +1 -1
  184. package/dist/team/worker-bootstrap.d.ts +2 -0
  185. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  186. package/dist/team/worker-bootstrap.js +6 -1
  187. package/dist/team/worker-bootstrap.js.map +1 -1
  188. package/dist/ultragoal/__tests__/artifacts.test.js +68 -1
  189. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  190. package/dist/ultragoal/artifacts.d.ts +5 -1
  191. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  192. package/dist/ultragoal/artifacts.js +41 -9
  193. package/dist/ultragoal/artifacts.js.map +1 -1
  194. package/dist/utils/__tests__/agents-md.test.js +16 -0
  195. package/dist/utils/__tests__/agents-md.test.js.map +1 -1
  196. package/dist/utils/__tests__/agents-model-table.test.js +7 -7
  197. package/dist/utils/__tests__/agents-model-table.test.js.map +1 -1
  198. package/dist/utils/__tests__/worktree-tool-context.test.d.ts +2 -0
  199. package/dist/utils/__tests__/worktree-tool-context.test.d.ts.map +1 -0
  200. package/dist/utils/__tests__/worktree-tool-context.test.js +118 -0
  201. package/dist/utils/__tests__/worktree-tool-context.test.js.map +1 -0
  202. package/dist/utils/agents-md.d.ts.map +1 -1
  203. package/dist/utils/agents-md.js +4 -3
  204. package/dist/utils/agents-md.js.map +1 -1
  205. package/dist/utils/worktree-tool-context.d.ts +25 -0
  206. package/dist/utils/worktree-tool-context.d.ts.map +1 -0
  207. package/dist/utils/worktree-tool-context.js +116 -0
  208. package/dist/utils/worktree-tool-context.js.map +1 -0
  209. package/package.json +1 -1
  210. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  211. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +96 -10
  212. package/plugins/oh-my-codex/skills/ai-slop-cleaner/SKILL.md +1 -1
  213. package/plugins/oh-my-codex/skills/code-review/SKILL.md +1 -1
  214. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +5 -5
  215. package/plugins/oh-my-codex/skills/prometheus-strict/SKILL.md +3 -3
  216. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +1 -1
  217. package/plugins/oh-my-codex/skills/team/SKILL.md +1 -1
  218. package/plugins/oh-my-codex/skills/ultrawork/references/agent-tiers.md +2 -2
  219. package/prompts/prometheus-strict-metis.md +1 -1
  220. package/skills/ai-slop-cleaner/SKILL.md +1 -1
  221. package/skills/code-review/SKILL.md +1 -1
  222. package/skills/ecomode/references/agent-tiers.md +1 -1
  223. package/skills/omx-setup/SKILL.md +5 -5
  224. package/skills/prometheus-strict/SKILL.md +3 -3
  225. package/skills/ralplan/SKILL.md +1 -1
  226. package/skills/team/SKILL.md +1 -1
  227. package/skills/ultrawork/references/agent-tiers.md +2 -2
  228. package/src/scripts/__tests__/codex-native-hook.test.ts +1818 -113
  229. package/src/scripts/codex-native-hook.ts +414 -27
@@ -18,6 +18,7 @@ import {
18
18
  import {
19
19
  isTrustedSubagentThread,
20
20
  readSubagentSessionSummary,
21
+ readSubagentSessionLedger,
21
22
  readSubagentTrackingState,
22
23
  recordSubagentTurnForSession,
23
24
  } from "../subagents/tracker.js";
@@ -94,9 +95,14 @@ import {
94
95
  CONDUCTOR_ORCHESTRATION_METADATA_PREFIXES,
95
96
  LEADER_CONDUCTOR_BLOCK,
96
97
  LEADER_CONDUCTOR_REUSE_AND_LEDGER_GUIDANCE,
98
+ NATIVE_SUBAGENT_SUPPORT_BLOCKER_FILE,
97
99
  actionKindForConductorArtifact,
98
100
  authorizeConductorAction,
101
+ buildUnsupportedNativeSubagentGuidance,
99
102
  classifyConductorArtifactKind,
103
+ isUnsupportedNativeSubagentEvidence,
104
+ resolveNativeSubagentSupportStatus,
105
+ type NativeSubagentUnsupportedReason,
100
106
  } from "../leader/contract.js";
101
107
  import { readRunState } from "../runtime/run-state.js";
102
108
  import { evaluateRalphCompletionAuditEvidence, isRalphCompletePhase } from "../ralph/completion-audit.js";
@@ -124,7 +130,6 @@ import {
124
130
  } from "../question/deep-interview.js";
125
131
  import { readAutopilotDeepInterviewQuestionWaitState } from "../question/autopilot-wait.js";
126
132
  import {
127
- buildDocumentRefreshAdvisoryOutput,
128
133
  evaluateFinalHandoffDocumentRefresh,
129
134
  isFinalHandoffDocumentRefreshCandidate,
130
135
  } from "../document-refresh/enforcer.js";
@@ -1792,6 +1797,101 @@ async function ensureOmxLocalIgnoreEntry(cwd: string): Promise<{ changed: boolea
1792
1797
  return { changed: true, excludePath };
1793
1798
  }
1794
1799
 
1800
+ function readSessionStartSource(payload: CodexHookPayload | undefined): string {
1801
+ return safeString(payload?.source ?? payload?.session_start_source ?? payload?.sessionStartSource).trim().toLowerCase();
1802
+ }
1803
+
1804
+ function shouldBuildSubagentReopenContext(options: {
1805
+ hookEventName?: CodexHookEventName | null;
1806
+ payload?: CodexHookPayload;
1807
+ }): boolean {
1808
+ if (options.hookEventName !== "SessionStart") return false;
1809
+ const source = readSessionStartSource(options.payload);
1810
+ return source === "startup" || source === "resume";
1811
+ }
1812
+
1813
+ function formatSubagentLedgerMetadata(entry: {
1814
+ role?: string;
1815
+ laneId?: string;
1816
+ scope?: string;
1817
+ status?: string;
1818
+ lastHandoffSummary?: string;
1819
+ resumeFailureReason?: string;
1820
+ }): string {
1821
+ const metadata = [
1822
+ entry.role ? `role: ${entry.role}` : null,
1823
+ entry.laneId ? `lane: ${entry.laneId}` : null,
1824
+ entry.scope ? `scope: ${entry.scope}` : null,
1825
+ entry.status ? `status: ${entry.status}` : null,
1826
+ entry.lastHandoffSummary ? `handoff: ${entry.lastHandoffSummary.slice(0, 120)}` : null,
1827
+ entry.resumeFailureReason ? `last failure: ${entry.resumeFailureReason.slice(0, 120)}` : null,
1828
+ ].filter((item): item is string => Boolean(item));
1829
+ return metadata.length > 0 ? ` (${metadata.join("; ")})` : "";
1830
+ }
1831
+
1832
+ async function buildPersistedSubagentReopenContext(
1833
+ cwd: string,
1834
+ sessionId: string,
1835
+ options: {
1836
+ hookEventName?: CodexHookEventName | null;
1837
+ payload?: CodexHookPayload;
1838
+ },
1839
+ ): Promise<string | null> {
1840
+ if (!shouldBuildSubagentReopenContext(options)) return null;
1841
+
1842
+ const ledger = await readSubagentSessionLedger(cwd, sessionId).catch(() => null);
1843
+ if (!ledger || ledger.savedSubagents.length === 0) return null;
1844
+
1845
+ const source = readSessionStartSource(options.payload);
1846
+ const reopenTargets = ledger.resumeTargets.filter((entry) => entry.status !== "unavailable");
1847
+ const unavailableTargets = ledger.unavailableSubagents;
1848
+ const failedTargets = ledger.savedSubagents.filter((entry) => entry.resumeFailedAt || entry.resumeFailureReason);
1849
+ const nowIso = new Date().toISOString();
1850
+
1851
+ await Promise.all(reopenTargets.map((entry) => recordSubagentTurnForSession(cwd, {
1852
+ sessionId,
1853
+ threadId: entry.threadId,
1854
+ kind: "subagent",
1855
+ role: entry.role,
1856
+ laneId: entry.laneId,
1857
+ scope: entry.scope,
1858
+ agentNickname: entry.agentNickname,
1859
+ status: entry.status,
1860
+ resumeRequestedAt: nowIso,
1861
+ preserveCompletionEvidence: true,
1862
+ }).catch(() => null)));
1863
+
1864
+ const lines = [
1865
+ "[Persisted subagent reopen]",
1866
+ `- SessionStart source: ${source}; saved subagent ids found: ${ledger.savedSubagents.length}.`,
1867
+ ];
1868
+
1869
+ if (reopenTargets.length > 0) {
1870
+ lines.push("- Reopen these persisted subagents by id before continuing work or spawning any same-role/same-lane replacement:");
1871
+ for (const entry of reopenTargets.slice(0, 12)) {
1872
+ lines.push(` - resume_agent(${JSON.stringify(entry.agentId)})${formatSubagentLedgerMetadata(entry)}`);
1873
+ }
1874
+ if (reopenTargets.length > 12) {
1875
+ lines.push(` - ... ${reopenTargets.length - 12} more saved subagent id(s) omitted from this compact SessionStart context; consult .omx/state/subagent-tracking.json before spawning replacements.`);
1876
+ }
1877
+ } else {
1878
+ lines.push("- No compatible saved subagent id is currently marked reopenable; do not spawn a replacement merely because reopen was unavailable.");
1879
+ }
1880
+
1881
+ lines.push("- Silver rule: when follow-up work targets an existing role/lane, reuse the matching reopened id; avoid duplicate same-type subagent spawns.");
1882
+ lines.push("- If resume_agent fails, surface a clear warning with the id and reason, then continue in the root or another compatible existing lane; do not spawn a new agent solely because reopen failed.");
1883
+
1884
+ const warningEntries = [...new Map([...unavailableTargets, ...failedTargets].map((entry) => [entry.agentId, entry])).values()];
1885
+ if (warningEntries.length > 0) {
1886
+ lines.push("- Reopen warnings:");
1887
+ for (const entry of warningEntries.slice(0, 8)) {
1888
+ lines.push(` - ${entry.agentId}${formatSubagentLedgerMetadata(entry)}`);
1889
+ }
1890
+ }
1891
+
1892
+ return lines.join("\n");
1893
+ }
1894
+
1795
1895
  async function buildSessionStartContext(
1796
1896
  cwd: string,
1797
1897
  sessionId: string,
@@ -1901,6 +2001,14 @@ async function buildSessionStartContext(
1901
2001
  sections.push(`[Subagents]\n- active subagent threads: ${subagentSummary.activeSubagentThreadIds.length}`);
1902
2002
  }
1903
2003
 
2004
+ const persistedSubagentReopenContext = await buildPersistedSubagentReopenContext(cwd, sessionId, {
2005
+ hookEventName: options.hookEventName,
2006
+ payload: options.payload,
2007
+ });
2008
+ if (persistedSubagentReopenContext) {
2009
+ sections.push(persistedSubagentReopenContext);
2010
+ }
2011
+
1904
2012
  return sections.length > 0 ? sections.join("\n\n") : null;
1905
2013
  }
1906
2014
 
@@ -2111,12 +2219,23 @@ function buildSkillStateCliInstruction(mode: string, statePath: string): string
2111
2219
 
2112
2220
  function buildAutopilotPromptActivationNote(
2113
2221
  skillState?: SkillActiveState | null,
2114
- options: { markedQuestionAnswer?: boolean; cwd?: string } = {},
2222
+ options: { markedQuestionAnswer?: boolean; cwd?: string; payload?: CodexHookPayload; sessionId?: string } = {},
2115
2223
  ): string | null {
2116
2224
  if (skillState?.initialized_mode !== "autopilot") return null;
2117
2225
  const teamHandoff = readTeamModeConfig(options.cwd).enabled
2118
2226
  ? " (+ $team if needed)"
2119
2227
  : "";
2228
+ const stateDir = getBaseStateDir(options.cwd);
2229
+ const nativeSubagentSupport = resolveNativeSubagentSupportStatus({
2230
+ payload: options.payload,
2231
+ persistedSupportBlocker: readJsonSyncIfExists(nativeSubagentSupportBlockerPath(stateDir)),
2232
+ persistedCapacityBlocker: readJsonSyncIfExists(nativeSubagentCapacityBlockerPath(stateDir)),
2233
+ cwd: options.cwd,
2234
+ sessionId: options.sessionId,
2235
+ });
2236
+ const conductorGuidance = nativeSubagentSupport.status === "unsupported"
2237
+ ? buildUnsupportedNativeSubagentGuidance(nativeSubagentSupport)
2238
+ : `${LEADER_CONDUCTOR_BLOCK} ${LEADER_CONDUCTOR_REUSE_AND_LEDGER_GUIDANCE}`;
2120
2239
  return [
2121
2240
  `Autopilot protocol: the durable default chain is $deep-interview -> $ralplan -> $ultragoal${teamHandoff} -> $code-review -> $ultraqa (deep-interview -> ralplan -> ultragoal -> code-review -> ultraqa).`,
2122
2241
  "Start/resume at current_phase=deep-interview unless the task is clear and bounded; if deep-interview is intentionally skipped, persist and state an explicit deep_interview_gate.skip_reason before moving to ralplan.",
@@ -2128,8 +2247,7 @@ function buildAutopilotPromptActivationNote(
2128
2247
  "The ralplan phase is not complete until Planner output has been reviewed sequentially by Architect and then Critic; do not hand off to Ultragoal or implementation until the ralplan state/artifact records both ralplan_architect_review and ralplan_critic_review with approval or an explicit blocker.",
2129
2248
  "Do not silently fall back to ordinary $plan/ralplan-only handling; keep autopilot-state.json, skill-active-state.json, HUD/statusline, and Codex goal-mode handoff guidance visible while the workflow is active.",
2130
2249
  "When Codex goal tools are available, call get_goal/create_goal only from the active thread handoff and treat the active goal as the completion contract until code-review and ultraqa are clean.",
2131
- LEADER_CONDUCTOR_BLOCK,
2132
- LEADER_CONDUCTOR_REUSE_AND_LEDGER_GUIDANCE,
2250
+ conductorGuidance,
2133
2251
  ].filter(Boolean).join(" ");
2134
2252
  }
2135
2253
 
@@ -2161,7 +2279,7 @@ function buildAdditionalContextMessage(
2161
2279
  : null;
2162
2280
  const deepInterviewConfigPromptActivationNote = buildDeepInterviewConfigInstruction(cwd, skillState);
2163
2281
  const markedQuestionAnswer = /^\s*\[omx question answered\]/i.test(prompt);
2164
- const autopilotPromptActivationNote = buildAutopilotPromptActivationNote(skillState, { markedQuestionAnswer, cwd });
2282
+ const autopilotPromptActivationNote = buildAutopilotPromptActivationNote(skillState, { markedQuestionAnswer, cwd, payload, sessionId: safeString(skillState?.session_id).trim() });
2165
2283
  return [
2166
2284
  `OMX native UserPromptSubmit continued active workflow skill "${continuedSkill}".`,
2167
2285
  promptPriorityMessage,
@@ -2187,7 +2305,7 @@ function buildAdditionalContextMessage(
2187
2305
  ? buildDeepInterviewQuestionBridgeInstruction(cwd, payload)
2188
2306
  : null;
2189
2307
  const deepInterviewConfigPromptActivationNote = buildDeepInterviewConfigInstruction(cwd, skillState);
2190
- const autopilotPromptActivationNote = buildAutopilotPromptActivationNote(skillState, { markedQuestionAnswer: true, cwd });
2308
+ const autopilotPromptActivationNote = buildAutopilotPromptActivationNote(skillState, { markedQuestionAnswer: true, cwd, payload, sessionId: safeString(skillState?.session_id).trim() });
2191
2309
  return [
2192
2310
  `OMX native UserPromptSubmit continued active workflow skill "${continuedSkill}"; workflow-like tokens inside the marked omx question answer are treated as answer text, not a new workflow activation.`,
2193
2311
  promptPriorityMessage,
@@ -2220,7 +2338,7 @@ function buildAdditionalContextMessage(
2220
2338
  const ultragoalPromptActivationNote = match.skill === "ultragoal"
2221
2339
  ? "Ultragoal protocol: use `omx ultragoal create-goals` / `complete-goals` / `checkpoint` for `.omx/ultragoal` artifacts, then use Codex goal model tools only from the active agent handoff (`get_goal`, `create_goal`, `update_goal`) and never overwrite a different active Codex goal. Ultragoal does not call `/goal clear`; for multiple sequential ultragoal runs in one Codex session/thread, manually clear the completed Codex goal in the UI before creating the next aggregate goal."
2222
2340
  : null;
2223
- const autopilotPromptActivationNote = buildAutopilotPromptActivationNote(skillState, { cwd });
2341
+ const autopilotPromptActivationNote = buildAutopilotPromptActivationNote(skillState, { cwd, payload, sessionId: safeString(skillState?.session_id).trim() });
2224
2342
  const combinedTransitionMessage = (() => {
2225
2343
  if (!skillState?.transition_message) return null;
2226
2344
  if (matches.length <= 1 || activeSkills.length <= 1) return skillState.transition_message;
@@ -2621,8 +2739,6 @@ async function buildModeBasedStopOutput(
2621
2739
  reason: `OMX ${mode} is still active (phase: ${phase}; ${diagnostic}); continue the task and gather fresh verification evidence before stopping.`,
2622
2740
  stopReason: `${mode}_${phase}`,
2623
2741
  systemMessage,
2624
- statePath,
2625
- canonicalDisagreement,
2626
2742
  };
2627
2743
  }
2628
2744
 
@@ -3094,6 +3210,18 @@ function nativeSubagentCapacityBlockerPath(stateDir: string): string {
3094
3210
  return join(stateDir, NATIVE_SUBAGENT_CAPACITY_BLOCKER_FILE);
3095
3211
  }
3096
3212
 
3213
+ function nativeSubagentSupportBlockerPath(stateDir: string): string {
3214
+ return join(stateDir, NATIVE_SUBAGENT_SUPPORT_BLOCKER_FILE);
3215
+ }
3216
+
3217
+ function readJsonSyncIfExists(path: string): Record<string, unknown> | null {
3218
+ try {
3219
+ return JSON.parse(readFileSync(path, "utf-8")) as Record<string, unknown>;
3220
+ } catch {
3221
+ return null;
3222
+ }
3223
+ }
3224
+
3097
3225
  function stringifyUnknown(value: unknown): string {
3098
3226
  if (typeof value === "string") return value;
3099
3227
  if (value === undefined || value === null) return "";
@@ -3121,6 +3249,45 @@ function isNativeSubagentCapacityFailure(payload: CodexHookPayload): boolean {
3121
3249
  return !toolName || /(?:spawn_agent|multi_agent|subagent|collab|agent)/i.test(toolName);
3122
3250
  }
3123
3251
 
3252
+ function nativeSubagentFailureReason(payload: CodexHookPayload): NativeSubagentUnsupportedReason | null {
3253
+ const evidence = payloadEvidenceText(payload);
3254
+ const toolName = safeString(payload.tool_name).trim();
3255
+ if (toolName && !/(?:spawn_agent|multi_agent|subagent|collab|agent)/i.test(toolName)) return null;
3256
+ if (/\bagent thread limit reached\b/i.test(evidence)) return null;
3257
+ if (/\bnative subagents? (?:unsupported|disabled|not enabled|unavailable|not found)\b/i.test(evidence)) return "native_subagents_unsupported";
3258
+ if (/\bmulti_agent_v1\b/i.test(evidence) && /\b(?:unavailable|unknown tool|disabled|not enabled|not found|unsupported)\b/i.test(evidence)) return "multi_agent_v1_unavailable";
3259
+ if (/\b(?:unknown tool|tool not found|not enabled|disabled|unavailable|unsupported)\b/i.test(evidence)) return "multi_agent_v1_unavailable";
3260
+ return null;
3261
+ }
3262
+
3263
+ function summarizeNativeSubagentSupportFailure(text: string): string {
3264
+ const normalized = text.replace(/\s+/g, " ").trim();
3265
+ return (normalized || "native subagent support unavailable").slice(0, 500);
3266
+ }
3267
+
3268
+ async function recordNativeSubagentSupportBlocker(
3269
+ cwd: string,
3270
+ stateDir: string,
3271
+ payload: CodexHookPayload,
3272
+ ): Promise<void> {
3273
+ const reason = nativeSubagentFailureReason(payload);
3274
+ if (!reason) return;
3275
+ const nowIso = new Date().toISOString();
3276
+ await mkdir(stateDir, { recursive: true });
3277
+ await writeFile(nativeSubagentSupportBlockerPath(stateDir), JSON.stringify({
3278
+ schema_version: 1,
3279
+ status: "unsupported",
3280
+ reason,
3281
+ ...(readPayloadSessionId(payload) ? { session_id: readPayloadSessionId(payload) } : {}),
3282
+ ...(readPayloadThreadId(payload) ? { thread_id: readPayloadThreadId(payload) } : {}),
3283
+ ...(readPayloadTurnId(payload) ? { turn_id: readPayloadTurnId(payload) } : {}),
3284
+ ...(safeString(payload.tool_name).trim() ? { tool_name: safeString(payload.tool_name).trim() } : {}),
3285
+ evidence: summarizeNativeSubagentSupportFailure(payloadEvidenceText(payload)),
3286
+ observed_at: nowIso,
3287
+ cwd,
3288
+ }, null, 2));
3289
+ }
3290
+
3124
3291
  function summarizeCapacityFailure(text: string): string {
3125
3292
  const normalized = text.replace(/\s+/g, " ").trim();
3126
3293
  if (!normalized) return "agent thread limit reached";
@@ -3329,6 +3496,8 @@ const PROTECTED_PLANNING_STATE_FILE_NAMES = new Set([
3329
3496
  "ralplan-state.json",
3330
3497
  "skill-active-state.json",
3331
3498
  ]);
3499
+ const RUNTIME_SESSION_ID_SEGMENT_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
3500
+
3332
3501
 
3333
3502
  const PLANNING_MODE_IMPLEMENTATION_TOOL_NAMES = new Set([
3334
3503
  "Write",
@@ -3419,6 +3588,33 @@ function isProtectedPlanningStatePath(relativePath: string): boolean {
3419
3588
  const fileName = relativePath.split("/").pop() ?? "";
3420
3589
  return PROTECTED_PLANNING_STATE_FILE_NAMES.has(fileName);
3421
3590
  }
3591
+ function isAllowedAuthoritativeRuntimePlanningStatePath(cwd: string, rawPath: string): boolean {
3592
+ const trimmed = rawPath.trim().replace(/^['"]|['"]$/g, "");
3593
+ if (!trimmed || trimmed.includes("\0")) return false;
3594
+
3595
+ let targetPath: string;
3596
+ let baseStateDir: string;
3597
+ try {
3598
+ targetPath = resolve(cwd, trimmed);
3599
+ baseStateDir = resolve(getBaseStateDir(cwd));
3600
+ } catch {
3601
+ return false;
3602
+ }
3603
+
3604
+ const localStateDir = resolve(cwd, ".omx", "state");
3605
+ if (baseStateDir === localStateDir) return false;
3606
+
3607
+ const relativeToBase = relative(baseStateDir, targetPath).replace(/\\/g, "/");
3608
+ if (!relativeToBase || relativeToBase.startsWith("..") || relativeToBase.startsWith("/")) return false;
3609
+
3610
+ const segments = relativeToBase.split("/");
3611
+ if (segments.length !== 3 || segments[0] !== "sessions") return false;
3612
+ const sessionId = segments[1] ?? "";
3613
+ const fileName = segments[2] ?? "";
3614
+ return RUNTIME_SESSION_ID_SEGMENT_PATTERN.test(sessionId)
3615
+ && PROTECTED_PLANNING_STATE_FILE_NAMES.has(fileName);
3616
+ }
3617
+
3422
3618
 
3423
3619
  function isPlanningTmpRelativePath(relativePath: string): boolean {
3424
3620
  return relativePath === ".omx/tmp" || relativePath.startsWith(".omx/tmp/");
@@ -3439,8 +3635,8 @@ function isAllowedPlanningArtifactPath(
3439
3635
  allowedPrefixes: readonly string[],
3440
3636
  ): boolean {
3441
3637
  const relativePath = normalizePlanningArtifactRelativePath(cwd, rawPath);
3442
- if (!relativePath) return false;
3443
- if (isProtectedPlanningStatePath(relativePath)) return false;
3638
+ if (!relativePath) return isAllowedAuthoritativeRuntimePlanningStatePath(cwd, rawPath);
3639
+ if (isProtectedPlanningStatePath(relativePath)) return isAllowedAuthoritativeRuntimePlanningStatePath(cwd, rawPath);
3444
3640
  if (isPlanningTmpRelativePath(relativePath)) {
3445
3641
  return allowedPrefixes.includes(".omx/tmp") && isAllowedPlanningTmpScratchPath(relativePath);
3446
3642
  }
@@ -3454,8 +3650,14 @@ function isAllowedDeepInterviewArtifactPath(cwd: string, rawPath: string): boole
3454
3650
  return isAllowedPlanningArtifactPath(cwd, rawPath, DEEP_INTERVIEW_ALLOWED_WRITE_PREFIXES);
3455
3651
  }
3456
3652
 
3653
+ function isAllowedRalplanDraftPath(cwd: string, rawPath: string): boolean {
3654
+ const relativePath = normalizePlanningArtifactRelativePath(cwd, rawPath);
3655
+ return relativePath !== null && /^\.omx\/drafts\/[^/]+\.md$/.test(relativePath);
3656
+ }
3657
+
3457
3658
  function isAllowedRalplanArtifactPath(cwd: string, rawPath: string): boolean {
3458
- return isAllowedPlanningArtifactPath(cwd, rawPath, RALPLAN_ALLOWED_WRITE_PREFIXES);
3659
+ return isAllowedRalplanDraftPath(cwd, rawPath)
3660
+ || isAllowedPlanningArtifactPath(cwd, rawPath, RALPLAN_ALLOWED_WRITE_PREFIXES);
3459
3661
  }
3460
3662
 
3461
3663
  interface RalplanBeadsCommandClassification {
@@ -3675,9 +3877,72 @@ function resolveCommandRedirectTarget(target: string, assignments: Map<string, s
3675
3877
  return resolved !== undefined ? resolved : target;
3676
3878
  }
3677
3879
 
3880
+ // Masks redirect metacharacters (`<`/`>`) that appear INSIDE shell quotes so a
3881
+ // quoted regex/source value (e.g. `gh issue create --body '...>{1,2}...'` or
3882
+ // `omx state write --input '{"reason":"a>b"}'`) is not misread as a redirect
3883
+ // write target. Escaped quotes at the top level (`\'`, `\"`) are literal
3884
+ // characters and must NOT open a span, and `$'...'` ANSI-C quoting processes
3885
+ // backslash escapes (so `\'` does not close it) — otherwise a genuine `>`
3886
+ // redirect could be masked behind a false span and its write missed. Only
3887
+ // characters inside a real quoted span are masked; unquoted redirect operators
3888
+ // survive intact. Unterminated/ambiguous quoting fails closed: the original
3889
+ // command is returned unmasked so genuine redirects stay visible to the scan.
3890
+ function maskQuotedRedirectMetacharsForCommandScan(command: string): string {
3891
+ let masked = "";
3892
+ // null = unquoted; "'" = single quotes (no escapes); '"' = double quotes
3893
+ // (backslash escapes); "$'" = ANSI-C $'...' (backslash escapes, incl. \').
3894
+ let quote: "'" | "\"" | "$'" | null = null;
3895
+ for (let index = 0; index < command.length; index += 1) {
3896
+ const char = command[index] ?? "";
3897
+ if (quote === null) {
3898
+ // A backslash escapes the next character at the top level, so an escaped
3899
+ // quote is a literal char and must not open a quoted span.
3900
+ if (char === "\\") {
3901
+ masked += char;
3902
+ const next = command[index + 1];
3903
+ if (next !== undefined) {
3904
+ masked += next;
3905
+ index += 1;
3906
+ }
3907
+ continue;
3908
+ }
3909
+ if (char === "$" && command[index + 1] === "'") {
3910
+ quote = "$'";
3911
+ masked += "$'";
3912
+ index += 1;
3913
+ continue;
3914
+ }
3915
+ if (char === "'" || char === "\"") quote = char;
3916
+ masked += char;
3917
+ continue;
3918
+ }
3919
+ if ((quote === "\"" || quote === "$'") && char === "\\") {
3920
+ // Escapes inside double/ANSI-C spans keep the backslash and its escaped
3921
+ // char (masking a redirect metachar so quoted data cannot be a false
3922
+ // target); the escaped char never closes the span.
3923
+ masked += char;
3924
+ const next = command[index + 1];
3925
+ if (next !== undefined) {
3926
+ masked += next === "<" || next === ">" ? "_" : next;
3927
+ index += 1;
3928
+ }
3929
+ continue;
3930
+ }
3931
+ const closesSpan = quote === "$'" ? char === "'" : char === quote;
3932
+ if (closesSpan) {
3933
+ quote = null;
3934
+ masked += char;
3935
+ continue;
3936
+ }
3937
+ masked += char === "<" || char === ">" ? "_" : char;
3938
+ }
3939
+ if (quote !== null) return command;
3940
+ return masked;
3941
+ }
3942
+
3678
3943
  function extractDeepInterviewCommandRedirectTargets(command: string): string[] {
3679
3944
  const targets: string[] = [];
3680
- const commandOutsideHeredocBodies = stripHeredocBodiesForCommandScan(command);
3945
+ const commandOutsideHeredocBodies = maskQuotedRedirectMetacharsForCommandScan(stripHeredocBodiesForCommandScan(command));
3681
3946
  for (const match of commandOutsideHeredocBodies.matchAll(/(?:^|[^>])>{1,2}\s*(["']?)([^\s&|;<>]+)\1/g)) {
3682
3947
  const candidate = safeString(match[2]).trim();
3683
3948
  if (candidate && !isNullDeviceRedirectTarget(candidate)) targets.push(candidate);
@@ -3775,7 +4040,7 @@ function findGitSubcommandIndex(words: string[], startIndex: number): number | n
3775
4040
  }
3776
4041
 
3777
4042
 
3778
- function commandHasDeepInterviewWriteIntent(command: string): boolean {
4043
+ function commandHasDeepInterviewWriteIntent(command: string, depth = 0): boolean {
3779
4044
  return commandInvokesApplyPatch(command)
3780
4045
  || extractDeepInterviewCommandRedirectTargets(command).length > 0
3781
4046
  || /\btee\s+(?:-a\s+)?[^\s&|;]+/.test(command)
@@ -3786,7 +4051,19 @@ function commandHasDeepInterviewWriteIntent(command: string): boolean {
3786
4051
  || extractConductorBashMutations(command).length > 0
3787
4052
  || extractConductorInterpreterWrites(command).length > 0
3788
4053
  || commandHasDestructiveGitSubcommand(command)
3789
- || commandHasPackageInstallIntent(command);
4054
+ || commandHasPackageInstallIntent(command)
4055
+ // Recurse into wrapped shells (`bash -lc "cat > f"`, `eval`, `env`) and
4056
+ // command substitutions so a real redirect INSIDE a nested command string
4057
+ // is still classified as write intent. This is required because quoted
4058
+ // redirect metacharacters are now masked at the outer scan (#3119): the
4059
+ // mask removes false positives from quoted DATA values while nested-command
4060
+ // recursion re-detects genuine redirects, preserving fail-closed blocking.
4061
+ // The depth guard mirrors evaluateConductorBashWrite's nesting bound;
4062
+ // extractors return strict substrings, so recursion always terminates.
4063
+ || (depth < CONDUCTOR_BASH_MAX_NESTING_DEPTH && (
4064
+ extractNestedShellCommandStringsForStateScan(command).some((nested) => commandHasDeepInterviewWriteIntent(nested, depth + 1))
4065
+ || extractNestedCommandSubstitutionStringsForStateScan(command).some((nested) => commandHasDeepInterviewWriteIntent(nested, depth + 1))
4066
+ ));
3790
4067
  }
3791
4068
 
3792
4069
  function extractDeepInterviewCommandWriteTargets(command: string): string[] {
@@ -5829,6 +6106,55 @@ function isPackageManagerCommandWord(word: string): boolean {
5829
6106
  return base === "npm" || base === "pnpm" || base === "yarn" || base === "npx" || base === "pnpx";
5830
6107
  }
5831
6108
 
6109
+ function isStructuredUltragoalSteeringShellCommand(command: string): boolean {
6110
+ const segments = splitShellCommandSegments(stripHeredocBodiesForCommandScan(command));
6111
+ let sawStructuredSteer = false;
6112
+
6113
+ for (const segment of segments) {
6114
+ const words = tokenizeShellWords(segment);
6115
+ let commandStart = true;
6116
+ let inForHeader = false;
6117
+
6118
+ for (let index = 0; index < words.length; index += 1) {
6119
+ const word = words[index] ?? "";
6120
+ if (!word) continue;
6121
+ if (isShellCommandSeparator(word)) {
6122
+ commandStart = true;
6123
+ continue;
6124
+ }
6125
+ if (inForHeader) {
6126
+ if (word === "do") {
6127
+ inForHeader = false;
6128
+ commandStart = true;
6129
+ }
6130
+ continue;
6131
+ }
6132
+ if (isShellGroupingSyntaxWord(word)) continue;
6133
+ if (commandStart && isEnvironmentAssignmentWord(word)) continue;
6134
+ if (commandStart && CONDUCTOR_BASH_COMPOUND_SYNTAX_WORDS.has(word)) {
6135
+ if (word === "for") inForHeader = true;
6136
+ continue;
6137
+ }
6138
+ if (commandStart && word.startsWith("-")) continue;
6139
+ if (!commandStart) continue;
6140
+
6141
+ const commandName = commandNameFromShellWord(word);
6142
+ if (commandName === "read" || commandName === "true" || commandName === ":") {
6143
+ commandStart = false;
6144
+ continue;
6145
+ }
6146
+ if (commandName === "omx" && words[index + 1] === "ultragoal" && words[index + 2] === "steer") {
6147
+ sawStructuredSteer = true;
6148
+ commandStart = false;
6149
+ continue;
6150
+ }
6151
+ return false;
6152
+ }
6153
+ }
6154
+
6155
+ return sawStructuredSteer;
6156
+ }
6157
+
5832
6158
  function hasDynamicNestedShellExecution(command: string): boolean {
5833
6159
  const commands = splitShellCommandSegments(stripHeredocBodiesForCommandScan(command));
5834
6160
  for (const segment of commands) {
@@ -5851,12 +6177,12 @@ function hasDynamicNestedShellExecution(command: string): boolean {
5851
6177
  if (commandStringIndex === null && (hasUnquotedShellStdinFlowAroundShellWord(words, index) || segment.includes("<<"))) return true;
5852
6178
  if (commandStringIndex !== null) {
5853
6179
  const nestedCommand = words[commandStringIndex] ?? "";
5854
- if (isDynamicNestedCommandString(nestedCommand)) return true;
6180
+ if (isDynamicNestedCommandString(nestedCommand) && !isStructuredUltragoalSteeringShellCommand(nestedCommand)) return true;
5855
6181
  }
5856
6182
  }
5857
6183
  if (word === "eval") {
5858
6184
  const nestedCommand = words.slice(index + 1).join(" ");
5859
- if (isDynamicNestedCommandString(nestedCommand)) return true;
6185
+ if (isDynamicNestedCommandString(nestedCommand) && !isStructuredUltragoalSteeringShellCommand(nestedCommand)) return true;
5860
6186
  }
5861
6187
  }
5862
6188
  }
@@ -6723,7 +7049,7 @@ async function buildRalplanPreToolUseBoundaryOutput(
6723
7049
  resolvedSessionId?: string,
6724
7050
  ): Promise<Record<string, unknown> | null> {
6725
7051
  const sessionId = safeString(resolvedSessionId ?? readPayloadSessionId(payload)).trim();
6726
- if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, sessionId)) return null;
7052
+ if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, stateDir, sessionId)) return null;
6727
7053
  const threadId = readPayloadThreadId(payload);
6728
7054
  const activeState = await readActiveRalplanStateForPreToolUse(cwd, stateDir, sessionId, threadId);
6729
7055
  if (!activeState) return null;
@@ -6777,7 +7103,7 @@ async function buildRalplanPreToolUseBoundaryOutput(
6777
7103
  hookEventName: "PreToolUse",
6778
7104
  additionalContext:
6779
7105
  `${planningModeDescription}. `
6780
- + "Write only planning artifacts under `.omx/context/`, `.omx/plans/`, `.omx/specs/`, `.omx/tmp/`, required `.omx/state/` files, or tracker metadata under `.beads/`. "
7106
+ + "Write only planning artifacts under `.omx/context/`, `.omx/plans/`, `.omx/specs/`, `.omx/tmp/`, required `.omx/state/` files, Markdown drafts under `.omx/drafts/*.md`, or tracker metadata under `.beads/`. "
6781
7107
  + "Do not edit implementation files or run implementation-focused writes from planning phases. "
6782
7108
  + `To execute, first process an explicit handoff such as ${formatExecutionHandoffList(cwd)}, which must emit terminal planning state before implementation begins.`,
6783
7109
  },
@@ -6791,7 +7117,7 @@ async function buildDeepInterviewPreToolUseBoundaryOutput(
6791
7117
  resolvedSessionId?: string,
6792
7118
  ): Promise<Record<string, unknown> | null> {
6793
7119
  const sessionId = safeString(resolvedSessionId ?? readPayloadSessionId(payload)).trim();
6794
- if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, sessionId)) return null;
7120
+ if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, stateDir, sessionId)) return null;
6795
7121
  const threadId = readPayloadThreadId(payload);
6796
7122
  const activeState = await readActiveDeepInterviewStateForPreToolUse(cwd, stateDir, sessionId, threadId);
6797
7123
  if (!activeState) return null;
@@ -6962,17 +7288,67 @@ interface ActiveConductorState {
6962
7288
  async function hasTrustedTypedSubagentProvenanceForPreToolUse(
6963
7289
  payload: CodexHookPayload,
6964
7290
  cwd: string,
7291
+ stateDir: string,
6965
7292
  sessionId: string,
7293
+ options: { allowUntypedProvenance?: boolean } = {},
6966
7294
  ): Promise<boolean> {
6967
7295
  if (hasTeamWorkerEnvironment()) return true;
6968
- if (!isTypedAgentRolePayload(payload)) return false;
6969
7296
  const trackingState = await readSubagentTrackingState(cwd).catch(() => null);
6970
7297
  const session = trackingState?.sessions?.[sessionId];
6971
7298
  if (!session) return false;
6972
7299
 
6973
7300
  const payloadThreadId = readPayloadThreadId(payload);
7301
+
7302
+ // Resolve the Main-root leader THREAD identity from the tracker's leader_thread_id
7303
+ // plus the canonical session's native_session_id (the leader's native thread). Only
7304
+ // genuine leader-thread identifiers may anchor the leader: owner_*_session_id are
7305
+ // session-level ids, not thread anchors, so they must NOT populate this set — their
7306
+ // mere presence would make it non-empty and suppress the fail-closed guard below
7307
+ // without ever excluding the leader thread (#3117 P4). The native_session_id anchor is
7308
+ // honored only when the root session pointer provably maps to the sessionId under
7309
+ // evaluation, so a stale/foreign session.json cannot supply another session's leader
7310
+ // anchor (#3117 P3); the tracker alone is insufficient because it can omit
7311
+ // leader_thread_id or corruptly label the leader kind:"subagent" (#3117 P2).
7312
+ const sessionState = await readRootSessionStateFromStateDir(stateDir).catch(() => null);
7313
+ const trackerLeaderThreadId = safeString(session.leader_thread_id).trim();
7314
+ const leaderIdentityAnchors = new Set<string>();
7315
+ if (trackerLeaderThreadId) leaderIdentityAnchors.add(trackerLeaderThreadId);
7316
+ if (sessionState && sessionId && payloadMatchesSessionPointer(sessionId, sessionState)) {
7317
+ const nativeSessionId = safeString(sessionState.native_session_id).trim();
7318
+ if (nativeSessionId) leaderIdentityAnchors.add(nativeSessionId);
7319
+ }
7320
+
7321
+ // Leader self-guard: the Main-root Conductor is never a delegated executor. Block it
7322
+ // ahead of every trust path, even when tracker or payload provenance is (possibly
7323
+ // corruptly) attached to the leader thread.
7324
+ if (payloadThreadId && leaderIdentityAnchors.has(payloadThreadId)) return false;
7325
+
7326
+ // Fail closed: without an authoritative leader anchor we cannot affirm the payload is
7327
+ // a non-leader delegated performer rather than a mislabeled leader, so we refuse trust
7328
+ // instead of inferring it from a corrupt tracker kind:"subagent" alone (#3117 P2).
7329
+ if (leaderIdentityAnchors.size === 0) return false;
7330
+
7331
+ // Planning boundary guards (ralplan, deep-interview) still require a recognized typed
7332
+ // agent role, so an untyped collaboration.spawn_agent child cannot write before an
7333
+ // execution handoff/approval. Only the Main-root Conductor/Ralph executing guard opts
7334
+ // into untyped tracker/runtime provenance (#3116, #3117).
7335
+ if (options.allowUntypedProvenance !== true && !isTypedAgentRolePayload(payload)) {
7336
+ return false;
7337
+ }
7338
+
7339
+ // Tracker-backed provenance: the payload's own thread is a recorded, non-leader
7340
+ // subagent for this session — the non-spoofable anchor that lets native
7341
+ // collaboration.spawn_agent children/descendants edit under the Conductor guard even
7342
+ // without a recognized typed role (#3116). subagent-tracking.json is derived from
7343
+ // child SessionStart transcript metadata, not the live tool-call payload.
6974
7344
  if (payloadThreadId && isTrustedSubagentThread(session, payloadThreadId)) return true;
6975
7345
 
7346
+ // Runtime-attached spawn provenance: trust a genuine spawned subagent turn whose
7347
+ // declared parent maps to this session's leader or a tracked thread. parentThreadId
7348
+ // comes from the runtime-set source.subagent.thread_spawn (not agent-controlled tool
7349
+ // arguments); an absent parent is rejected, the leader self-guard above blocks the
7350
+ // main root, and cross-session parents fail because they are not in this session's
7351
+ // tracked threads (#3116).
6976
7352
  const source = safeObject(payload.source);
6977
7353
  const subagent = safeObject(source.subagent);
6978
7354
  const threadSpawn = safeObject(subagent.thread_spawn);
@@ -6983,9 +7359,7 @@ async function hasTrustedTypedSubagentProvenanceForPreToolUse(
6983
7359
  ?? threadSpawn.leaderThreadId,
6984
7360
  ).trim();
6985
7361
  if (!parentThreadId) return false;
6986
- const leaderThreadId = safeString(session.leader_thread_id).trim();
6987
- if (payloadThreadId && leaderThreadId && payloadThreadId === leaderThreadId) return false;
6988
- return leaderThreadId === parentThreadId || parentThreadId in session.threads;
7362
+ return leaderIdentityAnchors.has(parentThreadId) || parentThreadId in session.threads;
6989
7363
  }
6990
7364
 
6991
7365
  function isActiveConductorModeState(state: Record<string, unknown> | null, mode: string, sessionId: string): boolean {
@@ -7014,7 +7388,7 @@ async function readActiveMainRootConductorStateForPreToolUse(
7014
7388
  }
7015
7389
  const threadId = readPayloadThreadId(payload);
7016
7390
  if (!sessionId) return null;
7017
- if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, sessionId)) return null;
7391
+ if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, stateDir, sessionId, { allowUntypedProvenance: true })) return null;
7018
7392
 
7019
7393
  const canonicalState = await readVisibleSkillActiveStateForStateDir(stateDir, sessionId);
7020
7394
  if (!canonicalState) return null;
@@ -7865,6 +8239,14 @@ export async function buildConductorPreToolUseWriteGuardOutput(
7865
8239
  ): Promise<Record<string, unknown> | null> {
7866
8240
  const activeState = await readActiveMainRootConductorStateForPreToolUse(payload, cwd, stateDir, resolvedSessionId);
7867
8241
  if (!activeState) return null;
8242
+ const nativeSubagentSupport = resolveNativeSubagentSupportStatus({
8243
+ payload,
8244
+ persistedSupportBlocker: await readJsonIfExists(nativeSubagentSupportBlockerPath(stateDir)),
8245
+ persistedCapacityBlocker: await readJsonIfExists(nativeSubagentCapacityBlockerPath(stateDir)),
8246
+ cwd,
8247
+ sessionId: resolvedSessionId || readPayloadSessionId(payload),
8248
+ });
8249
+
7868
8250
 
7869
8251
  const toolName = safeString(payload.tool_name).trim();
7870
8252
  const command = readPreToolUseCommand(payload);
@@ -7891,6 +8273,9 @@ export async function buildConductorPreToolUseWriteGuardOutput(
7891
8273
 
7892
8274
  if (!blocked) return null;
7893
8275
 
8276
+ const unsupportedNativeGuidance = isUnsupportedNativeSubagentEvidence(nativeSubagentSupport)
8277
+ ? ` ${buildUnsupportedNativeSubagentGuidance(nativeSubagentSupport)} Treat the active conductor workflow as blocked/cancelled for native delegation recovery; do not call multi_agent_v1.close_agent.`
8278
+ : "";
7894
8279
  return {
7895
8280
  decision: "block",
7896
8281
  reason: `Main-root Conductor mode is active (${activeState.mode} phase: ${formatPhase(activeState.phase, "active")}); direct plan/code writes are blocked and must be delegated; ${blockedDetail}.`,
@@ -7900,7 +8285,8 @@ export async function buildConductorPreToolUseWriteGuardOutput(
7900
8285
  `${LEADER_CONDUCTOR_GOLDEN_RULE} `
7901
8286
  + "Use specialized agents for source edits and plan/spec authorship. "
7902
8287
  + `Main-root Conductor may write only orchestration metadata/transport/ledger artifacts under ${CONDUCTOR_ORCHESTRATION_METADATA_PREFIXES.join(", ")}; path location alone is not authorization for substantive deliverables. `
7903
- + "Autopilot rework and typed subagent/worker lanes are exempt from this guard.",
8288
+ + unsupportedNativeGuidance
8289
+ + " Autopilot rework and typed subagent/worker lanes are exempt from this guard.",
7904
8290
  },
7905
8291
  };
7906
8292
  }
@@ -9278,7 +9664,7 @@ async function buildStopHookOutput(
9278
9664
  documentRefreshWarning.triggeringPaths.join("|"),
9279
9665
  canonicalSessionId,
9280
9666
  ),
9281
- buildDocumentRefreshAdvisoryOutput(documentRefreshWarning, "Stop"),
9667
+ { systemMessage: documentRefreshWarning.message },
9282
9668
  canonicalSessionId,
9283
9669
  { allowRepeatDuringStopHook: false },
9284
9670
  );
@@ -9643,6 +10029,7 @@ export async function dispatchCodexNativeHook(
9643
10029
  ?? buildNativePreToolUseOutput(payload);
9644
10030
  } else if (hookEventName === "PostToolUse") {
9645
10031
  await recordNativeSubagentCapacityBlocker(cwd, stateDir, payload).catch(() => {});
10032
+ await recordNativeSubagentSupportBlocker(cwd, stateDir, payload).catch(() => {});
9646
10033
  if (detectMcpTransportFailure(payload)) {
9647
10034
  await markTeamTransportFailure(cwd, payload);
9648
10035
  }