oh-my-codex 0.13.0 → 0.13.2

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 (141) hide show
  1. package/Cargo.lock +5 -5
  2. package/Cargo.toml +1 -1
  3. package/README.md +40 -6
  4. package/crates/omx-explore/src/main.rs +221 -10
  5. package/dist/catalog/__tests__/generator.test.js +2 -0
  6. package/dist/catalog/__tests__/generator.test.js.map +1 -1
  7. package/dist/cli/__tests__/index.test.js +150 -1
  8. package/dist/cli/__tests__/index.test.js.map +1 -1
  9. package/dist/cli/__tests__/setup-skills-overwrite.test.js +41 -3
  10. package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
  11. package/dist/cli/__tests__/update.test.js +25 -1
  12. package/dist/cli/__tests__/update.test.js.map +1 -1
  13. package/dist/cli/index.d.ts +1 -0
  14. package/dist/cli/index.d.ts.map +1 -1
  15. package/dist/cli/index.js +73 -9
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/cli/setup.d.ts.map +1 -1
  18. package/dist/cli/setup.js +15 -0
  19. package/dist/cli/setup.js.map +1 -1
  20. package/dist/cli/update.js +1 -1
  21. package/dist/cli/update.js.map +1 -1
  22. package/dist/hooks/__tests__/agents-overlay.test.js +20 -2
  23. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  24. package/dist/hooks/__tests__/analyze-routing-contract.test.d.ts +2 -0
  25. package/dist/hooks/__tests__/analyze-routing-contract.test.d.ts.map +1 -0
  26. package/dist/hooks/__tests__/analyze-routing-contract.test.js +36 -0
  27. package/dist/hooks/__tests__/analyze-routing-contract.test.js.map +1 -0
  28. package/dist/hooks/__tests__/analyze-skill-contract.test.d.ts +2 -0
  29. package/dist/hooks/__tests__/analyze-skill-contract.test.d.ts.map +1 -0
  30. package/dist/hooks/__tests__/analyze-skill-contract.test.js +48 -0
  31. package/dist/hooks/__tests__/analyze-skill-contract.test.js.map +1 -0
  32. package/dist/hooks/__tests__/keyword-detector.test.js +32 -0
  33. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  34. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +185 -8
  35. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  36. package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js +26 -0
  37. package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js.map +1 -1
  38. package/dist/hooks/__tests__/notify-hook-session-scope.test.js +44 -0
  39. package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
  40. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +126 -0
  41. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  42. package/dist/hooks/__tests__/session.test.js +21 -0
  43. package/dist/hooks/__tests__/session.test.js.map +1 -1
  44. package/dist/hooks/agents-overlay.d.ts.map +1 -1
  45. package/dist/hooks/agents-overlay.js +9 -0
  46. package/dist/hooks/agents-overlay.js.map +1 -1
  47. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  48. package/dist/hooks/keyword-detector.js +8 -1
  49. package/dist/hooks/keyword-detector.js.map +1 -1
  50. package/dist/hooks/session.d.ts.map +1 -1
  51. package/dist/hooks/session.js +9 -0
  52. package/dist/hooks/session.js.map +1 -1
  53. package/dist/hud/__tests__/state.test.js +55 -0
  54. package/dist/hud/__tests__/state.test.js.map +1 -1
  55. package/dist/hud/state.d.ts.map +1 -1
  56. package/dist/hud/state.js +23 -4
  57. package/dist/hud/state.js.map +1 -1
  58. package/dist/mcp/__tests__/bootstrap.test.js +38 -0
  59. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  60. package/dist/mcp/bootstrap.d.ts +1 -1
  61. package/dist/mcp/bootstrap.d.ts.map +1 -1
  62. package/dist/mcp/bootstrap.js +11 -3
  63. package/dist/mcp/bootstrap.js.map +1 -1
  64. package/dist/notifications/__tests__/reply-listener.test.js +34 -1
  65. package/dist/notifications/__tests__/reply-listener.test.js.map +1 -1
  66. package/dist/notifications/reply-listener.d.ts +1 -0
  67. package/dist/notifications/reply-listener.d.ts.map +1 -1
  68. package/dist/notifications/reply-listener.js +14 -2
  69. package/dist/notifications/reply-listener.js.map +1 -1
  70. package/dist/scripts/__tests__/codex-native-hook.test.js +248 -15
  71. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  72. package/dist/scripts/__tests__/generate-release-body.test.d.ts +2 -0
  73. package/dist/scripts/__tests__/generate-release-body.test.d.ts.map +1 -0
  74. package/dist/scripts/__tests__/generate-release-body.test.js +144 -0
  75. package/dist/scripts/__tests__/generate-release-body.test.js.map +1 -0
  76. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  77. package/dist/scripts/codex-native-hook.js +39 -49
  78. package/dist/scripts/codex-native-hook.js.map +1 -1
  79. package/dist/scripts/generate-release-body.d.ts +34 -0
  80. package/dist/scripts/generate-release-body.d.ts.map +1 -0
  81. package/dist/scripts/generate-release-body.js +249 -0
  82. package/dist/scripts/generate-release-body.js.map +1 -0
  83. package/dist/scripts/notify-fallback-watcher.js +43 -20
  84. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  85. package/dist/scripts/notify-hook/active-team.d.ts.map +1 -1
  86. package/dist/scripts/notify-hook/active-team.js +2 -1
  87. package/dist/scripts/notify-hook/active-team.js.map +1 -1
  88. package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
  89. package/dist/scripts/notify-hook/ralph-session-resume.js +17 -2
  90. package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
  91. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  92. package/dist/scripts/notify-hook/state-io.js +16 -0
  93. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  94. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  95. package/dist/scripts/notify-hook/team-leader-nudge.js +26 -5
  96. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  97. package/dist/scripts/notify-hook.js +1 -7
  98. package/dist/scripts/notify-hook.js.map +1 -1
  99. package/dist/team/__tests__/model-contract.test.js +6 -0
  100. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  101. package/dist/team/__tests__/tmux-session.test.js +1 -1
  102. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  103. package/dist/team/__tests__/worker-runtime-identity.test.d.ts +2 -0
  104. package/dist/team/__tests__/worker-runtime-identity.test.d.ts.map +1 -0
  105. package/dist/team/__tests__/worker-runtime-identity.test.js +250 -0
  106. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -0
  107. package/dist/team/leader-activity.d.ts.map +1 -1
  108. package/dist/team/leader-activity.js +26 -15
  109. package/dist/team/leader-activity.js.map +1 -1
  110. package/dist/team/model-contract.d.ts.map +1 -1
  111. package/dist/team/model-contract.js.map +1 -1
  112. package/dist/team/runtime.d.ts.map +1 -1
  113. package/dist/team/runtime.js +9 -8
  114. package/dist/team/runtime.js.map +1 -1
  115. package/dist/team/scaling.d.ts.map +1 -1
  116. package/dist/team/scaling.js +10 -9
  117. package/dist/team/scaling.js.map +1 -1
  118. package/dist/team/tmux-session.d.ts.map +1 -1
  119. package/dist/team/tmux-session.js +3 -2
  120. package/dist/team/tmux-session.js.map +1 -1
  121. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +3 -0
  122. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
  123. package/dist/wiki/__tests__/slug-nonascii.test.js +11 -5
  124. package/dist/wiki/__tests__/slug-nonascii.test.js.map +1 -1
  125. package/dist/wiki/storage.d.ts.map +1 -1
  126. package/dist/wiki/storage.js +2 -1
  127. package/dist/wiki/storage.js.map +1 -1
  128. package/package.json +3 -1
  129. package/skills/analyze/SKILL.md +101 -134
  130. package/src/scripts/__tests__/codex-native-hook.test.ts +297 -17
  131. package/src/scripts/__tests__/generate-release-body.test.ts +166 -0
  132. package/src/scripts/codex-native-hook.ts +99 -66
  133. package/src/scripts/generate-release-body.ts +295 -0
  134. package/src/scripts/notify-fallback-watcher.ts +44 -21
  135. package/src/scripts/notify-hook/active-team.ts +2 -1
  136. package/src/scripts/notify-hook/ralph-session-resume.ts +17 -2
  137. package/src/scripts/notify-hook/state-io.ts +16 -0
  138. package/src/scripts/notify-hook/team-leader-nudge.ts +24 -4
  139. package/src/scripts/notify-hook.ts +1 -6
  140. package/templates/AGENTS.md +1 -1
  141. package/templates/catalog-manifest.json +2 -4
