oh-my-codex 0.21.39 → 0.21.41

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 (74) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/dist/agents/native-config.d.ts +12 -0
  4. package/dist/agents/native-config.d.ts.map +1 -1
  5. package/dist/agents/native-config.js +12 -0
  6. package/dist/agents/native-config.js.map +1 -1
  7. package/dist/bin/__tests__/quality-gates-cli.test.d.ts +2 -0
  8. package/dist/bin/__tests__/quality-gates-cli.test.d.ts.map +1 -0
  9. package/dist/bin/__tests__/quality-gates-cli.test.js +110 -0
  10. package/dist/bin/__tests__/quality-gates-cli.test.js.map +1 -0
  11. package/dist/bin/quality-gates-cli.d.ts +18 -0
  12. package/dist/bin/quality-gates-cli.d.ts.map +1 -0
  13. package/dist/bin/quality-gates-cli.js +107 -0
  14. package/dist/bin/quality-gates-cli.js.map +1 -0
  15. package/dist/cli/__tests__/doctor-state-root-binding.test.js +18 -1
  16. package/dist/cli/__tests__/doctor-state-root-binding.test.js.map +1 -1
  17. package/dist/cli/__tests__/launch-fallback.test.js +11 -3
  18. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  19. package/dist/cli/doctor.d.ts.map +1 -1
  20. package/dist/cli/doctor.js +17 -4
  21. package/dist/cli/doctor.js.map +1 -1
  22. package/dist/cli/index.d.ts +2 -2
  23. package/dist/cli/index.d.ts.map +1 -1
  24. package/dist/cli/index.js +21 -1
  25. package/dist/cli/index.js.map +1 -1
  26. package/dist/cli/session-search.js +1 -1
  27. package/dist/config/__tests__/quality-gates.test.d.ts +2 -0
  28. package/dist/config/__tests__/quality-gates.test.d.ts.map +1 -0
  29. package/dist/config/__tests__/quality-gates.test.js +369 -0
  30. package/dist/config/__tests__/quality-gates.test.js.map +1 -0
  31. package/dist/config/models.d.ts +4 -1
  32. package/dist/config/models.d.ts.map +1 -1
  33. package/dist/config/models.js +4 -1
  34. package/dist/config/models.js.map +1 -1
  35. package/dist/config/quality-gates.d.ts +97 -0
  36. package/dist/config/quality-gates.d.ts.map +1 -0
  37. package/dist/config/quality-gates.js +232 -0
  38. package/dist/config/quality-gates.js.map +1 -0
  39. package/dist/hooks/__tests__/code-review-skill-contract.test.js +2 -2
  40. package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
  41. package/dist/hooks/__tests__/session.test.js +311 -1
  42. package/dist/hooks/__tests__/session.test.js.map +1 -1
  43. package/dist/hooks/session.d.ts +9 -8
  44. package/dist/hooks/session.d.ts.map +1 -1
  45. package/dist/hooks/session.js +125 -32
  46. package/dist/hooks/session.js.map +1 -1
  47. package/dist/mcp/__tests__/state-paths.test.js +47 -0
  48. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  49. package/dist/mcp/state-paths.d.ts +2 -2
  50. package/dist/mcp/state-paths.d.ts.map +1 -1
  51. package/dist/mcp/state-paths.js +4 -3
  52. package/dist/mcp/state-paths.js.map +1 -1
  53. package/dist/quality-gates/index.d.ts +7 -0
  54. package/dist/quality-gates/index.d.ts.map +1 -0
  55. package/dist/quality-gates/index.js +8 -0
  56. package/dist/quality-gates/index.js.map +1 -0
  57. package/dist/ralplan/__tests__/runtime.test.js +43 -1
  58. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  59. package/dist/ralplan/runtime-contract.d.ts +22 -0
  60. package/dist/ralplan/runtime-contract.d.ts.map +1 -1
  61. package/dist/ralplan/runtime.d.ts +10 -1
  62. package/dist/ralplan/runtime.d.ts.map +1 -1
  63. package/dist/ralplan/runtime.js +83 -5
  64. package/dist/ralplan/runtime.js.map +1 -1
  65. package/package.json +1 -1
  66. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  67. package/plugins/oh-my-codex/skills/code-review/SKILL.md +12 -2
  68. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +11 -0
  69. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +27 -7
  70. package/plugins/oh-my-codex/templates/AGENTS.md +17 -1
  71. package/skills/code-review/SKILL.md +12 -2
  72. package/skills/deep-interview/SKILL.md +11 -0
  73. package/skills/ralplan/SKILL.md +27 -7
  74. package/templates/AGENTS.md +17 -1
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Custom Quality Gates Configuration (Minimal)
3
+ *
4
+ * Supports configurable custom quality gates for workflow stages via .omx-config.json qualityGates property.
5
+ *
6
+ * Schema:
7
+ * {
8
+ * "qualityGates": {
9
+ * "deep-interview": ["skill-name" | {skill, mode?, model?}, ...],
10
+ * "ralplan": ["skill-name" | {skill, mode?, model?}, ...],
11
+ * "code-review": ["skill-name" | {skill, mode?, model?}, ...]
12
+ * }
13
+ * }
14
+ *
15
+ * Gates are native subagents that run alongside standard reviewers.
16
+ * Missing skills produce visible warnings and are skipped.
17
+ */
18
+ import { existsSync, readFileSync } from 'fs';
19
+ import { join } from 'path';
20
+ import { codexHome } from '../utils/paths.js';
21
+ import { REMOVED_SKILLS } from '../hooks/sunset-stub.js';
22
+ /**
23
+ * Reserved skill names that cannot be used as gates
24
+ * These are built-in skills managed by OMX
25
+ */
26
+ const RESERVED_SKILL_NAMES = new Set([
27
+ 'ai-slop-cleaner',
28
+ 'verification',
29
+ 'code-review',
30
+ 'deep-interview',
31
+ ]);
32
+ /**
33
+ * Sunset/deprecated skill names from sunset-stub.ts
34
+ */
35
+ function getSunsetSkillNames() {
36
+ return new Set(Object.keys(REMOVED_SKILLS).map(k => k.toLowerCase()));
37
+ }
38
+ /**
39
+ * Validate skill name: must be kebab-case, not reserved, not sunset
40
+ */
41
+ export function validateSkillName(skill) {
42
+ if (!skill || typeof skill !== 'string') {
43
+ return { valid: false, error: 'Skill name must be a non-empty string' };
44
+ }
45
+ // Check kebab-case format (including leading $ for skill references)
46
+ const normalized = skill.startsWith('$') ? skill.slice(1) : skill;
47
+ if (!normalized || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(normalized)) {
48
+ return { valid: false, error: 'Skill name must be kebab-case (lowercase with hyphens), optionally prefixed with $' };
49
+ }
50
+ // Check reserved names
51
+ if (RESERVED_SKILL_NAMES.has(normalized)) {
52
+ return { valid: false, error: `Skill "${skill}" is reserved for built-in gates` };
53
+ }
54
+ // Check sunset names
55
+ const sunsetNames = getSunsetSkillNames();
56
+ if (sunsetNames.has(normalized.toLowerCase())) {
57
+ return { valid: false, error: `Skill "${skill}" is deprecated and no longer supported` };
58
+ }
59
+ return { valid: true };
60
+ }
61
+ /**
62
+ * Normalize a gate entry (string or object) to the standard internal representation
63
+ */
64
+ export function normalizeGateEntry(entry) {
65
+ let skill;
66
+ let mode = 'blocking';
67
+ let model;
68
+ if (typeof entry === 'string') {
69
+ skill = entry;
70
+ }
71
+ else if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
72
+ const obj = entry;
73
+ if (!obj.skill || typeof obj.skill !== 'string') {
74
+ return null;
75
+ }
76
+ skill = obj.skill;
77
+ if (obj.mode && typeof obj.mode === 'string') {
78
+ if (!['blocking', 'advisory'].includes(obj.mode)) {
79
+ return null; // Invalid mode
80
+ }
81
+ mode = obj.mode;
82
+ }
83
+ if (obj.model && typeof obj.model === 'string') {
84
+ model = obj.model;
85
+ }
86
+ }
87
+ else {
88
+ return null; // Invalid entry format
89
+ }
90
+ // Validate skill name
91
+ const validation = validateSkillName(skill);
92
+ if (!validation.valid) {
93
+ return null;
94
+ }
95
+ // Return object without model field if it's undefined
96
+ const result = { skill, mode };
97
+ if (model !== undefined) {
98
+ result.model = model;
99
+ }
100
+ return result;
101
+ }
102
+ /**
103
+ * Read quality gates configuration from .omx-config.json
104
+ * Reads from both project ./.omx-config.json (if cwd provided) and $CODEX_HOME/.omx-config.json
105
+ * Project config takes precedence over CODEX_HOME config for the same stage
106
+ */
107
+ export function readQualityGatesConfig(codexHomeOverride, cwd) {
108
+ const result = {};
109
+ let hasConfig = false;
110
+ // Read CODEX_HOME config first (lower precedence)
111
+ const codexHomePath = join(codexHomeOverride || codexHome(), '.omx-config.json');
112
+ if (existsSync(codexHomePath)) {
113
+ try {
114
+ const raw = JSON.parse(readFileSync(codexHomePath, 'utf-8'));
115
+ if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
116
+ const qualityGates = raw.qualityGates;
117
+ if (qualityGates && typeof qualityGates === 'object' && !Array.isArray(qualityGates)) {
118
+ const codexHomeGates = qualityGates;
119
+ for (const [stage, gates] of Object.entries(codexHomeGates)) {
120
+ if (gates) {
121
+ result[stage] = gates;
122
+ hasConfig = true;
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
128
+ catch {
129
+ // Silently ignore parse errors in CODEX_HOME config
130
+ }
131
+ }
132
+ // Read project config (higher precedence - overwrites CODEX_HOME)
133
+ if (cwd) {
134
+ const projectConfigPath = join(cwd, '.omx-config.json');
135
+ if (existsSync(projectConfigPath)) {
136
+ try {
137
+ const raw = JSON.parse(readFileSync(projectConfigPath, 'utf-8'));
138
+ if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
139
+ const qualityGates = raw.qualityGates;
140
+ if (qualityGates && typeof qualityGates === 'object' && !Array.isArray(qualityGates)) {
141
+ const projectGates = qualityGates;
142
+ for (const [stage, gates] of Object.entries(projectGates)) {
143
+ if (gates) {
144
+ result[stage] = gates;
145
+ hasConfig = true;
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+ catch {
152
+ // Silently ignore parse errors in project config
153
+ }
154
+ }
155
+ }
156
+ return hasConfig ? result : null;
157
+ }
158
+ /**
159
+ * Get normalized quality gates for a specific stage
160
+ * Returns array of validated and normalized gate entries
161
+ * Reads from both project and CODEX_HOME config with project taking precedence
162
+ */
163
+ export function getGatesForStage(stage, codexHomeOverride, cwd) {
164
+ const config = readQualityGatesConfig(codexHomeOverride, cwd);
165
+ const stageGates = config?.[stage];
166
+ if (!stageGates || !Array.isArray(stageGates)) {
167
+ return [];
168
+ }
169
+ // Normalize and filter to valid entries
170
+ return stageGates
171
+ .map(entry => normalizeGateEntry(entry))
172
+ .filter((gate) => gate !== null);
173
+ }
174
+ /**
175
+ * Validate all gates in the configuration file
176
+ * Returns validated gates and list of validation errors
177
+ * Reads from both project and CODEX_HOME config with project taking precedence
178
+ */
179
+ export function validateAllQualityGates(codexHomeOverride, cwd) {
180
+ const gates = [];
181
+ const errors = [];
182
+ const config = readQualityGatesConfig(codexHomeOverride, cwd);
183
+ if (!config) {
184
+ return { gates, errors };
185
+ }
186
+ const stages = ['deep-interview', 'ralplan', 'code-review'];
187
+ for (const stage of stages) {
188
+ const stageGates = config[stage];
189
+ if (!stageGates || !Array.isArray(stageGates))
190
+ continue;
191
+ for (let i = 0; i < stageGates.length; i++) {
192
+ const entry = stageGates[i];
193
+ const entryStr = typeof entry === 'string' ? entry : JSON.stringify(entry);
194
+ const normalized = normalizeGateEntry(entry);
195
+ if (normalized) {
196
+ gates.push(normalized);
197
+ }
198
+ else {
199
+ let error = 'Invalid gate entry';
200
+ if (typeof entry === 'string') {
201
+ error = validateSkillName(entry).error || 'Invalid skill';
202
+ }
203
+ else if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
204
+ const obj = entry;
205
+ if (!obj || !obj.skill) {
206
+ error = 'Gate object must have a "skill" property';
207
+ }
208
+ else if (typeof obj.skill !== 'string') {
209
+ error = 'Gate skill must be a string';
210
+ }
211
+ else if (obj.mode && !['blocking', 'advisory'].includes(obj.mode)) {
212
+ error = `Invalid mode: ${obj.mode}; must be 'blocking' or 'advisory'`;
213
+ }
214
+ else {
215
+ error = validateSkillName(obj.skill).error || 'Invalid skill';
216
+ }
217
+ }
218
+ else {
219
+ error = 'Gate entry must be a string or object with {skill, mode?, model?}';
220
+ }
221
+ errors.push({
222
+ stage,
223
+ skillIndex: i,
224
+ entry: entryStr,
225
+ error,
226
+ });
227
+ }
228
+ }
229
+ }
230
+ return { gates, errors };
231
+ }
232
+ //# sourceMappingURL=quality-gates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quality-gates.js","sourceRoot":"","sources":["../../src/config/quality-gates.ts"],"names":[],"mappings":"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"}
@@ -46,8 +46,8 @@ describe('code-review skill contract', () => {
46
46
  assert.match(codeReviewSkill, /Else if `code-reviewer` recommendation is \*\*REQUEST CHANGES\*\*, final recommendation is \*\*REQUEST CHANGES\*\*/i);
47
47
  assert.match(codeReviewSkill, /Else if architect status is \*\*WATCH\*\*, final recommendation is \*\*COMMENT\*\*/i);
48
48
  assert.match(codeReviewSkill, /Else final recommendation follows the `code-reviewer` lane/i);
49
- assert.match(codeReviewSkill, /\*\*APPROVE\*\* only when `code-reviewer` returns APPROVE, architect status is `CLEAR`, and both independent lanes returned evidence/i);
50
- assert.match(codeReviewSkill, /\*\*REQUEST CHANGES\*\* for a blocker, unresolved high\/critical finding, or unavailable lane/i);
49
+ assert.match(codeReviewSkill, /\*\*APPROVE\*\* only when `code-reviewer` returns APPROVE, architect status is `CLEAR`, both independent lanes returned evidence/i);
50
+ assert.match(codeReviewSkill, /\*\*REQUEST CHANGES\*\* for a blocker, unresolved high\/critical finding, unavailable lane, or non-passing\/unavailable blocking quality gate/i);
51
51
  });
52
52
  it('bounds auto-fix wording to the supported omx ralph compatibility path only', () => {
53
53
  assert.match(codeReviewSkill, /On the supported `omx ralph` CLI compatibility path/i);
@@ -1 +1 @@
1
- {"version":3,"file":"code-review-skill-contract.test.js","sourceRoot":"","sources":["../../../src/hooks/__tests__/code-review-skill-contract.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"code-review-skill-contract.test.js","sourceRoot":"","sources":["../../../src/hooks/__tests__/code-review-skill-contract.test.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import { describe, it } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import { spawn } from 'node:child_process';
4
+ import { createHash } from 'node:crypto';
4
5
  import { once } from 'node:events';
5
6
  import { existsSync } from 'node:fs';
6
7
  import { link, lstat, mkdir, mkdtemp, readFile, readdir, readlink, rename, rm, symlink, writeFile } from 'node:fs/promises';
@@ -68,6 +69,38 @@ async function withOwnerEnvironment(sessionId, run) {
68
69
  process.env.OMX_SESSION_ID = previous;
69
70
  }
70
71
  }
72
+ async function withEnvironment(values, run) {
73
+ const previous = new Map(Object.keys(values).map((key) => [key, process.env[key]]));
74
+ for (const [key, value] of Object.entries(values)) {
75
+ if (value === undefined)
76
+ delete process.env[key];
77
+ else
78
+ process.env[key] = value;
79
+ }
80
+ try {
81
+ await run();
82
+ }
83
+ finally {
84
+ for (const [key, value] of previous) {
85
+ if (value === undefined)
86
+ delete process.env[key];
87
+ else
88
+ process.env[key] = value;
89
+ }
90
+ }
91
+ }
92
+ const MANAGED_CODEX_DAEMON_CMDLINE = '/home/test/.codex/packages/app-server/bin/codex app-server --remote-control --listen unix:// --managed-daemon';
93
+ function linuxProcessObservation(cmdline, birth) {
94
+ return {
95
+ kind: 'identity',
96
+ cmdline,
97
+ identity: {
98
+ platform: 'linux',
99
+ birth,
100
+ cmdline_hash: createHash('sha256').update(cmdline).digest('hex'),
101
+ },
102
+ };
103
+ }
71
104
  /**
72
105
  * Identity fields for a pointer that belongs to the HOST platform.
73
106
  *
@@ -774,7 +807,7 @@ describe('isSessionStale', () => {
774
807
  }), false);
775
808
  });
776
809
  });
777
- describe('verified-dead selected session pointer recovery', { concurrency: false }, () => {
810
+ describe('selected session pointer recovery', { concurrency: false }, () => {
778
811
  async function writePointer(cwd, overrides = {}) {
779
812
  const context = resolveSessionPointerContext(cwd);
780
813
  await mkdir(context.baseStateDir, { recursive: true });
@@ -789,6 +822,152 @@ describe('verified-dead selected session pointer recovery', { concurrency: false
789
822
  await writeFile(context.sessionPath, body, 'utf-8');
790
823
  return { path: context.sessionPath, body };
791
824
  }
825
+ async function writeManagedDaemonPointer(cwd) {
826
+ const context = resolveSessionPointerContext(cwd);
827
+ await mkdir(context.baseStateDir, { recursive: true });
828
+ const body = JSON.stringify({
829
+ session_id: '01a18453-9e49-7c21-9c51-cf411286e627',
830
+ native_session_id: '01a18453-9e49-7c21-9c51-cf411286e627',
831
+ started_at: '2026-10-28T23:03:37.205Z',
832
+ cwd,
833
+ state_root: context.baseStateDir,
834
+ pid: 57617,
835
+ platform: 'linux',
836
+ identity_schema_version: 2,
837
+ process_identity: linuxProcessObservation(MANAGED_CODEX_DAEMON_CMDLINE, '87768').identity,
838
+ pid_start_ticks: 87768,
839
+ pid_cmdline: MANAGED_CODEX_DAEMON_CMDLINE,
840
+ });
841
+ await writeFile(context.sessionPath, body, 'utf-8');
842
+ return { path: context.sessionPath, body };
843
+ }
844
+ it('lets resume replace a live managed-daemon owner without inheriting a stale OMX selector', async () => {
845
+ const root = await mkdtemp(join(tmpdir(), 'omx-session-pointer-daemon-resume-'));
846
+ const cwd = join(root, 'workspace');
847
+ const home = join(root, 'home');
848
+ const codexHome = join(root, 'codex-home');
849
+ const omxRoot = join(root, 'omx-root');
850
+ await mkdir(cwd, { recursive: true });
851
+ try {
852
+ await withEnvironment({
853
+ HOME: home,
854
+ CODEX_HOME: codexHome,
855
+ OMX_ROOT: omxRoot,
856
+ OMX_SESSION_ID: 'omx-inherited-stale',
857
+ }, async () => {
858
+ const pointer = await writeManagedDaemonPointer(cwd);
859
+ await withPointerDependencies({
860
+ runtimePlatform: 'linux',
861
+ probePid: () => 'alive',
862
+ observeProcess: () => linuxProcessObservation(MANAGED_CODEX_DAEMON_CMDLINE, '87768'),
863
+ }, async () => {
864
+ const context = resolveSessionPointerContext(cwd);
865
+ assert.equal((await readSessionPointer(context)).status, 'non-runtime-owner');
866
+ const established = await establishLaunchSessionBinding(cwd, 'omx-resume-after-daemon');
867
+ assert.equal(established.kind, 'committed-released');
868
+ if (established.kind !== 'committed-released')
869
+ return;
870
+ const resumedState = await readSessionState(cwd);
871
+ assert.equal(resumedState?.session_id, 'omx-resume-after-daemon');
872
+ assert.equal(resumedState?.owner_omx_session_id, undefined, 'unverified inherited OMX_SESSION_ID must not become owner authority');
873
+ assert.equal(await closeLaunchSessionBindingOnce(established.binding).then((result) => result.status), 'closed');
874
+ });
875
+ assert.notEqual(await readFile(pointer.path, 'utf-8'), pointer.body);
876
+ });
877
+ }
878
+ finally {
879
+ await rm(root, { recursive: true, force: true });
880
+ }
881
+ });
882
+ it('quarantines a live managed-daemon pointer and permits a subsequent launch without stopping the daemon', async () => {
883
+ const root = await mkdtemp(join(tmpdir(), 'omx-session-pointer-daemon-recover-'));
884
+ const cwd = join(root, 'workspace');
885
+ await mkdir(cwd, { recursive: true });
886
+ try {
887
+ await withEnvironment({
888
+ HOME: join(root, 'home'),
889
+ CODEX_HOME: join(root, 'codex-home'),
890
+ OMX_ROOT: join(root, 'omx-root'),
891
+ OMX_SESSION_ID: 'omx-inherited-stale',
892
+ }, async () => {
893
+ const pointer = await writeManagedDaemonPointer(cwd);
894
+ await withPointerDependencies({
895
+ runtimePlatform: 'linux',
896
+ probePid: () => 'alive',
897
+ observeProcess: () => linuxProcessObservation(MANAGED_CODEX_DAEMON_CMDLINE, '87768'),
898
+ token: () => SUCCESSOR_TOKEN,
899
+ }, async () => {
900
+ const recovered = await recoverDeadSessionPointer(cwd);
901
+ assert.equal(recovered.status, 'recovered', recovered.reason);
902
+ assert.equal(recovered.recovered, true);
903
+ assert.match(recovered.reason, /managed Codex daemon/);
904
+ assert.ok(recovered.quarantinePath);
905
+ assert.equal(await readFile(recovered.quarantinePath, 'utf-8'), pointer.body);
906
+ assert.equal((await readSessionPointer(resolveSessionPointerContext(cwd))).status, 'absent');
907
+ const established = await establishLaunchSessionBinding(cwd, 'omx-resume-after-recovery');
908
+ assert.equal(established.kind, 'committed-released');
909
+ if (established.kind === 'committed-released') {
910
+ assert.equal(await closeLaunchSessionBindingOnce(established.binding).then((result) => result.status), 'closed');
911
+ }
912
+ });
913
+ });
914
+ }
915
+ finally {
916
+ await rm(root, { recursive: true, force: true });
917
+ }
918
+ });
919
+ it('continues refusing recovery and a fresh launch for a live interactive OMX owner', async () => {
920
+ const root = await mkdtemp(join(tmpdir(), 'omx-session-pointer-interactive-owner-'));
921
+ const cwd = join(root, 'workspace');
922
+ const interactiveCmdline = '/home/test/.local/bin/codex resume active-conversation';
923
+ const interactiveObservation = linuxProcessObservation(interactiveCmdline, '99123');
924
+ await mkdir(cwd, { recursive: true });
925
+ try {
926
+ await withEnvironment({
927
+ HOME: join(root, 'home'),
928
+ CODEX_HOME: join(root, 'codex-home'),
929
+ OMX_ROOT: join(root, 'omx-root'),
930
+ OMX_SESSION_ID: undefined,
931
+ }, async () => {
932
+ const context = resolveSessionPointerContext(cwd);
933
+ await mkdir(context.baseStateDir, { recursive: true });
934
+ const pointerBody = JSON.stringify({
935
+ session_id: 'omx-live-interactive',
936
+ owner_omx_session_id: 'omx-live-interactive',
937
+ launch_lineage_token: TEST_TOKEN,
938
+ started_at: '2026-10-28T23:03:37.205Z',
939
+ cwd,
940
+ state_root: context.baseStateDir,
941
+ pid: 57618,
942
+ platform: 'linux',
943
+ identity_schema_version: 2,
944
+ process_identity: interactiveObservation.identity,
945
+ pid_start_ticks: 99123,
946
+ pid_cmdline: interactiveCmdline,
947
+ });
948
+ await writeFile(context.sessionPath, pointerBody, 'utf-8');
949
+ await withPointerDependencies({
950
+ runtimePlatform: 'linux',
951
+ probePid: () => 'alive',
952
+ observeProcess: () => interactiveObservation,
953
+ }, async () => {
954
+ const established = await establishLaunchSessionBinding(cwd, 'omx-competing-launch');
955
+ assert.equal(established.kind, 'precommit-aborted');
956
+ if (established.kind === 'precommit-aborted')
957
+ assert.equal(established.abort.code, 'session_pointer_owner_conflict');
958
+ const refused = await recoverDeadSessionPointer(cwd);
959
+ assert.equal(refused.status, 'usable');
960
+ assert.equal(refused.recovered, false);
961
+ assert.equal(refused.action, 'none');
962
+ assert.match(refused.reason, /usable or live/);
963
+ });
964
+ assert.equal(await readFile(context.sessionPath, 'utf-8'), pointerBody);
965
+ });
966
+ }
967
+ finally {
968
+ await rm(root, { recursive: true, force: true });
969
+ }
970
+ });
792
971
  it('quarantines exact dead evidence, is idempotent, and permits relaunch', async () => {
793
972
  const cwd = await mkdtemp(join(tmpdir(), 'omx-session-pointer-recover-dead-'));
794
973
  try {
@@ -3451,6 +3630,137 @@ describe('session pointer transaction', () => {
3451
3630
  await rm(cwd, { recursive: true, force: true });
3452
3631
  }
3453
3632
  });
3633
+ it('preserves managed-daemon owner sidecars without publishing them as canonical runtime owners', async () => {
3634
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-daemon-owner-sidecar-'));
3635
+ const nativeSessionId = 'native-daemon-sidecar';
3636
+ const observation = linuxProcessObservation(MANAGED_CODEX_DAEMON_CMDLINE, '87768');
3637
+ try {
3638
+ const context = resolveSessionPointerContext(cwd);
3639
+ const sidecarDir = join(context.baseStateDir, 'sessions', nativeSessionId);
3640
+ const sidecarPath = join(sidecarDir, 'session-owner.json');
3641
+ await mkdir(sidecarDir, { recursive: true });
3642
+ await writeFile(sidecarPath, JSON.stringify({
3643
+ session_id: nativeSessionId,
3644
+ native_session_id: nativeSessionId,
3645
+ started_at: '2026-10-28T23:03:37.205Z',
3646
+ cwd,
3647
+ state_root: context.baseStateDir,
3648
+ pid: 57617,
3649
+ platform: 'linux',
3650
+ identity_schema_version: 2,
3651
+ process_identity: observation.identity,
3652
+ pid_start_ticks: 87768,
3653
+ pid_cmdline: MANAGED_CODEX_DAEMON_CMDLINE,
3654
+ }), 'utf-8');
3655
+ await withPointerDependencies({
3656
+ runtimePlatform: 'linux',
3657
+ probePid: () => 'alive',
3658
+ observeProcess: () => observation,
3659
+ }, async () => {
3660
+ assert.equal((await readNativeSessionOwnerEvidence(cwd, nativeSessionId)).status, 'usable');
3661
+ const sidecarBefore = await readFile(sidecarPath, 'utf-8');
3662
+ const owner = await writeNativeSessionOwner(cwd, nativeSessionId, {
3663
+ pid: 57617,
3664
+ platform: 'linux',
3665
+ });
3666
+ assert.equal(owner.session_id, nativeSessionId);
3667
+ const sidecarAfterOwnerWrite = await readFile(sidecarPath, 'utf-8');
3668
+ assert.equal(JSON.parse(sidecarAfterOwnerWrite).session_id, nativeSessionId);
3669
+ await assert.rejects(reconcileNativeSessionStart(cwd, nativeSessionId, {
3670
+ pid: 57617,
3671
+ platform: 'linux',
3672
+ }), (error) => isSessionPointerLaunchAbort(error)
3673
+ && error.code === 'session_pointer_owner_conflict');
3674
+ assert.equal((await readSessionPointer(context)).status, 'absent');
3675
+ assert.equal(await readFile(sidecarPath, 'utf-8'), sidecarAfterOwnerWrite);
3676
+ assert.notEqual(sidecarAfterOwnerWrite, sidecarBefore);
3677
+ });
3678
+ }
3679
+ finally {
3680
+ await rm(cwd, { recursive: true, force: true });
3681
+ }
3682
+ });
3683
+ it('P1: rejects SessionStart republishing a managed daemon to the canonical pointer', async () => {
3684
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-daemon-canonical-republish-'));
3685
+ const nativeSessionId = 'native-daemon-codex';
3686
+ const daemonObservation = linuxProcessObservation(MANAGED_CODEX_DAEMON_CMDLINE, '87768');
3687
+ try {
3688
+ const context = resolveSessionPointerContext(cwd);
3689
+ // Setup: Create canonical pointer with managed daemon owner (non-runtime-owner status)
3690
+ const pointerPath = join(context.baseStateDir, 'session.json');
3691
+ await mkdir(dirname(pointerPath), { recursive: true });
3692
+ const initialDaemonState = {
3693
+ session_id: 'omx-session-from-daemon',
3694
+ native_session_id: nativeSessionId,
3695
+ started_at: '2026-10-28T23:00:00.000Z',
3696
+ cwd,
3697
+ state_root: context.baseStateDir,
3698
+ pid: 57617,
3699
+ platform: 'linux',
3700
+ identity_schema_version: 2,
3701
+ process_identity: daemonObservation.identity,
3702
+ pid_start_ticks: 87768,
3703
+ pid_cmdline: MANAGED_CODEX_DAEMON_CMDLINE,
3704
+ };
3705
+ await writeFile(pointerPath, JSON.stringify(initialDaemonState), 'utf-8');
3706
+ await withPointerDependencies({
3707
+ runtimePlatform: 'linux',
3708
+ probePid: () => 'alive',
3709
+ observeProcess: () => daemonObservation,
3710
+ }, async () => {
3711
+ // Verify the canonical pointer is classified as non-runtime-owner
3712
+ const canonicalPointerStatus = (await readSessionPointer(context)).status;
3713
+ assert.equal(canonicalPointerStatus, 'non-runtime-owner', 'canonical pointer should be non-runtime-owner before SessionStart');
3714
+ await assert.rejects(reconcileNativeSessionStart(cwd, nativeSessionId, {
3715
+ pid: 57617,
3716
+ platform: 'linux',
3717
+ }), (error) => isSessionPointerLaunchAbort(error)
3718
+ && error.code === 'session_pointer_unusable'
3719
+ && error.pointerStatus === 'non-runtime-owner');
3720
+ assert.equal(await readFile(pointerPath, 'utf-8'), JSON.stringify(initialDaemonState));
3721
+ assert.equal((await readSessionPointer(context)).status, 'non-runtime-owner');
3722
+ });
3723
+ }
3724
+ finally {
3725
+ await rm(cwd, { recursive: true, force: true });
3726
+ }
3727
+ });
3728
+ it('retains the OMX owner when managed-daemon SessionStart reconciles a launched pointer', async () => {
3729
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-daemon-reconcile-launch-owner-'));
3730
+ const ownerPid = 57616;
3731
+ const daemonPid = 57617;
3732
+ const ownerObservation = linuxProcessObservation('/usr/local/bin/omx', '87767');
3733
+ const daemonObservation = linuxProcessObservation(MANAGED_CODEX_DAEMON_CMDLINE, '87768');
3734
+ try {
3735
+ const context = resolveSessionPointerContext(cwd);
3736
+ await withPointerDependencies({
3737
+ runtimePlatform: 'linux',
3738
+ probePid: () => 'alive',
3739
+ observeProcess: (pid) => pid === daemonPid ? daemonObservation : ownerObservation,
3740
+ }, async () => {
3741
+ const launched = await writeSessionStart(cwd, 'omx-launch-owner', {
3742
+ pid: ownerPid,
3743
+ platform: 'linux',
3744
+ });
3745
+ const reconciled = await reconcileNativeSessionStart(cwd, 'native-daemon-reconcile', {
3746
+ pid: daemonPid,
3747
+ platform: 'linux',
3748
+ });
3749
+ assert.equal(reconciled.session_id, launched.session_id);
3750
+ assert.equal(reconciled.native_session_id, 'native-daemon-reconcile');
3751
+ assert.equal(reconciled.pid, ownerPid);
3752
+ assert.equal(reconciled.process_identity?.birth, ownerObservation.identity.birth);
3753
+ assert.notEqual(reconciled.process_identity?.cmdline_hash, daemonObservation.identity.cmdline_hash);
3754
+ const pointerAfter = await readSessionPointer(context);
3755
+ assert.equal(pointerAfter.status, 'usable');
3756
+ assert.equal(pointerAfter.state?.pid, ownerPid);
3757
+ assert.equal(pointerAfter.state?.native_session_id, 'native-daemon-reconcile');
3758
+ });
3759
+ }
3760
+ finally {
3761
+ await rm(cwd, { recursive: true, force: true });
3762
+ }
3763
+ });
3454
3764
  it('rejects a symlinked native owner state root', async () => {
3455
3765
  const cwd = await mkdtemp(join(tmpdir(), 'omx-session-owner-root-symlink-'));
3456
3766
  try {