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
@@ -6,7 +6,7 @@ import { pathToFileURL } from "url";
6
6
  import { readModeStateForActiveDecision, readModeStateForSession, updateModeState } from "../modes/base.js";
7
7
  import { redactAuthSecrets } from "../auth/redact.js";
8
8
  import { SKILL_ACTIVE_STATE_FILE, extractSessionIdFromInitializedStatePath, getSkillActiveStatePathsForStateDir, listActiveSkills, readSkillActiveState, readVisibleSkillActiveStateForStateDir, } from "../state/skill-active.js";
9
- import { isTrustedSubagentThread, readSubagentSessionSummary, readSubagentTrackingState, recordSubagentTurnForSession, } from "../subagents/tracker.js";
9
+ import { isTrustedSubagentThread, readSubagentSessionSummary, readSubagentSessionLedger, readSubagentTrackingState, recordSubagentTurnForSession, } from "../subagents/tracker.js";
10
10
  import { resolveCanonicalTeamStateRoot, resolveWorkerNotifyTeamStateRootPath } from "../team/state-root.js";
11
11
  import { inferTerminalLifecycleOutcome } from "../runtime/run-outcome.js";
12
12
  import { appendToLog, isSessionStale, isSessionStateUsable, readSessionState, readUsableSessionState, reconcileNativeSessionStart, } from "../hooks/session.js";
