oh-my-codex 0.18.15 → 0.18.16

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 (42) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/dist/cli/__tests__/doctor-artifact-ownership.test.js +63 -0
  4. package/dist/cli/__tests__/doctor-artifact-ownership.test.js.map +1 -1
  5. package/dist/cli/__tests__/session-search-help.test.js +3 -1
  6. package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
  7. package/dist/cli/__tests__/session-search.test.js +55 -1
  8. package/dist/cli/__tests__/session-search.test.js.map +1 -1
  9. package/dist/cli/doctor.d.ts.map +1 -1
  10. package/dist/cli/doctor.js +6 -2
  11. package/dist/cli/doctor.js.map +1 -1
  12. package/dist/cli/index.d.ts +1 -1
  13. package/dist/cli/index.d.ts.map +1 -1
  14. package/dist/cli/index.js +1 -1
  15. package/dist/cli/session-search.d.ts +6 -0
  16. package/dist/cli/session-search.d.ts.map +1 -1
  17. package/dist/cli/session-search.js +104 -4
  18. package/dist/cli/session-search.js.map +1 -1
  19. package/dist/hud/__tests__/state.test.js +121 -0
  20. package/dist/hud/__tests__/state.test.js.map +1 -1
  21. package/dist/hud/state.d.ts.map +1 -1
  22. package/dist/hud/state.js +33 -2
  23. package/dist/hud/state.js.map +1 -1
  24. package/dist/hud/types.d.ts +1 -1
  25. package/dist/hud/types.d.ts.map +1 -1
  26. package/dist/scripts/__tests__/codex-native-hook.test.js +184 -0
  27. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  28. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  29. package/dist/scripts/codex-native-hook.js +159 -1
  30. package/dist/scripts/codex-native-hook.js.map +1 -1
  31. package/dist/session-history/__tests__/friction.test.d.ts +2 -0
  32. package/dist/session-history/__tests__/friction.test.d.ts.map +1 -0
  33. package/dist/session-history/__tests__/friction.test.js +107 -0
  34. package/dist/session-history/__tests__/friction.test.js.map +1 -0
  35. package/dist/session-history/friction.d.ts +68 -0
  36. package/dist/session-history/friction.d.ts.map +1 -0
  37. package/dist/session-history/friction.js +365 -0
  38. package/dist/session-history/friction.js.map +1 -0
  39. package/package.json +1 -1
  40. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  41. package/src/scripts/__tests__/codex-native-hook.test.ts +224 -0
  42. package/src/scripts/codex-native-hook.ts +175 -1
@@ -2544,6 +2544,230 @@ describe("codex native hook dispatch", () => {
2544
2544
  }
2545
2545
  });
2546
2546
 