@@ -34,6 +34,8 @@ import {
34
34
  } from '../subagents/tracker.js';
35
35
  import { listNotifyCanonicalActiveTeams } from './notify-hook/active-team.js';
36
36
  import { sameFilePath } from '../utils/paths.js';
37
+ import { validateSessionId } from '../mcp/state-paths.js';
38
+ import { TEAM_NAME_SAFE_PATTERN } from '../team/contracts.js';
37
39
 
38
40
  function argValue(name: string, fallback = ''): string {
39
41
  const idx = process.argv.indexOf(name);
@@ -50,6 +52,21 @@ function safeString(v: unknown): string {
50
52
  return typeof v === 'string' ? v : '';
51
53
  }
52
54
 
55
+ function normalizeValidSessionId(value: unknown): string {
56
+ const trimmed = safeString(value).trim();
57
+ if (!trimmed) return '';
58
+ try {
59
+ return validateSessionId(trimmed) ?? '';
60
+ } catch {
61
+ return '';
62
+ }
63
+ }
64
+
65
+ function normalizeValidTeamName(value: unknown): string {
66
+ const trimmed = safeString(value).trim();
67
+ return TEAM_NAME_SAFE_PATTERN.test(trimmed) ? trimmed : '';
68
+ }
69
+
53
70
  function parsePositivePid(value: unknown): number | null {
54
71
  const pid = Math.trunc(asNumber(value as string | number | undefined, 0));
55
72
  return pid > 0 ? pid : null;
@@ -523,8 +540,8 @@ async function resolveActiveModeState(mode: string): Promise<ActiveModeResult> {
523
540
  const session = await readSessionState(cwd);
524
541
  if (session?.session_id) {
525
542
  if (isSessionStateAuthoritativeForCwd(session, cwd)) {
526
- currentSessionId = safeString(session.session_id).trim();
527
- currentSessionIsLive = !isSessionStale(session);
543
+ currentSessionId = normalizeValidSessionId(session.session_id);
544
+ currentSessionIsLive = currentSessionId !== '' && !isSessionStale(session);
528
545
  }
529
546
  if (currentSessionId && currentSessionIsLive) {
530
547
  candidateDirs.push(join(stateDir, 'sessions', currentSessionId));
@@ -590,8 +607,8 @@ async function resolveActiveTeamState(): Promise<ActiveTeamResult> {
590
607
  let currentSessionIsLive = false;
591
608
  const session = await readSessionState(cwd);
592
609
  if (session?.session_id) {
593
- currentSessionId = safeString(session.session_id).trim();
594
- currentSessionIsLive = !isSessionStale(session);
610
+ currentSessionId = normalizeValidSessionId(session.session_id);
611
+ currentSessionIsLive = currentSessionId !== '' && !isSessionStale(session);
595
612
  if (currentSessionId && currentSessionIsLive) {
596
613
  candidateDirs.push(join(stateDir, 'sessions', currentSessionId));
597
614
  }
@@ -610,7 +627,7 @@ async function resolveActiveTeamState(): Promise<ActiveTeamResult> {
610
627
  .catch(() => null);
611
628
  if (!parsed || typeof parsed !== 'object' || parsed.active !== true) continue;
612
629
 
613
- const teamName = safeString(parsed.team_name).trim();
630
+ const teamName = normalizeValidTeamName(parsed.team_name);
614
631
  if (!teamName) continue;
615
632
 
616
633
  const teamConfigDir = join(stateDir, 'team', teamName);
@@ -653,7 +670,9 @@ async function resolveActiveTeamState(): Promise<ActiveTeamResult> {
653
670
 
654
671
  const canonicalFallbackTeams = await listNotifyCanonicalActiveTeams(cwd, currentSessionId).catch(() => []);
655
672
  for (const team of canonicalFallbackTeams) {
656
- const teamConfigDir = join(stateDir, 'team', team.teamName);
673
+ const teamName = normalizeValidTeamName(team.teamName);
674
+ if (!teamName) continue;
675
+ const teamConfigDir = join(stateDir, 'team', teamName);
657
676
  const manifestPath = join(teamConfigDir, 'manifest.v2.json');
658
677
  const configPath = join(teamConfigDir, 'config.json');
659
678
  const teamConfigPath = existsSync(manifestPath) ? manifestPath : configPath;
@@ -678,10 +697,10 @@ async function resolveActiveTeamState(): Promise<ActiveTeamResult> {
678
697
  path: team.path,
679
698
  state: {
680
699
  active: true,
681
- team_name: team.teamName,
700
+ team_name: teamName,
682
701
  current_phase: team.phase,
683
702
  },
684
- team_name: team.teamName,
703
+ team_name: teamName,
685
704
  pane_count: paneStatus.paneCount,
686
705
  };
687
706
  }
@@ -755,10 +774,13 @@ async function readRalphSteerLock(path: string): Promise<RalphSteerLockRecord |
755
774
  }
756
775
  }
757
776
 
777
+ const RALPH_STEER_LOCK_MAX_RETRIES = 5;
778
+
758
779
  async function withRalphSteerLock<T>(task: () => Promise<T>): Promise<T | null> {
759
780
  await mkdir(dirname(ralphSteerLockPath), { recursive: true }).catch(() => {});
760
781
 
761
- while (true) {
782
+ let acquired = false;
783
+ for (let attempt = 0; attempt < RALPH_STEER_LOCK_MAX_RETRIES; attempt++) {
762
784
  let handle;
763
785
  try {
764
786
  handle = await open(ralphSteerLockPath, 'wx');
@@ -767,6 +789,7 @@ async function withRalphSteerLock<T>(task: () => Promise<T>): Promise<T | null>
767
789
  acquired_at: new Date().toISOString(),
768
790
  };
769
791
  await handle.writeFile(JSON.stringify(payload, null, 2));
792
+ acquired = true;
770
793
  break;
771
794
  } catch (error) {
772
795
  const code = error !== null && typeof error === 'object' ? (error as NodeJS.ErrnoException).code : '';
@@ -786,6 +809,11 @@ async function withRalphSteerLock<T>(task: () => Promise<T>): Promise<T | null>
786
809
  }
787
810
  }
788
811
 
812
+ if (!acquired) {
813
+ lastRalphContinueSteer.last_reason = 'global_lock_exhausted';
814
+ return null;
815
+ }
816
+
789
817
  try {
790
818
  return await task();
791
819
  } finally {
@@ -847,18 +875,18 @@ async function readRalphProgressGate(
847
875
  return { allow: true, reason: 'progress_stale', progress_at: progressAt, subagent_session_id: subagentSessionId };
848
876
  }
849
877
 
850
- function shouldSkipRalphContinue(now: number, candidateIso: string, startupIso: string): { skip: boolean; reason: string; anchorMs: number; anchorIso: string } {
878
+ function shouldSkipRalphContinue(now: number, candidateIso: string): { skip: boolean; reason: string; anchorMs: number; anchorIso: string } {
851
879
  const sharedMs = parseIsoMillis(candidateIso);
852
880
  const localMs = parseIsoMillis(lastRalphContinueSteer.last_sent_at);
853
- const startupAnchorIso = lastRalphContinueSteer.cooldown_anchor_at || startupIso;
881
+ const startupAnchorIso = lastRalphContinueSteer.cooldown_anchor_at;
854
882
  const startupAnchorMs = parseIsoMillis(startupAnchorIso);
855
- const startupCooldown = sharedMs === null && localMs === null;
856
- const anchorMs = sharedMs ?? localMs ?? startupAnchorMs ?? startedAt;
883
+ const startupCooldown = sharedMs === null && localMs === null && startupAnchorMs !== null;
884
+ const anchorMs = sharedMs ?? localMs ?? startupAnchorMs ?? 0;
857
885
  const anchorIso = sharedMs !== null
858
886
  ? candidateIso
859
887
  : (localMs !== null ? lastRalphContinueSteer.last_sent_at : startupAnchorIso);
860
888
  return {
861
- skip: now - anchorMs < RALPH_CONTINUE_CADENCE_MS,
889
+ skip: anchorMs > 0 && now - anchorMs < RALPH_CONTINUE_CADENCE_MS,
862
890
  reason: startupCooldown ? 'startup_cooldown' : (sharedMs !== null ? 'global_cooldown' : 'cooldown'),
863
891
  anchorMs,
864
892
  anchorIso,
@@ -1004,7 +1032,6 @@ async function writePidFileRecord(): Promise<void> {
1004
1032
  async function runRalphContinueSteerTick(): Promise<void> {
1005
1033
  const now = Date.now();
1006
1034
  const nowIso = new Date(now).toISOString();
1007
- const startupIso = new Date(startedAt).toISOString();
1008
1035
  const activeRalph = await resolveActiveRalphState();
1009
1036
  const activePaneId = safeString(activeRalph.state?.tmux_pane_id).trim();
1010
1037
  lastRalphContinueSteer = {
@@ -1030,13 +1057,9 @@ async function runRalphContinueSteerTick(): Promise<void> {
1030
1057
  return;
1031
1058
  }
1032
1059
 
1033
- if (parseIsoMillis(lastRalphContinueSteer.last_sent_at) === null && parseIsoMillis(lastRalphContinueSteer.cooldown_anchor_at) === null) {
1034
- lastRalphContinueSteer.cooldown_anchor_at = startupIso;
1035
- }
1036
-
1037
1060
  const sharedBeforeLock = await readRalphSteerTimestamp();
1038
1061
  lastRalphContinueSteer.shared_last_sent_at = sharedBeforeLock;
1039
- const initialCooldown = shouldSkipRalphContinue(now, sharedBeforeLock, startupIso);
1062
+ const initialCooldown = shouldSkipRalphContinue(now, sharedBeforeLock);
1040
1063
  if (initialCooldown.skip) {
1041
1064
  lastRalphContinueSteer.last_reason = initialCooldown.reason;
1042
1065
  if (!sharedBeforeLock && initialCooldown.reason === 'startup_cooldown') {
@@ -1048,7 +1071,7 @@ async function runRalphContinueSteerTick(): Promise<void> {
1048
1071
  const outcome = await withRalphSteerLock(async () => {
1049
1072
  const sharedLastSentAt = await readRalphSteerTimestamp();
1050
1073
  lastRalphContinueSteer.shared_last_sent_at = sharedLastSentAt;
1051
- const cooldown = shouldSkipRalphContinue(Date.now(), sharedLastSentAt, startupIso);
1074
+ const cooldown = shouldSkipRalphContinue(Date.now(), sharedLastSentAt);
1052
1075
  if (cooldown.skip) {
1053
1076
  lastRalphContinueSteer.last_reason = cooldown.reason;
1054
1077
  if (!sharedLastSentAt && cooldown.reason === 'startup_cooldown') {
@@ -3,6 +3,7 @@ import { readdir } from 'fs/promises';
3
3
  import { join } from 'path';
4
4
  import { readTeamManifestV2, readTeamPhase } from '../../team/state.js';
5
5
  import { resolveCanonicalTeamStateRoot } from '../../team/state-root.js';
6
+ import { TEAM_NAME_SAFE_PATTERN } from '../../team/contracts.js';
6
7
  import { isTerminalPhase, safeString } from './utils.js';
7
8
 
8
9
  export interface NotifyCanonicalActiveTeam {
@@ -28,7 +29,7 @@ export async function listNotifyCanonicalActiveTeams(
28
29
  for (const entry of entries) {
29
30
  if (!entry.isDirectory()) continue;
30
31
  const teamName = entry.name.trim();
31
- if (!teamName) continue;
32
+ if (!teamName || !TEAM_NAME_SAFE_PATTERN.test(teamName)) continue;
32
33
 
33
34
  const [manifest, phaseState] = await Promise.all([
34
35
  readTeamManifestV2(teamName, cwd),
@@ -128,7 +128,22 @@ function isActiveRalphCandidate(state: Record<string, unknown> | null): state is
128
128
  return state.active === true && !isTerminalRalphPhase(state.current_phase);
129
129
  }
130
130
 
131
- async function readCurrentOmxSessionId(stateDir: string): Promise<string> {
131
+ function readSessionIdFromEnvironment(env: NodeJS.ProcessEnv = process.env): string {
132
+ const candidates = [env.OMX_SESSION_ID, env.CODEX_SESSION_ID, env.SESSION_ID];
133
+ for (const candidate of candidates) {
134
+ const sessionId = safeString(candidate).trim();
135
+ if (SESSION_ID_PATTERN.test(sessionId)) return sessionId;
136
+ }
137
+ return '';
138
+ }
139
+
140
+ async function readCurrentOmxSessionId(stateDir: string, env: NodeJS.ProcessEnv = process.env): Promise<string> {
141
+ const envSessionId = readSessionIdFromEnvironment(env);
142
+ if (envSessionId) {
143
+ const envScopedDir = join(stateDir, 'sessions', envSessionId);
144
+ if (existsSync(envScopedDir)) return envSessionId;
145
+ }
146
+
132
147
  const session = await readUsableSessionState(resolve(stateDir, '..', '..'));
133
148
  const sessionId = safeString(session?.session_id).trim();
134
149
  return SESSION_ID_PATTERN.test(sessionId) ? sessionId : '';
@@ -194,7 +209,7 @@ export async function reconcileRalphSessionResume({
194
209
  const lockedResult = await withRalphResumeLock(stateDir, async () => {
195
210
  await hooks?.afterLockAcquired?.();
196
211
 
197
- const currentOmxSessionId = await readCurrentOmxSessionId(stateDir);
212
+ const currentOmxSessionId = await readCurrentOmxSessionId(stateDir, env);
198
213
  if (!currentOmxSessionId) {
199
214
  return {
200
215
  currentOmxSessionId: '',
@@ -25,7 +25,23 @@ function isSafeStateFileName(fileName: string): boolean {
25
25
  && !fileName.includes('\\');
26
26
  }
27
27
 
28
+ function readSessionIdFromEnvironment(env: NodeJS.ProcessEnv = process.env): string | undefined {
29
+ const candidates = [env.OMX_SESSION_ID, env.CODEX_SESSION_ID, env.SESSION_ID];
30
+ for (const candidate of candidates) {
31
+ const sessionId = safeString(candidate).trim();
32
+ if (!SESSION_ID_PATTERN.test(sessionId)) continue;
33
+ return sessionId;
34
+ }
35
+ return undefined;
36
+ }
37
+
28
38
  export async function readCurrentSessionId(baseStateDir: string): Promise<string | undefined> {
39
+ const envSessionId = readSessionIdFromEnvironment();
40
+ if (envSessionId) {
41
+ const envScopedDir = join(baseStateDir, 'sessions', envSessionId);
42
+ if (existsSync(envScopedDir)) return envSessionId;
43
+ }
44
+
29
45
  const cwd = resolve(baseStateDir, '..', '..');
30
46
  const session = await readUsableSessionState(cwd);
31
47
  const sessionId = safeString(session?.session_id);
@@ -23,6 +23,8 @@ import { isLeaderRuntimeStale } from '../../team/leader-activity.js';
23
23
  import { appendTeamDeliveryLog } from '../../team/delivery-log.js';
24
24
  import { writeTeamLeaderAttention } from '../../team/state.js';
25
25
  import { readLatestTeamProgressEvidenceMs } from '../../team/progress-evidence.js';
26
+ import { validateSessionId } from '../../mcp/state-paths.js';
27
+ import { TEAM_NAME_SAFE_PATTERN } from '../../team/contracts.js';
26
28
  const LEADER_PANE_MISSING_NO_INJECTION_REASON = 'leader_pane_missing_no_injection';
27
29
  const LEADER_PANE_SHELL_NO_INJECTION_REASON = 'leader_pane_shell_no_injection';
28
30
  const LEADER_PANE_SAME_CLASSIFIED_STATE_SUPPRESSED_REASON = 'pane_already_shows_same_classified_state';
@@ -34,6 +36,21 @@ const ACK_LIKE_PATTERNS = [
34
36
  /^(?:on it|will do|i(?:'|')ll do it|working on it)[.!]*$/i,
35
37
  ];
36
38
 
39
+ function normalizeValidSessionId(value) {
40
+ const trimmed = safeString(value).trim();
41
+ if (!trimmed) return '';
42
+ try {
43
+ return validateSessionId(trimmed) ?? '';
44
+ } catch {
45
+ return '';
46
+ }
47
+ }
48
+
49
+ function normalizeValidTeamName(value) {
50
+ const trimmed = safeString(value).trim();
51
+ return TEAM_NAME_SAFE_PATTERN.test(trimmed) ? trimmed : '';
52
+ }
53
+
37
54
  export function resolveLeaderNudgeIntervalMs() {
38
55
  const raw = safeString(process.env.OMX_TEAM_LEADER_NUDGE_MS || '');
39
56
  const parsed = asNumber(raw);
@@ -251,8 +268,9 @@ async function resolveCurrentSessionId(stateDir) {
251
268
  || process.env.SESSION_ID
252
269
  || '',
253
270
  ).trim();
254
- if (fromEnv) return fromEnv;
255
- return safeString((await readUsableSessionState(resolve(stateDir, '..', '..')))?.session_id).trim();
271
+ const envSessionId = normalizeValidSessionId(fromEnv);
272
+ if (envSessionId) return envSessionId;
273
+ return normalizeValidSessionId((await readUsableSessionState(resolve(stateDir, '..', '..')))?.session_id);
256
274
  }
257
275
 
258
276
  async function readWorkerStatusSnapshot(stateDir, teamName, workerName) {
@@ -601,7 +619,7 @@ export async function maybeNudgeTeamLeader({
601
619
  if (!existsSync(teamStatePath)) continue;
602
620
  const parsed = JSON.parse(await readFile(teamStatePath, 'utf-8'));
603
621
  if (!parsed) continue;
604
- const teamName = safeString(parsed.team_name || '').trim();
622
+ const teamName = normalizeValidTeamName(parsed.team_name || '');
605
623
  if (!teamName) continue;
606
624
 
607
625
  const phaseSnapshot = await readTeamPhaseSnapshot(stateDir, teamName, nowIso);
@@ -619,7 +637,9 @@ export async function maybeNudgeTeamLeader({
619
637
 
620
638
  const canonicalFallbackTeams = await listNotifyCanonicalActiveTeams(cwd, currentSessionId).catch(() => []);
621
639
  for (const team of canonicalFallbackTeams) {
622
- candidateTeamNames.add(team.teamName);
640
+ const teamName = normalizeValidTeamName(team.teamName);
641
+ if (!teamName) continue;
642
+ candidateTeamNames.add(teamName);
623
643
  }
624
644
 
625
645
  // Use pre-computed staleness (captured before HUD state was updated this turn)
@@ -586,13 +586,8 @@ async function main() {
586
586
  shouldSendSessionIdleHookEvent,
587
587
  recordSessionIdleHookEventSent,
588
588
  } = await import('../notifications/idle-cooldown.js');
589
- const sessionJsonPath = join(stateDir, 'session.json');
590
589
  const idleFingerprint = buildIdleNotificationFingerprint(payload);
591
- let notifySessionId = '';
592
- try {
593
- const sessionData = JSON.parse(await readFile(sessionJsonPath, 'utf-8'));
594
- notifySessionId = safeString(sessionData && sessionData.session_id ? sessionData.session_id : '');
595
- } catch { /* no session file */ }
590
+ const notifySessionId = getEffectiveSessionId();
596
591
 
597
592
  const shouldNotifyLifecycle = notifySessionId
598
593
  && shouldSendIdleNotification(stateDir, notifySessionId, idleFingerprint);
@@ -218,7 +218,7 @@ Runtime availability gate:
218
218
  | "autopilot", "build me", "I want a" | `$autopilot` | Runtime-only: read `~/.codex/skills/autopilot/SKILL.md`, execute autonomous pipeline only inside OMX CLI/runtime |
219
219
  | "ultrawork", "ulw", "parallel" | `$ultrawork` | Runtime-only: read `~/.codex/skills/ultrawork/SKILL.md`, execute parallel agents only inside OMX CLI/runtime |
220
220
  | "ultraqa" | `$ultraqa` | Runtime-only: read `~/.codex/skills/ralph/SKILL.md`, run persistent completion and verification loop only inside OMX CLI/runtime (UltraQA compatibility alias) |
221
- | "analyze", "investigate" | `$analyze` | Read `~/.codex/prompts/debugger.md`, run root-cause analysis (analyze compatibility alias) |
221
+ | "analyze", "investigate" | `$analyze` | Read `~/.codex/skills/analyze/SKILL.md`, run read-only deep analysis with ranked synthesis, explicit confidence, and concrete file references |
222
222
  | "plan this", "plan the", "let's plan" | `$plan` | Read `~/.codex/skills/plan/SKILL.md`, start planning workflow |
223
223
  | "interview", "deep interview", "gather requirements", "interview me", "don't assume", "ouroboros" | `$deep-interview` | Read `~/.codex/skills/deep-interview/SKILL.md`, run Ouroboros-inspired Socratic ambiguity-gated interview workflow |
224
224
  | "ralplan", "consensus plan" | `$ralplan` | Read `~/.codex/skills/ralplan/SKILL.md`, start consensus planning with RALPLAN-DR structured deliberation (short by default, `--deliberate` for high-risk) |
@@ -41,8 +41,7 @@
41
41
  {
42
42
  "name": "ultraqa",
43
43
  "category": "execution",
44
- "status": "merged",
45
- "canonical": "ralph",
44
+ "status": "active",
46
45
  "core": false,
47
46
  "internalRequired": false
48
47
  },
@@ -79,8 +78,7 @@
79
78
  {
80
79
  "name": "analyze",
81
80
  "category": "shortcut",
82
- "status": "alias",
83
- "canonical": "debugger",
81
+ "status": "active",
84
82
  "core": false,
85
83
  "internalRequired": false
86
84
  },