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.
- package/Cargo.lock +6 -6
- package/Cargo.toml +1 -1
- package/dist/agents/native-config.d.ts +12 -0
- package/dist/agents/native-config.d.ts.map +1 -1
- package/dist/agents/native-config.js +12 -0
- package/dist/agents/native-config.js.map +1 -1
- package/dist/bin/__tests__/quality-gates-cli.test.d.ts +2 -0
- package/dist/bin/__tests__/quality-gates-cli.test.d.ts.map +1 -0
- package/dist/bin/__tests__/quality-gates-cli.test.js +110 -0
- package/dist/bin/__tests__/quality-gates-cli.test.js.map +1 -0
- package/dist/bin/quality-gates-cli.d.ts +18 -0
- package/dist/bin/quality-gates-cli.d.ts.map +1 -0
- package/dist/bin/quality-gates-cli.js +107 -0
- package/dist/bin/quality-gates-cli.js.map +1 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.js +18 -1
- package/dist/cli/__tests__/doctor-state-root-binding.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +11 -3
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +17 -4
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +21 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/session-search.js +1 -1
- package/dist/config/__tests__/quality-gates.test.d.ts +2 -0
- package/dist/config/__tests__/quality-gates.test.d.ts.map +1 -0
- package/dist/config/__tests__/quality-gates.test.js +369 -0
- package/dist/config/__tests__/quality-gates.test.js.map +1 -0
- package/dist/config/models.d.ts +4 -1
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +4 -1
- package/dist/config/models.js.map +1 -1
- package/dist/config/quality-gates.d.ts +97 -0
- package/dist/config/quality-gates.d.ts.map +1 -0
- package/dist/config/quality-gates.js +232 -0
- package/dist/config/quality-gates.js.map +1 -0
- package/dist/hooks/__tests__/code-review-skill-contract.test.js +2 -2
- package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/session.test.js +311 -1
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/session.d.ts +9 -8
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +125 -32
- package/dist/hooks/session.js.map +1 -1
- package/dist/mcp/__tests__/state-paths.test.js +47 -0
- package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
- package/dist/mcp/state-paths.d.ts +2 -2
- package/dist/mcp/state-paths.d.ts.map +1 -1
- package/dist/mcp/state-paths.js +4 -3
- package/dist/mcp/state-paths.js.map +1 -1
- package/dist/quality-gates/index.d.ts +7 -0
- package/dist/quality-gates/index.d.ts.map +1 -0
- package/dist/quality-gates/index.js +8 -0
- package/dist/quality-gates/index.js.map +1 -0
- package/dist/ralplan/__tests__/runtime.test.js +43 -1
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/runtime-contract.d.ts +22 -0
- package/dist/ralplan/runtime-contract.d.ts.map +1 -1
- package/dist/ralplan/runtime.d.ts +10 -1
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +83 -5
- package/dist/ralplan/runtime.js.map +1 -1
- package/package.json +1 -1
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/skills/code-review/SKILL.md +12 -2
- package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +11 -0
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +27 -7
- package/plugins/oh-my-codex/templates/AGENTS.md +17 -1
- package/skills/code-review/SKILL.md +12 -2
- package/skills/deep-interview/SKILL.md +11 -0
- package/skills/ralplan/SKILL.md +27 -7
- 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`,
|
|
50
|
-
assert.match(codeReviewSkill, /\*\*REQUEST CHANGES\*\* for a blocker, unresolved high\/critical finding, or unavailable
|
|
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('
|
|
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 {
|