2547
+ it("allows same-session/current-thread Ralph Stop continuation", async () => {
2548
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralph-stop-current-session-"));
2549
+ try {
2550
+ const stateDir = join(cwd, ".omx", "state");
2551
+ await writeNativeMappedSessionState(cwd, stateDir, "omx-current-ralph", "codex-current-ralph");
2552
+ await writeJson(join(stateDir, "sessions", "omx-current-ralph", "ralph-state.json"), {
2553
+ active: true,
2554
+ mode: "ralph",
2555
+ current_phase: "executing",
2556
+ owner_omx_session_id: "omx-current-ralph",
2557
+ owner_codex_session_id: "codex-current-ralph",
2558
+ owner_codex_thread_id: "thread-current-ralph",
2559
+ task_description: "Finish issue 2974 stale Ralph Stop-hook guard",
2560
+ });
2561
+
2562
+ const result = await dispatchCodexNativeHook(
2563
+ {
2564
+ hook_event_name: "Stop",
2565
+ cwd,
2566
+ session_id: "codex-current-ralph",
2567
+ thread_id: "thread-current-ralph",
2568
+ last_user_message: "continue the current ralph issue 2974 task",
2569
+ },
2570
+ { cwd },
2571
+ );
2572
+
2573
+ assert.equal(result.outputJson?.decision, "block");
2574
+ assert.equal(result.outputJson?.stopReason, "ralph_executing");
2575
+ assert.match(String(result.outputJson?.systemMessage), /Ralph is still active/);
2576
+ } finally {
2577
+ await rm(cwd, { recursive: true, force: true });
2578
+ }
2579
+ });
2580
+
2581
+ it("does not resume stale different-session global Ralph Stop state", async () => {
2582
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralph-stop-stale-global-"));
2583
+ try {
2584
+ await writeJson(join(cwd, ".omx", "state", "ralph-state.json"), {
2585
+ active: true,
2586
+ mode: "ralph",
2587
+ current_phase: "executing",
2588
+ owner_codex_session_id: "codex-old-ralph",
2589
+ owner_codex_thread_id: "thread-old-ralph",
2590
+ task_description: "Finish issue 2974 stale Ralph Stop-hook guard",
2591
+ });
2592
+
2593
+ const result = await dispatchCodexNativeHook(
2594
+ {
2595
+ hook_event_name: "Stop",
2596
+ cwd,
2597
+ thread_id: "thread-new-ralph",
2598
+ last_user_message: "continue the current ralph issue 2974 task",
2599
+ },
2600
+ { cwd },
2601
+ );
2602
+
2603
+ assert.equal(result.outputJson?.stopReason, undefined);
2604
+ assert.notEqual(result.outputJson?.decision, "block");
2605
+ } finally {
2606
+ await rm(cwd, { recursive: true, force: true });
2607
+ }
2608
+ });
2609
+
2610
+ it("does not resume global Ralph Stop state for low-overlap unrelated tasks", async () => {
2611
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralph-stop-low-overlap-"));
2612
+ try {
2613
+ await writeJson(join(cwd, ".omx", "state", "ralph-state.json"), {
2614
+ active: true,
2615
+ mode: "ralph",
2616
+ current_phase: "executing",
2617
+ task_description: "Refactor documentation navigation and README examples",
2618
+ });
2619
+
2620
+ const result = await dispatchCodexNativeHook(
2621
+ {
2622
+ hook_event_name: "Stop",
2623
+ cwd,
2624
+ last_user_message: "continue auditing billing webhooks and payment retries",
2625
+ },
2626
+ { cwd },
2627
+ );
2628
+
2629
+ assert.equal(result.outputJson?.stopReason, undefined);
2630
+ assert.notEqual(result.outputJson?.decision, "block");
2631
+ } finally {
2632
+ await rm(cwd, { recursive: true, force: true });
2633
+ }
2634
+ });
2635
+
2636
+ it("allows explicit current user continuation for live-risk Ralph tasks when owner context is current", async () => {
2637
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralph-stop-live-current-"));
2638
+ try {
2639
+ await writeJson(join(cwd, ".omx", "state", "ralph-state.json"), {
2640
+ active: true,
2641
+ mode: "ralph",
2642
+ current_phase: "executing",
2643
+ owner_codex_thread_id: "thread-live-ralph",
2644
+ task_description: "Deploy billing migration rollback guard for production payments",
2645
+ });
2646
+
2647
+ const result = await dispatchCodexNativeHook(
2648
+ {
2649
+ hook_event_name: "Stop",
2650
+ cwd,
2651
+ thread_id: "thread-live-ralph",
2652
+ last_user_message: "continue the current ralph deploy billing migration rollback guard",
2653
+ },
2654
+ { cwd },
2655
+ );
2656
+
2657
+ assert.equal(result.outputJson?.decision, "block");
2658
+ assert.equal(result.outputJson?.stopReason, "ralph_executing");
2659
+ } finally {
2660
+ await rm(cwd, { recursive: true, force: true });
2661
+ }
2662
+ });
2663
+
2664
+ it("allows current-owner global Ralph Stop continuation with generic current-task wording", async () => {
2665
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralph-stop-current-owner-generic-"));
2666
+ try {
2667
+ await writeJson(join(cwd, ".omx", "state", "ralph-state.json"), {
2668
+ active: true,
2669
+ mode: "ralph",
2670
+ current_phase: "executing",
2671
+ owner_codex_thread_id: "thread-current-owner-generic",
2672
+ task_description: "Refactor documentation navigation and README examples",
2673
+ });
2674
+
2675
+ const result = await dispatchCodexNativeHook(
2676
+ {
2677
+ hook_event_name: "Stop",
2678
+ cwd,
2679
+ thread_id: "thread-current-owner-generic",
2680
+ last_user_message: "continue the current ralph task",
2681
+ },
2682
+ { cwd },
2683
+ );
2684
+
2685
+ assert.equal(result.outputJson?.decision, "block");
2686
+ assert.equal(result.outputJson?.stopReason, "ralph_executing");
2687
+ } finally {
2688
+ await rm(cwd, { recursive: true, force: true });
2689
+ }
2690
+ });
2691
+
2692
+ it("allows same-thread global Ralph Stop continuation without payload text for non-live tasks", async () => {
2693
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralph-stop-same-thread-no-text-"));
2694
+ try {
2695
+ await writeJson(join(cwd, ".omx", "state", "ralph-state.json"), {
2696
+ active: true,
2697
+ mode: "ralph",
2698
+ current_phase: "executing",
2699
+ owner_codex_thread_id: "thread-same-thread-no-text",
2700
+ task_description: "Refactor documentation navigation and README examples",
2701
+ });
2702
+
2703
+ const result = await dispatchCodexNativeHook(
2704
+ {
2705
+ hook_event_name: "Stop",
2706
+ cwd,
2707
+ thread_id: "thread-same-thread-no-text",
2708
+ },
2709
+ { cwd },
2710
+ );
2711
+
2712
+ assert.equal(result.outputJson?.decision, "block");
2713
+ assert.equal(result.outputJson?.stopReason, "ralph_executing");
2714
+ } finally {
2715
+ await rm(cwd, { recursive: true, force: true });
2716
+ }
2717
+ });
2718
+
2719
+ it("blocks no-text global Ralph Stop continuation for live-risk tasks", async () => {
2720
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralph-stop-live-no-text-"));
2721
+ try {
2722
+ await writeJson(join(cwd, ".omx", "state", "ralph-state.json"), {
2723
+ active: true,
2724
+ mode: "ralph",
2725
+ current_phase: "executing",
2726
+ owner_codex_thread_id: "thread-live-no-text",
2727
+ task_description: "Deploy billing migration rollback guard for production payments",
2728
+ });
2729
+
2730
+ const result = await dispatchCodexNativeHook(
2731
+ {
2732
+ hook_event_name: "Stop",
2733
+ cwd,
2734
+ thread_id: "thread-live-no-text",
2735
+ },
2736
+ { cwd },
2737
+ );
2738
+
2739
+ assert.equal(result.outputJson?.stopReason, undefined);
2740
+ assert.notEqual(result.outputJson?.decision, "block");
2741
+ } finally {
2742
+ await rm(cwd, { recursive: true, force: true });
2743
+ }
2744
+ });
2745
+
2746
+ it("blocks no-text global Ralph Stop continuation for issue 2974 operational live-risk tasks", async () => {
2747
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralph-stop-issue-2974-live-no-text-"));
2748
+ try {
2749
+ await writeJson(join(cwd, ".omx", "state", "ralph-state.json"), {
2750
+ active: true,
2751
+ mode: "ralph",
2752
+ current_phase: "executing",
2753
+ task_description: "Implement Telegram assistant GPT switch plan on VPS service with cron restart send notify workflow",
2754
+ });
2755
+
2756
+ const result = await dispatchCodexNativeHook(
2757
+ {
2758
+ hook_event_name: "Stop",
2759
+ cwd,
2760
+ },
2761
+ { cwd },
2762
+ );
2763
+
2764
+ assert.equal(result.outputJson?.stopReason, undefined);
2765
+ assert.notEqual(result.outputJson?.decision, "block");
2766
+ } finally {
2767
+ await rm(cwd, { recursive: true, force: true });
2768
+ }
2769
+ });
2770
+
2547
2771
  it("resolves the Codex owner from ancestry without mistaking codex-native-hook wrappers for Codex", () => {
2548
2772
  const commands = new Map<number, string>([
2549
2773
  [2100, 'sh -c node "/repo/dist/scripts/codex-native-hook.js"'],
@@ -178,6 +178,37 @@ const SHORT_FOLLOWUP_PRIORITY_PATTERNS = [
178
178
  /(?:按照|按|基于)(?:这个|上述|当前)?(?:plan|计划|方案)/u,
179
179
  /\b(?:follow up|latest request|this turn|current turn|newest request)\b/i,
180
180
  ] as const;
181
+ const RALPH_CONTINUATION_INTENT_PATTERNS = [
182
+ /\b(?:continue|resume|keep going|carry on|proceed|finish|complete)\b/i,
183
+ /\b(?:same|current|active|that|this)\s+(?:ralph|task|work|job|workflow)\b/i,
184
+ /\b(?:ralph)\b.{0,40}\b(?:continue|resume|finish|complete)\b/i,
185
+ /\b(?:continue|resume|finish|complete)\b.{0,40}\b(?:ralph)\b/i,
186
+ ] as const;
187
+ const RALPH_LIVE_RISK_PATTERNS = [
188
+ /\b(?:prod|production|live|customer|user\s+data|billing|payment|credential|secret|token|key)\b/i,
189
+ /\b(?:deploy|release|publish|merge|push|delete|remove|drop|destroy|migrate|migration)\b/i,
190
+ /\b(?:database|db|terraform|kubectl|kubernetes|aws|gcp|azure|external|destructive)\b/i,
191
+ /\b(?:telegram|vps|service|restart|send|notify|notification|notifications|cron)\b/i,
192
+ ] as const;
193
+ const RALPH_TASK_TEXT_FIELDS = [
194
+ "task_description",
195
+ "taskDescription",
196
+ "objective",
197
+ "task",
198
+ "prompt",
199
+ "initial_prompt",
200
+ "initialPrompt",
201
+ "user_prompt",
202
+ "userPrompt",
203
+ "last_user_message",
204
+ "lastUserMessage",
205
+ "task_slug",
206
+ ] as const;
207
+ const RALPH_INTENT_STOPWORDS = new Set([
208
+ "a", "an", "and", "are", "as", "at", "be", "by", "for", "from", "in", "is", "it",
209
+ "of", "on", "or", "that", "the", "this", "to", "with", "you", "your", "task", "work",
210
+ "ralph", "continue", "resume", "finish", "complete", "fix", "issue",
211
+ ]);
181
212
  const MAX_SESSION_META_LINE_BYTES = 256 * 1024;
182
213
 
183
214
  function safeString(value: unknown): string {
@@ -795,6 +826,7 @@ interface RalphStopOwnershipContext {
795
826
  threadId: string;
796
827
  currentNativeSessionId: string;
797
828
  tmuxPaneId: string;
829
+ payload?: CodexHookPayload;
798
830
  }
799
831
 
800
832
  function isRalphStartingPhase(state: Record<string, unknown>): boolean {
@@ -851,6 +883,135 @@ function hasValue(values: string[], value: string): boolean {
851
883
  return value !== "" && values.some((candidate) => candidate === value);
852
884
  }
853
885
 
886
+ function hasPositiveRalphStopOwnerMatch(
887
+ state: Record<string, unknown>,
888
+ context: RalphStopOwnershipContext,
889
+ ): boolean {
890
+ const ownerOmxSessionId = safeString(state.owner_omx_session_id).trim();
891
+ if (ownerOmxSessionId && ownerOmxSessionId === context.sessionId) return true;
892
+
893
+ const stateSessionId = safeString(state.session_id).trim();
894
+ if (!ownerOmxSessionId && stateSessionId && stateSessionId === context.sessionId) return true;
895
+
896
+ const codexOwnerSessionId = safeString(state.owner_codex_session_id).trim();
897
+ if (codexOwnerSessionId) {
898
+ const stopCodexSessionIds = [
899
+ context.payloadSessionId,
900
+ context.currentNativeSessionId,
901
+ context.sessionId,
902
+ ].filter(Boolean);
903
+ if (hasValue(stopCodexSessionIds, codexOwnerSessionId)) return true;
904
+ }
905
+
906
+ const stateThreadId = safeString(state.owner_codex_thread_id ?? state.thread_id).trim();
907
+ if (stateThreadId && context.threadId && stateThreadId === context.threadId) return true;
908
+
909
+ const statePaneId = safeString(state.tmux_pane_id).trim();
910
+ return statePaneId !== "" && context.tmuxPaneId !== "" && statePaneId === context.tmuxPaneId;
911
+ }
912
+
913
+ function textMatchesAny(text: string, patterns: readonly RegExp[]): boolean {
914
+ return patterns.some((pattern) => pattern.test(text));
915
+ }
916
+
917
+ function extractRalphTaskText(state: Record<string, unknown>): string {
918
+ const values: string[] = [];
919
+ for (const field of RALPH_TASK_TEXT_FIELDS) {
920
+ const value = safeString(state[field]).trim();
921
+ if (value) values.push(value);
922
+ }
923
+
924
+ const taskMetadata = state.task_metadata ?? state.taskMetadata;
925
+ if (taskMetadata && typeof taskMetadata === "object") {
926
+ const metadata = taskMetadata as Record<string, unknown>;
927
+ for (const field of RALPH_TASK_TEXT_FIELDS) {
928
+ const value = safeString(metadata[field]).trim();
929
+ if (value) values.push(value);
930
+ }
931
+ }
932
+
933
+ return values.join("\n");
934
+ }
935
+
936
+ function extractRalphStopUserText(payload?: CodexHookPayload): string {
937
+ if (!payload) return "";
938
+ return [
939
+ payload.prompt,
940
+ payload.user_prompt,
941
+ payload.userPrompt,
942
+ payload.input,
943
+ payload.last_user_message,
944
+ payload.lastUserMessage,
945
+ ].map(safeString).filter(Boolean).join("\n");
946
+ }
947
+
948
+ function tokenizeRalphIntentText(text: string): Set<string> {
949
+ const tokens = new Set<string>();
950
+ for (const match of text.toLowerCase().matchAll(/[a-z0-9][a-z0-9_-]{2,}/g)) {
951
+ const token = match[0].replace(/^issue-/, "");
952
+ if (!token || RALPH_INTENT_STOPWORDS.has(token)) continue;
953
+ tokens.add(token);
954
+ }
955
+ return tokens;
956
+ }
957
+
958
+ function countTokenOverlap(left: Set<string>, right: Set<string>): number {
959
+ let overlap = 0;
960
+ for (const token of left) {
961
+ if (right.has(token)) overlap += 1;
962
+ }
963
+ return overlap;
964
+ }
965
+
966
+ function hasCurrentRalphTaskOverlap(state: Record<string, unknown>, userText: string): boolean {
967
+ const taskText = extractRalphTaskText(state);
968
+ const taskTokens = tokenizeRalphIntentText(taskText);
969
+ const userTokens = tokenizeRalphIntentText(userText);
970
+ if (taskTokens.size === 0) return false;
971
+ if (userTokens.size === 0) return false;
972
+
973
+ const overlap = countTokenOverlap(taskTokens, userTokens);
974
+ const smallerSide = Math.min(taskTokens.size, userTokens.size);
975
+ return overlap >= 2 || (smallerSide <= 2 && overlap >= 1);
976
+ }
977
+
978
+ function hasMeaningfulRalphTaskText(state: Record<string, unknown>): boolean {
979
+ return tokenizeRalphIntentText(extractRalphTaskText(state)).size > 0;
980
+ }
981
+
982
+ function isRalphLiveRiskContinuation(state: Record<string, unknown>, userText: string): boolean {
983
+ return textMatchesAny(`${extractRalphTaskText(state)}\n${userText}`, RALPH_LIVE_RISK_PATTERNS);
984
+ }
985
+
986
+ function shouldAllowGlobalRalphStopContinuation(
987
+ state: Record<string, unknown>,
988
+ context: RalphStopOwnershipContext,
989
+ ): boolean {
990
+ const userText = extractRalphStopUserText(context.payload);
991
+ const hasContinuationIntent = textMatchesAny(userText, RALPH_CONTINUATION_INTENT_PATTERNS);
992
+ const hasTaskOverlap = hasCurrentRalphTaskOverlap(state, userText);
993
+ const hasTaskText = hasMeaningfulRalphTaskText(state);
994
+ const hasUserTaskText = tokenizeRalphIntentText(userText).size > 0;
995
+ const hasPositiveOwnerMatch = hasPositiveRalphStopOwnerMatch(state, context);
996
+
997
+ if (!activeRalphStateMatchesStopOwner(state, context)) return false;
998
+ if (!userText.trim()) {
999
+ if (isRalphLiveRiskContinuation(state, userText)) return false;
1000
+ return hasPositiveOwnerMatch || !hasRalphOwnerHint(state);
1001
+ }
1002
+ if (isRalphLiveRiskContinuation(state, userText)) {
1003
+ return hasContinuationIntent && (hasPositiveOwnerMatch || hasTaskOverlap);
1004
+ }
1005
+ if (hasPositiveOwnerMatch && hasContinuationIntent) {
1006
+ return true;
1007
+ }
1008
+ if (hasTaskText && hasUserTaskText) {
1009
+ return hasTaskOverlap;
1010
+ }
1011
+
1012
+ return hasContinuationIntent || hasTaskOverlap;
1013
+ }
1014
+
854
1015
  function activeRalphStateMatchesStopOwner(
855
1016
  state: Record<string, unknown>,
856
1017
  context: RalphStopOwnershipContext,
@@ -1097,6 +1258,7 @@ async function readActiveRalphState(
1097
1258
  payloadSessionId?: string;
1098
1259
  threadId?: string;
1099
1260
  tmuxPaneId?: string;
1261
+ payload?: CodexHookPayload;
1100
1262
  },
1101
1263
  ): Promise<ActiveRalphStopState | null> {
1102
1264
  const [rawSessionInfo, usableSessionInfo] = await Promise.all([
@@ -1183,7 +1345,18 @@ async function readActiveRalphState(
1183
1345
 
1184
1346
  const directPath = join(stateDir, "ralph-state.json");
1185
1347
  const direct = await readJsonIfExists(directPath);
1186
- if (direct?.active === true && shouldContinueRun(direct)) {
1348
+ if (
1349
+ direct?.active === true
1350
+ && shouldContinueRun(direct)
1351
+ && shouldAllowGlobalRalphStopContinuation(direct, {
1352
+ sessionId: safeString(ownerContext?.payloadSessionId).trim(),
1353
+ payloadSessionId: safeString(ownerContext?.payloadSessionId).trim(),
1354
+ threadId: safeString(ownerContext?.threadId).trim(),
1355
+ currentNativeSessionId,
1356
+ tmuxPaneId: safeString(ownerContext?.tmuxPaneId).trim(),
1357
+ payload: ownerContext?.payload,
1358
+ })
1359
+ ) {
1187
1360
  return { state: direct, path: directPath };
1188
1361
  }
1189
1362
 
@@ -4547,6 +4720,7 @@ async function buildStopHookOutput(
4547
4720
  payloadSessionId: sessionId,
4548
4721
  threadId,
4549
4722
  tmuxPaneId: safeString(process.env.TMUX_PANE).trim(),
4723
+ payload,
4550
4724
  };
4551
4725
  const ralphCompletionAuditBlock = options.skipRalphStopBlock === true
4552
4726
  ? null