@@ -31,7 +31,7 @@ import { onPreCompact as buildWikiPreCompactContext, onSessionStart as buildWiki
31
31
  import { readAutoresearchCompletionStatus, readAutoresearchModeStateForActiveDecision } from "../autoresearch/skill-validation.js";
32
32
  import { AGENT_DEFINITIONS } from "../agents/definitions.js";
33
33
  import { deriveAutopilotChildPhase, normalizeAutopilotPhase } from "../autopilot/fsm.js";
34
- import { CONDUCTOR_ORCHESTRATION_METADATA_PREFIXES, LEADER_CONDUCTOR_BLOCK, LEADER_CONDUCTOR_REUSE_AND_LEDGER_GUIDANCE, actionKindForConductorArtifact, authorizeConductorAction, classifyConductorArtifactKind, } from "../leader/contract.js";
34
+ import { CONDUCTOR_ORCHESTRATION_METADATA_PREFIXES, LEADER_CONDUCTOR_BLOCK, LEADER_CONDUCTOR_REUSE_AND_LEDGER_GUIDANCE, NATIVE_SUBAGENT_SUPPORT_BLOCKER_FILE, actionKindForConductorArtifact, authorizeConductorAction, buildUnsupportedNativeSubagentGuidance, classifyConductorArtifactKind, isUnsupportedNativeSubagentEvidence, resolveNativeSubagentSupportStatus, } from "../leader/contract.js";
35
35
  import { readRunState } from "../runtime/run-state.js";
36
36
  import { evaluateRalphCompletionAuditEvidence, isRalphCompletePhase } from "../ralph/completion-audit.js";
37
37
  import { buildCodexGoalTerminalCleanupNotice, } from "../goal-workflows/codex-goal-snapshot.js";
@@ -42,7 +42,7 @@ import { readTriageConfig } from "../hooks/triage-config.js";
42
42
  import { readTriageState, writeTriageState, shouldSuppressFollowup, promptSignature, } from "../hooks/triage-state.js";
43
43
  import { isPendingDeepInterviewQuestionEnforcement, reconcileDeepInterviewQuestionEnforcementFromAnsweredRecords, } from "../question/deep-interview.js";
44
44
  import { readAutopilotDeepInterviewQuestionWaitState } from "../question/autopilot-wait.js";
45
- import { buildDocumentRefreshAdvisoryOutput, evaluateFinalHandoffDocumentRefresh, isFinalHandoffDocumentRefreshCandidate, } from "../document-refresh/enforcer.js";
45
+ import { evaluateFinalHandoffDocumentRefresh, isFinalHandoffDocumentRefreshCandidate, } from "../document-refresh/enforcer.js";
46
46
  import { buildExecFollowupStopOutput } from "../exec/followup.js";
47
47
  import { MAX_NATIVE_STDIN_JSON_BYTES, extractRawCodexHookEventName, } from "./hook-payload-guard.js";
48
48
  const TERMINAL_MODE_PHASES = new Set(["complete", "completed", "failed", "cancelled"]);
@@ -1463,6 +1463,76 @@ async function ensureOmxLocalIgnoreEntry(cwd) {
1463
1463
  await writeFile(excludePath, next);
1464
1464
  return { changed: true, excludePath };
1465
1465
  }
1466
+ function readSessionStartSource(payload) {
1467
+ return safeString(payload?.source ?? payload?.session_start_source ?? payload?.sessionStartSource).trim().toLowerCase();
1468
+ }
1469
+ function shouldBuildSubagentReopenContext(options) {
1470
+ if (options.hookEventName !== "SessionStart")
1471
+ return false;
1472
+ const source = readSessionStartSource(options.payload);
1473
+ return source === "startup" || source === "resume";
1474
+ }
1475
+ function formatSubagentLedgerMetadata(entry) {
1476
+ const metadata = [
1477
+ entry.role ? `role: ${entry.role}` : null,
1478
+ entry.laneId ? `lane: ${entry.laneId}` : null,
1479
+ entry.scope ? `scope: ${entry.scope}` : null,
1480
+ entry.status ? `status: ${entry.status}` : null,
1481
+ entry.lastHandoffSummary ? `handoff: ${entry.lastHandoffSummary.slice(0, 120)}` : null,
1482
+ entry.resumeFailureReason ? `last failure: ${entry.resumeFailureReason.slice(0, 120)}` : null,
1483
+ ].filter((item) => Boolean(item));
1484
+ return metadata.length > 0 ? ` (${metadata.join("; ")})` : "";
1485
+ }
1486
+ async function buildPersistedSubagentReopenContext(cwd, sessionId, options) {
1487
+ if (!shouldBuildSubagentReopenContext(options))
1488
+ return null;
1489
+ const ledger = await readSubagentSessionLedger(cwd, sessionId).catch(() => null);
1490
+ if (!ledger || ledger.savedSubagents.length === 0)
1491
+ return null;
1492
+ const source = readSessionStartSource(options.payload);
1493
+ const reopenTargets = ledger.resumeTargets.filter((entry) => entry.status !== "unavailable");
1494
+ const unavailableTargets = ledger.unavailableSubagents;
1495
+ const failedTargets = ledger.savedSubagents.filter((entry) => entry.resumeFailedAt || entry.resumeFailureReason);
1496
+ const nowIso = new Date().toISOString();
1497
+ await Promise.all(reopenTargets.map((entry) => recordSubagentTurnForSession(cwd, {
1498
+ sessionId,
1499
+ threadId: entry.threadId,
1500
+ kind: "subagent",
1501
+ role: entry.role,
1502
+ laneId: entry.laneId,
1503
+ scope: entry.scope,
1504
+ agentNickname: entry.agentNickname,
1505
+ status: entry.status,
1506
+ resumeRequestedAt: nowIso,
1507
+ preserveCompletionEvidence: true,
1508
+ }).catch(() => null)));
1509
+ const lines = [
1510
+ "[Persisted subagent reopen]",
1511
+ `- SessionStart source: ${source}; saved subagent ids found: ${ledger.savedSubagents.length}.`,
1512
+ ];
1513
+ if (reopenTargets.length > 0) {
1514
+ lines.push("- Reopen these persisted subagents by id before continuing work or spawning any same-role/same-lane replacement:");
1515
+ for (const entry of reopenTargets.slice(0, 12)) {
1516
+ lines.push(` - resume_agent(${JSON.stringify(entry.agentId)})${formatSubagentLedgerMetadata(entry)}`);
1517
+ }
1518
+ if (reopenTargets.length > 12) {
1519
+ 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.`);
1520
+ }
1521
+ }
1522
+ else {
1523
+ lines.push("- No compatible saved subagent id is currently marked reopenable; do not spawn a replacement merely because reopen was unavailable.");
1524
+ }
1525
+ lines.push("- Silver rule: when follow-up work targets an existing role/lane, reuse the matching reopened id; avoid duplicate same-type subagent spawns.");
1526
+ 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.");
1527
+ const warningEntries = [...new Map([...unavailableTargets, ...failedTargets].map((entry) => [entry.agentId, entry])).values()];
1528
+ if (warningEntries.length > 0) {
1529
+ lines.push("- Reopen warnings:");
1530
+ for (const entry of warningEntries.slice(0, 8)) {
1531
+ lines.push(` - ${entry.agentId}${formatSubagentLedgerMetadata(entry)}`);
1532
+ }
1533
+ }
1534
+ return lines.join("\n");
1535
+ }
1466
1536
  async function buildSessionStartContext(cwd, sessionId, options = {}) {
1467
1537
  const sections = [];
1468
1538
  sections.push(buildExecutionEnvironmentSection(cwd, {
@@ -1560,6 +1630,13 @@ async function buildSessionStartContext(cwd, sessionId, options = {}) {
1560
1630
  if (subagentSummary && subagentSummary.activeSubagentThreadIds.length > 0) {
1561
1631
  sections.push(`[Subagents]\n- active subagent threads: ${subagentSummary.activeSubagentThreadIds.length}`);
1562
1632
  }
1633
+ const persistedSubagentReopenContext = await buildPersistedSubagentReopenContext(cwd, sessionId, {
1634
+ hookEventName: options.hookEventName,
1635
+ payload: options.payload,
1636
+ });
1637
+ if (persistedSubagentReopenContext) {
1638
+ sections.push(persistedSubagentReopenContext);
1639
+ }
1563
1640
  return sections.length > 0 ? sections.join("\n\n") : null;
1564
1641
  }
1565
1642
  function resolveExecutionEnvironment(cwd, options = {}) {
@@ -1722,6 +1799,17 @@ function buildAutopilotPromptActivationNote(skillState, options = {}) {
1722
1799
  const teamHandoff = readTeamModeConfig(options.cwd).enabled
1723
1800
  ? " (+ $team if needed)"
1724
1801
  : "";
1802
+ const stateDir = getBaseStateDir(options.cwd);
1803
+ const nativeSubagentSupport = resolveNativeSubagentSupportStatus({
1804
+ payload: options.payload,
1805
+ persistedSupportBlocker: readJsonSyncIfExists(nativeSubagentSupportBlockerPath(stateDir)),
1806
+ persistedCapacityBlocker: readJsonSyncIfExists(nativeSubagentCapacityBlockerPath(stateDir)),
1807
+ cwd: options.cwd,
1808
+ sessionId: options.sessionId,
1809
+ });
1810
+ const conductorGuidance = nativeSubagentSupport.status === "unsupported"
1811
+ ? buildUnsupportedNativeSubagentGuidance(nativeSubagentSupport)
1812
+ : `${LEADER_CONDUCTOR_BLOCK} ${LEADER_CONDUCTOR_REUSE_AND_LEDGER_GUIDANCE}`;
1725
1813
  return [
1726
1814
  `Autopilot protocol: the durable default chain is $deep-interview -> $ralplan -> $ultragoal${teamHandoff} -> $code-review -> $ultraqa (deep-interview -> ralplan -> ultragoal -> code-review -> ultraqa).`,
1727
1815
  "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.",
@@ -1733,8 +1821,7 @@ function buildAutopilotPromptActivationNote(skillState, options = {}) {
1733
1821
  "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.",
1734
1822
  "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.",
1735
1823
  "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.",
1736
- LEADER_CONDUCTOR_BLOCK,
1737
- LEADER_CONDUCTOR_REUSE_AND_LEDGER_GUIDANCE,
1824
+ conductorGuidance,
1738
1825
  ].filter(Boolean).join(" ");
1739
1826
  }
1740
1827
  function formatExecutionHandoffList(cwd) {
@@ -1761,7 +1848,7 @@ function buildAdditionalContextMessage(prompt, skillState, cwd = process.cwd(),
1761
1848
  : null;
1762
1849
  const deepInterviewConfigPromptActivationNote = buildDeepInterviewConfigInstruction(cwd, skillState);
1763
1850
  const markedQuestionAnswer = /^\s*\[omx question answered\]/i.test(prompt);
1764
- const autopilotPromptActivationNote = buildAutopilotPromptActivationNote(skillState, { markedQuestionAnswer, cwd });
1851
+ const autopilotPromptActivationNote = buildAutopilotPromptActivationNote(skillState, { markedQuestionAnswer, cwd, payload, sessionId: safeString(skillState?.session_id).trim() });
1765
1852
  return [
1766
1853
  `OMX native UserPromptSubmit continued active workflow skill "${continuedSkill}".`,
1767
1854
  promptPriorityMessage,
@@ -1785,7 +1872,7 @@ function buildAdditionalContextMessage(prompt, skillState, cwd = process.cwd(),
1785
1872
  ? buildDeepInterviewQuestionBridgeInstruction(cwd, payload)
1786
1873
  : null;
1787
1874
  const deepInterviewConfigPromptActivationNote = buildDeepInterviewConfigInstruction(cwd, skillState);
1788
- const autopilotPromptActivationNote = buildAutopilotPromptActivationNote(skillState, { markedQuestionAnswer: true, cwd });
1875
+ const autopilotPromptActivationNote = buildAutopilotPromptActivationNote(skillState, { markedQuestionAnswer: true, cwd, payload, sessionId: safeString(skillState?.session_id).trim() });
1789
1876
  return [
1790
1877
  `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.`,
1791
1878
  promptPriorityMessage,
@@ -1818,7 +1905,7 @@ function buildAdditionalContextMessage(prompt, skillState, cwd = process.cwd(),
1818
1905
  const ultragoalPromptActivationNote = match.skill === "ultragoal"
1819
1906
  ? "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."
1820
1907
  : null;
1821
- const autopilotPromptActivationNote = buildAutopilotPromptActivationNote(skillState, { cwd });
1908
+ const autopilotPromptActivationNote = buildAutopilotPromptActivationNote(skillState, { cwd, payload, sessionId: safeString(skillState?.session_id).trim() });
1822
1909
  const combinedTransitionMessage = (() => {
1823
1910
  if (!skillState?.transition_message)
1824
1911
  return null;
@@ -2165,8 +2252,6 @@ async function buildModeBasedStopOutput(mode, cwd, sessionId) {
2165
2252
  reason: `OMX ${mode} is still active (phase: ${phase}; ${diagnostic}); continue the task and gather fresh verification evidence before stopping.`,
2166
2253
  stopReason: `${mode}_${phase}`,
2167
2254
  systemMessage,
2168
- statePath,
2169
- canonicalDisagreement,
2170
2255
  };
2171
2256
  }
2172
2257
  export function looksLikeGoalCompletionPrompt(text) {
@@ -2574,6 +2659,17 @@ function readPayloadTurnId(payload) {
2574
2659
  function nativeSubagentCapacityBlockerPath(stateDir) {
2575
2660
  return join(stateDir, NATIVE_SUBAGENT_CAPACITY_BLOCKER_FILE);
2576
2661
  }
2662
+ function nativeSubagentSupportBlockerPath(stateDir) {
2663
+ return join(stateDir, NATIVE_SUBAGENT_SUPPORT_BLOCKER_FILE);
2664
+ }
2665
+ function readJsonSyncIfExists(path) {
2666
+ try {
2667
+ return JSON.parse(readFileSync(path, "utf-8"));
2668
+ }
2669
+ catch {
2670
+ return null;
2671
+ }
2672
+ }
2577
2673
  function stringifyUnknown(value) {
2578
2674
  if (typeof value === "string")
2579
2675
  return value;
@@ -2602,6 +2698,44 @@ function isNativeSubagentCapacityFailure(payload) {
2602
2698
  const toolName = safeString(payload.tool_name).trim();
2603
2699
  return !toolName || /(?:spawn_agent|multi_agent|subagent|collab|agent)/i.test(toolName);
2604
2700
  }
2701
+ function nativeSubagentFailureReason(payload) {
2702
+ const evidence = payloadEvidenceText(payload);
2703
+ const toolName = safeString(payload.tool_name).trim();
2704
+ if (toolName && !/(?:spawn_agent|multi_agent|subagent|collab|agent)/i.test(toolName))
2705
+ return null;
2706
+ if (/\bagent thread limit reached\b/i.test(evidence))
2707
+ return null;
2708
+ if (/\bnative subagents? (?:unsupported|disabled|not enabled|unavailable|not found)\b/i.test(evidence))
2709
+ return "native_subagents_unsupported";
2710
+ if (/\bmulti_agent_v1\b/i.test(evidence) && /\b(?:unavailable|unknown tool|disabled|not enabled|not found|unsupported)\b/i.test(evidence))
2711
+ return "multi_agent_v1_unavailable";
2712
+ if (/\b(?:unknown tool|tool not found|not enabled|disabled|unavailable|unsupported)\b/i.test(evidence))
2713
+ return "multi_agent_v1_unavailable";
2714
+ return null;
2715
+ }
2716
+ function summarizeNativeSubagentSupportFailure(text) {
2717
+ const normalized = text.replace(/\s+/g, " ").trim();
2718
+ return (normalized || "native subagent support unavailable").slice(0, 500);
2719
+ }
2720
+ async function recordNativeSubagentSupportBlocker(cwd, stateDir, payload) {
2721
+ const reason = nativeSubagentFailureReason(payload);
2722
+ if (!reason)
2723
+ return;
2724
+ const nowIso = new Date().toISOString();
2725
+ await mkdir(stateDir, { recursive: true });
2726
+ await writeFile(nativeSubagentSupportBlockerPath(stateDir), JSON.stringify({
2727
+ schema_version: 1,
2728
+ status: "unsupported",
2729
+ reason,
2730
+ ...(readPayloadSessionId(payload) ? { session_id: readPayloadSessionId(payload) } : {}),
2731
+ ...(readPayloadThreadId(payload) ? { thread_id: readPayloadThreadId(payload) } : {}),
2732
+ ...(readPayloadTurnId(payload) ? { turn_id: readPayloadTurnId(payload) } : {}),
2733
+ ...(safeString(payload.tool_name).trim() ? { tool_name: safeString(payload.tool_name).trim() } : {}),
2734
+ evidence: summarizeNativeSubagentSupportFailure(payloadEvidenceText(payload)),
2735
+ observed_at: nowIso,
2736
+ cwd,
2737
+ }, null, 2));
2738
+ }
2605
2739
  function summarizeCapacityFailure(text) {
2606
2740
  const normalized = text.replace(/\s+/g, " ").trim();
2607
2741
  if (!normalized)
@@ -2791,6 +2925,7 @@ const PROTECTED_PLANNING_STATE_FILE_NAMES = new Set([
2791
2925
  "ralplan-state.json",
2792
2926
  "skill-active-state.json",
2793
2927
  ]);
2928
+ const RUNTIME_SESSION_ID_SEGMENT_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
2794
2929
  const PLANNING_MODE_IMPLEMENTATION_TOOL_NAMES = new Set([
2795
2930
  "Write",
2796
2931
  "Edit",
@@ -2875,6 +3010,33 @@ function isProtectedPlanningStatePath(relativePath) {
2875
3010
  const fileName = relativePath.split("/").pop() ?? "";
2876
3011
  return PROTECTED_PLANNING_STATE_FILE_NAMES.has(fileName);
2877
3012
  }
3013
+ function isAllowedAuthoritativeRuntimePlanningStatePath(cwd, rawPath) {
3014
+ const trimmed = rawPath.trim().replace(/^['"]|['"]$/g, "");
3015
+ if (!trimmed || trimmed.includes("\0"))
3016
+ return false;
3017
+ let targetPath;
3018
+ let baseStateDir;
3019
+ try {
3020
+ targetPath = resolve(cwd, trimmed);
3021
+ baseStateDir = resolve(getBaseStateDir(cwd));
3022
+ }
3023
+ catch {
3024
+ return false;
3025
+ }
3026
+ const localStateDir = resolve(cwd, ".omx", "state");
3027
+ if (baseStateDir === localStateDir)
3028
+ return false;
3029
+ const relativeToBase = relative(baseStateDir, targetPath).replace(/\\/g, "/");
3030
+ if (!relativeToBase || relativeToBase.startsWith("..") || relativeToBase.startsWith("/"))
3031
+ return false;
3032
+ const segments = relativeToBase.split("/");
3033
+ if (segments.length !== 3 || segments[0] !== "sessions")
3034
+ return false;
3035
+ const sessionId = segments[1] ?? "";
3036
+ const fileName = segments[2] ?? "";
3037
+ return RUNTIME_SESSION_ID_SEGMENT_PATTERN.test(sessionId)
3038
+ && PROTECTED_PLANNING_STATE_FILE_NAMES.has(fileName);
3039
+ }
2878
3040
  function isPlanningTmpRelativePath(relativePath) {
2879
3041
  return relativePath === ".omx/tmp" || relativePath.startsWith(".omx/tmp/");
2880
3042
  }
@@ -2890,9 +3052,9 @@ function isAllowedPlanningTmpScratchPath(relativePath) {
2890
3052
  function isAllowedPlanningArtifactPath(cwd, rawPath, allowedPrefixes) {
2891
3053
  const relativePath = normalizePlanningArtifactRelativePath(cwd, rawPath);
2892
3054
  if (!relativePath)
2893
- return false;
3055
+ return isAllowedAuthoritativeRuntimePlanningStatePath(cwd, rawPath);
2894
3056
  if (isProtectedPlanningStatePath(relativePath))
2895
- return false;
3057
+ return isAllowedAuthoritativeRuntimePlanningStatePath(cwd, rawPath);
2896
3058
  if (isPlanningTmpRelativePath(relativePath)) {
2897
3059
  return allowedPrefixes.includes(".omx/tmp") && isAllowedPlanningTmpScratchPath(relativePath);
2898
3060
  }
@@ -2901,8 +3063,13 @@ function isAllowedPlanningArtifactPath(cwd, rawPath, allowedPrefixes) {
2901
3063
  function isAllowedDeepInterviewArtifactPath(cwd, rawPath) {
2902
3064
  return isAllowedPlanningArtifactPath(cwd, rawPath, DEEP_INTERVIEW_ALLOWED_WRITE_PREFIXES);
2903
3065
  }
3066
+ function isAllowedRalplanDraftPath(cwd, rawPath) {
3067
+ const relativePath = normalizePlanningArtifactRelativePath(cwd, rawPath);
3068
+ return relativePath !== null && /^\.omx\/drafts\/[^/]+\.md$/.test(relativePath);
3069
+ }
2904
3070
  function isAllowedRalplanArtifactPath(cwd, rawPath) {
2905
- return isAllowedPlanningArtifactPath(cwd, rawPath, RALPLAN_ALLOWED_WRITE_PREFIXES);
3071
+ return isAllowedRalplanDraftPath(cwd, rawPath)
3072
+ || isAllowedPlanningArtifactPath(cwd, rawPath, RALPLAN_ALLOWED_WRITE_PREFIXES);
2906
3073
  }
2907
3074
  function shellTokenizeLiteralCommand(command) {
2908
3075
  const tokens = [];
@@ -3110,9 +3277,73 @@ function resolveCommandRedirectTarget(target, assignments) {
3110
3277
  const resolved = assignments.get(safeString(variableMatch[1]));
3111
3278
  return resolved !== undefined ? resolved : target;
3112
3279
  }
3280
+ // Masks redirect metacharacters (`<`/`>`) that appear INSIDE shell quotes so a
3281
+ // quoted regex/source value (e.g. `gh issue create --body '...>{1,2}...'` or
3282
+ // `omx state write --input '{"reason":"a>b"}'`) is not misread as a redirect
3283
+ // write target. Escaped quotes at the top level (`\'`, `\"`) are literal
3284
+ // characters and must NOT open a span, and `$'...'` ANSI-C quoting processes
3285
+ // backslash escapes (so `\'` does not close it) — otherwise a genuine `>`
3286
+ // redirect could be masked behind a false span and its write missed. Only
3287
+ // characters inside a real quoted span are masked; unquoted redirect operators
3288
+ // survive intact. Unterminated/ambiguous quoting fails closed: the original
3289
+ // command is returned unmasked so genuine redirects stay visible to the scan.
3290
+ function maskQuotedRedirectMetacharsForCommandScan(command) {
3291
+ let masked = "";
3292
+ // null = unquoted; "'" = single quotes (no escapes); '"' = double quotes
3293
+ // (backslash escapes); "$'" = ANSI-C $'...' (backslash escapes, incl. \').
3294
+ let quote = null;
3295
+ for (let index = 0; index < command.length; index += 1) {
3296
+ const char = command[index] ?? "";
3297
+ if (quote === null) {
3298
+ // A backslash escapes the next character at the top level, so an escaped
3299
+ // quote is a literal char and must not open a quoted span.
3300
+ if (char === "\\") {
3301
+ masked += char;
3302
+ const next = command[index + 1];
3303
+ if (next !== undefined) {
3304
+ masked += next;
3305
+ index += 1;
3306
+ }
3307
+ continue;
3308
+ }
3309
+ if (char === "$" && command[index + 1] === "'") {
3310
+ quote = "$'";
3311
+ masked += "$'";
3312
+ index += 1;
3313
+ continue;
3314
+ }
3315
+ if (char === "'" || char === "\"")
3316
+ quote = char;
3317
+ masked += char;
3318
+ continue;
3319
+ }
3320
+ if ((quote === "\"" || quote === "$'") && char === "\\") {
3321
+ // Escapes inside double/ANSI-C spans keep the backslash and its escaped
3322
+ // char (masking a redirect metachar so quoted data cannot be a false
3323
+ // target); the escaped char never closes the span.
3324
+ masked += char;
3325
+ const next = command[index + 1];
3326
+ if (next !== undefined) {
3327
+ masked += next === "<" || next === ">" ? "_" : next;
3328
+ index += 1;
3329
+ }
3330
+ continue;
3331
+ }
3332
+ const closesSpan = quote === "$'" ? char === "'" : char === quote;
3333
+ if (closesSpan) {
3334
+ quote = null;
3335
+ masked += char;
3336
+ continue;
3337
+ }
3338
+ masked += char === "<" || char === ">" ? "_" : char;
3339
+ }
3340
+ if (quote !== null)
3341
+ return command;
3342
+ return masked;
3343
+ }
3113
3344
  function extractDeepInterviewCommandRedirectTargets(command) {
3114
3345
  const targets = [];
3115
- const commandOutsideHeredocBodies = stripHeredocBodiesForCommandScan(command);
3346
+ const commandOutsideHeredocBodies = maskQuotedRedirectMetacharsForCommandScan(stripHeredocBodiesForCommandScan(command));
3116
3347
  for (const match of commandOutsideHeredocBodies.matchAll(/(?:^|[^>])>{1,2}\s*(["']?)([^\s&|;<>]+)\1/g)) {
3117
3348
  const candidate = safeString(match[2]).trim();
3118
3349
  if (candidate && !isNullDeviceRedirectTarget(candidate))
@@ -3224,7 +3455,7 @@ function findGitSubcommandIndex(words, startIndex) {
3224
3455
  }
3225
3456
  return null;
3226
3457
  }
3227
- function commandHasDeepInterviewWriteIntent(command) {
3458
+ function commandHasDeepInterviewWriteIntent(command, depth = 0) {
3228
3459
  return commandInvokesApplyPatch(command)
3229
3460
  || extractDeepInterviewCommandRedirectTargets(command).length > 0
3230
3461
  || /\btee\s+(?:-a\s+)?[^\s&|;]+/.test(command)
@@ -3235,7 +3466,17 @@ function commandHasDeepInterviewWriteIntent(command) {
3235
3466
  || extractConductorBashMutations(command).length > 0
3236
3467
  || extractConductorInterpreterWrites(command).length > 0
3237
3468
  || commandHasDestructiveGitSubcommand(command)
3238
- || commandHasPackageInstallIntent(command);
3469
+ || commandHasPackageInstallIntent(command)
3470
+ // Recurse into wrapped shells (`bash -lc "cat > f"`, `eval`, `env`) and
3471
+ // command substitutions so a real redirect INSIDE a nested command string
3472
+ // is still classified as write intent. This is required because quoted
3473
+ // redirect metacharacters are now masked at the outer scan (#3119): the
3474
+ // mask removes false positives from quoted DATA values while nested-command
3475
+ // recursion re-detects genuine redirects, preserving fail-closed blocking.
3476
+ // The depth guard mirrors evaluateConductorBashWrite's nesting bound;
3477
+ // extractors return strict substrings, so recursion always terminates.
3478
+ || (depth < CONDUCTOR_BASH_MAX_NESTING_DEPTH && (extractNestedShellCommandStringsForStateScan(command).some((nested) => commandHasDeepInterviewWriteIntent(nested, depth + 1))
3479
+ || extractNestedCommandSubstitutionStringsForStateScan(command).some((nested) => commandHasDeepInterviewWriteIntent(nested, depth + 1))));
3239
3480
  }
3240
3481
  function extractDeepInterviewCommandWriteTargets(command) {
3241
3482
  const assignments = extractCommandLiteralAssignments(command);
@@ -5271,6 +5512,56 @@ function isPackageManagerCommandWord(word) {
5271
5512
  const base = shellWordBaseName(word);
5272
5513
  return base === "npm" || base === "pnpm" || base === "yarn" || base === "npx" || base === "pnpx";
5273
5514
  }
5515
+ function isStructuredUltragoalSteeringShellCommand(command) {
5516
+ const segments = splitShellCommandSegments(stripHeredocBodiesForCommandScan(command));
5517
+ let sawStructuredSteer = false;
5518
+ for (const segment of segments) {
5519
+ const words = tokenizeShellWords(segment);
5520
+ let commandStart = true;
5521
+ let inForHeader = false;
5522
+ for (let index = 0; index < words.length; index += 1) {
5523
+ const word = words[index] ?? "";
5524
+ if (!word)
5525
+ continue;
5526
+ if (isShellCommandSeparator(word)) {
5527
+ commandStart = true;
5528
+ continue;
5529
+ }
5530
+ if (inForHeader) {
5531
+ if (word === "do") {
5532
+ inForHeader = false;
5533
+ commandStart = true;
5534
+ }
5535
+ continue;
5536
+ }
5537
+ if (isShellGroupingSyntaxWord(word))
5538
+ continue;
5539
+ if (commandStart && isEnvironmentAssignmentWord(word))
5540
+ continue;
5541
+ if (commandStart && CONDUCTOR_BASH_COMPOUND_SYNTAX_WORDS.has(word)) {
5542
+ if (word === "for")
5543
+ inForHeader = true;
5544
+ continue;
5545
+ }
5546
+ if (commandStart && word.startsWith("-"))
5547
+ continue;
5548
+ if (!commandStart)
5549
+ continue;
5550
+ const commandName = commandNameFromShellWord(word);
5551
+ if (commandName === "read" || commandName === "true" || commandName === ":") {
5552
+ commandStart = false;
5553
+ continue;
5554
+ }
5555
+ if (commandName === "omx" && words[index + 1] === "ultragoal" && words[index + 2] === "steer") {
5556
+ sawStructuredSteer = true;
5557
+ commandStart = false;
5558
+ continue;
5559
+ }
5560
+ return false;
5561
+ }
5562
+ }
5563
+ return sawStructuredSteer;
5564
+ }
5274
5565
  function hasDynamicNestedShellExecution(command) {
5275
5566
  const commands = splitShellCommandSegments(stripHeredocBodiesForCommandScan(command));
5276
5567
  for (const segment of commands) {
@@ -5298,13 +5589,13 @@ function hasDynamicNestedShellExecution(command) {
5298
5589
  return true;
5299
5590
  if (commandStringIndex !== null) {
5300
5591
  const nestedCommand = words[commandStringIndex] ?? "";
5301
- if (isDynamicNestedCommandString(nestedCommand))
5592
+ if (isDynamicNestedCommandString(nestedCommand) && !isStructuredUltragoalSteeringShellCommand(nestedCommand))
5302
5593
  return true;
5303
5594
  }
5304
5595
  }
5305
5596
  if (word === "eval") {
5306
5597
  const nestedCommand = words.slice(index + 1).join(" ");
5307
- if (isDynamicNestedCommandString(nestedCommand))
5598
+ if (isDynamicNestedCommandString(nestedCommand) && !isStructuredUltragoalSteeringShellCommand(nestedCommand))
5308
5599
  return true;
5309
5600
  }
5310
5601
  }
@@ -6185,7 +6476,7 @@ function buildDeepInterviewBashBlockedDetail(cwd, command) {
6185
6476
  }
6186
6477
  async function buildRalplanPreToolUseBoundaryOutput(payload, cwd, stateDir, resolvedSessionId) {
6187
6478
  const sessionId = safeString(resolvedSessionId ?? readPayloadSessionId(payload)).trim();
6188
- if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, sessionId))
6479
+ if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, stateDir, sessionId))
6189
6480
  return null;
6190
6481
  const threadId = readPayloadThreadId(payload);
6191
6482
  const activeState = await readActiveRalplanStateForPreToolUse(cwd, stateDir, sessionId, threadId);
@@ -6236,7 +6527,7 @@ async function buildRalplanPreToolUseBoundaryOutput(payload, cwd, stateDir, reso
6236
6527
  hookSpecificOutput: {
6237
6528
  hookEventName: "PreToolUse",
6238
6529
  additionalContext: `${planningModeDescription}. `
6239
- + "Write only planning artifacts under `.omx/context/`, `.omx/plans/`, `.omx/specs/`, `.omx/tmp/`, required `.omx/state/` files, or tracker metadata under `.beads/`. "
6530
+ + "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/`. "
6240
6531
  + "Do not edit implementation files or run implementation-focused writes from planning phases. "
6241
6532
  + `To execute, first process an explicit handoff such as ${formatExecutionHandoffList(cwd)}, which must emit terminal planning state before implementation begins.`,
6242
6533
  },
@@ -6244,7 +6535,7 @@ async function buildRalplanPreToolUseBoundaryOutput(payload, cwd, stateDir, reso
6244
6535
  }
6245
6536
  async function buildDeepInterviewPreToolUseBoundaryOutput(payload, cwd, stateDir, resolvedSessionId) {
6246
6537
  const sessionId = safeString(resolvedSessionId ?? readPayloadSessionId(payload)).trim();
6247
- if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, sessionId))
6538
+ if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, stateDir, sessionId))
6248
6539
  return null;
6249
6540
  const threadId = readPayloadThreadId(payload);
6250
6541
  const activeState = await readActiveDeepInterviewStateForPreToolUse(cwd, stateDir, sessionId, threadId);
@@ -6362,18 +6653,64 @@ async function buildPlanningRootPointerConflictPreToolUseOutput(payload, cwd, st
6362
6653
  }
6363
6654
  return blocked ? buildRalplanRootPointerConflictBlock(ralplanState) : null;
6364
6655
  }
6365
- async function hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, sessionId) {
6656
+ async function hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, stateDir, sessionId, options = {}) {
6366
6657
  if (hasTeamWorkerEnvironment())
6367
6658
  return true;
6368
- if (!isTypedAgentRolePayload(payload))
6369
- return false;
6370
6659
  const trackingState = await readSubagentTrackingState(cwd).catch(() => null);
6371
6660
  const session = trackingState?.sessions?.[sessionId];
6372
6661
  if (!session)
6373
6662
  return false;
6374
6663
  const payloadThreadId = readPayloadThreadId(payload);
6664
+ // Resolve the Main-root leader THREAD identity from the tracker's leader_thread_id
6665
+ // plus the canonical session's native_session_id (the leader's native thread). Only
6666
+ // genuine leader-thread identifiers may anchor the leader: owner_*_session_id are
6667
+ // session-level ids, not thread anchors, so they must NOT populate this set — their
6668
+ // mere presence would make it non-empty and suppress the fail-closed guard below
6669
+ // without ever excluding the leader thread (#3117 P4). The native_session_id anchor is
6670
+ // honored only when the root session pointer provably maps to the sessionId under
6671
+ // evaluation, so a stale/foreign session.json cannot supply another session's leader
6672
+ // anchor (#3117 P3); the tracker alone is insufficient because it can omit
6673
+ // leader_thread_id or corruptly label the leader kind:"subagent" (#3117 P2).
6674
+ const sessionState = await readRootSessionStateFromStateDir(stateDir).catch(() => null);
6675
+ const trackerLeaderThreadId = safeString(session.leader_thread_id).trim();
6676
+ const leaderIdentityAnchors = new Set();
6677
+ if (trackerLeaderThreadId)
6678
+ leaderIdentityAnchors.add(trackerLeaderThreadId);
6679
+ if (sessionState && sessionId && payloadMatchesSessionPointer(sessionId, sessionState)) {
6680
+ const nativeSessionId = safeString(sessionState.native_session_id).trim();
6681
+ if (nativeSessionId)
6682
+ leaderIdentityAnchors.add(nativeSessionId);
6683
+ }
6684
+ // Leader self-guard: the Main-root Conductor is never a delegated executor. Block it
6685
+ // ahead of every trust path, even when tracker or payload provenance is (possibly
6686
+ // corruptly) attached to the leader thread.
6687
+ if (payloadThreadId && leaderIdentityAnchors.has(payloadThreadId))
6688
+ return false;
6689
+ // Fail closed: without an authoritative leader anchor we cannot affirm the payload is
6690
+ // a non-leader delegated performer rather than a mislabeled leader, so we refuse trust
6691
+ // instead of inferring it from a corrupt tracker kind:"subagent" alone (#3117 P2).
6692
+ if (leaderIdentityAnchors.size === 0)
6693
+ return false;
6694
+ // Planning boundary guards (ralplan, deep-interview) still require a recognized typed
6695
+ // agent role, so an untyped collaboration.spawn_agent child cannot write before an
6696
+ // execution handoff/approval. Only the Main-root Conductor/Ralph executing guard opts
6697
+ // into untyped tracker/runtime provenance (#3116, #3117).
6698
+ if (options.allowUntypedProvenance !== true && !isTypedAgentRolePayload(payload)) {
6699
+ return false;
6700
+ }
6701
+ // Tracker-backed provenance: the payload's own thread is a recorded, non-leader
6702
+ // subagent for this session — the non-spoofable anchor that lets native
6703
+ // collaboration.spawn_agent children/descendants edit under the Conductor guard even
6704
+ // without a recognized typed role (#3116). subagent-tracking.json is derived from
6705
+ // child SessionStart transcript metadata, not the live tool-call payload.
6375
6706
  if (payloadThreadId && isTrustedSubagentThread(session, payloadThreadId))
6376
6707
  return true;
6708
+ // Runtime-attached spawn provenance: trust a genuine spawned subagent turn whose
6709
+ // declared parent maps to this session's leader or a tracked thread. parentThreadId
6710
+ // comes from the runtime-set source.subagent.thread_spawn (not agent-controlled tool
6711
+ // arguments); an absent parent is rejected, the leader self-guard above blocks the
6712
+ // main root, and cross-session parents fail because they are not in this session's
6713
+ // tracked threads (#3116).
6377
6714
  const source = safeObject(payload.source);
6378
6715
  const subagent = safeObject(source.subagent);
6379
6716
  const threadSpawn = safeObject(subagent.thread_spawn);
@@ -6383,10 +6720,7 @@ async function hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, sess
6383
6720
  ?? threadSpawn.leaderThreadId).trim();
6384
6721
  if (!parentThreadId)
6385
6722
  return false;
6386
- const leaderThreadId = safeString(session.leader_thread_id).trim();
6387
- if (payloadThreadId && leaderThreadId && payloadThreadId === leaderThreadId)
6388
- return false;
6389
- return leaderThreadId === parentThreadId || parentThreadId in session.threads;
6723
+ return leaderIdentityAnchors.has(parentThreadId) || parentThreadId in session.threads;
6390
6724
  }
6391
6725
  function isActiveConductorModeState(state, mode, sessionId) {
6392
6726
  if (!state || state.active !== true)
@@ -6413,7 +6747,7 @@ async function readActiveMainRootConductorStateForPreToolUse(payload, cwd, state
6413
6747
  const threadId = readPayloadThreadId(payload);
6414
6748
  if (!sessionId)
6415
6749
  return null;
6416
- if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, sessionId))
6750
+ if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, stateDir, sessionId, { allowUntypedProvenance: true }))
6417
6751
  return null;
6418
6752
  const canonicalState = await readVisibleSkillActiveStateForStateDir(stateDir, sessionId);
6419
6753
  if (!canonicalState)
@@ -7228,6 +7562,13 @@ export async function buildConductorPreToolUseWriteGuardOutput(payload, cwd, sta
7228
7562
  const activeState = await readActiveMainRootConductorStateForPreToolUse(payload, cwd, stateDir, resolvedSessionId);
7229
7563
  if (!activeState)
7230
7564
  return null;
7565
+ const nativeSubagentSupport = resolveNativeSubagentSupportStatus({
7566
+ payload,
7567
+ persistedSupportBlocker: await readJsonIfExists(nativeSubagentSupportBlockerPath(stateDir)),
7568
+ persistedCapacityBlocker: await readJsonIfExists(nativeSubagentCapacityBlockerPath(stateDir)),
7569
+ cwd,
7570
+ sessionId: resolvedSessionId || readPayloadSessionId(payload),
7571
+ });
7231
7572
  const toolName = safeString(payload.tool_name).trim();
7232
7573
  const command = readPreToolUseCommand(payload);
7233
7574
  const pathCandidates = readPreToolUsePathCandidates(payload);
@@ -7254,6 +7595,9 @@ export async function buildConductorPreToolUseWriteGuardOutput(payload, cwd, sta
7254
7595
  }
7255
7596
  if (!blocked)
7256
7597
  return null;
7598
+ const unsupportedNativeGuidance = isUnsupportedNativeSubagentEvidence(nativeSubagentSupport)
7599
+ ? ` ${buildUnsupportedNativeSubagentGuidance(nativeSubagentSupport)} Treat the active conductor workflow as blocked/cancelled for native delegation recovery; do not call multi_agent_v1.close_agent.`
7600
+ : "";
7257
7601
  return {
7258
7602
  decision: "block",
7259
7603
  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}.`,
@@ -7262,7 +7606,8 @@ export async function buildConductorPreToolUseWriteGuardOutput(payload, cwd, sta
7262
7606
  additionalContext: `${LEADER_CONDUCTOR_GOLDEN_RULE} `
7263
7607
  + "Use specialized agents for source edits and plan/spec authorship. "
7264
7608
  + `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. `
7265
- + "Autopilot rework and typed subagent/worker lanes are exempt from this guard.",
7609
+ + unsupportedNativeGuidance
7610
+ + " Autopilot rework and typed subagent/worker lanes are exempt from this guard.",
7266
7611
  },
7267
7612
  };
7268
7613
  }
@@ -8222,7 +8567,7 @@ async function buildStopHookOutput(payload, cwd, stateDir, options = {}) {
8222
8567
  if (isFinalHandoffDocumentRefreshCandidate(lastAssistantMessage)) {
8223
8568
  const documentRefreshWarning = evaluateFinalHandoffDocumentRefresh(cwd, lastAssistantMessage);
8224
8569
  if (documentRefreshWarning) {
8225
- return await maybeReturnRepeatableStopOutput(payload, stateDir, buildRepeatableStopSignature(payload, "document-refresh-stop", documentRefreshWarning.triggeringPaths.join("|"), canonicalSessionId), buildDocumentRefreshAdvisoryOutput(documentRefreshWarning, "Stop"), canonicalSessionId, { allowRepeatDuringStopHook: false });
8570
+ return await maybeReturnRepeatableStopOutput(payload, stateDir, buildRepeatableStopSignature(payload, "document-refresh-stop", documentRefreshWarning.triggeringPaths.join("|"), canonicalSessionId), { systemMessage: documentRefreshWarning.message }, canonicalSessionId, { allowRepeatDuringStopHook: false });
8226
8571
  }
8227
8572
  }
8228
8573
  return null;
@@ -8512,6 +8857,7 @@ export async function dispatchCodexNativeHook(payload, options = {}) {
8512
8857
  }
8513
8858
  else if (hookEventName === "PostToolUse") {
8514
8859
  await recordNativeSubagentCapacityBlocker(cwd, stateDir, payload).catch(() => { });
8860
+ await recordNativeSubagentSupportBlocker(cwd, stateDir, payload).catch(() => { });
8515
8861
  if (detectMcpTransportFailure(payload)) {
8516
8862
  await markTeamTransportFailure(cwd, payload);
8517
8863
  }