oh-my-codex 0.17.2 → 0.18.0

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 (178) hide show
  1. package/Cargo.lock +13 -5
  2. package/Cargo.toml +2 -1
  3. package/README.md +1 -0
  4. package/crates/omx-api/Cargo.toml +19 -0
  5. package/crates/omx-api/src/lib.rs +2940 -0
  6. package/crates/omx-api/src/main.rs +10 -0
  7. package/crates/omx-api/tests/cli.rs +558 -0
  8. package/crates/omx-explore/src/main.rs +4 -0
  9. package/crates/omx-sparkshell/src/codex_bridge.rs +437 -123
  10. package/crates/omx-sparkshell/src/exec.rs +4 -0
  11. package/crates/omx-sparkshell/src/main.rs +738 -29
  12. package/crates/omx-sparkshell/src/prompt.rs +25 -3
  13. package/crates/omx-sparkshell/src/redaction.rs +241 -0
  14. package/crates/omx-sparkshell/tests/execution.rs +479 -238
  15. package/dist/cli/__tests__/api.test.d.ts +2 -0
  16. package/dist/cli/__tests__/api.test.d.ts.map +1 -0
  17. package/dist/cli/__tests__/api.test.js +175 -0
  18. package/dist/cli/__tests__/api.test.js.map +1 -0
  19. package/dist/cli/__tests__/ask.test.js +72 -5
  20. package/dist/cli/__tests__/ask.test.js.map +1 -1
  21. package/dist/cli/__tests__/autoresearch-goal.test.js +14 -1
  22. package/dist/cli/__tests__/autoresearch-goal.test.js.map +1 -1
  23. package/dist/cli/__tests__/doctor-warning-copy.test.js +51 -0
  24. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  25. package/dist/cli/__tests__/explore.test.js +23 -0
  26. package/dist/cli/__tests__/explore.test.js.map +1 -1
  27. package/dist/cli/__tests__/index.test.js +123 -5
  28. package/dist/cli/__tests__/index.test.js.map +1 -1
  29. package/dist/cli/__tests__/launch-fallback.test.js +76 -0
  30. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  31. package/dist/cli/__tests__/package-bin-contract.test.js +4 -3
  32. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  33. package/dist/cli/__tests__/question.test.js +45 -22
  34. package/dist/cli/__tests__/question.test.js.map +1 -1
  35. package/dist/cli/__tests__/setup-agents-overwrite.test.js +2 -0
  36. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  37. package/dist/cli/__tests__/setup-install-mode.test.js +138 -0
  38. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  39. package/dist/cli/__tests__/setup-scope.test.js +8 -2
  40. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  41. package/dist/cli/__tests__/sparkshell-cli.test.js +5 -0
  42. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  43. package/dist/cli/__tests__/version-sync-contract.test.js +4 -0
  44. package/dist/cli/__tests__/version-sync-contract.test.js.map +1 -1
  45. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +1 -1
  46. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  47. package/dist/cli/api.d.ts +26 -0
  48. package/dist/cli/api.d.ts.map +1 -0
  49. package/dist/cli/api.js +153 -0
  50. package/dist/cli/api.js.map +1 -0
  51. package/dist/cli/doctor.d.ts.map +1 -1
  52. package/dist/cli/doctor.js +39 -4
  53. package/dist/cli/doctor.js.map +1 -1
  54. package/dist/cli/explore.d.ts +2 -0
  55. package/dist/cli/explore.d.ts.map +1 -1
  56. package/dist/cli/explore.js +43 -1
  57. package/dist/cli/explore.js.map +1 -1
  58. package/dist/cli/index.d.ts +10 -4
  59. package/dist/cli/index.d.ts.map +1 -1
  60. package/dist/cli/index.js +128 -10
  61. package/dist/cli/index.js.map +1 -1
  62. package/dist/cli/native-assets.d.ts +2 -1
  63. package/dist/cli/native-assets.d.ts.map +1 -1
  64. package/dist/cli/native-assets.js +1 -0
  65. package/dist/cli/native-assets.js.map +1 -1
  66. package/dist/cli/setup.d.ts.map +1 -1
  67. package/dist/cli/setup.js +6 -1
  68. package/dist/cli/setup.js.map +1 -1
  69. package/dist/cli/sparkshell.d.ts.map +1 -1
  70. package/dist/cli/sparkshell.js +20 -3
  71. package/dist/cli/sparkshell.js.map +1 -1
  72. package/dist/config/generator.d.ts.map +1 -1
  73. package/dist/config/generator.js +90 -0
  74. package/dist/config/generator.js.map +1 -1
  75. package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts +2 -0
  76. package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts.map +1 -0
  77. package/dist/hooks/__tests__/best-practice-research-skill.test.js +27 -0
  78. package/dist/hooks/__tests__/best-practice-research-skill.test.js.map +1 -0
  79. package/dist/hooks/__tests__/keyword-detector.test.js +11 -0
  80. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  81. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +6 -0
  82. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  83. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +4 -0
  84. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
  85. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  86. package/dist/hooks/keyword-registry.js +1 -0
  87. package/dist/hooks/keyword-registry.js.map +1 -1
  88. package/dist/hud/__tests__/reconcile.test.js +2 -2
  89. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  90. package/dist/hud/__tests__/tmux.test.js +23 -18
  91. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  92. package/dist/hud/tmux.d.ts.map +1 -1
  93. package/dist/hud/tmux.js +7 -6
  94. package/dist/hud/tmux.js.map +1 -1
  95. package/dist/mcp/__tests__/bootstrap.test.js +75 -1
  96. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  97. package/dist/mcp/bootstrap.d.ts +3 -1
  98. package/dist/mcp/bootstrap.d.ts.map +1 -1
  99. package/dist/mcp/bootstrap.js +71 -2
  100. package/dist/mcp/bootstrap.js.map +1 -1
  101. package/dist/scripts/__tests__/codex-native-hook.test.js +737 -26
  102. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  103. package/dist/scripts/__tests__/notify-dispatcher.test.js +183 -1
  104. package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
  105. package/dist/scripts/__tests__/smoke-packed-install.test.js +4 -1
  106. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  107. package/dist/scripts/build-api.d.ts +2 -0
  108. package/dist/scripts/build-api.d.ts.map +1 -0
  109. package/dist/scripts/build-api.js +44 -0
  110. package/dist/scripts/build-api.js.map +1 -0
  111. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  112. package/dist/scripts/codex-native-hook.js +208 -8
  113. package/dist/scripts/codex-native-hook.js.map +1 -1
  114. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  115. package/dist/scripts/codex-native-pre-post.js +89 -24
  116. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  117. package/dist/scripts/notify-dispatcher.js +88 -0
  118. package/dist/scripts/notify-dispatcher.js.map +1 -1
  119. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  120. package/dist/scripts/notify-hook/team-dispatch.js +27 -9
  121. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  122. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  123. package/dist/scripts/notify-hook/team-leader-nudge.js +26 -11
  124. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  125. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +1 -0
  126. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  127. package/dist/scripts/notify-hook/team-tmux-guard.js +38 -0
  128. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  129. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  130. package/dist/scripts/notify-hook/team-worker-stop.js +27 -14
  131. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  132. package/dist/scripts/run-provider-advisor.js +9 -3
  133. package/dist/scripts/run-provider-advisor.js.map +1 -1
  134. package/dist/scripts/smoke-packed-install.d.ts +1 -1
  135. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  136. package/dist/scripts/smoke-packed-install.js +2 -0
  137. package/dist/scripts/smoke-packed-install.js.map +1 -1
  138. package/dist/team/__tests__/runtime.test.js +2 -2
  139. package/dist/team/__tests__/runtime.test.js.map +1 -1
  140. package/dist/team/__tests__/tmux-session.test.js +153 -25
  141. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  142. package/dist/team/tmux-session.d.ts +1 -0
  143. package/dist/team/tmux-session.d.ts.map +1 -1
  144. package/dist/team/tmux-session.js +55 -10
  145. package/dist/team/tmux-session.js.map +1 -1
  146. package/dist/utils/__tests__/agents-md.test.js +45 -1
  147. package/dist/utils/__tests__/agents-md.test.js.map +1 -1
  148. package/dist/utils/agents-md.d.ts +2 -0
  149. package/dist/utils/agents-md.d.ts.map +1 -1
  150. package/dist/utils/agents-md.js +19 -0
  151. package/dist/utils/agents-md.js.map +1 -1
  152. package/dist/verification/__tests__/ci-rust-gates.test.js +85 -10
  153. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  154. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +1 -0
  155. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
  156. package/package.json +4 -3
  157. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  158. package/plugins/oh-my-codex/skills/best-practice-research/SKILL.md +83 -0
  159. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +1 -0
  160. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +1 -1
  161. package/prompts/researcher.md +15 -10
  162. package/skills/best-practice-research/SKILL.md +83 -0
  163. package/skills/deep-interview/SKILL.md +1 -0
  164. package/skills/ralplan/SKILL.md +1 -1
  165. package/src/scripts/__tests__/codex-native-hook.test.ts +810 -4
  166. package/src/scripts/__tests__/notify-dispatcher.test.ts +223 -1
  167. package/src/scripts/__tests__/smoke-packed-install.test.ts +8 -2
  168. package/src/scripts/build-api.ts +48 -0
  169. package/src/scripts/codex-native-hook.ts +262 -10
  170. package/src/scripts/codex-native-pre-post.ts +103 -24
  171. package/src/scripts/notify-dispatcher.ts +97 -0
  172. package/src/scripts/notify-hook/team-dispatch.ts +27 -8
  173. package/src/scripts/notify-hook/team-leader-nudge.ts +25 -11
  174. package/src/scripts/notify-hook/team-tmux-guard.ts +42 -0
  175. package/src/scripts/notify-hook/team-worker-stop.ts +24 -13
  176. package/src/scripts/run-provider-advisor.ts +11 -3
  177. package/src/scripts/smoke-packed-install.ts +2 -0
  178. package/templates/catalog-manifest.json +7 -0
@@ -1,6 +1,6 @@
1
1
  import { execFileSync } from "child_process";
2
2
  import { closeSync, existsSync, openSync, readFileSync, readSync } from "fs";
3
- import { appendFile, mkdir, readFile, readdir, writeFile } from "fs/promises";
3
+ import { appendFile, mkdir, readFile, readdir, stat, writeFile } from "fs/promises";
4
4
  import { extname, join, relative, resolve } from "path";
5
5
  import { pathToFileURL } from "url";
6
6
  import { readModeState, readModeStateForActiveDecision, readModeStateForSession, updateModeState } from "../modes/base.js";
@@ -38,6 +38,7 @@ const TEAM_STOP_BLOCKING_TASK_STATUSES = new Set(["pending", "in_progress", "blo
38
38
  const TEAM_WORKER_TERMINAL_RUN_STATES = new Set(["done", "complete", "completed", "failed", "stopped", "cancelled"]);
39
39
  const NATIVE_STOP_STATE_FILE = "native-stop-state.json";
40
40
  const ORDINARY_STOP_NO_PROGRESS_DEFAULT_MAX_REPEATS = 8;
41
+ const RALPH_ORPHANED_STARTING_STALE_MS = 15 * 60_000;
41
42
  const ORDINARY_STOP_NO_PROGRESS_DEFAULT_IDLE_MS = 10 * 60_000;
42
43
  const ORDINARY_STOP_NO_PROGRESS_MAX_MESSAGE_LENGTH = 240;
43
44
  const STABLE_FINAL_RECOMMENDATION_PATTERNS = [
@@ -344,6 +345,47 @@ async function readActiveAutoresearchState(cwd, sessionId) {
344
345
  function isRalphStartingPhase(state) {
345
346
  return safeString(state.current_phase ?? state.currentPhase).trim().toLowerCase() === "starting";
346
347
  }
348
+ function parseTimestampMs(value) {
349
+ const text = safeString(value).trim();
350
+ if (!text)
351
+ return null;
352
+ const ms = Date.parse(text);
353
+ return Number.isFinite(ms) ? ms : null;
354
+ }
355
+ function numericValue(value) {
356
+ if (typeof value === "number" && Number.isFinite(value))
357
+ return value;
358
+ if (typeof value === "string" && value.trim()) {
359
+ const parsed = Number(value);
360
+ return Number.isFinite(parsed) ? parsed : null;
361
+ }
362
+ return null;
363
+ }
364
+ function hasRalphOwnerHint(state) {
365
+ return [
366
+ state.owner_omx_session_id,
367
+ state.owner_codex_session_id,
368
+ state.owner_codex_thread_id,
369
+ state.thread_id,
370
+ state.tmux_pane_id,
371
+ state.task_slug,
372
+ ].some((value) => safeString(value).trim() !== "");
373
+ }
374
+ async function isStaleOrphanedRalphStartingState(state, path, nowMs = Date.now()) {
375
+ if (!isRalphStartingPhase(state))
376
+ return false;
377
+ if (numericValue(state.iteration) !== 0)
378
+ return false;
379
+ if (hasRalphOwnerHint(state))
380
+ return false;
381
+ const timestampMs = parseTimestampMs(state.updated_at)
382
+ ?? parseTimestampMs(state.started_at)
383
+ ?? parseTimestampMs(state.created_at)
384
+ ?? await stat(path).then((info) => info.mtimeMs, () => null);
385
+ if (timestampMs === null)
386
+ return false;
387
+ return nowMs - timestampMs > RALPH_ORPHANED_STARTING_STALE_MS;
388
+ }
347
389
  function hasValue(values, value) {
348
390
  return value !== "" && values.some((candidate) => candidate === value);
349
391
  }
@@ -410,6 +452,89 @@ async function hasConsistentRalphSkillActivation(stateDir, sessionId) {
410
452
  return false;
411
453
  return true;
412
454
  }
455
+ function isShadowableRalphStartingSeed(state) {
456
+ if (state.active !== true)
457
+ return false;
458
+ if (!isRalphStartingPhase(state))
459
+ return false;
460
+ if (state.completion_audit || state.completionAudit)
461
+ return false;
462
+ const iteration = numericValue(state.iteration);
463
+ return iteration === null || iteration <= 0;
464
+ }
465
+ function hasPassingCompletedRalphAudit(state, cwd) {
466
+ if (!state)
467
+ return false;
468
+ if (state.mode && safeString(state.mode) !== "ralph")
469
+ return false;
470
+ if (!isRalphCompletePhase(state.current_phase ?? state.currentPhase))
471
+ return false;
472
+ if (state.active === true)
473
+ return false;
474
+ return evaluateRalphCompletionAuditEvidence(state, cwd).complete === true;
475
+ }
476
+ function shouldRetireShadowedRalphStartingSeed(seedState, completedState, cwd, ownerContext) {
477
+ if (!isShadowableRalphStartingSeed(seedState))
478
+ return false;
479
+ if (!hasPassingCompletedRalphAudit(completedState, cwd))
480
+ return false;
481
+ if (!completedState)
482
+ return false;
483
+ const completedSessionId = safeString(ownerContext?.completedSessionId ?? completedState.session_id).trim();
484
+ if (completedSessionId
485
+ && !activeRalphStateMatchesStopOwner(completedState, {
486
+ sessionId: completedSessionId,
487
+ payloadSessionId: safeString(ownerContext?.payloadSessionId).trim(),
488
+ threadId: safeString(ownerContext?.threadId).trim(),
489
+ currentNativeSessionId: safeString(ownerContext?.currentNativeSessionId).trim(),
490
+ tmuxPaneId: safeString(ownerContext?.tmuxPaneId).trim(),
491
+ })) {
492
+ return false;
493
+ }
494
+ const seedThreadId = safeString(seedState.owner_codex_thread_id ?? seedState.thread_id).trim();
495
+ const completedThreadId = safeString(completedState?.owner_codex_thread_id ?? completedState?.thread_id).trim();
496
+ const stopThreadId = safeString(ownerContext?.threadId).trim();
497
+ if (seedThreadId && completedThreadId && seedThreadId !== completedThreadId)
498
+ return false;
499
+ if (seedThreadId && stopThreadId && seedThreadId !== stopThreadId)
500
+ return false;
501
+ if (completedThreadId && stopThreadId && completedThreadId !== stopThreadId)
502
+ return false;
503
+ const seedPaneId = safeString(seedState.tmux_pane_id).trim();
504
+ const completedPaneId = safeString(completedState?.tmux_pane_id).trim();
505
+ const stopPaneId = safeString(ownerContext?.tmuxPaneId).trim();
506
+ if (seedPaneId && completedPaneId && seedPaneId !== completedPaneId)
507
+ return false;
508
+ if (seedPaneId && stopPaneId && seedPaneId !== stopPaneId)
509
+ return false;
510
+ if (completedPaneId && stopPaneId && completedPaneId !== stopPaneId)
511
+ return false;
512
+ const seedStartedAt = parseTimestampMs(seedState.started_at ?? seedState.startedAt);
513
+ const completedAt = parseTimestampMs(completedState?.completed_at ?? completedState?.completedAt);
514
+ if (completedAt === null)
515
+ return false;
516
+ if (seedStartedAt !== null && seedStartedAt > completedAt)
517
+ return false;
518
+ return true;
519
+ }
520
+ async function retireShadowedRalphStartingSeed(path, seedState, completedSessionId, completedPath, completedState) {
521
+ const nowIso = new Date().toISOString();
522
+ const completedAt = safeString(completedState.completed_at ?? completedState.completedAt).trim() || nowIso;
523
+ const next = {
524
+ ...seedState,
525
+ active: false,
526
+ current_phase: "complete",
527
+ completed_at: completedAt,
528
+ stop_reason: "shadowed_by_completed_canonical_ralph",
529
+ shadowed_by_completed_canonical_ralph: {
530
+ session_id: completedSessionId,
531
+ state_path: completedPath,
532
+ completed_at: completedAt,
533
+ reconciled_at: nowIso,
534
+ },
535
+ };
536
+ await writeFile(path, JSON.stringify(next, null, 2));
537
+ }
413
538
  async function readRalphCompletionAuditBlockState(cwd, stateDir, preferredSessionId, ownerContext) {
414
539
  const [rawSessionInfo, usableSessionInfo] = await Promise.all([
415
540
  readSessionState(cwd),
@@ -480,6 +605,12 @@ async function readActiveRalphState(cwd, stateDir, preferredSessionId, ownerCont
480
605
  safeString(preferredSessionId).trim(),
481
606
  currentOmxSessionId,
482
607
  ].filter(Boolean))];
608
+ const completedCanonicalPath = currentOmxSessionId
609
+ ? getStateFilePath("ralph-state.json", cwd, currentOmxSessionId)
610
+ : "";
611
+ const completedCanonicalState = completedCanonicalPath
612
+ ? await readJsonIfExists(completedCanonicalPath)
613
+ : null;
483
614
  // Ralph Stop stays authoritative-scope-only once the Stop payload is session-bound.
484
615
  // That is intentionally stricter than generic state MCP reads: do not scan sibling
485
616
  // session scopes or fall back to root when a current/explicit session is in play.
@@ -492,10 +623,27 @@ async function readActiveRalphState(cwd, stateDir, preferredSessionId, ownerCont
492
623
  }
493
624
  const sessionScopedPath = getStateFilePath("ralph-state.json", cwd, sessionId);
494
625
  const sessionScoped = await readJsonIfExists(sessionScopedPath);
495
- if (sessionScoped?.active === true
496
- && isRalphStartingPhase(sessionScoped)
497
- && !(await isVisibleRalphActiveForSession(stateDir, sessionId))) {
498
- continue;
626
+ if (sessionScoped?.active === true) {
627
+ if (currentOmxSessionId
628
+ && sessionId !== currentOmxSessionId
629
+ && completedCanonicalState
630
+ && shouldRetireShadowedRalphStartingSeed(sessionScoped, completedCanonicalState, cwd, {
631
+ completedSessionId: currentOmxSessionId,
632
+ payloadSessionId: safeString(ownerContext?.payloadSessionId).trim(),
633
+ threadId: safeString(ownerContext?.threadId).trim(),
634
+ currentNativeSessionId,
635
+ tmuxPaneId: safeString(ownerContext?.tmuxPaneId).trim(),
636
+ })) {
637
+ await retireShadowedRalphStartingSeed(sessionScopedPath, sessionScoped, currentOmxSessionId, completedCanonicalPath, completedCanonicalState);
638
+ continue;
639
+ }
640
+ if (await isStaleOrphanedRalphStartingState(sessionScoped, sessionScopedPath)) {
641
+ continue;
642
+ }
643
+ if (isRalphStartingPhase(sessionScoped)
644
+ && !(await isVisibleRalphActiveForSession(stateDir, sessionId))) {
645
+ continue;
646
+ }
499
647
  }
500
648
  if (sessionScoped?.active === true
501
649
  && shouldContinueRun(sessionScoped)
@@ -1345,11 +1493,14 @@ async function buildModeBasedStopOutput(mode, cwd, sessionId) {
1345
1493
  if (!state || !shouldContinueRun(state))
1346
1494
  return null;
1347
1495
  const phase = formatPhase(state.current_phase);
1496
+ const systemMessage = mode === "autopilot" && phase.toLowerCase().replace(/_/g, "-") === "code-review"
1497
+ ? "OMX autopilot is still active (phase: code-review). Run the required $code-review step before completing or clearing Autopilot state."
1498
+ : `OMX ${mode} is still active (phase: ${phase}).`;
1348
1499
  return {
1349
1500
  decision: "block",
1350
1501
  reason: `OMX ${mode} is still active (phase: ${phase}); continue the task and gather fresh verification evidence before stopping.`,
1351
1502
  stopReason: `${mode}_${phase}`,
1352
- systemMessage: `OMX ${mode} is still active (phase: ${phase}).`,
1503
+ systemMessage,
1353
1504
  };
1354
1505
  }
1355
1506
  export function looksLikeGoalCompletionPrompt(text) {
@@ -1359,6 +1510,11 @@ export function looksLikeGoalCompletionPrompt(text) {
1359
1510
  || /\b(?:ultragoal|performance[-\s]goal|autoresearch[-\s]goal)\b.{0,80}\b(?:complete|checkpoint|finish|close|mark)\b/i.test(text)
1360
1511
  || /(?:^|[.!?]\s+)(?:the\s+)?goal\s+(?:is\s+|now\s+|has\s+been\s+)?(?:complete|completed|finished|closed)(?:\s*(?:[.!?]|$)|\s*[:;]\s*\S|\s*[—–-]\s*\S)/i.test(text);
1361
1512
  }
1513
+ function reportsAutoresearchGoalObjectiveMismatch(text) {
1514
+ return /\bautoresearch[-\s]goal\b/i.test(text)
1515
+ && /\b(?:complete|completion|reconciliation)\b/i.test(text)
1516
+ && /objective mismatch/i.test(text);
1517
+ }
1362
1518
  async function findActiveGoalWorkflowReconciliationRequirement(cwd) {
1363
1519
  const ultragoal = await readJsonIfExists(join(cwd, ".omx", "ultragoal", "goals.json"));
1364
1520
  const aggregateCompletion = safeObject(ultragoal?.aggregateCompletion);
@@ -1402,10 +1558,17 @@ async function findActiveGoalWorkflowReconciliationRequirement(cwd) {
1402
1558
  const completion = await readJsonIfExists(join(autoresearchRoot, entry.name, "completion.json"));
1403
1559
  const completionVerdict = safeString(completion?.verdict);
1404
1560
  const completionPassed = completion?.passed === true || completionVerdict === "pass";
1405
- if (mission?.workflow === "autoresearch-goal" && status && status !== "complete" && completionPassed) {
1561
+ if (mission?.workflow === "autoresearch-goal"
1562
+ && status
1563
+ && status !== "complete"
1564
+ && completionPassed) {
1406
1565
  return {
1407
1566
  workflow: "autoresearch-goal",
1408
1567
  command: `omx autoresearch-goal complete --slug ${safeString(mission.slug) || entry.name} --codex-goal-json '<get_goal JSON or path>'`,
1568
+ remediation: [
1569
+ "If that command fails with a Codex goal objective mismatch after a fresh get_goal snapshot, do not repeat the same complete command blindly in this thread.",
1570
+ "Either retry with a correct fresh snapshot or record an explicit blocked verdict for this autoresearch-goal and continue it from a fresh Codex thread.",
1571
+ ].join(" "),
1409
1572
  };
1410
1573
  }
1411
1574
  }
@@ -1431,6 +1594,9 @@ async function buildGoalWorkflowReconciliationStopOutput(payload, cwd) {
1431
1594
  const requirement = await findActiveGoalWorkflowReconciliationRequirement(cwd);
1432
1595
  if (!requirement)
1433
1596
  return null;
1597
+ if (requirement.workflow === "autoresearch-goal" && reportsAutoresearchGoalObjectiveMismatch(lastAssistantMessage)) {
1598
+ return null;
1599
+ }
1434
1600
  const systemMessage = [
1435
1601
  `OMX ${requirement.workflow} requires get_goal snapshot reconciliation before completion; call get_goal and pass --codex-goal-json to ${requirement.command}.`,
1436
1602
  requirement.remediation,
@@ -1533,7 +1699,7 @@ function readPayloadSessionId(payload) {
1533
1699
  return safeString(payload.session_id ?? payload.sessionId).trim();
1534
1700
  }
1535
1701
  function readPayloadThreadId(payload) {
1536
- return safeString(payload.thread_id ?? payload.threadId).trim();
1702
+ return safeString(payload.owner_codex_thread_id ?? payload.thread_id ?? payload.threadId).trim();
1537
1703
  }
1538
1704
  function readPayloadTurnId(payload) {
1539
1705
  return safeString(payload.turn_id ?? payload.turnId).trim();
@@ -1608,6 +1774,8 @@ async function readBlockingSkillForStop(cwd, stateDir, sessionId, threadId, requ
1608
1774
  const modeSnapshot = getRunContinuationSnapshot(modeState);
1609
1775
  if (modeSnapshot?.terminal === true)
1610
1776
  continue;
1777
+ if (await shouldIgnoreSessionSkillBlockerForCanonicalInactiveRoot(cwd, stateDir, skill, sessionId, threadId))
1778
+ continue;
1611
1779
  const phase = formatPhase(modeState.current_phase, formatPhase(visibleEntries.find((entry) => entry.skill === skill)?.phase, "planning"));
1612
1780
  if (TERMINAL_MODE_PHASES.has(phase.toLowerCase()) || phase === "completing") {
1613
1781
  continue;
@@ -1648,6 +1816,38 @@ function isTerminalOrInactiveModeState(state) {
1648
1816
  const phase = safeString(state.current_phase ?? state.currentPhase).trim().toLowerCase();
1649
1817
  return phase !== "" && TERMINAL_MODE_PHASES.has(phase);
1650
1818
  }
1819
+ function rootSkillStateHasNoActiveSkillForStopContext(rootState, skill, sessionId, threadId) {
1820
+ if (!rootState)
1821
+ return false;
1822
+ return !listActiveSkills(rootState).some((entry) => (entry.skill === skill
1823
+ && matchesSkillStopContext(entry, rootState, sessionId, threadId)));
1824
+ }
1825
+ function rootModeStateIsCanonicalForStopContext(state, cwd, sessionId, threadId) {
1826
+ if (!modeStateMatchesSkillStopContext(state, cwd, sessionId))
1827
+ return false;
1828
+ const stateSessionId = safeString(state.owner_omx_session_id
1829
+ ?? state.session_id
1830
+ ?? state.codex_session_id
1831
+ ?? state.owner_codex_session_id).trim();
1832
+ if (sessionId && stateSessionId !== sessionId)
1833
+ return false;
1834
+ const stateThreadId = safeString(state.owner_codex_thread_id ?? state.thread_id).trim();
1835
+ if (threadId && stateThreadId && stateThreadId !== threadId)
1836
+ return false;
1837
+ return true;
1838
+ }
1839
+ async function shouldIgnoreSessionSkillBlockerForCanonicalInactiveRoot(cwd, stateDir, skill, sessionId, threadId) {
1840
+ const rootModeState = await readJsonIfExists(join(stateDir, `${skill}-state.json`));
1841
+ if (!rootModeState)
1842
+ return false;
1843
+ if (!rootModeStateIsCanonicalForStopContext(rootModeState, cwd, sessionId, threadId))
1844
+ return false;
1845
+ if (!isTerminalOrInactiveModeState(rootModeState))
1846
+ return false;
1847
+ const { rootPath } = getSkillActiveStatePathsForStateDir(stateDir);
1848
+ const rootSkillState = await readSkillActiveState(rootPath);
1849
+ return rootSkillStateHasNoActiveSkillForStopContext(rootSkillState, skill, sessionId, threadId);
1850
+ }
1651
1851
  async function readSessionScopedModeStateForRootSkill(cwd, stateDir, skill, sessionIds) {
1652
1852
  for (const sessionId of sessionIds) {
1653
1853
  const state = await readStopSessionPinnedState(`${skill}-state.json`, cwd, sessionId, stateDir);